added terrain3d
This commit is contained in:
67
addons/terrain_3d/extras/hex_grid.gdshaderinc
Normal file
67
addons/terrain_3d/extras/hex_grid.gdshaderinc
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
|
||||
// This shader snippet draws a hex grid
|
||||
|
||||
// To use it, add this line to the top of your shader:
|
||||
// #include "res://addons/terrain_3d/extras/hex_grid.gdshaderinc"
|
||||
|
||||
// And this line at the bottom of your shader:
|
||||
// draw_hex_grid(uv2, _region_texel_size, w_normal, ALBEDO);
|
||||
|
||||
mat2 rotate2d(float _angle) {
|
||||
return mat2(vec2(cos(_angle),-sin(_angle)), vec2(sin(_angle), cos(_angle)));
|
||||
}
|
||||
|
||||
void draw_hex_grid(vec2 uv, float texel_size, vec3 normal, inout vec3 albedo) {
|
||||
float hex_size = 0.02;
|
||||
float line_thickness = 0.04;
|
||||
|
||||
vec2 guv = (uv - vec2(0.5 * texel_size)) / hex_size;
|
||||
|
||||
// Convert UV to axial hex coordinates
|
||||
float q = (sqrt(3.0) / 3.0 * guv.x - 1.0 / 3.0 * guv.y);
|
||||
float r = (2.0 / 3.0 * guv.y);
|
||||
|
||||
// Cube coordinates for the hex (q, r, -q-r)
|
||||
float x = q;
|
||||
float z = r;
|
||||
float y = -x - z;
|
||||
|
||||
// Round to the nearest hex center
|
||||
vec3 rounded = round(vec3(x, y, z));
|
||||
vec3 diff = abs(vec3(x, y, z) - rounded);
|
||||
|
||||
// Fix rounding errors
|
||||
if (diff.x > diff.y && diff.x > diff.z) {
|
||||
rounded.x = -rounded.y - rounded.z;
|
||||
} else if (diff.y > diff.z) {
|
||||
rounded.y = -rounded.x - rounded.z;
|
||||
} else {
|
||||
rounded.z = -rounded.x - rounded.y;
|
||||
}
|
||||
|
||||
// Find the hex center in UV space
|
||||
vec2 hex_center = vec2(
|
||||
sqrt(3.0) * rounded.x + sqrt(3.0) / 2.0 * rounded.z,
|
||||
3.0 / 2.0 * rounded.z
|
||||
);
|
||||
|
||||
// Relative position within the hex
|
||||
vec2 local_pos = guv - hex_center;
|
||||
vec2 lines_uv = local_pos;
|
||||
float line = 1.0;
|
||||
|
||||
for (int i = 0; i < 6; i++) {
|
||||
vec2 luv = lines_uv * rotate2d(radians(60.0 * float(i) + 30.0));
|
||||
float dist = abs(dot(luv + vec2(0.90), vec2(0.0, 1.0)));
|
||||
line = min(line, dist);
|
||||
}
|
||||
|
||||
// Filter lines by slope
|
||||
float slope = 4.; // Can also assign to (auto_slope * 4.) to match grass placement
|
||||
float slope_factor = clamp(dot(vec3(0., 1., 0.), slope * (normal - 1.) + 1.), 0., 1.);
|
||||
|
||||
// Draw hex grid
|
||||
albedo = mix(albedo, vec3(1.0), smoothstep(line_thickness + 0.02, line_thickness, line) * slope_factor);
|
||||
// Draw Hex center dot
|
||||
albedo = mix(albedo, vec3(0.0, 0.5, 0.5), smoothstep(0.11, 0.10, length(local_pos)) * slope_factor);
|
||||
}
|
||||
1
addons/terrain_3d/extras/hex_grid.gdshaderinc.uid
Normal file
1
addons/terrain_3d/extras/hex_grid.gdshaderinc.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://mri8pfoj2mfk
|
||||
@@ -1,4 +1,5 @@
|
||||
## Import From SimpleGrassTextured
|
||||
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
|
||||
# Import From SimpleGrassTextured
|
||||
#
|
||||
# This script demonstrates how to import transforms from SimpleGrassTextured. To use it:
|
||||
#
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://bm8g6gkvrerqy
|
||||
uid://bllcuwetve45k
|
||||
|
||||
400
addons/terrain_3d/extras/lightweight.gdshader
Normal file
400
addons/terrain_3d/extras/lightweight.gdshader
Normal file
@@ -0,0 +1,400 @@
|
||||
shader_type spatial;
|
||||
render_mode blend_mix,depth_draw_opaque,cull_back,diffuse_burley,specular_schlick_ggx,skip_vertex_transform;
|
||||
|
||||
/* This is an example stripped down shader with maximum performance in mind.
|
||||
* Only Autoshader/Base/Over/Blend/Holes/Colormap are supported.
|
||||
* All terrain normal calculations take place in vetex() as well as control map reads
|
||||
* for the bilinear blend, when not skippable have moved to vertex() too.
|
||||
*
|
||||
* A single controlmap lookup in fragment is added at distances where the vertices spread too wide.
|
||||
*/
|
||||
|
||||
// Defined Constants
|
||||
#define SKIP_PASS 0
|
||||
#define VERTEX_PASS 1
|
||||
#define FRAGMENT_PASS 2
|
||||
|
||||
#if CURRENT_RENDERER == RENDERER_COMPATIBILITY
|
||||
#define fma(a, b, c) ((a) * (b) + (c))
|
||||
#define dFdxCoarse(a) dFdx(a)
|
||||
#define dFdyCoarse(a) dFdy(a)
|
||||
#endif
|
||||
|
||||
// Private uniforms
|
||||
uniform vec3 _camera_pos = vec3(0.f);
|
||||
uniform float _mesh_size = 48.f;
|
||||
uniform uint _background_mode = 1u; // NONE = 0, FLAT = 1, NOISE = 2
|
||||
uniform uint _mouse_layer = 0x80000000u; // Layer 32
|
||||
uniform float _vertex_spacing = 1.0;
|
||||
uniform float _vertex_density = 1.0; // = 1/_vertex_spacing
|
||||
uniform float _region_size = 1024.0;
|
||||
uniform float _region_texel_size = 0.0009765625; // = 1/1024
|
||||
uniform int _region_map_size = 32;
|
||||
uniform int _region_map[1024];
|
||||
uniform vec2 _region_locations[1024];
|
||||
uniform float _texture_normal_depth_array[32];
|
||||
uniform float _texture_ao_strength_array[32];
|
||||
uniform float _texture_roughness_mod_array[32];
|
||||
uniform float _texture_uv_scale_array[32];
|
||||
uniform vec4 _texture_color_array[32];
|
||||
uniform highp sampler2DArray _height_maps : repeat_disable;
|
||||
uniform highp sampler2DArray _control_maps : repeat_disable;
|
||||
uniform highp sampler2DArray _color_maps : source_color, filter_linear_mipmap, repeat_disable;
|
||||
uniform highp sampler2DArray _texture_array_albedo : source_color, filter_linear_mipmap, repeat_enable;
|
||||
uniform highp sampler2DArray _texture_array_normal : hint_normal, filter_linear_mipmap, repeat_enable;
|
||||
|
||||
|
||||
// Public uniforms
|
||||
uniform float auto_slope : hint_range(0, 10) = 1.0;
|
||||
uniform float auto_height_reduction : hint_range(0, 1) = 0.1;
|
||||
uniform int auto_base_texture : hint_range(0, 31) = 0;
|
||||
uniform int auto_overlay_texture : hint_range(0, 31) = 1;
|
||||
|
||||
uniform bool height_blending = true;
|
||||
uniform bool world_space_normal_blend = true;
|
||||
uniform float blend_sharpness : hint_range(0, 1) = 0.87;
|
||||
|
||||
// Varyings & Types
|
||||
|
||||
struct Material {
|
||||
vec4 alb_ht;
|
||||
vec4 nrm_rg;
|
||||
int base;
|
||||
int over;
|
||||
float blend;
|
||||
float nrm_depth;
|
||||
float ao_str;
|
||||
};
|
||||
|
||||
|
||||
varying vec3 v_vertex;
|
||||
varying vec3 v_normal;
|
||||
varying flat uint v_control[4];
|
||||
varying flat int v_lerp;
|
||||
varying mat3 v_tbn;
|
||||
|
||||
////////////////////////
|
||||
// Vertex
|
||||
////////////////////////
|
||||
|
||||
// Takes in world space XZ (UV) coordinates & search depth (only applicable for background mode none)
|
||||
// Returns ivec3 with:
|
||||
// XY: (0 to _region_size - 1) coordinates within a region
|
||||
// Z: layer index used for texturearrays, -1 if not in a region
|
||||
ivec3 get_index_coord(const vec2 uv, const int search) {
|
||||
vec2 r_uv = round(uv);
|
||||
vec2 o_uv = mod(r_uv,_region_size);
|
||||
ivec2 pos;
|
||||
int bounds, layer_index = -1;
|
||||
for (int i = -1; i < clamp(search, SKIP_PASS, FRAGMENT_PASS); i++) {
|
||||
if ((layer_index == -1 && _background_mode == 0u ) || i < 0) {
|
||||
r_uv -= i == -1 ? vec2(0.0) : vec2(float(o_uv.x <= o_uv.y), float(o_uv.y <= o_uv.x));
|
||||
pos = ivec2(floor((r_uv) * _region_texel_size)) + (_region_map_size / 2);
|
||||
bounds = int(uint(pos.x | pos.y) < uint(_region_map_size));
|
||||
layer_index = (_region_map[ pos.y * _region_map_size + pos.x ] * bounds - 1);
|
||||
}
|
||||
}
|
||||
return ivec3(ivec2(mod(r_uv,_region_size)), layer_index);
|
||||
}
|
||||
|
||||
// Takes in descaled (world_space / region_size) world to region space XZ (UV2) coordinates, returns vec3 with:
|
||||
// XY: (0. to 1.) coordinates within a region
|
||||
// Z: layer index used for texturearrays, -1 if not in a region
|
||||
vec3 get_index_uv(const vec2 uv2) {
|
||||
ivec2 pos = ivec2(floor(uv2)) + (_region_map_size / 2);
|
||||
int bounds = int(uint(pos.x | pos.y) < uint(_region_map_size));
|
||||
int layer_index = _region_map[ pos.y * _region_map_size + pos.x ] * bounds - 1;
|
||||
return vec3(uv2 - _region_locations[layer_index], float(layer_index));
|
||||
}
|
||||
|
||||
void vertex() {
|
||||
// Get vertex of flat plane in world coordinates and set world UV
|
||||
v_vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||
|
||||
// Camera distance to vertex on flat plane
|
||||
float v_vertex_xz_dist = length(v_vertex.xz - _camera_pos.xz);
|
||||
|
||||
// Geomorph vertex, set end and start for linear height interpolate
|
||||
float scale = MODEL_MATRIX[0][0];
|
||||
float vertex_lerp = smoothstep(0.55, 0.95, (v_vertex_xz_dist / scale - _mesh_size - 4.0) / (_mesh_size - 2.0));
|
||||
vec2 v_fract = fract(VERTEX.xz * 0.5) * 2.0;
|
||||
// For LOD0 morph from a regular grid to an alternating grid to align with LOD1+
|
||||
vec2 shift = (scale < _vertex_spacing + 1e-6) ? // LOD0 or not
|
||||
// Shift from regular to symetric
|
||||
mix(v_fract, vec2(v_fract.x, -v_fract.y),
|
||||
round(fract(round(mod(v_vertex.z * _vertex_density, 4.0)) *
|
||||
round(mod(v_vertex.x * _vertex_density, 4.0)) * 0.25))
|
||||
) :
|
||||
// Symetric shift
|
||||
v_fract * round((fract(v_vertex.xz * 0.25 / scale) - 0.5) * 4.0);
|
||||
vec2 start_pos = v_vertex.xz * _vertex_density;
|
||||
vec2 end_pos = (v_vertex.xz - shift * scale) * _vertex_density;
|
||||
v_vertex.xz -= shift * scale * vertex_lerp;
|
||||
|
||||
// UV coordinates in world space. Values are 0 to _region_size within regions
|
||||
UV = v_vertex.xz * _vertex_density;
|
||||
|
||||
// UV coordinates in region space + texel offset. Values are 0 to 1 within regions
|
||||
UV2 = fma(UV, vec2(_region_texel_size), vec2(0.5 * _region_texel_size));
|
||||
|
||||
const vec3 offsets = vec3(0, 1, 2);
|
||||
ivec3 indexUV[4];
|
||||
// control map lookups in vertex, used for bilinear blend in fragment.
|
||||
indexUV[0] = get_index_coord(start_pos + offsets.xy, VERTEX_PASS);
|
||||
indexUV[1] = get_index_coord(start_pos + offsets.yy, VERTEX_PASS);
|
||||
indexUV[2] = get_index_coord(start_pos + offsets.yx, VERTEX_PASS);
|
||||
indexUV[3] = get_index_coord(start_pos + offsets.xx, VERTEX_PASS);
|
||||
// Mask off Scale/Rotation/Navigation bits to 0, as they are not used.
|
||||
#define CONTROL_MASK 0xFFFFC07Du
|
||||
v_control[0] = floatBitsToUint(texelFetch(_control_maps, indexUV[0], 0)).r & CONTROL_MASK;
|
||||
v_control[1] = floatBitsToUint(texelFetch(_control_maps, indexUV[1], 0)).r & CONTROL_MASK;
|
||||
v_control[2] = floatBitsToUint(texelFetch(_control_maps, indexUV[2], 0)).r & CONTROL_MASK;
|
||||
v_control[3] = floatBitsToUint(texelFetch(_control_maps, indexUV[3], 0)).r & CONTROL_MASK;
|
||||
bool full_auto = !bool((v_control[0] & v_control[1] & v_control[2] & v_control[3]) & 0x1u);
|
||||
bool identical = !(
|
||||
(v_control[0] == v_control[1]) &&
|
||||
(v_control[1] == v_control[2]) &&
|
||||
(v_control[2] == v_control[3]));
|
||||
// Verticies are close enough, full auto shader, or all 4 indicies match, skip bilinear blend in fragment.
|
||||
v_lerp = scale < _vertex_spacing + 1e-3 && vertex_lerp < 1e-3 && (full_auto || identical) ? 1 : 0;
|
||||
|
||||
// Discard vertices for Holes. 1 lookup
|
||||
bool hole = bool(v_control[3] >>2u & 0x1u);
|
||||
|
||||
// Show holes to all cameras except mouse camera (on exactly 1 layer)
|
||||
if ( !(CAMERA_VISIBLE_LAYERS == _mouse_layer) &&
|
||||
(hole || (_background_mode == 0u && indexUV[3].z == -1))) {
|
||||
v_vertex.x = 0. / 0.;
|
||||
} else {
|
||||
// Set final vertex height & calculate vertex normals. 3 lookups
|
||||
ivec3 uv_a = get_index_coord(start_pos, VERTEX_PASS);
|
||||
ivec3 uv_b = get_index_coord(end_pos, VERTEX_PASS);
|
||||
float h = mix(texelFetch(_height_maps, uv_a, 0).r,texelFetch(_height_maps, uv_b, 0).r,vertex_lerp);
|
||||
float u = mix(texelFetch(_height_maps, get_index_coord(start_pos + vec2(1,0), VERTEX_PASS), 0).r,
|
||||
texelFetch(_height_maps, get_index_coord(end_pos + vec2(1,0), VERTEX_PASS), 0).r, vertex_lerp);
|
||||
float v = mix(texelFetch(_height_maps, get_index_coord(start_pos + vec2(0,1), VERTEX_PASS), 0).r,
|
||||
texelFetch(_height_maps, get_index_coord(end_pos + vec2(0,1), VERTEX_PASS), 0).r, vertex_lerp);
|
||||
v_vertex.y = h;
|
||||
v_normal = vec3(h - u, _vertex_spacing, h - v);
|
||||
}
|
||||
|
||||
// Convert model space to view space w/ skip_vertex_transform render mode
|
||||
VERTEX = (VIEW_MATRIX * vec4(v_vertex, 1.0)).xyz;
|
||||
|
||||
// Apply terrain normals
|
||||
vec3 w_normal = normalize(v_normal);
|
||||
vec3 w_tangent = normalize(cross(w_normal, vec3(0.0, 0.0, 1.0)));
|
||||
vec3 w_binormal = normalize(cross(w_normal, w_tangent));
|
||||
|
||||
v_tbn = mat3(w_tangent, w_normal, w_binormal);
|
||||
|
||||
NORMAL = normalize((VIEW_MATRIX * vec4(w_normal, 0.0)).xyz);
|
||||
BINORMAL = normalize((VIEW_MATRIX * vec4(w_binormal, 0.0)).xyz);
|
||||
TANGENT = normalize((VIEW_MATRIX * vec4(w_tangent, 0.0)).xyz);
|
||||
}
|
||||
|
||||
////////////////////////
|
||||
// Fragment
|
||||
////////////////////////
|
||||
|
||||
vec3 unpack_normal(vec4 rgba) {
|
||||
return fma(rgba.xzy, vec3(2.0), vec3(-1.0));
|
||||
}
|
||||
|
||||
vec3 pack_normal(vec3 n) {
|
||||
return fma(normalize(n.xzy), vec3(0.5), vec3(0.5));
|
||||
}
|
||||
|
||||
vec4 height_blend4(vec4 a_value, float a_height, vec4 b_value, float b_height, float blend) {
|
||||
if(height_blending) {
|
||||
float ma = max(a_height + (1.0 - blend), b_height + blend) - (1.001 - blend_sharpness);
|
||||
float b1 = max(a_height + (1.0 - blend) - ma, 0.0);
|
||||
float b2 = max(b_height + blend - ma, 0.0);
|
||||
return (a_value * b1 + b_value * b2) / (b1 + b2);
|
||||
} else {
|
||||
float contrast = 1.0 - blend_sharpness;
|
||||
float factor = (blend - contrast) / contrast;
|
||||
return mix(a_value, b_value, clamp(factor, 0.0, 1.0));
|
||||
}
|
||||
}
|
||||
|
||||
float height_blend1(float a_value, float a_height, float b_value, float b_height, float blend) {
|
||||
if(height_blending) {
|
||||
float ma = max(a_height + (1.0 - blend), b_height + blend) - (1.001 - blend_sharpness);
|
||||
float b1 = max(a_height + (1.0 - blend) - ma, 0.0);
|
||||
float b2 = max(b_height + blend - ma, 0.0);
|
||||
return (a_value * b1 + b_value * b2) / (b1 + b2);
|
||||
} else {
|
||||
float contrast = 1.0 - blend_sharpness;
|
||||
float factor = (blend - contrast) / contrast;
|
||||
return mix(a_value, b_value, clamp(factor, 0.0, 1.0));
|
||||
}
|
||||
}
|
||||
|
||||
// 2-4 lookups ( 2-6 with dual scaling )
|
||||
void get_material(vec4 ddxy, uint control, vec3 iuv_center, out Material out_mat) {
|
||||
out_mat = Material(vec4(0.), vec4(0.), 0, 0, 0.0, 0.0, 0.0);
|
||||
int region = int(iuv_center.z);
|
||||
vec2 base_uv = v_vertex.xz * 0.5;
|
||||
ddxy *= 0.5;
|
||||
|
||||
// Enable Autoshader if outside regions or painted in regions, otherwise manual painted
|
||||
bool auto_shader = region < 0 || bool(control & 0x1u);
|
||||
out_mat.base = int(auto_shader) * auto_base_texture + int(!auto_shader) * int(control >>27u & 0x1Fu);
|
||||
out_mat.over = int(auto_shader) * auto_overlay_texture + int(!auto_shader) * int(control >> 22u & 0x1Fu);
|
||||
out_mat.blend = float(auto_shader) * clamp(
|
||||
(auto_slope * 2. * ( v_tbn[1].y - 1.) + 1.)
|
||||
- auto_height_reduction * .01 * v_vertex.y // Reduce as vertices get higher
|
||||
, 0., 1.) +
|
||||
float(!auto_shader) * float(control >>14u & 0xFFu) * 0.003921568627450; // 1./255.0
|
||||
|
||||
out_mat.nrm_depth = _texture_normal_depth_array[out_mat.base];
|
||||
out_mat.ao_str = _texture_ao_strength_array[out_mat.base];
|
||||
|
||||
vec2 matUV = base_uv;
|
||||
vec4 albedo_ht = vec4(0.);
|
||||
vec4 normal_rg = vec4(0.5, 0.5, 1.0, 1.0);
|
||||
vec4 albedo_far = vec4(0.);
|
||||
vec4 normal_far = vec4(0.5, 0.5, 1.0, 1.0);
|
||||
float mat_scale = _texture_uv_scale_array[out_mat.base];
|
||||
vec4 base_dd = ddxy;
|
||||
|
||||
if (out_mat.blend < 1.0) {
|
||||
// 2 lookups
|
||||
//each time we change scale, recalculate antitiling from baseline to maintain continuity.
|
||||
matUV = base_uv * mat_scale;
|
||||
base_dd *= mat_scale;
|
||||
albedo_ht = textureGrad(_texture_array_albedo, vec3(matUV, float(out_mat.base)), base_dd.xy, base_dd.zw);
|
||||
normal_rg = textureGrad(_texture_array_normal, vec3(matUV, float(out_mat.base)), base_dd.xy, base_dd.zw);
|
||||
|
||||
// Unpack & rotate base normal for blending
|
||||
normal_rg.xyz = unpack_normal(normal_rg);
|
||||
}
|
||||
// Apply color to base
|
||||
albedo_ht.rgb *= _texture_color_array[out_mat.base].rgb;
|
||||
|
||||
// Apply Roughness modifier to base
|
||||
normal_rg.a = clamp(normal_rg.a + _texture_roughness_mod_array[out_mat.base], 0., 1.);
|
||||
|
||||
out_mat.alb_ht = albedo_ht;
|
||||
out_mat.nrm_rg = normal_rg;
|
||||
|
||||
if (out_mat.blend > 0.) {
|
||||
// 2 lookups
|
||||
// Setup overlay texture to blend
|
||||
float mat_scale2 = _texture_uv_scale_array[out_mat.over];
|
||||
vec2 matUV2 = base_uv * mat_scale2;
|
||||
vec4 over_dd = ddxy * mat_scale2;
|
||||
vec4 albedo_ht2 = textureGrad(_texture_array_albedo, vec3(matUV2, float(out_mat.over)), over_dd.xy, over_dd.zw);
|
||||
vec4 normal_rg2 = textureGrad(_texture_array_normal, vec3(matUV2, float(out_mat.over)), over_dd.xy, over_dd.zw);
|
||||
|
||||
// Unpack & rotate overlay normal for blending
|
||||
normal_rg2.xyz = unpack_normal(normal_rg2);
|
||||
|
||||
// Apply color to overlay
|
||||
albedo_ht2.rgb *= _texture_color_array[out_mat.over].rgb;
|
||||
|
||||
// Apply Roughness modifier to overlay
|
||||
normal_rg2.a = clamp(normal_rg2.a + _texture_roughness_mod_array[out_mat.over], 0., 1.);
|
||||
|
||||
// apply world space normal weighting from base, to overlay layer
|
||||
// Its a matrix Mult, but the value is rather high, so not cutting this one.
|
||||
if (world_space_normal_blend) {
|
||||
albedo_ht2.a *= bool(control >>3u & 0x1u) ? 1.0 : clamp((v_tbn * normal_rg.xyz).y, 0.0, 1.0);
|
||||
}
|
||||
|
||||
// Blend overlay and base
|
||||
out_mat.alb_ht = height_blend4(albedo_ht, albedo_ht.a, albedo_ht2, albedo_ht2.a, out_mat.blend);
|
||||
out_mat.nrm_rg = height_blend4(normal_rg, albedo_ht.a, normal_rg2, albedo_ht2.a, out_mat.blend);
|
||||
out_mat.nrm_depth = height_blend1(_texture_normal_depth_array[out_mat.base], albedo_ht.a,
|
||||
_texture_normal_depth_array[out_mat.over], albedo_ht2.a, out_mat.blend);
|
||||
out_mat.ao_str = height_blend1(_texture_ao_strength_array[out_mat.base], albedo_ht.a,
|
||||
_texture_ao_strength_array[out_mat.over], albedo_ht2.a, out_mat.blend);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
// Recover UVs
|
||||
vec2 uv = UV;
|
||||
vec2 uv2 = UV2;
|
||||
|
||||
vec3 base_ddx = dFdxCoarse(v_vertex);
|
||||
vec3 base_ddy = dFdyCoarse(v_vertex);
|
||||
vec4 base_derivatives = vec4(base_ddx.xz, base_ddy.xz);
|
||||
float region_mip = log2(max(length(base_ddx.xz), length(base_ddy.xz)) * _vertex_density);
|
||||
|
||||
// Colormap. 1 lookup
|
||||
// For speed sake, we'll live with cross region artifacts.
|
||||
#define COLOR_MAP vec4(1.0, 1.0, 1.0, 0.5)
|
||||
vec3 region_uv = get_index_uv(uv2);
|
||||
vec4 color_map = region_uv.z > -1.0 ? textureLod(_color_maps, region_uv, region_mip) : COLOR_MAP;
|
||||
|
||||
Material mat[4];
|
||||
uint control = floatBitsToUint(texelFetch(_control_maps, get_index_coord(floor(uv), FRAGMENT_PASS), 0)).r;
|
||||
get_material(base_derivatives, control, region_uv, mat[3]);
|
||||
|
||||
vec4 albedo_height = mat[3].alb_ht;
|
||||
vec4 normal_rough = mat[3].nrm_rg;
|
||||
float normal_map_depth = mat[3].nrm_depth;
|
||||
float ao_strength = mat[3].ao_str;
|
||||
|
||||
// Only do blend if we really have to.
|
||||
if (v_lerp == 1) {
|
||||
get_material(base_derivatives, v_control[0], region_uv, mat[0]);
|
||||
get_material(base_derivatives, v_control[1], region_uv, mat[1]);
|
||||
get_material(base_derivatives, v_control[2], region_uv, mat[2]);
|
||||
|
||||
// we dont need weights before this point when using vertex normals.
|
||||
vec2 weight = fract(uv);
|
||||
vec2 invert = 1.0 - weight;
|
||||
vec4 weights = vec4(
|
||||
invert.x * weight.y, // 0
|
||||
weight.x * weight.y, // 1
|
||||
weight.x * invert.y, // 2
|
||||
invert.x * invert.y // 3
|
||||
);
|
||||
|
||||
// Interpolate Albedo/Height/Normal/Roughness
|
||||
albedo_height =
|
||||
mat[0].alb_ht * weights[0] +
|
||||
mat[1].alb_ht * weights[1] +
|
||||
mat[2].alb_ht * weights[2] +
|
||||
mat[3].alb_ht * weights[3] ;
|
||||
|
||||
normal_rough =
|
||||
mat[0].nrm_rg * weights[0] +
|
||||
mat[1].nrm_rg * weights[1] +
|
||||
mat[2].nrm_rg * weights[2] +
|
||||
mat[3].nrm_rg * weights[3] ;
|
||||
|
||||
normal_map_depth =
|
||||
mat[0].nrm_depth * weights[0] +
|
||||
mat[1].nrm_depth * weights[1] +
|
||||
mat[2].nrm_depth * weights[2] +
|
||||
mat[3].nrm_depth * weights[3] ;
|
||||
|
||||
ao_strength =
|
||||
mat[0].ao_str * weights[0] +
|
||||
mat[1].ao_str * weights[1] +
|
||||
mat[2].ao_str * weights[2] +
|
||||
mat[3].ao_str * weights[3] ;
|
||||
}
|
||||
|
||||
// Wetness/roughness modifier, converting 0 - 1 range to -1 to 1 range
|
||||
float roughness = fma(color_map.a - 0.5, 2.0, normal_rough.a);
|
||||
|
||||
// Apply PBR
|
||||
ALBEDO = albedo_height.rgb * color_map.rgb;
|
||||
ROUGHNESS = roughness;
|
||||
SPECULAR = 1. - normal_rough.a;
|
||||
NORMAL_MAP = pack_normal(normal_rough.rgb);
|
||||
NORMAL_MAP_DEPTH = normal_map_depth;
|
||||
|
||||
// Higher and/or facing up, less occluded.
|
||||
// This is also virtually free.
|
||||
float ao = (1.0 - (albedo_height.a * log(2.1 - ao_strength))) * (1.0 - normal_rough.y);
|
||||
AO = clamp(1.0 - ao * ao_strength, albedo_height.a, 1.0);
|
||||
AO_LIGHT_AFFECT = albedo_height.a;
|
||||
|
||||
}
|
||||
1
addons/terrain_3d/extras/lightweight.gdshader.uid
Normal file
1
addons/terrain_3d/extras/lightweight.gdshader.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bbx2xhanpq5l3
|
||||
163
addons/terrain_3d/extras/lowpoly_colormap.gdshader
Normal file
163
addons/terrain_3d/extras/lowpoly_colormap.gdshader
Normal file
@@ -0,0 +1,163 @@
|
||||
// Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
|
||||
// This is an example of a minimal, low-poly style shader colored by the color map and wetness tools.
|
||||
// No textures are needed or used in this shader.
|
||||
|
||||
shader_type spatial;
|
||||
render_mode blend_mix,depth_draw_opaque,cull_back,diffuse_burley,specular_schlick_ggx,skip_vertex_transform;
|
||||
|
||||
// Defined Constants
|
||||
#define SKIP_PASS 0
|
||||
#define VERTEX_PASS 1
|
||||
#define FRAGMENT_PASS 2
|
||||
|
||||
#if CURRENT_RENDERER == RENDERER_COMPATIBILITY
|
||||
#define fma(a, b, c) ((a) * (b) + (c))
|
||||
#define dFdxCoarse(a) dFdx(a)
|
||||
#define dFdyCoarse(a) dFdy(a)
|
||||
#endif
|
||||
|
||||
// Private uniforms
|
||||
uniform vec3 _camera_pos = vec3(0.f);
|
||||
uniform float _mesh_size = 48.f;
|
||||
uniform uint _background_mode = 1u; // NONE = 0, FLAT = 1, NOISE = 2
|
||||
uniform uint _mouse_layer = 0x80000000u; // Layer 32
|
||||
uniform float _vertex_spacing = 1.0;
|
||||
uniform float _vertex_density = 1.0; // = 1/_vertex_spacing
|
||||
uniform float _region_size = 1024.0;
|
||||
uniform float _region_texel_size = 0.0009765625; // = 1/1024
|
||||
uniform int _region_map_size = 32;
|
||||
uniform int _region_map[1024];
|
||||
uniform vec2 _region_locations[1024];
|
||||
uniform highp sampler2DArray _height_maps : repeat_disable;
|
||||
uniform highp sampler2DArray _control_maps : repeat_disable;
|
||||
uniform highp sampler2DArray _color_maps : source_color, filter_nearest_mipmap, repeat_disable;
|
||||
|
||||
// Public uniforms
|
||||
uniform vec3 default_albedo : source_color = vec3(.38, .35, .3);
|
||||
uniform float default_roughness : hint_range(0.0, 1.0, 0.01) = 0.8;
|
||||
|
||||
// Varyings & Types
|
||||
// Some are required for editor functions
|
||||
varying float v_vertex_xz_dist;
|
||||
varying vec3 v_vertex;
|
||||
|
||||
////////////////////////
|
||||
// Vertex
|
||||
////////////////////////
|
||||
|
||||
// Takes in world space XZ (UV) coordinates & search depth (only applicable for background mode none)
|
||||
// Returns ivec3 with:
|
||||
// XY: (0 to _region_size - 1) coordinates within a region
|
||||
// Z: layer index used for texturearrays, -1 if not in a region
|
||||
ivec3 get_index_coord(const vec2 uv, const int search) {
|
||||
vec2 r_uv = round(uv);
|
||||
vec2 o_uv = mod(r_uv,_region_size);
|
||||
ivec2 pos;
|
||||
int bounds, layer_index = -1;
|
||||
for (int i = -1; i < clamp(search, SKIP_PASS, FRAGMENT_PASS); i++) {
|
||||
if ((layer_index == -1 && _background_mode == 0u ) || i < 0) {
|
||||
r_uv -= i == -1 ? vec2(0.0) : vec2(float(o_uv.x <= o_uv.y), float(o_uv.y <= o_uv.x));
|
||||
pos = ivec2(floor((r_uv) * _region_texel_size)) + (_region_map_size / 2);
|
||||
bounds = int(uint(pos.x | pos.y) < uint(_region_map_size));
|
||||
layer_index = (_region_map[ pos.y * _region_map_size + pos.x ] * bounds - 1);
|
||||
}
|
||||
}
|
||||
return ivec3(ivec2(mod(r_uv,_region_size)), layer_index);
|
||||
}
|
||||
|
||||
// Takes in descaled (world_space / region_size) world to region space XZ (UV2) coordinates, returns vec3 with:
|
||||
// XY: (0. to 1.) coordinates within a region
|
||||
// Z: layer index used for texturearrays, -1 if not in a region
|
||||
vec3 get_index_uv(const vec2 uv2) {
|
||||
ivec2 pos = ivec2(floor(uv2)) + (_region_map_size / 2);
|
||||
int bounds = int(uint(pos.x | pos.y) < uint(_region_map_size));
|
||||
int layer_index = _region_map[ pos.y * _region_map_size + pos.x ] * bounds - 1;
|
||||
return vec3(uv2 - _region_locations[layer_index], float(layer_index));
|
||||
}
|
||||
|
||||
void vertex() {
|
||||
// Get vertex of flat plane in world coordinates and set world UV
|
||||
v_vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||
|
||||
// Camera distance to vertex on flat plane
|
||||
v_vertex_xz_dist = length(v_vertex.xz - _camera_pos.xz);
|
||||
|
||||
// Geomorph vertex, set end and start for linear height interpolate
|
||||
float scale = MODEL_MATRIX[0][0];
|
||||
float vertex_lerp = smoothstep(0.55, 0.95, (v_vertex_xz_dist / scale - _mesh_size - 4.0) / (_mesh_size - 2.0));
|
||||
vec2 v_fract = fract(VERTEX.xz * 0.5) * 2.0;
|
||||
// For LOD0 morph from a regular grid to an alternating grid to align with LOD1+
|
||||
vec2 shift = (scale < _vertex_spacing + 1e-6) ? // LOD0 or not
|
||||
// Shift from regular to symetric
|
||||
mix(v_fract, vec2(v_fract.x, -v_fract.y),
|
||||
round(fract(round(mod(v_vertex.z * _vertex_density, 4.0)) *
|
||||
round(mod(v_vertex.x * _vertex_density, 4.0)) * 0.25))
|
||||
) :
|
||||
// Symetric shift
|
||||
v_fract * round((fract(v_vertex.xz * 0.25 / scale) - 0.5) * 4.0);
|
||||
vec2 start_pos = v_vertex.xz * _vertex_density;
|
||||
vec2 end_pos = (v_vertex.xz - shift * scale) * _vertex_density;
|
||||
v_vertex.xz -= shift * scale * vertex_lerp;
|
||||
|
||||
// UV coordinates in world space. Values are 0 to _region_size within regions
|
||||
UV = v_vertex.xz * _vertex_density;
|
||||
|
||||
// UV coordinates in region space + texel offset. Values are 0 to 1 within regions
|
||||
UV2 = fma(UV, vec2(_region_texel_size), vec2(0.5 * _region_texel_size));
|
||||
|
||||
// Discard vertices for Holes. 1 lookup
|
||||
ivec3 region = get_index_coord(start_pos, VERTEX_PASS);
|
||||
uint control = floatBitsToUint(texelFetch(_control_maps, region, 0)).r;
|
||||
bool hole = bool(control >>2u & 0x1u);
|
||||
|
||||
// Show holes to all cameras except mouse camera (on exactly 1 layer)
|
||||
if ( !(CAMERA_VISIBLE_LAYERS == _mouse_layer) &&
|
||||
(hole || (_background_mode == 0u && region.z < 0))) {
|
||||
v_vertex.x = 0. / 0.;
|
||||
} else {
|
||||
// Interpolate Geomorph Start & End, set height. 2 Lookups.
|
||||
ivec3 uv_a = get_index_coord(start_pos, VERTEX_PASS);
|
||||
ivec3 uv_b = get_index_coord(end_pos, VERTEX_PASS);
|
||||
float h = mix(texelFetch(_height_maps, uv_a, 0).r, texelFetch(_height_maps, uv_b, 0).r, vertex_lerp);
|
||||
v_vertex.y = h;
|
||||
}
|
||||
|
||||
// Convert model space to view space w/ skip_vertex_transform render mode
|
||||
VERTEX = (VIEW_MATRIX * vec4(v_vertex, 1.0)).xyz;
|
||||
NORMAL = normalize((MODELVIEW_MATRIX * vec4(NORMAL, 0.0)).xyz);
|
||||
BINORMAL = normalize((MODELVIEW_MATRIX * vec4(BINORMAL, 0.0)).xyz);
|
||||
TANGENT = normalize((MODELVIEW_MATRIX * vec4(TANGENT, 0.0)).xyz);
|
||||
}
|
||||
|
||||
////////////////////////
|
||||
// Fragment
|
||||
////////////////////////
|
||||
|
||||
void fragment() {
|
||||
// Recover UVs
|
||||
vec2 uv = UV;
|
||||
vec2 uv2 = UV2;
|
||||
|
||||
// Apply terrain normals
|
||||
vec3 ddx = dFdxCoarse(VERTEX);
|
||||
vec3 ddy = dFdyCoarse(VERTEX);
|
||||
NORMAL = normalize(cross(ddy, ddx));
|
||||
TANGENT = normalize(cross(NORMAL, vec3(0.0, 0.0, 1.0)));
|
||||
BINORMAL = normalize(cross(NORMAL, TANGENT));
|
||||
|
||||
// Determine if we're in a region or not (region_uv.z>0)
|
||||
vec3 region_uv = get_index_uv(uv2);
|
||||
|
||||
// Colormap. 1 lookup
|
||||
float lod = log2(max(length(ddx.xz), length(ddy.xz)) * _vertex_density);
|
||||
vec4 color_map = region_uv.z > -1.0 ?
|
||||
textureLod(_color_maps, region_uv, lod) : vec4(1., 1., 1., .5);
|
||||
|
||||
// Wetness/roughness modifier, converting 0 - 1 range to -1 to 1 range
|
||||
float roughness = fma(color_map.a - 0.5, 2.0, default_roughness);
|
||||
|
||||
// Apply PBR
|
||||
ALBEDO = default_albedo * color_map.rgb;
|
||||
ROUGHNESS = roughness;
|
||||
SPECULAR = 1.0 - roughness;
|
||||
}
|
||||
1
addons/terrain_3d/extras/lowpoly_colormap.gdshader.uid
Normal file
1
addons/terrain_3d/extras/lowpoly_colormap.gdshader.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bda7fq1rh3nmv
|
||||
134
addons/terrain_3d/extras/lowpoly_minimum.gdshader
Normal file
134
addons/terrain_3d/extras/lowpoly_minimum.gdshader
Normal file
@@ -0,0 +1,134 @@
|
||||
// Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
|
||||
// This shader is a version of minimum.gdshader with flat normals for a low poly look.
|
||||
// Increase vertex_spacing for a better result.
|
||||
|
||||
shader_type spatial;
|
||||
render_mode blend_mix,depth_draw_opaque,cull_back,diffuse_burley,specular_schlick_ggx,skip_vertex_transform;
|
||||
|
||||
// Defined Constants
|
||||
#define SKIP_PASS 0
|
||||
#define VERTEX_PASS 1
|
||||
#define FRAGMENT_PASS 2
|
||||
|
||||
#if CURRENT_RENDERER == RENDERER_COMPATIBILITY
|
||||
#define fma(a, b, c) ((a) * (b) + (c))
|
||||
#define dFdxCoarse(a) dFdx(a)
|
||||
#define dFdyCoarse(a) dFdy(a)
|
||||
#endif
|
||||
|
||||
// Private uniforms
|
||||
uniform vec3 _camera_pos = vec3(0.f);
|
||||
uniform float _mesh_size = 48.f;
|
||||
uniform uint _background_mode = 1u; // NONE = 0, FLAT = 1, NOISE = 2
|
||||
uniform uint _mouse_layer = 0x80000000u; // Layer 32
|
||||
uniform float _vertex_spacing = 1.0;
|
||||
uniform float _vertex_density = 1.0; // = 1/_vertex_spacing
|
||||
uniform float _region_size = 1024.0;
|
||||
uniform float _region_texel_size = 0.0009765625; // = 1/1024
|
||||
uniform int _region_map_size = 32;
|
||||
uniform int _region_map[1024];
|
||||
uniform vec2 _region_locations[1024];
|
||||
uniform highp sampler2DArray _height_maps : repeat_disable;
|
||||
uniform highp sampler2DArray _control_maps : repeat_disable;
|
||||
|
||||
// Varyings & Types
|
||||
// Some are required for editor functions
|
||||
varying float v_vertex_xz_dist;
|
||||
varying vec3 v_vertex;
|
||||
|
||||
////////////////////////
|
||||
// Vertex
|
||||
////////////////////////
|
||||
|
||||
// Takes in world space XZ (UV) coordinates & search depth (only applicable for background mode none)
|
||||
// Returns ivec3 with:
|
||||
// XY: (0 to _region_size - 1) coordinates within a region
|
||||
// Z: layer index used for texturearrays, -1 if not in a region
|
||||
ivec3 get_index_coord(const vec2 uv, const int search) {
|
||||
vec2 r_uv = round(uv);
|
||||
vec2 o_uv = mod(r_uv,_region_size);
|
||||
ivec2 pos;
|
||||
int bounds, layer_index = -1;
|
||||
for (int i = -1; i < clamp(search, SKIP_PASS, FRAGMENT_PASS); i++) {
|
||||
if ((layer_index == -1 && _background_mode == 0u ) || i < 0) {
|
||||
r_uv -= i == -1 ? vec2(0.0) : vec2(float(o_uv.x <= o_uv.y), float(o_uv.y <= o_uv.x));
|
||||
pos = ivec2(floor((r_uv) * _region_texel_size)) + (_region_map_size / 2);
|
||||
bounds = int(uint(pos.x | pos.y) < uint(_region_map_size));
|
||||
layer_index = (_region_map[ pos.y * _region_map_size + pos.x ] * bounds - 1);
|
||||
}
|
||||
}
|
||||
return ivec3(ivec2(mod(r_uv,_region_size)), layer_index);
|
||||
}
|
||||
|
||||
void vertex() {
|
||||
// Get vertex of flat plane in world coordinates and set world UV
|
||||
v_vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||
|
||||
// Camera distance to vertex on flat plane
|
||||
v_vertex_xz_dist = length(v_vertex.xz - _camera_pos.xz);
|
||||
|
||||
// Geomorph vertex, set end and start for linear height interpolate
|
||||
float scale = MODEL_MATRIX[0][0];
|
||||
float vertex_lerp = smoothstep(0.55, 0.95, (v_vertex_xz_dist / scale - _mesh_size - 4.0) / (_mesh_size - 2.0));
|
||||
vec2 v_fract = fract(VERTEX.xz * 0.5) * 2.0;
|
||||
// For LOD0 morph from a regular grid to an alternating grid to align with LOD1+
|
||||
vec2 shift = (scale < _vertex_spacing + 1e-6) ? // LOD0 or not
|
||||
// Shift from regular to symetric
|
||||
mix(v_fract, vec2(v_fract.x, -v_fract.y),
|
||||
round(fract(round(mod(v_vertex.z * _vertex_density, 4.0)) *
|
||||
round(mod(v_vertex.x * _vertex_density, 4.0)) * 0.25))
|
||||
) :
|
||||
// Symetric shift
|
||||
v_fract * round((fract(v_vertex.xz * 0.25 / scale) - 0.5) * 4.0);
|
||||
vec2 start_pos = v_vertex.xz * _vertex_density;
|
||||
vec2 end_pos = (v_vertex.xz - shift * scale) * _vertex_density;
|
||||
v_vertex.xz -= shift * scale * vertex_lerp;
|
||||
|
||||
// UV coordinates in world space. Values are 0 to _region_size within regions
|
||||
UV = v_vertex.xz * _vertex_density;
|
||||
|
||||
// UV coordinates in region space + texel offset. Values are 0 to 1 within regions
|
||||
UV2 = fma(UV, vec2(_region_texel_size), vec2(0.5 * _region_texel_size));
|
||||
|
||||
// Discard vertices for Holes. 1 lookup
|
||||
ivec3 region = get_index_coord(start_pos, VERTEX_PASS);
|
||||
uint control = floatBitsToUint(texelFetch(_control_maps, region, 0)).r;
|
||||
bool hole = bool(control >>2u & 0x1u);
|
||||
|
||||
// Show holes to all cameras except mouse camera (on exactly 1 layer)
|
||||
if ( !(CAMERA_VISIBLE_LAYERS == _mouse_layer) &&
|
||||
(hole || (_background_mode == 0u && region.z < 0))) {
|
||||
v_vertex.x = 0. / 0.;
|
||||
} else {
|
||||
// Interpolate Geomorph Start & End, set height. 2 Lookups.
|
||||
ivec3 uv_a = get_index_coord(start_pos, VERTEX_PASS);
|
||||
ivec3 uv_b = get_index_coord(end_pos, VERTEX_PASS);
|
||||
float h = mix(texelFetch(_height_maps, uv_a, 0).r, texelFetch(_height_maps, uv_b, 0).r, vertex_lerp);
|
||||
v_vertex.y = h;
|
||||
}
|
||||
|
||||
// Convert model space to view space w/ skip_vertex_transform render mode
|
||||
VERTEX = (VIEW_MATRIX * vec4(v_vertex, 1.0)).xyz;
|
||||
NORMAL = normalize((MODELVIEW_MATRIX * vec4(NORMAL, 0.0)).xyz);
|
||||
BINORMAL = normalize((MODELVIEW_MATRIX * vec4(BINORMAL, 0.0)).xyz);
|
||||
TANGENT = normalize((MODELVIEW_MATRIX * vec4(TANGENT, 0.0)).xyz);
|
||||
}
|
||||
|
||||
////////////////////////
|
||||
// Fragment
|
||||
////////////////////////
|
||||
|
||||
void fragment() {
|
||||
// Recover UVs
|
||||
vec2 uv = UV;
|
||||
vec2 uv2 = UV2;
|
||||
|
||||
// Apply terrain normals
|
||||
NORMAL = normalize(cross(dFdyCoarse(VERTEX),dFdxCoarse(VERTEX)));
|
||||
TANGENT = normalize(cross(NORMAL, vec3(0.0, 0.0, 1.0)));
|
||||
BINORMAL = normalize(cross(NORMAL, TANGENT));
|
||||
|
||||
// Apply PBR
|
||||
ALBEDO = vec3(.2);
|
||||
ROUGHNESS = .7;
|
||||
}
|
||||
1
addons/terrain_3d/extras/lowpoly_minimum.gdshader.uid
Normal file
1
addons/terrain_3d/extras/lowpoly_minimum.gdshader.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://x11v7w7v8hqa
|
||||
@@ -1,125 +1,120 @@
|
||||
// Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
|
||||
// This shader is the minimum needed to allow the terrain to function, without any texturing.
|
||||
|
||||
shader_type spatial;
|
||||
render_mode blend_mix,depth_draw_opaque,cull_back,diffuse_burley,specular_schlick_ggx,skip_vertex_transform;
|
||||
|
||||
// Defined Constants
|
||||
#define SKIP_PASS 0
|
||||
#define VERTEX_PASS 1
|
||||
#define FRAGMENT_PASS 2
|
||||
|
||||
#if CURRENT_RENDERER == RENDERER_COMPATIBILITY
|
||||
#define fma(a, b, c) ((a) * (b) + (c))
|
||||
#define dFdxCoarse(a) dFdx(a)
|
||||
#define dFdyCoarse(a) dFdy(a)
|
||||
#endif
|
||||
|
||||
// Private uniforms
|
||||
uniform float _region_size = 1024.0;
|
||||
uniform float _region_texel_size = 0.0009765625; // = 1/1024
|
||||
// Commented uniforms aren't needed for this shader, but are available for your own needs.
|
||||
uniform vec3 _camera_pos = vec3(0.f);
|
||||
uniform float _mesh_size = 48.f;
|
||||
uniform uint _background_mode = 1u; // NONE = 0, FLAT = 1, NOISE = 2
|
||||
uniform uint _mouse_layer = 0x80000000u; // Layer 32
|
||||
uniform float _vertex_spacing = 1.0;
|
||||
uniform float _vertex_density = 1.0; // = 1/_vertex_spacing
|
||||
uniform float _region_size = 1024.0;
|
||||
uniform float _region_texel_size = 0.0009765625; // = 1/1024
|
||||
uniform int _region_map_size = 32;
|
||||
uniform int _region_map[1024];
|
||||
uniform vec2 _region_locations[1024];
|
||||
uniform sampler2DArray _height_maps : repeat_disable;
|
||||
uniform usampler2DArray _control_maps : repeat_disable;
|
||||
uniform sampler2DArray _color_maps : source_color, filter_linear_mipmap_anisotropic, repeat_disable;
|
||||
uniform sampler2DArray _texture_array_albedo : source_color, filter_linear_mipmap_anisotropic, repeat_enable;
|
||||
uniform sampler2DArray _texture_array_normal : hint_normal, filter_linear_mipmap_anisotropic, repeat_enable;
|
||||
uniform sampler2D noise_texture : source_color, filter_linear_mipmap_anisotropic, repeat_enable;
|
||||
|
||||
uniform float _texture_uv_scale_array[32];
|
||||
uniform float _texture_detile_array[32];
|
||||
uniform vec4 _texture_color_array[32];
|
||||
uniform uint _background_mode = 1u; // NONE = 0, FLAT = 1, NOISE = 2
|
||||
uniform uint _mouse_layer = 0x80000000u; // Layer 32
|
||||
|
||||
// Public uniforms
|
||||
uniform float vertex_normals_distance : hint_range(0, 1024) = 128.0;
|
||||
//uniform float _texture_uv_scale_array[32];
|
||||
//uniform float _texture_detile_array[32];
|
||||
//uniform vec4 _texture_color_array[32];
|
||||
uniform highp sampler2DArray _height_maps : repeat_disable;
|
||||
uniform highp sampler2DArray _control_maps : repeat_disable;
|
||||
//uniform highp sampler2DArray _color_maps : source_color, filter_linear_mipmap_anisotropic, repeat_disable;
|
||||
//uniform highp sampler2DArray _texture_array_albedo : source_color, filter_linear_mipmap_anisotropic, repeat_enable;
|
||||
//uniform highp sampler2DArray _texture_array_normal : hint_normal, filter_linear_mipmap_anisotropic, repeat_enable;
|
||||
|
||||
// Varyings & Types
|
||||
varying flat vec3 v_vertex; // World coordinate vertex location
|
||||
varying flat vec3 v_camera_pos;
|
||||
// Some are required for editor functions
|
||||
varying float v_vertex_xz_dist;
|
||||
varying flat ivec3 v_region;
|
||||
varying flat vec2 v_uv_offset;
|
||||
varying flat vec2 v_uv2_offset;
|
||||
varying vec3 v_normal;
|
||||
varying float v_region_border_mask;
|
||||
varying vec3 v_vertex;
|
||||
|
||||
////////////////////////
|
||||
// Vertex
|
||||
////////////////////////
|
||||
|
||||
// Takes in UV world space coordinates, returns ivec3 with:
|
||||
// XY: (0 to _region_size) coordinates within a region
|
||||
// Takes in world space XZ (UV) coordinates & search depth (only applicable for background mode none)
|
||||
// Returns ivec3 with:
|
||||
// XY: (0 to _region_size - 1) coordinates within a region
|
||||
// Z: layer index used for texturearrays, -1 if not in a region
|
||||
ivec3 get_region_uv(const vec2 uv) {
|
||||
ivec2 pos = ivec2(floor(uv * _region_texel_size)) + (_region_map_size / 2);
|
||||
int bounds = int(uint(pos.x | pos.y) < uint(_region_map_size));
|
||||
int layer_index = _region_map[ pos.y * _region_map_size + pos.x ] * bounds - 1;
|
||||
return ivec3(ivec2(mod(uv,_region_size)), layer_index);
|
||||
}
|
||||
|
||||
// Takes in UV2 region space coordinates, returns vec3 with:
|
||||
// XY: (0 to 1) coordinates within a region
|
||||
// Z: layer index used for texturearrays, -1 if not in a region
|
||||
vec3 get_region_uv2(const vec2 uv2) {
|
||||
// Remove Texel Offset to ensure correct region index.
|
||||
ivec2 pos = ivec2(floor(uv2 - vec2(_region_texel_size * 0.5))) + (_region_map_size / 2);
|
||||
int bounds = int(uint(pos.x | pos.y) < uint(_region_map_size));
|
||||
int layer_index = _region_map[ pos.y * _region_map_size + pos.x ] * bounds - 1;
|
||||
return vec3(uv2 - _region_locations[layer_index], float(layer_index));
|
||||
}
|
||||
|
||||
// 1 lookup
|
||||
float get_height(vec2 uv) {
|
||||
highp float height = 0.0;
|
||||
vec3 region = get_region_uv2(uv);
|
||||
if (region.z >= 0.) {
|
||||
height = texture(_height_maps, region).r;
|
||||
ivec3 get_index_coord(const vec2 uv, const int search) {
|
||||
vec2 r_uv = round(uv);
|
||||
vec2 o_uv = mod(r_uv,_region_size);
|
||||
ivec2 pos;
|
||||
int bounds, layer_index = -1;
|
||||
for (int i = -1; i < clamp(search, SKIP_PASS, FRAGMENT_PASS); i++) {
|
||||
if ((layer_index == -1 && _background_mode == 0u ) || i < 0) {
|
||||
r_uv -= i == -1 ? vec2(0.0) : vec2(float(o_uv.x <= o_uv.y), float(o_uv.y <= o_uv.x));
|
||||
pos = ivec2(floor((r_uv) * _region_texel_size)) + (_region_map_size / 2);
|
||||
bounds = int(uint(pos.x | pos.y) < uint(_region_map_size));
|
||||
layer_index = (_region_map[ pos.y * _region_map_size + pos.x ] * bounds - 1);
|
||||
}
|
||||
}
|
||||
return height;
|
||||
return ivec3(ivec2(mod(r_uv,_region_size)), layer_index);
|
||||
}
|
||||
|
||||
void vertex() {
|
||||
// Get camera pos in world vertex coords
|
||||
v_camera_pos = INV_VIEW_MATRIX[3].xyz;
|
||||
|
||||
// Get vertex of flat plane in world coordinates and set world UV
|
||||
v_vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||
|
||||
// Camera distance to vertex on flat plane
|
||||
v_vertex_xz_dist = length(v_vertex.xz - v_camera_pos.xz);
|
||||
v_vertex_xz_dist = length(v_vertex.xz - _camera_pos.xz);
|
||||
|
||||
// Geomorph vertex, set end and start for linear height interpolate
|
||||
float scale = MODEL_MATRIX[0][0];
|
||||
float vertex_lerp = smoothstep(0.55, 0.95, (v_vertex_xz_dist / scale - _mesh_size - 4.0) / (_mesh_size - 2.0));
|
||||
vec2 v_fract = fract(VERTEX.xz * 0.5) * 2.0;
|
||||
// For LOD0 morph from a regular grid to an alternating grid to align with LOD1+
|
||||
vec2 shift = (scale < _vertex_spacing + 1e-6) ? // LOD0 or not
|
||||
// Shift from regular to symetric
|
||||
mix(v_fract, vec2(v_fract.x, -v_fract.y),
|
||||
round(fract(round(mod(v_vertex.z * _vertex_density, 4.0)) *
|
||||
round(mod(v_vertex.x * _vertex_density, 4.0)) * 0.25))
|
||||
) :
|
||||
// Symetric shift
|
||||
v_fract * round((fract(v_vertex.xz * 0.25 / scale) - 0.5) * 4.0);
|
||||
vec2 start_pos = v_vertex.xz * _vertex_density;
|
||||
vec2 end_pos = (v_vertex.xz - shift * scale) * _vertex_density;
|
||||
v_vertex.xz -= shift * scale * vertex_lerp;
|
||||
|
||||
// UV coordinates in world space. Values are 0 to _region_size within regions
|
||||
UV = round(v_vertex.xz * _vertex_density);
|
||||
UV = v_vertex.xz * _vertex_density;
|
||||
|
||||
// UV coordinates in region space + texel offset. Values are 0 to 1 within regions
|
||||
UV2 = fma(UV, vec2(_region_texel_size), vec2(0.5 * _region_texel_size));
|
||||
|
||||
// Discard vertices for Holes. 1 lookup
|
||||
v_region = get_region_uv(UV);
|
||||
uint control = texelFetch(_control_maps, v_region, 0).r;
|
||||
ivec3 v_region = get_index_coord(start_pos, VERTEX_PASS);
|
||||
uint control = floatBitsToUint(texelFetch(_control_maps, v_region, 0)).r;
|
||||
bool hole = bool(control >>2u & 0x1u);
|
||||
|
||||
// Show holes to all cameras except mouse camera (on exactly 1 layer)
|
||||
if ( !(CAMERA_VISIBLE_LAYERS == _mouse_layer) &&
|
||||
(hole || (_background_mode == 0u && (get_region_uv(UV - _region_texel_size) & v_region).z < 0))) {
|
||||
VERTEX.x = 0. / 0.;
|
||||
} else {
|
||||
// Set final vertex height & calculate vertex normals. 3 lookups.
|
||||
VERTEX.y = get_height(UV2);
|
||||
v_vertex.y = VERTEX.y;
|
||||
v_normal = vec3(
|
||||
v_vertex.y - get_height(UV2 + vec2(_region_texel_size, 0)),
|
||||
_vertex_spacing,
|
||||
v_vertex.y - get_height(UV2 + vec2(0, _region_texel_size))
|
||||
);
|
||||
// Due to a bug caused by the GPUs linear interpolation across edges of region maps,
|
||||
// mask region edges and use vertex normals only across region boundaries.
|
||||
v_region_border_mask = mod(UV.x + 2.5, _region_size) - fract(UV.x) < 5.0 || mod(UV.y + 2.5, _region_size) - fract(UV.y) < 5.0 ? 1. : 0.;
|
||||
if ( !(CAMERA_VISIBLE_LAYERS == _mouse_layer) &&
|
||||
(hole || (_background_mode == 0u && v_region.z < 0))) {
|
||||
v_vertex.x = 0. / 0.;
|
||||
} else {
|
||||
// Interpolate Geomorph Start & End, set height. 2 Lookups.
|
||||
ivec3 uv_a = get_index_coord(start_pos, VERTEX_PASS);
|
||||
ivec3 uv_b = get_index_coord(end_pos, VERTEX_PASS);
|
||||
float h = mix(texelFetch(_height_maps, uv_a, 0).r, texelFetch(_height_maps, uv_b, 0).r, vertex_lerp);
|
||||
v_vertex.y = h;
|
||||
}
|
||||
|
||||
// Transform UVs to local to avoid poor precision during varying interpolation.
|
||||
v_uv_offset = MODEL_MATRIX[3].xz * _vertex_density;
|
||||
UV -= v_uv_offset;
|
||||
v_uv2_offset = v_uv_offset * _region_texel_size;
|
||||
UV2 -= v_uv2_offset;
|
||||
|
||||
// Convert model space to view space w/ skip_vertex_transform render mode
|
||||
VERTEX = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||
VERTEX = (VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||
VERTEX = (VIEW_MATRIX * vec4(v_vertex, 1.0)).xyz;
|
||||
NORMAL = normalize((MODELVIEW_MATRIX * vec4(NORMAL, 0.0)).xyz);
|
||||
BINORMAL = normalize((MODELVIEW_MATRIX * vec4(BINORMAL, 0.0)).xyz);
|
||||
TANGENT = normalize((MODELVIEW_MATRIX * vec4(TANGENT, 0.0)).xyz);
|
||||
@@ -129,36 +124,85 @@ void vertex() {
|
||||
// Fragment
|
||||
////////////////////////
|
||||
|
||||
// 0 - 3 lookups
|
||||
vec3 get_normal(vec2 uv, out vec3 tangent, out vec3 binormal) {
|
||||
float u, v, height;
|
||||
vec3 normal;
|
||||
// Use vertex normals within radius of vertex_normals_distance, and along region borders.
|
||||
if (v_region_border_mask > 0.5 || v_vertex_xz_dist < vertex_normals_distance) {
|
||||
normal = normalize(v_normal);
|
||||
} else {
|
||||
height = get_height(uv);
|
||||
u = height - get_height(uv + vec2(_region_texel_size, 0));
|
||||
v = height - get_height(uv + vec2(0, _region_texel_size));
|
||||
normal = normalize(vec3(u, _vertex_spacing, v));
|
||||
}
|
||||
tangent = cross(normal, vec3(0, 0, 1));
|
||||
binormal = cross(normal, tangent);
|
||||
return normal;
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
// Recover UVs
|
||||
vec2 uv = UV + v_uv_offset;
|
||||
vec2 uv2 = UV2 + v_uv2_offset;
|
||||
vec2 uv = UV;
|
||||
vec2 uv2 = UV2;
|
||||
|
||||
// Calculate Terrain Normals. 4 lookups
|
||||
vec3 w_tangent, w_binormal;
|
||||
vec3 w_normal = get_normal(uv2, w_tangent, w_binormal);
|
||||
// Lookup offsets, ID and blend weight
|
||||
const vec3 offsets = vec3(0, 1, 2);
|
||||
vec2 index_id = floor(uv);
|
||||
vec2 weight = fract(uv);
|
||||
vec2 invert = 1.0 - weight;
|
||||
vec4 weights = vec4(
|
||||
invert.x * weight.y, // 0
|
||||
weight.x * weight.y, // 1
|
||||
weight.x * invert.y, // 2
|
||||
invert.x * invert.y // 3
|
||||
);
|
||||
|
||||
vec3 base_ddx = dFdxCoarse(v_vertex);
|
||||
vec3 base_ddy = dFdyCoarse(v_vertex);
|
||||
vec4 base_derivatives = vec4(base_ddx.xz, base_ddy.xz);
|
||||
// Calculate the effective mipmap for regionspace, and if less than 0,
|
||||
// skip all extra lookups required for bilinear blend.
|
||||
float region_mip = log2(max(length(base_ddx.xz), length(base_ddy.xz)) * _vertex_density);
|
||||
bool bilerp = region_mip < 0.0;
|
||||
|
||||
ivec3 indexUV[4];
|
||||
// control map lookups, used for some normal lookups as well
|
||||
indexUV[0] = get_index_coord(index_id + offsets.xy, FRAGMENT_PASS);
|
||||
indexUV[1] = get_index_coord(index_id + offsets.yy, FRAGMENT_PASS);
|
||||
indexUV[2] = get_index_coord(index_id + offsets.yx, FRAGMENT_PASS);
|
||||
indexUV[3] = get_index_coord(index_id + offsets.xx, FRAGMENT_PASS);
|
||||
|
||||
// Terrain normals
|
||||
vec3 index_normal[4];
|
||||
float h[8];
|
||||
// allows additional derivatives, eg world noise, brush previews etc
|
||||
float u = 0.0;
|
||||
float v = 0.0;
|
||||
|
||||
// Re-use the indexUVs for the first lookups, skipping some math. 3 lookups
|
||||
h[3] = texelFetch(_height_maps, indexUV[3], 0).r; // 0 (0,0)
|
||||
h[2] = texelFetch(_height_maps, indexUV[2], 0).r; // 1 (1,0)
|
||||
h[0] = texelFetch(_height_maps, indexUV[0], 0).r; // 2 (0,1)
|
||||
index_normal[3] = normalize(vec3(h[3] - h[2] + u, _vertex_spacing, h[3] - h[0] + v));
|
||||
|
||||
// Set flat world normal - overriden if bilerp is true
|
||||
vec3 w_normal = index_normal[3];
|
||||
|
||||
// Branching smooth normals must be done seperatley for correct normals at all 4 index ids
|
||||
if (bilerp) {
|
||||
// 5 lookups
|
||||
// Fetch the additional required height values for smooth normals
|
||||
h[1] = texelFetch(_height_maps, indexUV[1], 0).r; // 3 (1,1)
|
||||
h[4] = texelFetch(_height_maps, get_index_coord(index_id + offsets.yz, FRAGMENT_PASS), 0).r; // 4 (1,2)
|
||||
h[5] = texelFetch(_height_maps, get_index_coord(index_id + offsets.zy, FRAGMENT_PASS), 0).r; // 5 (2,1)
|
||||
h[6] = texelFetch(_height_maps, get_index_coord(index_id + offsets.zx, FRAGMENT_PASS), 0).r; // 6 (2,0)
|
||||
h[7] = texelFetch(_height_maps, get_index_coord(index_id + offsets.xz, FRAGMENT_PASS), 0).r; // 7 (0,2)
|
||||
|
||||
// Calculate the normal for the remaining index ids.
|
||||
index_normal[0] = normalize(vec3(h[0] - h[1] + u, _vertex_spacing, h[0] - h[7] + v));
|
||||
index_normal[1] = normalize(vec3(h[1] - h[5] + u, _vertex_spacing, h[1] - h[4] + v));
|
||||
index_normal[2] = normalize(vec3(h[2] - h[6] + u, _vertex_spacing, h[2] - h[1] + v));
|
||||
|
||||
// Set interpolated world normal
|
||||
w_normal =
|
||||
index_normal[0] * weights[0] +
|
||||
index_normal[1] * weights[1] +
|
||||
index_normal[2] * weights[2] +
|
||||
index_normal[3] * weights[3] ;
|
||||
}
|
||||
|
||||
// Apply terrain normals
|
||||
vec3 w_tangent = normalize(cross(w_normal, vec3(0.0, 0.0, 1.0)));
|
||||
vec3 w_binormal = normalize(cross(w_normal, w_tangent));
|
||||
NORMAL = mat3(VIEW_MATRIX) * w_normal;
|
||||
TANGENT = mat3(VIEW_MATRIX) * w_tangent;
|
||||
BINORMAL = mat3(VIEW_MATRIX) * w_binormal;
|
||||
|
||||
// Apply PBR
|
||||
ALBEDO=vec3(.2);
|
||||
ALBEDO = vec3(.2);
|
||||
ROUGHNESS = .7;
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://c8qog5mlaoeno
|
||||
uid://01qauauvd8aa
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
|
||||
# This script is an addon for HungryProton's Scatter https://github.com/HungryProton/scatter
|
||||
# It provides a `Project on Terrain3D` modifier, which allows Scatter
|
||||
# to detect the terrain height from Terrain3D without using collision.
|
||||
# Copy this file into /addons/proton_scatter/src/modifiers
|
||||
# Then uncomment everything below
|
||||
# In the editor, add this modifier to Scatter, then set your Terrain3D node
|
||||
|
||||
# This script is an addon for HungryProton's Scatter https://github.com/HungryProton/scatter
|
||||
# It allows Scatter to detect the terrain height from Terrain3D
|
||||
#
|
||||
# Copy this file into /addons/proton_scatter/src/modifiers
|
||||
# Then uncomment everything below (select, press CTRL+K)
|
||||
# In the editor, add this modifier, then set your Terrain3D node
|
||||
# In the editor, add this modifier to Scatter, then set your Terrain3D node
|
||||
|
||||
#@tool
|
||||
#extends "base_modifier.gd"
|
||||
@@ -20,6 +16,7 @@
|
||||
#
|
||||
#@export var terrain_node : NodePath
|
||||
#@export var align_with_collision_normal := false
|
||||
#@export_range(0.0, 90.0, 0.1) var max_slope = 90.0
|
||||
#
|
||||
#var _terrain: Terrain3D
|
||||
#
|
||||
@@ -68,20 +65,29 @@
|
||||
## Get global transform
|
||||
#var gt: Transform3D = domain.get_global_transform()
|
||||
#var gt_inverse := gt.affine_inverse()
|
||||
#var new_transforms_array: Array[Transform3D] = []
|
||||
#var remapped_max_slope: float = remap(max_slope, 0.0, 90.0, 0.0, 1.0)
|
||||
#for i in transforms.list.size():
|
||||
#var location: Vector3 = (gt * transforms.list[i]).origin
|
||||
#var t: Transform3D = transforms.list[i]
|
||||
#
|
||||
#var location: Vector3 = (gt * t).origin
|
||||
#var height: float = _terrain.data.get_height(location)
|
||||
#var normal: Vector3 = _terrain.data.get_normal(location)
|
||||
#
|
||||
#if align_with_collision_normal and not is_nan(normal.x):
|
||||
#transforms.list[i].basis.y = normal
|
||||
#transforms.list[i].basis.x = -transforms.list[i].basis.z.cross(normal)
|
||||
#transforms.list[i].basis = transforms.list[i].basis.orthonormalized()
|
||||
#t.basis.y = normal
|
||||
#t.basis.x = -t.basis.z.cross(normal)
|
||||
#t.basis = t.basis.orthonormalized()
|
||||
#
|
||||
#transforms.list[i].origin.y = gt.origin.y if is_nan(height) else height - gt.origin.y
|
||||
#if abs(Vector3.UP.dot(normal)) >= (1.0 - remapped_max_slope):
|
||||
#t.origin.y = gt.origin.y if is_nan(height) else height - gt.origin.y
|
||||
#new_transforms_array.push_back(t)
|
||||
#
|
||||
#transforms.list.clear()
|
||||
#transforms.list.append_array(new_transforms_array)
|
||||
#
|
||||
#if transforms.is_empty():
|
||||
#warning += """Every point has been removed. Possible reasons include: \n
|
||||
#warning += """All transforms have been removed. Possible reasons include: \n
|
||||
#+ No collider is close enough to the shapes.
|
||||
#+ Ray length is too short.
|
||||
#+ Ray direction is incorrect.
|
||||
|
||||
@@ -1 +1 @@
|
||||
uid://ce403ehalp57b
|
||||
uid://g3opjh3m3iww
|
||||
|
||||
52
addons/terrain_3d/extras/region_mover.gd
Normal file
52
addons/terrain_3d/extras/region_mover.gd
Normal file
@@ -0,0 +1,52 @@
|
||||
# This script can be used to move your regions by an offset.
|
||||
# Eventually this tool will find its way into a built in UI
|
||||
#
|
||||
# Attach it to your Terrain3D node
|
||||
# Save and reload your scene
|
||||
# Select your Terrain3D node
|
||||
# Enter a valid `offset` where all regions will be within -16, +15
|
||||
# Run it
|
||||
# It should unload the regions, rename files, and reload them
|
||||
# Clear the script and resave your scene
|
||||
|
||||
|
||||
@tool
|
||||
extends Terrain3D
|
||||
|
||||
|
||||
@export var offset: Vector2i
|
||||
@export var run: bool = false : set = start_rename
|
||||
|
||||
|
||||
func start_rename(val: bool = false) -> void:
|
||||
if val == false or offset == Vector2i.ZERO:
|
||||
return
|
||||
|
||||
var dir_name: String = data_directory
|
||||
data_directory = ""
|
||||
var dir := DirAccess.open(dir_name)
|
||||
if not dir:
|
||||
print("An error occurred when trying to access the path: ", data_directory)
|
||||
return
|
||||
|
||||
var affected_files: PackedStringArray
|
||||
var files: PackedStringArray = dir.get_files()
|
||||
for file_name in files:
|
||||
if file_name.match("terrain3d*.res") and not dir.current_is_dir():
|
||||
var region_loc: Vector2i = Terrain3DUtil.filename_to_location(file_name)
|
||||
var new_loc: Vector2i = region_loc + offset
|
||||
if new_loc.x < -16 or new_loc.x > 15 or new_loc.y < -16 or new_loc.y > 15:
|
||||
push_error("New location %.0v out of bounds for region %.0v. Aborting" % [ new_loc, region_loc ])
|
||||
return
|
||||
var new_name: String = "tmp_" + Terrain3DUtil.location_to_filename(new_loc)
|
||||
dir.rename(file_name, new_name)
|
||||
affected_files.push_back(new_name)
|
||||
print("File: %s renamed to: %s" % [ file_name, new_name ])
|
||||
|
||||
for file_name in affected_files:
|
||||
var new_name: String = file_name.trim_prefix("tmp_")
|
||||
dir.rename(file_name, new_name)
|
||||
print("File: %s renamed to: %s" % [ file_name, new_name ])
|
||||
|
||||
data_directory = dir_name
|
||||
EditorInterface.get_resource_filesystem().scan()
|
||||
1
addons/terrain_3d/extras/region_mover.gd.uid
Normal file
1
addons/terrain_3d/extras/region_mover.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bngnvtbm6ifkk
|
||||
Reference in New Issue
Block a user