Added iso_scale parameter to heightmap generators, which eliminates terracing. As a result, sdf_mode is now useless and was removed.

master
Marc Gilleron 2020-01-26 20:08:25 +00:00
parent 7161942ee1
commit 4626b4d7a3
7 changed files with 79 additions and 360 deletions

View File

@ -3,46 +3,39 @@
#include "../util/fixed_array.h" #include "../util/fixed_array.h"
VoxelStreamHeightmap::VoxelStreamHeightmap() { VoxelStreamHeightmap::VoxelStreamHeightmap() {
_heightmap.settings.range.base = -50.0;
_heightmap.settings.range.span = 200.0;
_heightmap.settings.mode = HeightmapSdf::SDF_VERTICAL_AVERAGE;
} }
void VoxelStreamHeightmap::set_channel(VoxelBuffer::ChannelId channel) { void VoxelStreamHeightmap::set_channel(VoxelBuffer::ChannelId channel) {
ERR_FAIL_INDEX(channel, VoxelBuffer::MAX_CHANNELS); ERR_FAIL_INDEX(channel, VoxelBuffer::MAX_CHANNELS);
_channel = channel; _channel = channel;
if (_channel != VoxelBuffer::CHANNEL_SDF) {
_heightmap.clear_cache();
}
} }
VoxelBuffer::ChannelId VoxelStreamHeightmap::get_channel() const { VoxelBuffer::ChannelId VoxelStreamHeightmap::get_channel() const {
return _channel; return _channel;
} }
void VoxelStreamHeightmap::set_sdf_mode(SdfMode mode) {
ERR_FAIL_INDEX(mode, SDF_MODE_COUNT);
_heightmap.settings.mode = (HeightmapSdf::Mode)mode;
}
VoxelStreamHeightmap::SdfMode VoxelStreamHeightmap::get_sdf_mode() const {
return (VoxelStreamHeightmap::SdfMode)_heightmap.settings.mode;
}
void VoxelStreamHeightmap::set_height_start(float start) { void VoxelStreamHeightmap::set_height_start(float start) {
_heightmap.settings.range.base = start; _range.start = start;
} }
float VoxelStreamHeightmap::get_height_start() const { float VoxelStreamHeightmap::get_height_start() const {
return _heightmap.settings.range.base; return _range.start;
} }
void VoxelStreamHeightmap::set_height_range(float range) { void VoxelStreamHeightmap::set_height_range(float range) {
_heightmap.settings.range.span = range; _range.height = range;
} }
float VoxelStreamHeightmap::get_height_range() const { float VoxelStreamHeightmap::get_height_range() const {
return _heightmap.settings.range.span; return _range.height;
}
void VoxelStreamHeightmap::set_iso_scale(float iso_scale) {
_iso_scale = iso_scale;
}
float VoxelStreamHeightmap::get_iso_scale() const {
return _iso_scale;
} }
void VoxelStreamHeightmap::_bind_methods() { void VoxelStreamHeightmap::_bind_methods() {
@ -50,21 +43,17 @@ void VoxelStreamHeightmap::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_channel", "channel"), &VoxelStreamHeightmap::set_channel); ClassDB::bind_method(D_METHOD("set_channel", "channel"), &VoxelStreamHeightmap::set_channel);
ClassDB::bind_method(D_METHOD("get_channel"), &VoxelStreamHeightmap::get_channel); ClassDB::bind_method(D_METHOD("get_channel"), &VoxelStreamHeightmap::get_channel);
ClassDB::bind_method(D_METHOD("set_sdf_mode", "mode"), &VoxelStreamHeightmap::set_sdf_mode);
ClassDB::bind_method(D_METHOD("get_sdf_mode"), &VoxelStreamHeightmap::get_sdf_mode);
ClassDB::bind_method(D_METHOD("set_height_start", "start"), &VoxelStreamHeightmap::set_height_start); ClassDB::bind_method(D_METHOD("set_height_start", "start"), &VoxelStreamHeightmap::set_height_start);
ClassDB::bind_method(D_METHOD("get_height_start"), &VoxelStreamHeightmap::get_height_start); ClassDB::bind_method(D_METHOD("get_height_start"), &VoxelStreamHeightmap::get_height_start);
ClassDB::bind_method(D_METHOD("set_height_range", "range"), &VoxelStreamHeightmap::set_height_range); ClassDB::bind_method(D_METHOD("set_height_range", "range"), &VoxelStreamHeightmap::set_height_range);
ClassDB::bind_method(D_METHOD("get_height_range"), &VoxelStreamHeightmap::get_height_range); ClassDB::bind_method(D_METHOD("get_height_range"), &VoxelStreamHeightmap::get_height_range);
ClassDB::bind_method(D_METHOD("set_iso_scale", "scale"), &VoxelStreamHeightmap::set_iso_scale);
ClassDB::bind_method(D_METHOD("get_iso_scale"), &VoxelStreamHeightmap::get_iso_scale);
ADD_PROPERTY(PropertyInfo(Variant::INT, "channel", PROPERTY_HINT_ENUM, VoxelBuffer::CHANNEL_ID_HINT_STRING), "set_channel", "get_channel"); ADD_PROPERTY(PropertyInfo(Variant::INT, "channel", PROPERTY_HINT_ENUM, VoxelBuffer::CHANNEL_ID_HINT_STRING), "set_channel", "get_channel");
ADD_PROPERTY(PropertyInfo(Variant::INT, "sdf_mode", PROPERTY_HINT_ENUM, HeightmapSdf::MODE_HINT_STRING), "set_sdf_mode", "get_sdf_mode");
ADD_PROPERTY(PropertyInfo(Variant::REAL, "height_start"), "set_height_start", "get_height_start"); ADD_PROPERTY(PropertyInfo(Variant::REAL, "height_start"), "set_height_start", "get_height_start");
ADD_PROPERTY(PropertyInfo(Variant::REAL, "height_range"), "set_height_range", "get_height_range"); ADD_PROPERTY(PropertyInfo(Variant::REAL, "height_range"), "set_height_range", "get_height_range");
ADD_PROPERTY(PropertyInfo(Variant::REAL, "iso_scale"), "set_iso_scale", "get_iso_scale");
BIND_ENUM_CONSTANT(SDF_VERTICAL);
BIND_ENUM_CONSTANT(SDF_VERTICAL_AVERAGE);
BIND_ENUM_CONSTANT(SDF_SEGMENT);
} }

View File

@ -1,7 +1,6 @@
#ifndef VOXEL_STREAM_HEIGHTMAP_H #ifndef VOXEL_STREAM_HEIGHTMAP_H
#define VOXEL_STREAM_HEIGHTMAP_H #define VOXEL_STREAM_HEIGHTMAP_H
#include "../util/heightmap_sdf.h"
#include "voxel_stream.h" #include "voxel_stream.h"
#include <core/image.h> #include <core/image.h>
@ -10,38 +9,31 @@ class VoxelStreamHeightmap : public VoxelStream {
public: public:
VoxelStreamHeightmap(); VoxelStreamHeightmap();
enum SdfMode {
SDF_VERTICAL = HeightmapSdf::SDF_VERTICAL,
SDF_VERTICAL_AVERAGE = HeightmapSdf::SDF_VERTICAL_AVERAGE,
SDF_SEGMENT = HeightmapSdf::SDF_SEGMENT,
SDF_MODE_COUNT = HeightmapSdf::SDF_MODE_COUNT
};
void set_channel(VoxelBuffer::ChannelId channel); void set_channel(VoxelBuffer::ChannelId channel);
VoxelBuffer::ChannelId get_channel() const; VoxelBuffer::ChannelId get_channel() const;
void set_sdf_mode(SdfMode mode);
SdfMode get_sdf_mode() const;
void set_height_start(float start); void set_height_start(float start);
float get_height_start() const; float get_height_start() const;
void set_height_range(float range); void set_height_range(float range);
float get_height_range() const; float get_height_range() const;
void set_iso_scale(float iso_scale);
float get_iso_scale() const;
protected: protected:
template <typename Height_F> template <typename Height_F>
void generate(VoxelBuffer &out_buffer, Height_F height_func, int ox, int oy, int oz, int lod) { void generate(VoxelBuffer &out_buffer, Height_F height_func, Vector3i origin, int lod) {
const int channel = _channel; const int channel = _channel;
const Vector3i bs = out_buffer.get_size(); const Vector3i bs = out_buffer.get_size();
bool use_sdf = channel == VoxelBuffer::CHANNEL_SDF; bool use_sdf = channel == VoxelBuffer::CHANNEL_SDF;
if (oy > get_height_start() + get_height_range()) { if (origin.y > get_height_start() + get_height_range()) {
// The bottom of the block is above the highest ground can go (default is air) // The bottom of the block is above the highest ground can go (default is air)
return; return;
} }
if (oy + (bs.y << lod) < get_height_start()) { if (origin.y + (bs.y << lod) < get_height_start()) {
// The top of the block is below the lowest ground can go // The top of the block is below the lowest ground can go
out_buffer.clear_channel(_channel, use_sdf ? 0 : _matter_type); out_buffer.clear_channel(_channel, use_sdf ? 0 : _matter_type);
return; return;
@ -51,28 +43,17 @@ protected:
if (use_sdf) { if (use_sdf) {
if (lod == 0) { int gz = origin.z;
// When sampling SDF, we may need to precompute values to speed it up depending on the chosen mode. for (int z = 0; z < bs.z; ++z, gz += stride) {
// Unfortunately, only LOD0 can use a cache. lower lods would require a much larger one,
// otherwise it would interpolate along higher stride, thus voxel values depend on LOD, and then cause discontinuities.
_heightmap.build_cache(height_func, bs.x, bs.z, ox, oz, stride);
}
for (int z = 0; z < bs.z; ++z) { int gx = origin.x;
for (int x = 0; x < bs.x; ++x) { for (int x = 0; x < bs.x; ++x, gx += stride) {
// SDF may vary along the column so we use a helper for more precision float h = _range.xform(height_func(gx, gz));
int gy = origin.y;
if (lod == 0) { for (int y = 0; y < bs.y; ++y, gy += stride) {
_heightmap.get_column_from_cache( float sdf = _iso_scale * (gy - h);
[&out_buffer, x, z, channel](int ly, float v) { out_buffer.set_voxel_f(v, x, ly, z, channel); }, out_buffer.set_voxel_f(sdf, x, y, z, channel);
x, oy, z, bs.y, stride);
} else {
HeightmapSdf::get_column_stateless(
[&out_buffer, x, z, channel](int ly, float v) { out_buffer.set_voxel_f(v, x, ly, z, channel); },
[&height_func, this](int lx, int lz) { return _heightmap.settings.range.xform(height_func(lx, lz)); },
_heightmap.settings.mode,
ox + (x << lod), oy, oz + (z << lod), stride, bs.y);
} }
} // for x } // for x
@ -81,15 +62,15 @@ protected:
} else { } else {
// Blocky // Blocky
int gz = oz; int gz = origin.z;
for (int z = 0; z < bs.z; ++z, gz += stride) { for (int z = 0; z < bs.z; ++z, gz += stride) {
int gx = ox; int gx = origin.x;
for (int x = 0; x < bs.x; ++x, gx += stride) { for (int x = 0; x < bs.x; ++x, gx += stride) {
// Output is blocky, so we can go for just one sample // Output is blocky, so we can go for just one sample
float h = _heightmap.settings.range.xform(height_func(gx, gz)); float h = _range.xform(height_func(gx, gz));
h -= oy; h -= origin.y;
int ih = int(h); int ih = int(h);
if (ih > 0) { if (ih > 0) {
if (ih > bs.y) { if (ih > bs.y) {
@ -106,12 +87,19 @@ protected:
private: private:
static void _bind_methods(); static void _bind_methods();
private: struct Range {
HeightmapSdf _heightmap; float start = -50;
float height = 200;
inline float xform(float x) const {
return x * height + start;
}
};
VoxelBuffer::ChannelId _channel = VoxelBuffer::CHANNEL_TYPE; VoxelBuffer::ChannelId _channel = VoxelBuffer::CHANNEL_TYPE;
int _matter_type = 1; int _matter_type = 1;
Range _range;
float _iso_scale = 0.1;
}; };
VARIANT_ENUM_CAST(VoxelStreamHeightmap::SdfMode)
#endif // VOXEL_STREAM_HEIGHTMAP_H #endif // VOXEL_STREAM_HEIGHTMAP_H

View File

@ -8,6 +8,15 @@ inline float get_height_repeat(Image &im, int x, int y) {
return im.get_pixel(wrap(x, im.get_width()), wrap(y, im.get_height())).r; return im.get_pixel(wrap(x, im.get_width()), wrap(y, im.get_height())).r;
} }
inline float get_height_blurred(Image &im, int x, int y) {
float h = get_height_repeat(im, x, y);
h += get_height_repeat(im, x + 1, y);
h += get_height_repeat(im, x - 1, y);
h += get_height_repeat(im, x, y + 1);
h += get_height_repeat(im, x, y - 1);
return h * 0.2f;
}
} // namespace } // namespace
VoxelStreamImage::VoxelStreamImage() { VoxelStreamImage::VoxelStreamImage() {
@ -21,6 +30,14 @@ Ref<Image> VoxelStreamImage::get_image() const {
return _image; return _image;
} }
void VoxelStreamImage::set_blur_enabled(bool enable) {
_blur_enabled = enable;
}
bool VoxelStreamImage::is_blur_enabled() const {
return _blur_enabled;
}
void VoxelStreamImage::emerge_block(Ref<VoxelBuffer> p_out_buffer, Vector3i origin_in_voxels, int lod) { void VoxelStreamImage::emerge_block(Ref<VoxelBuffer> p_out_buffer, Vector3i origin_in_voxels, int lod) {
ERR_FAIL_COND(_image.is_null()); ERR_FAIL_COND(_image.is_null());
@ -30,9 +47,15 @@ void VoxelStreamImage::emerge_block(Ref<VoxelBuffer> p_out_buffer, Vector3i orig
image.lock(); image.lock();
VoxelStreamHeightmap::generate(out_buffer, if (_blur_enabled) {
[&image](int x, int z) { return get_height_repeat(image, x, z); }, VoxelStreamHeightmap::generate(out_buffer,
origin_in_voxels.x, origin_in_voxels.y, origin_in_voxels.z, lod); [&image](int x, int z) { return get_height_blurred(image, x, z); },
origin_in_voxels, lod);
} else {
VoxelStreamHeightmap::generate(out_buffer,
[&image](int x, int z) { return get_height_repeat(image, x, z); },
origin_in_voxels, lod);
}
image.unlock(); image.unlock();

View File

@ -13,6 +13,9 @@ public:
void set_image(Ref<Image> im); void set_image(Ref<Image> im);
Ref<Image> get_image() const; Ref<Image> get_image() const;
void set_blur_enabled(bool enable);
bool is_blur_enabled() const;
void emerge_block(Ref<VoxelBuffer> p_out_buffer, Vector3i origin_in_voxels, int lod); void emerge_block(Ref<VoxelBuffer> p_out_buffer, Vector3i origin_in_voxels, int lod);
private: private:
@ -20,6 +23,8 @@ private:
private: private:
Ref<Image> _image; Ref<Image> _image;
// Mostly here as demo/tweak. It's better recommended to use an EXR/float image.
bool _blur_enabled = false;
}; };
#endif // HEADER_VOXEL_STREAM_IMAGE #endif // HEADER_VOXEL_STREAM_IMAGE

View File

@ -29,12 +29,12 @@ void VoxelStreamNoise2D::emerge_block(Ref<VoxelBuffer> p_out_buffer, Vector3i or
if (_curve.is_null()) { if (_curve.is_null()) {
VoxelStreamHeightmap::generate(out_buffer, VoxelStreamHeightmap::generate(out_buffer,
[&noise](int x, int z) { return 0.5 + 0.5 * noise.get_noise_2d(x, z); }, [&noise](int x, int z) { return 0.5 + 0.5 * noise.get_noise_2d(x, z); },
origin_in_voxels.x, origin_in_voxels.y, origin_in_voxels.z, lod); origin_in_voxels, lod);
} else { } else {
Curve &curve = **_curve; Curve &curve = **_curve;
VoxelStreamHeightmap::generate(out_buffer, VoxelStreamHeightmap::generate(out_buffer,
[&noise, &curve](int x, int z) { return curve.interpolate_baked(0.5 + 0.5 * noise.get_noise_2d(x, z)); }, [&noise, &curve](int x, int z) { return curve.interpolate_baked(0.5 + 0.5 * noise.get_noise_2d(x, z)); },
origin_in_voxels.x, origin_in_voxels.y, origin_in_voxels.z, lod); origin_in_voxels, lod);
} }
out_buffer.compress_uniform_channels(); out_buffer.compress_uniform_channels();

View File

@ -1,42 +0,0 @@
#include "heightmap_sdf.h"
const char *HeightmapSdf::MODE_HINT_STRING = "Vertical,VerticalAverage,Segment";
float HeightmapSdf::get_constrained_segment_sdf(float p_yp, float p_ya, float p_yb, float p_xb) {
// P
// . B
// . /
// . / y
// ./ |
// A o--x
float s = p_yp >= p_ya ? 1 : -1;
if (Math::absf(p_yp - p_ya) > 1.f && Math::absf(p_yp - p_yb) > 1.f) {
return s;
}
Vector2 p(0, p_yp);
Vector2 a(0, p_ya);
Vector2 b(p_xb, p_yb);
Vector2 closest_point;
// TODO Optimize given the particular case we are in
Vector2 n = b - a;
real_t l2 = n.length_squared();
if (l2 < 1e-20) {
closest_point = a; // Both points are the same, just give any.
} else {
real_t d = n.dot(p - a) / l2;
if (d <= 0.0) {
closest_point = a; // Before first point.
} else if (d >= 1.0) {
closest_point = b; // After first point.
} else {
closest_point = a + n * d; // Inside.
}
}
return s * closest_point.distance_to(p);
}

View File

@ -1,244 +0,0 @@
#ifndef HEIGHTMAP_SDF_H
#define HEIGHTMAP_SDF_H
#include "array_slice.h"
#include <core/math/vector2.h>
#include <vector>
// Utility class to sample a heightmap as a 3D distance field.
// Provides a stateless function, or an accelerated area method using a cache.
// Note: this isn't general-purpose, it has been made for several use cases found in this module.
class HeightmapSdf {
public:
enum Mode {
SDF_VERTICAL = 0, // Lowest quality, fastest
SDF_VERTICAL_AVERAGE,
SDF_SEGMENT,
SDF_MODE_COUNT
};
static const char *MODE_HINT_STRING;
struct Range {
float base = -50;
float span = 200;
inline float xform(float x) const {
return x * span + base;
}
};
struct Settings {
Mode mode = SDF_VERTICAL;
Range range;
};
struct Cache {
std::vector<float> heights;
int size_z = 0;
inline float get_local(int x, int z) const {
const int i = x + z * size_z;
#ifdef TOOLS_ENABLED
CRASH_COND(i >= heights.size());
#endif
return heights[i];
}
};
Settings settings;
// Precomputes data to accelerate the next area fetch.
// ox, oz and stride are in world space.
// Coordinates sent to the height function are in world space.
template <typename Height_F>
void build_cache(Height_F height_func, int cache_size_x, int cache_size_z, int ox, int oz, int stride) {
CRASH_COND(cache_size_x < 0);
CRASH_COND(cache_size_z < 0);
if (settings.mode == SDF_SEGMENT) {
// Pad
cache_size_x += 2;
cache_size_z += 2;
ox -= stride;
oz -= stride;
}
unsigned int area = cache_size_x * cache_size_z;
if (area != _cache.heights.size()) {
_cache.heights.resize(area);
}
_cache.size_z = cache_size_z;
int i = 0;
int gz = oz;
for (int z = 0; z < cache_size_z; ++z, gz += stride) {
int gx = ox;
for (int x = 0; x < cache_size_x; ++x, gx += stride) {
switch (settings.mode) {
case SDF_VERTICAL:
case SDF_SEGMENT:
_cache.heights[i++] = settings.range.xform(height_func(gx, gz));
break;
case SDF_VERTICAL_AVERAGE:
_cache.heights[i++] = settings.range.xform(get_height_blurred(height_func, gx, gz));
break;
default:
CRASH_NOW();
break;
}
}
}
}
void clear_cache() {
_cache.heights.clear();
}
// Core functionality is here.
// Slower than using a cache, but doesn't rely on heap memory.
// fx and fz use the same coordinate space as the height function.
// gy0 and stride are world space.
// Coordinates sent to the output function are in grid space.
template <typename Height_F, typename Output_F>
static void get_column_stateless(Output_F output_func, Height_F height_func, Mode mode, int fx, int gy0, int fz, int stride, int size_y) {
switch (mode) {
case SDF_VERTICAL: {
float h = height_func(fx, fz);
int gy = gy0;
for (int y = 0; y < size_y; ++y, gy += stride) {
float sdf = gy - h;
output_func(y, sdf);
}
} break;
case SDF_VERTICAL_AVERAGE: {
float h = get_height_blurred(height_func, fx, fz);
int gy = gy0;
for (int y = 0; y < size_y; ++y, gy += stride) {
float sdf = gy - h;
output_func(y, sdf);
}
} break;
case SDF_SEGMENT: {
// Calculate distance to 8 segments going from the point at XZ to its neighbor points,
// and pick the smallest distance.
// Note: stride is intentionally not used for neighbor sampling.
// More than 1 isn't really supported, because it causes inconsistencies when nearest-neighbor LOD is used.
float h0 = height_func(fx - 1, fz - 1);
float h1 = height_func(fx, fz - 1);
float h2 = height_func(fx + 1, fz - 1);
float h3 = height_func(fx - 1, fz);
float h4 = height_func(fx, fz);
float h5 = height_func(fx + 1, fz);
float h6 = height_func(fx - 1, fz + 1);
float h7 = height_func(fx, fz + 1);
float h8 = height_func(fx + 1, fz + 1);
const float sqrt2 = 1.414213562373095;
int gy = gy0;
for (int y = 0; y < size_y; ++y, gy += stride) {
float sdf0 = get_constrained_segment_sdf(gy, h4, h0, sqrt2);
float sdf1 = get_constrained_segment_sdf(gy, h4, h1, 1);
float sdf2 = get_constrained_segment_sdf(gy, h4, h2, sqrt2);
float sdf3 = get_constrained_segment_sdf(gy, h4, h3, 1);
float sdf4 = gy - h4;
float sdf5 = get_constrained_segment_sdf(gy, h4, h5, 1);
float sdf6 = get_constrained_segment_sdf(gy, h4, h6, sqrt2);
float sdf7 = get_constrained_segment_sdf(gy, h4, h7, 1);
float sdf8 = get_constrained_segment_sdf(gy, h4, h8, sqrt2);
float sdf = sdf4;
if (Math::absf(sdf0) < Math::absf(sdf)) {
sdf = sdf0;
}
if (Math::absf(sdf1) < Math::absf(sdf)) {
sdf = sdf1;
}
if (Math::absf(sdf2) < Math::absf(sdf)) {
sdf = sdf2;
}
if (Math::absf(sdf3) < Math::absf(sdf)) {
sdf = sdf3;
}
if (Math::absf(sdf5) < Math::absf(sdf)) {
sdf = sdf5;
}
if (Math::absf(sdf6) < Math::absf(sdf)) {
sdf = sdf6;
}
if (Math::absf(sdf7) < Math::absf(sdf)) {
sdf = sdf7;
}
if (Math::absf(sdf8) < Math::absf(sdf)) {
sdf = sdf8;
}
output_func(y, sdf);
}
} break;
default:
CRASH_NOW();
break;
} // sdf mode
}
// Fastest if a cache has been built before. Prefer this when fetching areas.
// Coordinates sent to the output function are in grid space.
template <typename Output_F>
inline void get_column_from_cache(Output_F output_func, int grid_x, int world_y0, int grid_z, int grid_size_y, int stride) {
Mode mode = settings.mode;
if (mode == SDF_VERTICAL_AVERAGE) {
// Precomputed in cache, sample directly
mode = SDF_VERTICAL;
} else if (mode == SDF_SEGMENT) {
// Pad
++grid_x;
++grid_z;
}
get_column_stateless(output_func,
[&](int x, int z) { return _cache.get_local(x, z); },
mode, grid_x, world_y0, grid_z, stride, grid_size_y);
}
private:
static float get_constrained_segment_sdf(float p_yp, float p_ya, float p_yb, float p_xb);
template <typename Height_F>
static inline float get_height_blurred(Height_F height_func, int x, int y) {
float h = height_func(x, y);
h += height_func(x + 1, y);
h += height_func(x - 1, y);
h += height_func(x, y + 1);
h += height_func(x, y - 1);
return h * 0.2f;
}
Cache _cache;
};
#endif // HEIGHTMAP_SDF_H