Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HeightMapShape3D update with Image data #87889

Merged
merged 1 commit into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions doc/classes/HeightMapShape3D.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@
<description>
A 3D heightmap shape, intended for use in physics. Usually used to provide a shape for a [CollisionShape3D]. This is useful for terrain, but it is limited as overhangs (such as caves) cannot be stored. Holes in a [HeightMapShape3D] are created by assigning very low values to points in the desired area.
[b]Performance:[/b] [HeightMapShape3D] is faster to check collisions against than [ConcavePolygonShape3D], but it is significantly slower than primitive shapes like [BoxShape3D].
A heightmap collision shape can also be build by using an [Image] reference:
[codeblocks]
[gdscript]
var heightmap_texture: Texture = ResourceLoader.load("res://heightmap_image.exr")
var heightmap_image: Image = heightmap_texture.get_image()
heightmap_image.convert(Image.FORMAT_RF)

var height_min: float = 0.0
var height_max: float = 10.0

update_map_data_from_image(heightmap_image, height_min, height_max)
[/gdscript]
[/codeblocks]
</description>
<tutorials>
</tutorials>
Expand All @@ -22,6 +35,17 @@
Returns the smallest height value found in [member map_data]. Recalculates only when [member map_data] changes.
</description>
</method>
smix8 marked this conversation as resolved.
Show resolved Hide resolved
<method name="update_map_data_from_image">
<return type="void" />
<param index="0" name="image" type="Image" />
<param index="1" name="height_min" type="float" />
<param index="2" name="height_max" type="float" />
<description>
Updates [member map_data] with data read from an [Image] reference. Automatically resizes heightmap [member map_width] and [member map_depth] to fit the full image width and height.
The image needs to be in either [constant Image.FORMAT_RF] (32 bit), [constant Image.FORMAT_RH] (16 bit), or [constant Image.FORMAT_R8] (8 bit).
Each image pixel is read in as a float on the range from [code]0.0[/code] (black pixel) to [code]1.0[/code] (white pixel). This range value gets remapped to [param height_min] and [param height_max] to form the final height value.
</description>
</method>
</methods>
<members>
<member name="map_data" type="PackedFloat32Array" setter="set_map_data" getter="get_map_data" default="PackedFloat32Array(0, 0, 0, 0)">
Expand Down
101 changes: 101 additions & 0 deletions scene/resources/3d/height_map_shape_3d.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

#include "height_map_shape_3d.h"

#include "core/io/image.h"
#include "servers/physics_server_3d.h"

Vector<Vector3> HeightMapShape3D::get_debug_mesh_lines() const {
Expand Down Expand Up @@ -187,6 +188,104 @@ real_t HeightMapShape3D::get_max_height() const {
return max_height;
}

void HeightMapShape3D::update_map_data_from_image(const Ref<Image> &p_image, real_t p_height_min, real_t p_height_max) {
ERR_FAIL_COND_MSG(p_image.is_null(), "Heightmap update image requires a valid Image reference.");
ERR_FAIL_COND_MSG(p_image->get_format() != Image::FORMAT_RF && p_image->get_format() != Image::FORMAT_RH && p_image->get_format() != Image::FORMAT_R8, "Heightmap update image requires Image in format FORMAT_RF (32 bit), FORMAT_RH (16 bit), or FORMAT_R8 (8 bit).");
ERR_FAIL_COND_MSG(p_image->get_width() < 2, "Heightmap update image requires a minimum Image width of 2.");
ERR_FAIL_COND_MSG(p_image->get_height() < 2, "Heightmap update image requires a minimum Image height of 2.");
ERR_FAIL_COND_MSG(p_height_min > p_height_max, "Heightmap update image requires height_max to be greater than height_min.");

map_width = p_image->get_width();
map_depth = p_image->get_height();
map_data.resize(map_width * map_depth);

real_t new_min_height = FLT_MAX;
real_t new_max_height = -FLT_MAX;

float remap_height_min = float(p_height_min);
float remap_height_max = float(p_height_max);

real_t *map_data_ptrw = map_data.ptrw();

switch (p_image->get_format()) {
case Image::FORMAT_RF: {
const float *image_data_ptr = (float *)p_image->get_data().ptr();

for (int i = 0; i < map_data.size(); i++) {
float pixel_value = image_data_ptr[i];

DEV_ASSERT(pixel_value >= 0.0 && pixel_value <= 1.0);

real_t height_value = Math::remap(pixel_value, 0.0f, 1.0f, remap_height_min, remap_height_max);

if (height_value < new_min_height) {
new_min_height = height_value;
}
if (height_value > new_max_height) {
new_max_height = height_value;
}

map_data_ptrw[i] = height_value;
}
Comment on lines +217 to +229
Copy link
Member

@Calinou Calinou Apr 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this code can be moved below the switch statement, and have the default branch use return instead. This way, we can avoid repetition in the 2 branches below.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not easily, the way pixel_value is calculated depends on the data type, and it needs to be calculate in the for loop for each index.


} break;

case Image::FORMAT_RH: {
const uint16_t *image_data_ptr = (uint16_t *)p_image->get_data().ptr();

for (int i = 0; i < map_data.size(); i++) {
float pixel_value = Math::half_to_float(image_data_ptr[i]);

DEV_ASSERT(pixel_value >= 0.0 && pixel_value <= 1.0);

real_t height_value = Math::remap(pixel_value, 0.0f, 1.0f, remap_height_min, remap_height_max);

if (height_value < new_min_height) {
new_min_height = height_value;
}
if (height_value > new_max_height) {
new_max_height = height_value;
}

map_data_ptrw[i] = height_value;
}

} break;

case Image::FORMAT_R8: {
const uint8_t *image_data_ptr = (uint8_t *)p_image->get_data().ptr();

for (int i = 0; i < map_data.size(); i++) {
float pixel_value = float(image_data_ptr[i] / 255.0);

DEV_ASSERT(pixel_value >= 0.0 && pixel_value <= 1.0);

real_t height_value = Math::remap(pixel_value, 0.0f, 1.0f, remap_height_min, remap_height_max);

if (height_value < new_min_height) {
new_min_height = height_value;
}
if (height_value > new_max_height) {
new_max_height = height_value;
}

map_data_ptrw[i] = height_value;
}

} break;

default: {
return;
}
}

min_height = new_min_height;
max_height = new_max_height;

_update_shape();
emit_changed();
}

void HeightMapShape3D::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_map_width", "width"), &HeightMapShape3D::set_map_width);
ClassDB::bind_method(D_METHOD("get_map_width"), &HeightMapShape3D::get_map_width);
Expand All @@ -197,6 +296,8 @@ void HeightMapShape3D::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_min_height"), &HeightMapShape3D::get_min_height);
ClassDB::bind_method(D_METHOD("get_max_height"), &HeightMapShape3D::get_max_height);

ClassDB::bind_method(D_METHOD("update_map_data_from_image", "image", "height_min", "height_max"), &HeightMapShape3D::update_map_data_from_image);

ADD_PROPERTY(PropertyInfo(Variant::INT, "map_width", PROPERTY_HINT_RANGE, "0.001,100,0.001,or_greater"), "set_map_width", "get_map_width");
ADD_PROPERTY(PropertyInfo(Variant::INT, "map_depth", PROPERTY_HINT_RANGE, "0.001,100,0.001,or_greater"), "set_map_depth", "get_map_depth");
ADD_PROPERTY(PropertyInfo(Variant::PACKED_FLOAT32_ARRAY, "map_data"), "set_map_data", "get_map_data");
Expand Down
4 changes: 4 additions & 0 deletions scene/resources/3d/height_map_shape_3d.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@

#include "scene/resources/3d/shape_3d.h"

class Image;

class HeightMapShape3D : public Shape3D {
GDCLASS(HeightMapShape3D, Shape3D);

Expand All @@ -57,6 +59,8 @@ class HeightMapShape3D : public Shape3D {
real_t get_min_height() const;
real_t get_max_height() const;

void update_map_data_from_image(const Ref<Image> &p_image, real_t p_height_min, real_t p_height_max);

virtual Vector<Vector3> get_debug_mesh_lines() const override;
virtual real_t get_enclosing_radius() const override;

Expand Down
Loading