How to bind GDExtension a method with parameters?

Godot Version

4.4.1

Question

I’m having trouble trying to figure out how to bind a method in my GDExtension addon that takes two parameters and returns an image. The docs only talk about how to define properties, and skips over regular methods, so I feel I’m having to guess about this.

My C++ class looks like

//world_surface_noise.h
namespace godot {

class WorldSurfaceNoise : public Resource {
	GDCLASS(WorldSurfaceNoise, Resource)
...
public:
...
    Ref<Image> generate_height_map(Vector2i map_size, Rect2 uv_region) const;
};

}
//world_surface_noise.cpp

using namespace godot;

void WorldSurfaceNoise::_bind_methods() {
...
	ClassDB::bind_method(D_METHOD("generate_height_map", "map_size", "uv_region"), &WorldSurfaceNoise::generate_height_map);
}

Ref<Image> WorldSurfaceNoise::generate_height_map(Vector2i map_size, Rect2 uv_region) const {
...
}

In GDScript, I’m trying to call this method

func update_image():
	if noise_source:
		var img:Image = noise_source.generate_height_map(image_size, Rect2(0, 0, 1, 1))
		print("img ", img)
		set_image(img)

I’m getting the error message

  ERROR: Cannot call GDExtension method bind 'generate_height_map' on placeholder instance.
  ERROR: Invalid image

What’s the right way to set up binding this method?

What you’ve shown looks quite similar to what I’ve used myself. Does adding properties work for you? Can you show more of your code?

This is the whole thing. The properties appear in the property inspector, but I can’t read them using method calls.

Errors:

  ERROR: Cannot call GDExtension method bind 'get_octaves' on placeholder instance.
  ERROR: Cannot call GDExtension method bind 'generate_height_map' on placeholder instance.
  ERROR: Invalid image
//world_surface_noise.h
#ifndef WORLD_SURFACE_NOISE_H
#define WORLD_SURFACE_NOISE_H

#include <godot_cpp/classes/resource.hpp>
#include <godot_cpp/classes/curve.hpp>
#include <godot_cpp/classes/fast_noise_lite.hpp>
#include <godot_cpp/classes/image.hpp>
//#include <godot_cpp/classes/packed_float32_array.hpp>
#include <string>


namespace godot {

class WorldSurfaceNoise : public Resource {
	GDCLASS(WorldSurfaceNoise, Resource)

private:
    int seed = 0;
    int octaves = 4;
    float lacunarity = 2;
    float gain = .5;

    bool wrap_x = true;
    bool wrap_y = true;
    Vector2 world_radius = Vector2(1, 1);

protected:
	static void _bind_methods();

public:
    WorldSurfaceNoise() {}
	~WorldSurfaceNoise() {}
    
    int get_seed() const;
    void set_seed(int _seed);

    int get_octaves() const;
    void set_octaves(int _octaves);

    float get_lacunarity() const;
    void set_lacunarity(float _lacunarity);

    float get_gain() const;
    void set_gain(float _gain);

    bool is_wrap_x() const;
    void set_wrap_x(bool _wrap_x);

    bool is_wrap_y() const;
    void set_wrap_y(bool _wrap_y);

    Vector2 get_world_radius() const;
    void set_world_radius(Vector2 radius);



    Ref<Image> generate_height_map(Vector2i map_size, Rect2 uv_region) const;

    float sample_height(Vector2 uv) const;

};

}

#endif
//world_surface_noise.cpp
#include "world_surface_noise.h"
#include <godot_cpp/core/class_db.hpp>

using namespace godot;

void WorldSurfaceNoise::_bind_methods() {
	ClassDB::bind_method(D_METHOD("get_seed"), &WorldSurfaceNoise::get_seed);
	ClassDB::bind_method(D_METHOD("set_seed", "p_seed"), &WorldSurfaceNoise::set_seed);
	ADD_PROPERTY(PropertyInfo(Variant::INT, "seed"), "set_seed", "get_seed");

	ClassDB::bind_method(D_METHOD("get_octaves"), &WorldSurfaceNoise::get_octaves);
	ClassDB::bind_method(D_METHOD("set_octaves", "p_octaves"), &WorldSurfaceNoise::set_octaves);
	ADD_PROPERTY(PropertyInfo(Variant::INT, "octaves", PROPERTY_HINT_RANGE, "1,16,1,or_greater"), "set_octaves", "get_octaves");

	ClassDB::bind_method(D_METHOD("get_lacunarity"), &WorldSurfaceNoise::get_lacunarity);
	ClassDB::bind_method(D_METHOD("set_lacunarity", "p_lacunarity"), &WorldSurfaceNoise::set_lacunarity);
	ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "lacunarity", PROPERTY_HINT_RANGE, "0.0,4.0,0.01,or_greater"), "set_lacunarity", "get_lacunarity");

	ClassDB::bind_method(D_METHOD("get_gain"), &WorldSurfaceNoise::get_gain);
	ClassDB::bind_method(D_METHOD("set_gain", "p_gain"), &WorldSurfaceNoise::set_gain);
	ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "gain", PROPERTY_HINT_RANGE, "0.0,1.0,0.01,or_greater"), "set_gain", "get_gain");
	
	ClassDB::bind_method(D_METHOD("is_wrap_x"), &WorldSurfaceNoise::is_wrap_x);
	ClassDB::bind_method(D_METHOD("set_wrap_x", "p_wrap_x"), &WorldSurfaceNoise::set_wrap_x);
	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "wrap_x"), "set_wrap_x", "is_wrap_x");

	ClassDB::bind_method(D_METHOD("is_wrap_y"), &WorldSurfaceNoise::is_wrap_y);
	ClassDB::bind_method(D_METHOD("set_wrap_y", "p_wrap_y"), &WorldSurfaceNoise::set_wrap_y);
	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "wrap_y"), "set_wrap_y", "is_wrap_y");

	ClassDB::bind_method(D_METHOD("get_world_radius"), &WorldSurfaceNoise::get_world_radius);
	ClassDB::bind_method(D_METHOD("set_world_radius", "p_world_radius"), &WorldSurfaceNoise::set_world_radius);
	ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "world_radius"), "set_world_radius", "get_world_radius");

	ClassDB::bind_method(D_METHOD("sample_height", "uv"), &WorldSurfaceNoise::sample_height);
	ClassDB::bind_method(D_METHOD("generate_height_map", "map_size", "uv_region"), &WorldSurfaceNoise::generate_height_map);
}


int WorldSurfaceNoise::get_seed() const {
    return seed;
}

void WorldSurfaceNoise::set_seed(int p_seed) {
    if (seed == p_seed) {
        return; // No change, no need to emit changed signal
    }
    seed = p_seed;
    emit_changed();
}

int WorldSurfaceNoise::get_octaves() const {
    return octaves;
}

void WorldSurfaceNoise::set_octaves(int p_octaves) {
    if (octaves == p_octaves) {
        return; // No change, no need to emit changed signal
    }
    octaves = p_octaves;
    emit_changed();
}

float WorldSurfaceNoise::get_lacunarity() const {
    return lacunarity;
}   

void WorldSurfaceNoise::set_lacunarity(float p_lacunarity) {
    if (lacunarity == p_lacunarity) {
        return; // No change, no need to emit changed signal
    }
    lacunarity = p_lacunarity;
    emit_changed();
}

float WorldSurfaceNoise::get_gain() const {
    return gain;
}

void WorldSurfaceNoise::set_gain(float p_gain) {
    if (gain == p_gain) {
        return; // No change, no need to emit changed signal
    }
    gain = p_gain;
    emit_changed();
}

bool WorldSurfaceNoise::is_wrap_x() const {
    return wrap_x;
}

void WorldSurfaceNoise::set_wrap_x(bool p_wrap_x) {
    if (wrap_x == p_wrap_x) {
        return; // No change, no need to emit changed signal
    }
    wrap_x = p_wrap_x;
    emit_changed();
}

bool WorldSurfaceNoise::is_wrap_y() const {
    return wrap_y;
}

void WorldSurfaceNoise::set_wrap_y(bool p_wrap_y) {
    if (wrap_y == p_wrap_y) {
        return; // No change, no need to emit changed signal
    }
    wrap_y = p_wrap_y;
    emit_changed();
}

Vector2 WorldSurfaceNoise::get_world_radius() const {
    return world_radius;
}

void WorldSurfaceNoise::set_world_radius(Vector2 radius) {
    if (world_radius == radius) {
        return; // No change, no need to emit changed signal
    }
    world_radius = radius;
    emit_changed();
}

Ref<Image> WorldSurfaceNoise::generate_height_map(Vector2i map_size, Rect2 uv_region) const {
    PackedFloat32Array data;
    data.resize(map_size.x * map_size.y);

    for (int y = 0; y < map_size.y; y++) {
        for (int x = 0; x < map_size.x; x++) {
            Vector2 uv = Vector2(x, y) / Vector2(map_size);
            uv = uv_region.position + uv * uv_region.size;

            data[x + y * map_size.x] = sample_height(uv);
        }
    }

    Ref<Image> image = Image::create_from_data(map_size.x, map_size.y, false, Image::FORMAT_RF, data.to_byte_array());
    return image;
}

float WorldSurfaceNoise::sample_height(Vector2 uv) const {
    FastNoiseLite noise;
    noise.set_seed(seed);
    noise.set_noise_type(FastNoiseLite::NoiseType::TYPE_PERLIN);
    noise.set_fractal_type(FastNoiseLite::FractalType::FRACTAL_FBM);
    noise.set_fractal_octaves(octaves);
    noise.set_fractal_lacunarity(lacunarity);
    noise.set_fractal_gain(gain);

    float height;

    if (wrap_x && wrap_y) {
        Vector4 _uv = Vector4(sinf(uv.x * Math_PI * 2.0f) * world_radius.x,
            sinf(uv.y * Math_PI * 2.0f) * world_radius.y,
            cosf(uv.x * Math_PI * 2.0f) * world_radius.x,
            cosf(uv.y * Math_PI * 2.0f) * world_radius.y);
        
        height = noise.get_noise_3d(_uv.x, _uv.y, _uv.z);
    }
    else if (wrap_x) {
        Vector3 _uv = Vector3(sinf(uv.x * Math_PI * 2.0f) * world_radius.x,
            uv.y * world_radius.y,
            cosf(uv.x * Math_PI * 2.0f) * world_radius.x);
        
        height = noise.get_noise_3dv(_uv);
    }
    else {
        height = noise.get_noise_2dv(uv * world_radius);
    }

    return height;
}
#MyTexture2D.gd
@tool
extends ImageTexture
class_name MyTexture2D

@export var noise_source:WorldSurfaceNoise:
	set(v):
		if v == noise_source:
			return
			
		noise_source = v
		update_image()

@export var image_size:Vector2i = Vector2i(512, 512):
	set(v):
		if v == image_size:
			return
			
		image_size = v
		update_image()
		

func update_image():
	if noise_source:
		print("update_image()")
		var octaves:int = noise_source.get_octaves()
		print ("ocatves ", octaves)
		
		var img:Image = noise_source.generate_height_map(image_size, Rect2(0, 0, 1, 1))
		print("img ", img)
		set_image(img)
	pass

The issue was that I was calling this from a @tool script and I needed to register my classes with GDREGISTER_CLASS

2 Likes