added terrain3d

This commit is contained in:
derek
2025-03-31 14:14:50 -05:00
parent 27175618c0
commit bd767d2927
148 changed files with 2602 additions and 1381 deletions

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 Cory Petkovsek, Roope Palmroos, and Contributors.
Copyright (c) 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -3,34 +3,26 @@
# Terrain3D
A high performance, editable terrain system for Godot 4.
## Features
* Written in C++ as a GDExtension addon, which works with official engine builds
* Written in C++ as a GDExtension addon, which works with official builds of Godot Engine
* Can be accessed by GDScript, C#, and any language Godot supports
* Geometric Clipmap Mesh Terrain, as used in The Witcher 3. See [System Architecture](https://terrain3d.readthedocs.io/en/stable/docs/system_architecture.html)
* Geomorphing Geometric Clipmap Mesh Terrain, as used in The Witcher 3. See [System Architecture](https://terrain3d.readthedocs.io/en/stable/docs/system_architecture.html)
* Terrains as small as 64x64m up to 65.5x65.5km (4295km^2) in variable sized regions
* Up to 32 textures
* Up to 10 levels of detail
* Foliage instancing
* Up to 10 levels of detail for the terrain mesh
* Foliage instancing, with up to 10 levels of detail, and a shadow impostor
* Sculpting, holes, texture painting, texture detiling, painting colors and wetness
* Imports heightmaps from [HTerrain](https://github.com/Zylann/godot_heightmap_plugin/), WorldMachine, Unity, Unreal and any tool that can export a heightmap (raw/r16/exr/+). See [importing data](https://terrain3d.readthedocs.io/en/stable/docs/import_export.html)
* Imports heightmaps from [HTerrain](https://github.com/Zylann/godot_heightmap_plugin/), Gaea, World Creator, World Machine, Unity, Unreal and any tool that can export a heightmap. See [heightmaps](https://terrain3d.readthedocs.io/en/stable/docs/heightmaps.html)
See [Project Status](https://terrain3d.readthedocs.io/en/stable/docs/project_status.html) for details.
## Getting Started
1. Read the [Installation & Upgrades](https://terrain3d.readthedocs.io/en/stable/docs/installation.html) instructions.
2. For support, read [Getting Help](https://terrain3d.readthedocs.io/en/stable/docs/getting_help.html) or join our [Discord server](https://tokisan.com/discord).
2. For support, read [Getting Help](https://terrain3d.readthedocs.io/en/stable/docs/getting_help.html) and join our [Discord server](https://tokisan.com/discord).
3. Watch the tutorial videos:
**Installation, Setup, Basic Usage**
[![Using Terrain3D - Part 1](https://i.ytimg.com/vi/oV8c9alXVwU/hqdefault.jpg)](https://youtu.be/oV8c9alXVwU)
**Texture Painting, Holes, Navigation, Advanced Usage**
[![Using Terrain3D - Part 2](https://i.ytimg.com/vi/YtiAI2F6Xkk/hqdefault.jpg)](https://youtu.be/YtiAI2F6Xkk)
3. Watch the [tutorial videos](https://terrain3d.readthedocs.io/en/stable/docs/tutorial_videos.html).
## Credit
@@ -41,9 +33,8 @@ Developed for the Godot community by:
| **Cory Petkovsek, Tokisan Games** | [<img src="https://github.com/dmhendricks/signature-social-icons/blob/master/icons/round-flat-filled/35px/twitter.png?raw=true" width="24"/>](https://twitter.com/TokisanGames) [<img src="https://github.com/dmhendricks/signature-social-icons/blob/master/icons/round-flat-filled/35px/github.png?raw=true" width="24"/>](https://github.com/TokisanGames) [<img src="https://github.com/dmhendricks/signature-social-icons/blob/master/icons/round-flat-filled/35px/www.png?raw=true" width="24"/>](https://tokisan.com/) [<img src="https://github.com/dmhendricks/signature-social-icons/blob/master/icons/round-flat-filled/35px/discord.png?raw=true" width="24"/>](https://tokisan.com/discord) [<img src="https://github.com/dmhendricks/signature-social-icons/blob/master/icons/round-flat-filled/35px/youtube.png?raw=true" width="24"/>](https://www.youtube.com/@TokisanGames)|
| **Roope Palmroos, Outobugi Games** | [<img src="https://github.com/dmhendricks/signature-social-icons/blob/master/icons/round-flat-filled/35px/twitter.png?raw=true" width="24"/>](https://twitter.com/outobugi) [<img src="https://github.com/dmhendricks/signature-social-icons/blob/master/icons/round-flat-filled/35px/github.png?raw=true" width="24"/>](https://github.com/outobugi) [<img src="https://github.com/dmhendricks/signature-social-icons/blob/master/icons/round-flat-filled/35px/www.png?raw=true" width="24"/>](https://outobugi.com/) [<img src="https://github.com/dmhendricks/signature-social-icons/blob/master/icons/round-flat-filled/35px/youtube.png?raw=true" width="24"/>](https://www.youtube.com/@outobugi)|
And other contributors displayed on the right of the github page and in [AUTHORS.md](https://github.com/TokisanGames/Terrain3D/blob/main/AUTHORS.md).
And other contributors displayed on the right of the github page and in [AUTHORS.md](https://terrain3d.readthedocs.io/en/stable/docs/authors.html).
Geometry clipmap mesh code created by [Mike J. Savage](https://mikejsavage.co.uk/blog/geometry-clipmaps.html). Blog and repository code released under the MIT license per email communication with Mike.
## Contributing

Binary file not shown.

View File

@@ -1 +0,0 @@
uid://cuhwgfm8bkknk

View 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);
}

View File

@@ -0,0 +1 @@
uid://mri8pfoj2mfk

View File

@@ -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:
#

View File

@@ -1 +1 @@
uid://bm8g6gkvrerqy
uid://bllcuwetve45k

View 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;
}

View File

@@ -0,0 +1 @@
uid://bbx2xhanpq5l3

View 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;
}

View File

@@ -0,0 +1 @@
uid://bda7fq1rh3nmv

View 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;
}

View File

@@ -0,0 +1 @@
uid://x11v7w7v8hqa

View File

@@ -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;
}

View File

@@ -1 +1 @@
uid://c8qog5mlaoeno
uid://01qauauvd8aa

View File

@@ -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.

View File

@@ -1 +1 @@
uid://ce403ehalp57b
uid://g3opjh3m3iww

View 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()

View File

@@ -0,0 +1 @@
uid://bngnvtbm6ifkk

View File

@@ -1,3 +1,5 @@
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
# Bake LOD Dialog for Terrain3D
@tool
extends ConfirmationDialog

View File

@@ -0,0 +1 @@
uid://cqmt8f5x5c2ad

View File

@@ -1,6 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://bhvrrmb8bk1bt"]
[ext_resource type="Script" uid="uid://dft2g3p2pjw12" path="res://addons/terrain_3d/src/bake_lod_dialog.gd" id="1_sf76d"]
[ext_resource type="Script" path="res://addons/terrain_3d/menu/bake_lod_dialog.gd" id="1_sf76d"]
[node name="bake_lod_dialog" type="ConfirmationDialog"]
title = "Bake Terrain3D Mesh"

View File

@@ -1,6 +1,8 @@
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
# Baker for Terrain3D
extends Node
const BakeLodDialog: PackedScene = preload("res://addons/terrain_3d/src/bake_lod_dialog.tscn")
const BakeLodDialog: PackedScene = preload("res://addons/terrain_3d/menu/bake_lod_dialog.tscn")
const BAKE_MESH_DESCRIPTION: String = "This will create a child MeshInstance3D. LOD4+ is recommended. LOD0 is slow and dense with vertices every 1 unit. It is not an optimal mesh."
const BAKE_OCCLUDER_DESCRIPTION: String = "This will create a child OccluderInstance3D. LOD4+ is recommended and will take 5+ seconds per region to generate. LOD0 is unnecessarily dense and slow."
const SET_UP_NAVIGATION_DESCRIPTION: String = "This operation will:
@@ -194,7 +196,7 @@ func _bake_nav_region_nav_mesh(p_nav_region: NavigationRegion3D) -> void:
assert(nav_mesh != null)
var source_geometry_data := NavigationMeshSourceGeometryData3D.new()
NavigationMeshGenerator.parse_source_geometry_data(nav_mesh, source_geometry_data, p_nav_region)
NavigationServer3D.parse_source_geometry_data(nav_mesh, source_geometry_data, p_nav_region)
for terrain in find_nav_region_terrains(p_nav_region):
var aabb: AABB = nav_mesh.filter_baking_aabb
@@ -204,7 +206,7 @@ func _bake_nav_region_nav_mesh(p_nav_region: NavigationRegion3D) -> void:
if not faces.is_empty():
source_geometry_data.add_faces(faces, Transform3D.IDENTITY)
NavigationMeshGenerator.bake_from_source_geometry_data(nav_mesh, source_geometry_data)
NavigationServer3D.bake_from_source_geometry_data(nav_mesh, source_geometry_data)
_postprocess_nav_mesh(nav_mesh)
@@ -374,10 +376,8 @@ func _do_set_up_navigation(p_nav_region: NavigationRegion3D, p_terrain: Terrain3
var index: int = p_terrain.get_index()
var t_owner: Node = p_terrain.owner
parent.remove_child(p_terrain)
p_nav_region.add_child(p_terrain)
parent.add_child(p_nav_region, true)
p_terrain.reparent(p_nav_region)
parent.move_child(p_nav_region, index)
p_nav_region.owner = t_owner
@@ -391,10 +391,8 @@ func _undo_set_up_navigation(p_nav_region: NavigationRegion3D, p_terrain: Terrai
var index: int = p_nav_region.get_index()
var t_owner: Node = p_nav_region.get_owner()
p_terrain.reparent(parent)
parent.remove_child(p_nav_region)
p_nav_region.remove_child(p_terrain)
parent.add_child(p_terrain, true)
parent.move_child(p_terrain, index)
p_terrain.owner = t_owner

View File

@@ -0,0 +1 @@
uid://4ulaeevj5jvi

View File

@@ -1,8 +1,10 @@
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
# Channel Packer for Terrain3D
extends RefCounted
const WINDOW_SCENE: String = "res://addons/terrain_3d/src/channel_packer.tscn"
const TEMPLATE_PATH: String = "res://addons/terrain_3d/src/channel_packer_import_template.txt"
const DRAG_DROP_SCRIPT: String = "res://addons/terrain_3d/src/channel_packer_dragdrop.gd"
const WINDOW_SCENE: String = "res://addons/terrain_3d/menu/channel_packer.tscn"
const TEMPLATE_PATH: String = "res://addons/terrain_3d/menu/channel_packer_import_template.txt"
const DRAG_DROP_SCRIPT: String = "res://addons/terrain_3d/menu/channel_packer_dragdrop.gd"
enum {
INFO,
WARN,
@@ -47,7 +49,7 @@ var normal_vector: Vector3
func pack_textures_popup() -> void:
if window != null:
window.show()
window.move_to_foreground()
window.grab_focus()
window.move_to_center()
return
window = (load(WINDOW_SCENE) as PackedScene).instantiate()
@@ -108,9 +110,8 @@ func pack_textures_popup() -> void:
_init_texture_picker(window.find_child("HeightVBox"), IMAGE_HEIGHT)
_init_texture_picker(window.find_child("NormalVBox"), IMAGE_NORMAL)
_init_texture_picker(window.find_child("RoughnessVBox"), IMAGE_ROUGHNESS)
var pack_button_path: String = "Panel/MarginContainer/VBoxContainer/PackButton"
(window.get_node(pack_button_path) as Button).pressed.connect(_on_pack_button_pressed)
(window.find_child("PackButton") as Button).pressed.connect(_on_pack_button_pressed)
func _on_close_requested() -> void:
@@ -370,7 +371,7 @@ func _on_save_file_selected(p_dst_path) -> void:
save_file_dialog.title = "Save Packed Normal/Roughness Texture"
save_file_dialog.call_deferred("popup_centered_ratio")
save_file_dialog.call_deferred("move_to_foreground")
save_file_dialog.call_deferred("grab_focus")
func _alignment_basis(normal: Vector3) -> Basis:
@@ -393,8 +394,8 @@ func _set_normal_vector(source: Image, quiet: bool = false) -> void:
# Calculate texture normal sum direction
var normal: Image = source
var sum: Color = Color(0.0, 0.0, 0.0, 0.0)
for x in normal.get_height():
for y in normal.get_width():
for x in normal.get_width():
for y in normal.get_height():
sum += normal.get_pixel(x, y)
var div: float = normal.get_height() * normal.get_width()
sum /= Color(div, div, div)
@@ -409,8 +410,8 @@ func _align_normals(source: Image, iteration: int = 0) -> void:
# generate matrix to re-align the normalmap
var mat3: Basis = _alignment_basis(normal_vector)
# re-align the normal map pixels
for x in source.get_height():
for y in source.get_width():
for x in source.get_width():
for y in source.get_height():
var old_pixel: Color = source.get_pixel(x, y)
var vector_pixel: Vector3 = Vector3(old_pixel.r, old_pixel.g, old_pixel.b)
vector_pixel *= 2.0
@@ -450,10 +451,9 @@ func _pack_textures(p_rgb_image: Image, p_a_image: Image, p_dst_path: String, p_
if not output_image:
_show_message(ERROR, "Failed to pack textures")
return FAILED
if output_image.detect_used_channels() != 5:
_show_message(ERROR, "Packing Error, Alpha Channel empty")
return FAILED
if output_image.detect_alpha() != Image.ALPHA_BLEND:
_show_message(WARN, "Warning, Alpha channel empty")
output_image.save_png(p_dst_path)
_create_import_file(p_dst_path)
_show_message(INFO, "Packed to " + p_dst_path + ".")

View File

@@ -0,0 +1 @@
uid://bwldx4itd58o7

View File

@@ -0,0 +1,530 @@
[gd_scene load_steps=7 format=3 uid="uid://nud6dwjcnj5v"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ysabf"]
bg_color = Color(0.211765, 0.239216, 0.290196, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lcvna"]
bg_color = Color(0.168627, 0.211765, 0.266667, 1)
border_width_left = 3
border_width_top = 3
border_width_right = 3
border_width_bottom = 3
border_color = Color(0.270588, 0.435294, 0.580392, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cb0xf"]
bg_color = Color(0.137255, 0.137255, 0.137255, 1)
draw_center = false
border_width_left = 3
border_width_top = 3
border_width_right = 3
border_width_bottom = 3
border_color = Color(0.784314, 0.784314, 0.784314, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_7qdas"]
[sub_resource type="ButtonGroup" id="ButtonGroup_wnxik"]
[sub_resource type="ButtonGroup" id="ButtonGroup_bs6ki"]
[node name="Window" type="Window"]
title = "Terrain3D Channel Packer"
initial_position = 1
size = Vector2i(583, 856)
wrap_controls = true
always_on_top = true
[node name="PanelContainer" type="PanelContainer" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_ysabf")
[node name="MarginContainer" type="MarginContainer" parent="PanelContainer"]
layout_mode = 2
theme_override_constants/margin_left = 5
theme_override_constants/margin_top = 5
theme_override_constants/margin_right = 5
theme_override_constants/margin_bottom = 5
[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/MarginContainer"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="AlbedoHeightPanel" type="PanelContainer" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_lcvna")
[node name="MarginContainer" type="MarginContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel"]
layout_mode = 2
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 10
[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer"]
layout_mode = 2
[node name="AlbedoVBox" type="VBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="AlbedoLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"]
layout_mode = 2
text = "Albedo texture"
[node name="AlbedoHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoHBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PickButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoHBox"]
layout_mode = 2
[node name="ClearButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoHBox"]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"]
layout_mode = 2
size_flags_vertical = 4
theme_override_constants/margin_top = 10
[node name="Panel" type="Panel" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/MarginContainer"]
custom_minimum_size = Vector2(110, 110)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
theme_override_styles/panel = SubResource("StyleBoxFlat_cb0xf")
[node name="TextureRect" type="TextureRect" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -50.0
offset_top = -50.0
offset_right = 50.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
[node name="TextureButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas")
[node name="AlbedoWHHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"]
layout_mode = 2
alignment = 1
[node name="AlbedoW" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="AlbedoH" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="HBoxContainer2" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"]
layout_mode = 2
alignment = 1
[node name="LuminanceAsHeightButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/HBoxContainer2"]
layout_mode = 2
text = " Generate Height from Luminance"
icon_alignment = 2
[node name="HeightVBox" type="VBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="HeightLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
text = "Height texture"
[node name="HeightHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightHBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PickButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightHBox"]
layout_mode = 2
[node name="ClearButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightHBox"]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
size_flags_vertical = 4
theme_override_constants/margin_top = 10
[node name="Panel" type="Panel" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/MarginContainer"]
custom_minimum_size = Vector2(110, 110)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
theme_override_styles/panel = SubResource("StyleBoxFlat_cb0xf")
[node name="TextureRect" type="TextureRect" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -50.0
offset_top = -50.0
offset_right = 50.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
[node name="TextureButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas")
[node name="HeightWHHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
alignment = 1
[node name="HeightW" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="HeightH" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="HBoxContainer2" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
alignment = 1
[node name="ConvertDepthToHeight" type="CheckBox" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer2"]
layout_mode = 2
text = " Convert Depth to Height"
icon_alignment = 2
[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
alignment = 1
[node name="HeightChannelLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"]
layout_mode = 2
text = " Source Channel: "
horizontal_alignment = 2
[node name="HeightChannelR" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_pressed = true
button_group = SubResource("ButtonGroup_wnxik")
text = "R"
[node name="HeightChannelB" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_wnxik")
text = "G"
[node name="HeightChannelG" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_wnxik")
text = "B"
[node name="HeightChannelA" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_wnxik")
text = "A"
[node name="NormalRoughnessPanel" type="PanelContainer" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_lcvna")
[node name="MarginContainer" type="MarginContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel"]
layout_mode = 2
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 10
[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer"]
layout_mode = 2
[node name="NormalVBox" type="VBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="NormalLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
text = "Normal texture"
[node name="NormalHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalHBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PickButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalHBox"]
layout_mode = 2
[node name="ClearButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalHBox"]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
size_flags_vertical = 4
theme_override_constants/margin_top = 10
[node name="Panel" type="Panel" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/MarginContainer"]
custom_minimum_size = Vector2(110, 110)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
theme_override_styles/panel = SubResource("StyleBoxFlat_cb0xf")
[node name="TextureRect" type="TextureRect" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -50.0
offset_top = -50.0
offset_right = 50.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
[node name="TextureButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas")
[node name="NormalWHHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
alignment = 1
[node name="NormalW" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="NormalH" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
alignment = 1
[node name="InvertGreenChannelCheckBox" type="CheckBox" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/HBoxContainer"]
layout_mode = 2
text = " Convert DirectX to OpenGL"
[node name="HBoxContainer2" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
alignment = 1
[node name="AlignNormalsCheckBox" type="CheckBox" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/HBoxContainer2"]
layout_mode = 2
text = " Orthoganolise Normals"
[node name="RoughnessVBox" type="VBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="RoughnessLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
text = "Roughness texture"
[node name="RoughnessHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessHBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PickButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessHBox"]
layout_mode = 2
[node name="ClearButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessHBox"]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
size_flags_vertical = 4
theme_override_constants/margin_top = 10
[node name="Panel" type="Panel" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/MarginContainer"]
custom_minimum_size = Vector2(110, 110)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
theme_override_styles/panel = SubResource("StyleBoxFlat_cb0xf")
[node name="TextureRect" type="TextureRect" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -50.0
offset_top = -50.0
offset_right = 50.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
[node name="TextureButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas")
[node name="RoughnessWHHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
alignment = 1
[node name="RoughnessW" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="RoughnessH" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="HBoxContainer2" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
alignment = 1
[node name="InvertSmoothCheckBox" type="CheckBox" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer2"]
layout_mode = 2
text = " Convert Smoothness to Roughness"
[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
alignment = 1
[node name="RoughnessChannelLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"]
layout_mode = 2
text = " Source Channel: "
horizontal_alignment = 2
[node name="RoughnessChannelR" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_pressed = true
button_group = SubResource("ButtonGroup_bs6ki")
text = "R"
[node name="RoughnessChannelG" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_bs6ki")
text = "G"
[node name="RoughnessChannelB" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_bs6ki")
text = "B"
[node name="RoughnessChannelA" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_bs6ki")
text = "A"
[node name="GeneralOptionsLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
text = "General Options"
horizontal_alignment = 1
vertical_alignment = 1
[node name="GeneralOptionsHBox" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
alignment = 1
[node name="ResizeToggle" type="CheckBox" parent="PanelContainer/MarginContainer/VBoxContainer/GeneralOptionsHBox"]
layout_mode = 2
text = " Resize Packed Image"
[node name="ResizeOptionButton" type="SpinBox" parent="PanelContainer/MarginContainer/VBoxContainer/GeneralOptionsHBox"]
visible = false
layout_mode = 2
tooltip_text = "A value of 0 disables resizing."
min_value = 128.0
max_value = 4096.0
step = 128.0
value = 1024.0
[node name="VSeparator" type="VSeparator" parent="PanelContainer/MarginContainer/VBoxContainer/GeneralOptionsHBox"]
layout_mode = 2
[node name="GenerateMipmapsCheckBox" type="CheckBox" parent="PanelContainer/MarginContainer/VBoxContainer/GeneralOptionsHBox"]
layout_mode = 2
button_pressed = true
text = "Generate Mipmaps"
[node name="HighQualityCheckBox" type="CheckBox" parent="PanelContainer/MarginContainer/VBoxContainer/GeneralOptionsHBox"]
layout_mode = 2
text = "Import High Quality"
[node name="PackButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
text = "Pack textures as..."
[node name="StatusLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer"]
custom_minimum_size = Vector2(0, 60)
layout_mode = 2
horizontal_alignment = 1
autowrap_mode = 3
[node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
alignment = 1
[node name="CloseButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "Close"
[connection signal="toggled" from="PanelContainer/MarginContainer/VBoxContainer/GeneralOptionsHBox/ResizeToggle" to="PanelContainer/MarginContainer/VBoxContainer/GeneralOptionsHBox/ResizeOptionButton" method="set_visible"]

View File

@@ -1,3 +1,5 @@
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
# Channel Packer Dragdropper for Terrain3D
@tool
extends Button

View File

@@ -0,0 +1 @@
uid://br45krrqbw8bg

View File

@@ -1,13 +1,13 @@
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
# Directory Setup for Terrain3D
extends Node
const DIRECTORY_SETUP: String = "res://addons/terrain_3d/src/directory_setup.tscn"
const DIRECTORY_SETUP: String = "res://addons/terrain_3d/menu/directory_setup.tscn"
var plugin: EditorPlugin
var dialog: ConfirmationDialog
var select_dir_btn: Button
var selected_dir_le: LineEdit
var select_upg_btn: Button
var upgrade_file_le: LineEdit
var editor_file_dialog: EditorFileDialog
@@ -18,7 +18,6 @@ func _init() -> void:
editor_file_dialog.access = EditorFileDialog.ACCESS_RESOURCES
editor_file_dialog.ok_button_text = "Open"
editor_file_dialog.title = "Open a folder or file"
editor_file_dialog.file_selected.connect(_on_file_selected)
editor_file_dialog.dir_selected.connect(_on_dir_selected)
editor_file_dialog.size = Vector2i(850, 550)
editor_file_dialog.transient = false
@@ -34,22 +33,14 @@ func directory_setup_popup() -> void:
# Nodes
select_dir_btn = dialog.get_node("Margin/VBox/DirHBox/SelectDir")
selected_dir_le = dialog.get_node("Margin/VBox/DirHBox/LineEdit")
select_upg_btn = dialog.get_node("Margin/VBox/UpgradeHBox/SelectResFile")
upgrade_file_le = dialog.get_node("Margin/VBox/UpgradeHBox/LineEdit")
upgrade_file_le.text = ""
if plugin.terrain.data_directory:
selected_dir_le.text = plugin.terrain.data_directory
if plugin.terrain.storage:
upgrade_file_le.text = plugin.terrain.storage.get_path()
# Icons
plugin.ui.set_button_editor_icon(select_upg_btn, "Folder")
plugin.ui.set_button_editor_icon(select_dir_btn, "Folder")
#Signals
select_upg_btn.pressed.connect(_on_select_file_pressed.bind(EditorFileDialog.FILE_MODE_OPEN_FILE))
select_dir_btn.pressed.connect(_on_select_file_pressed.bind(EditorFileDialog.FILE_MODE_OPEN_DIR))
dialog.confirmed.connect(_on_close_requested)
dialog.canceled.connect(_on_close_requested)
@@ -73,10 +64,6 @@ func _on_dir_selected(path: String) -> void:
selected_dir_le.text = path
func _on_file_selected(path: String) -> void:
upgrade_file_le.text = path
func _on_ok_pressed() -> void:
if not plugin.terrain:
push_error("Not connected terrain. Click the Terrain3D node first")
@@ -97,20 +84,3 @@ func _on_ok_pressed() -> void:
print("Setting terrain directory: ", selected_dir_le.text)
plugin.terrain.data_directory = selected_dir_le.text
if not upgrade_file_le.text.is_empty():
if data_found:
push_warning("Target directory already has terrain data. Specify an empty directory to upgrade")
return
if not FileAccess.file_exists(upgrade_file_le.text):
push_error("File doesn't exist: ", upgrade_file_le.text)
return
if not plugin.terrain.storage or \
( plugin.terrain.storage and plugin.terrain.storage.get_path() != upgrade_file_le.text):
print("Loading storage file: ", upgrade_file_le.text)
plugin.terrain.set_storage(load(upgrade_file_le.text))
if plugin.terrain.storage:
print("Begining upgrade of: ", upgrade_file_le.text)
plugin.terrain.split_storage()

View File

@@ -0,0 +1 @@
uid://0034ukv2mngn

View File

@@ -3,14 +3,14 @@
[node name="DirectorySetup" type="ConfirmationDialog"]
title = "Terrain3D Data Directory Setup"
position = Vector2i(0, 36)
size = Vector2i(750, 574)
size = Vector2i(750, 330)
visible = true
[node name="Margin" type="MarginContainer" parent="."]
offset_left = 8.0
offset_top = 8.0
offset_right = 742.0
offset_bottom = 525.0
offset_bottom = 281.0
theme_override_constants/margin_left = 20
theme_override_constants/margin_top = 20
theme_override_constants/margin_right = 20
@@ -43,24 +43,6 @@ placeholder_text = "Data directory"
[node name="SelectDir" type="Button" parent="Margin/VBox/DirHBox"]
layout_mode = 2
[node name="UpgradeLabel" type="Label" parent="Margin/VBox"]
custom_minimum_size = Vector2(400, 0)
layout_mode = 2
text = "
If you wish to upgrade a storage file from v0.8.4 - v0.9.2, specify it below. Data will be stored in the directory above upon save. The original file will not be touched."
autowrap_mode = 3
[node name="UpgradeHBox" type="HBoxContainer" parent="Margin/VBox"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="Margin/VBox/UpgradeHBox"]
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Storage .res to upgrade"
[node name="SelectResFile" type="Button" parent="Margin/VBox/UpgradeHBox"]
layout_mode = 2
[node name="Spacer" type="Control" parent="Margin/VBox"]
custom_minimum_size = Vector2(0, 40)
layout_mode = 2

View File

@@ -1,9 +1,11 @@
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
# Menu for Terrain3D
extends HBoxContainer
const DirectoryWizard: Script = preload("res://addons/terrain_3d/src/directory_setup.gd")
const Packer: Script = preload("res://addons/terrain_3d/src/channel_packer.gd")
const Baker: Script = preload("res://addons/terrain_3d/src/baker.gd")
const DirectoryWizard: Script = preload("res://addons/terrain_3d/menu/directory_setup.gd")
const Packer: Script = preload("res://addons/terrain_3d/menu/channel_packer.gd")
const Baker: Script = preload("res://addons/terrain_3d/menu/baker.gd")
var plugin: EditorPlugin
var menu_button: MenuButton = MenuButton.new()
@@ -31,7 +33,7 @@ func _enter_tree() -> void:
add_child(directory_setup)
add_child(baker)
menu_button.text = "Terrain3D Tools"
menu_button.text = "Terrain3D"
menu_button.get_popup().add_item("Directory Setup...", MENU_DIRECTORY_SETUP)
menu_button.get_popup().add_item("Pack Textures...", MENU_PACK_TEXTURES)
menu_button.get_popup().add_separator("", MENU_SEPARATOR)

View File

@@ -0,0 +1 @@
uid://3gxvahogxa10

View File

@@ -3,5 +3,5 @@
name="Terrain3D"
description="A high performance, editable terrain system for Godot 4."
author="Cory Petkovsek & Roope Palmroos"
version="0.9.3a"
script="editor.gd"
version="1.0.0"
script="src/editor_plugin.gd"

View File

@@ -1,6 +1,7 @@
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
# Asset Dock for Terrain3D
@tool
extends PanelContainer
#class_name Terrain3DAssetDock
signal confirmation_closed
signal confirmation_confirmed
@@ -444,9 +445,6 @@ class ListContainer extends Container:
func _ready() -> void:
set_v_size_flags(SIZE_EXPAND_FILL)
set_h_size_flags(SIZE_EXPAND_FILL)
focus_style = get_theme_stylebox("focus", "Button").duplicate()
focus_style.set_border_width_all(2)
focus_style.set_border_color(Color(1, 1, 1, .67))
func clear() -> void:
@@ -588,17 +586,13 @@ class ListContainer extends Container:
last_offset = 3
set_selected_id(clamp(selected_id, 0, entries.size() - last_offset))
# Update editor with selected brush
plugin.ui._on_setting_changed()
func get_selected_id() -> int:
return selected_id
func set_entry_width(value: float) -> void:
width = clamp(value, 56, 230)
width = clamp(value, 66, 230)
redraw()
@@ -651,45 +645,86 @@ class ListEntry extends VBoxContainer:
var is_selected: bool = false
var asset_list: Terrain3DAssets
var button_clear: TextureButton
var button_edit: TextureButton
var name_label: Label
@onready var add_icon: Texture2D = get_theme_icon("Add", "EditorIcons")
@onready var button_row := HBoxContainer.new()
@onready var button_clear := TextureButton.new()
@onready var button_edit := TextureButton.new()
@onready var spacer := Control.new()
@onready var button_enabled := TextureButton.new()
@onready var clear_icon: Texture2D = get_theme_icon("Close", "EditorIcons")
@onready var edit_icon: Texture2D = get_theme_icon("Edit", "EditorIcons")
@onready var enabled_icon: Texture2D = get_theme_icon("GuiVisibilityVisible", "EditorIcons")
@onready var disabled_icon: Texture2D = get_theme_icon("GuiVisibilityHidden", "EditorIcons")
var name_label: Label
@onready var add_icon: Texture2D = get_theme_icon("Add", "EditorIcons")
@onready var background: StyleBox = get_theme_stylebox("pressed", "Button")
var focus_style: StyleBox
@onready var focus_style: StyleBox = get_theme_stylebox("focus", "Button").duplicate()
func _ready() -> void:
setup_buttons()
setup_label()
focus_style.set_border_width_all(2)
focus_style.set_border_color(Color(1, 1, 1, .67))
func setup_buttons() -> void:
var icon_size: Vector2 = Vector2(12, 12)
var margin_container := MarginContainer.new()
margin_container.mouse_filter = Control.MOUSE_FILTER_PASS
margin_container.add_theme_constant_override("margin_top", 5)
margin_container.add_theme_constant_override("margin_left", 5)
margin_container.add_theme_constant_override("margin_right", 5)
add_child(margin_container)
button_clear = TextureButton.new()
button_clear.set_texture_normal(clear_icon)
button_clear.set_custom_minimum_size(icon_size)
button_clear.set_h_size_flags(Control.SIZE_SHRINK_END)
button_clear.set_visible(resource != null)
button_clear.pressed.connect(clear)
add_child(button_clear)
button_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button_row.alignment = BoxContainer.ALIGNMENT_CENTER
button_row.mouse_filter = Control.MOUSE_FILTER_PASS
margin_container.add_child(button_row)
if type == Terrain3DAssets.TYPE_MESH:
button_enabled.set_texture_normal(enabled_icon)
button_enabled.set_texture_pressed(disabled_icon)
button_enabled.set_custom_minimum_size(icon_size)
button_enabled.set_h_size_flags(Control.SIZE_SHRINK_END)
button_enabled.set_visible(resource != null)
button_enabled.toggle_mode = true
button_enabled.mouse_filter = Control.MOUSE_FILTER_PASS
button_enabled.pressed.connect(enable)
button_row.add_child(button_enabled)
button_edit = TextureButton.new()
button_edit.set_texture_normal(edit_icon)
button_edit.set_custom_minimum_size(icon_size)
button_edit.set_h_size_flags(Control.SIZE_SHRINK_END)
button_edit.set_visible(resource != null)
button_edit.mouse_filter = Control.MOUSE_FILTER_PASS
button_edit.pressed.connect(edit)
add_child(button_edit)
button_row.add_child(button_edit)
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
spacer.mouse_filter = Control.MOUSE_FILTER_PASS
button_row.add_child(spacer)
button_clear.set_texture_normal(clear_icon)
button_clear.set_custom_minimum_size(icon_size)
button_clear.set_h_size_flags(Control.SIZE_SHRINK_END)
button_clear.set_visible(resource != null)
button_clear.mouse_filter = Control.MOUSE_FILTER_PASS
button_clear.pressed.connect(clear)
button_row.add_child(button_clear)
func setup_label() -> void:
name_label = Label.new()
add_child(name_label, true)
name_label.visible = false
name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
name_label.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
name_label.size_flags_vertical = Control.SIZE_EXPAND_FILL
name_label.add_theme_color_override("font_color", Color.WHITE)
name_label.add_theme_color_override("font_shadow_color", Color.BLACK)
name_label.add_theme_constant_override("shadow_offset_x", 1)
name_label.add_theme_constant_override("shadow_offset_y", 1)
name_label.add_theme_constant_override("shadow_offset_x", 1.)
name_label.add_theme_constant_override("shadow_offset_y", 1.)
name_label.add_theme_font_size_override("font_size", 15)
name_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
name_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
@@ -702,6 +737,9 @@ class ListEntry extends VBoxContainer:
func _notification(p_what) -> void:
match p_what:
NOTIFICATION_DRAW:
# Hide spacer if icons are crowding small textures
spacer.visible = size.x > 70 or type == Terrain3DAssets.TYPE_TEXTURE
var rect: Rect2 = Rect2(Vector2.ZERO, get_size())
if !resource:
draw_style_box(background, rect)
@@ -723,6 +761,7 @@ class ListEntry extends VBoxContainer:
texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS
else:
draw_rect(rect, Color(.15, .15, .15, 1.))
button_enabled.set_pressed_no_signal(!resource.is_enabled())
name_label.add_theme_font_size_override("font_size", 4 + rect.size.x/10)
if drop_data:
draw_style_box(focus_style, rect)
@@ -791,8 +830,8 @@ class ListEntry extends VBoxContainer:
var ma := Terrain3DMeshAsset.new()
if resource is Terrain3DMeshAsset:
ma.id = resource.id
ma.set_scene_file(res)
set_edited_resource(ma, false)
ma.set_scene_file(res)
resource = ma
elif res is Terrain3DMeshAsset and type == Terrain3DAssets.TYPE_MESH:
if resource is Terrain3DMeshAsset:
@@ -808,6 +847,8 @@ class ListEntry extends VBoxContainer:
if resource:
resource.setting_changed.connect(_on_resource_changed)
resource.file_changed.connect(_on_resource_changed)
if resource is Terrain3DMeshAsset:
resource.instancer_setting_changed.connect(_on_resource_changed)
if button_clear:
button_clear.set_visible(resource != null)
@@ -818,6 +859,7 @@ class ListEntry extends VBoxContainer:
func _on_resource_changed() -> void:
queue_redraw()
emit_signal("changed", resource)
@@ -834,3 +876,8 @@ class ListEntry extends VBoxContainer:
func edit() -> void:
emit_signal("selected")
emit_signal("inspected", resource)
func enable() -> void:
if resource is Terrain3DMeshAsset:
resource.set_enabled(!resource.is_enabled())

View File

@@ -1 +1 @@
uid://bcfr30tpffi5o
uid://bgoifepft1hjw

View File

@@ -1,6 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://dkb6hii5e48m2"]
[ext_resource type="Script" uid="uid://bcfr30tpffi5o" path="res://addons/terrain_3d/src/asset_dock.gd" id="1_e23pg"]
[ext_resource type="Script" path="res://addons/terrain_3d/src/asset_dock.gd" id="1_e23pg"]
[node name="Terrain3D" type="PanelContainer"]
custom_minimum_size = Vector2(256, 95)
@@ -40,10 +40,9 @@ custom_minimum_size = Vector2(80, 30)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 0
item_count = 9
selected = 7
item_count = 9
popup/item_0/text = "Left_UL"
popup/item_0/id = 0
popup/item_1/text = "Left_BL"
popup/item_1/id = 1
popup/item_2/text = "Left_UR"
@@ -65,7 +64,7 @@ popup/item_8/id = 8
custom_minimum_size = Vector2(80, 10)
layout_mode = 2
size_flags_horizontal = 3
min_value = 56.0
min_value = 66.0
max_value = 230.0
value = 83.0

View File

@@ -1 +0,0 @@
uid://dft2g3p2pjw12

View File

@@ -1 +0,0 @@
uid://byi465tyumga0

View File

@@ -1 +0,0 @@
uid://y7pm2ndr5k2m

View File

@@ -1,553 +0,0 @@
[gd_scene load_steps=7 format=3 uid="uid://nud6dwjcnj5v"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ysabf"]
bg_color = Color(0.211765, 0.239216, 0.290196, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lcvna"]
bg_color = Color(0.168627, 0.211765, 0.266667, 1)
border_width_left = 3
border_width_top = 3
border_width_right = 3
border_width_bottom = 3
border_color = Color(0.270588, 0.435294, 0.580392, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cb0xf"]
bg_color = Color(0.137255, 0.137255, 0.137255, 1)
draw_center = false
border_width_left = 3
border_width_top = 3
border_width_right = 3
border_width_bottom = 3
border_color = Color(0.784314, 0.784314, 0.784314, 1)
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_7qdas"]
[sub_resource type="ButtonGroup" id="ButtonGroup_wnxik"]
[sub_resource type="ButtonGroup" id="ButtonGroup_bs6ki"]
[node name="Window" type="Window"]
title = "Terrain3D Channel Packer"
initial_position = 1
size = Vector2i(680, 835)
unresizable = true
always_on_top = true
[node name="Panel" type="Panel" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_ysabf")
[node name="MarginContainer" type="MarginContainer" parent="Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 5.0
offset_top = 5.0
offset_right = -5.0
offset_bottom = 5.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/margin_left = 5
theme_override_constants/margin_top = 5
theme_override_constants/margin_right = 5
theme_override_constants/margin_bottom = 5
[node name="VBoxContainer" type="VBoxContainer" parent="Panel/MarginContainer"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="AlbedoHeightPanel" type="Panel" parent="Panel/MarginContainer/VBoxContainer"]
custom_minimum_size = Vector2(0, 290)
layout_mode = 2
mouse_filter = 1
theme_override_styles/panel = SubResource("StyleBoxFlat_lcvna")
[node name="MarginContainer" type="MarginContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 10
[node name="HBoxContainer" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer"]
layout_mode = 2
[node name="AlbedoVBox" type="VBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="AlbedoLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"]
layout_mode = 2
text = "Albedo texture"
[node name="AlbedoHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoHBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PickButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoHBox"]
layout_mode = 2
[node name="ClearButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoHBox"]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"]
layout_mode = 2
size_flags_vertical = 4
theme_override_constants/margin_top = 10
[node name="Panel" type="Panel" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/MarginContainer"]
custom_minimum_size = Vector2(110, 110)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
theme_override_styles/panel = SubResource("StyleBoxFlat_cb0xf")
[node name="TextureRect" type="TextureRect" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -50.0
offset_top = -50.0
offset_right = 50.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
[node name="TextureButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas")
[node name="AlbedoWHHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"]
layout_mode = 2
alignment = 1
[node name="AlbedoW" type="Label" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="AlbedoH" type="Label" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/AlbedoWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="HBoxContainer2" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox"]
layout_mode = 2
alignment = 1
[node name="LuminanceAsHeightButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/AlbedoVBox/HBoxContainer2"]
layout_mode = 2
text = " Generate Height from Luminance"
icon_alignment = 2
[node name="HeightVBox" type="VBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="HeightLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
text = "Height texture"
[node name="HeightHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightHBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PickButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightHBox"]
layout_mode = 2
[node name="ClearButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightHBox"]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
size_flags_vertical = 4
theme_override_constants/margin_top = 10
[node name="Panel" type="Panel" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/MarginContainer"]
custom_minimum_size = Vector2(110, 110)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
theme_override_styles/panel = SubResource("StyleBoxFlat_cb0xf")
[node name="TextureRect" type="TextureRect" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -50.0
offset_top = -50.0
offset_right = 50.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
[node name="TextureButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas")
[node name="HeightWHHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
alignment = 1
[node name="HeightW" type="Label" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="HeightH" type="Label" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HeightWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="HBoxContainer2" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
alignment = 1
[node name="ConvertDepthToHeight" type="CheckBox" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer2"]
layout_mode = 2
text = " Convert Depth to Height"
icon_alignment = 2
[node name="HBoxContainer" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox"]
layout_mode = 2
alignment = 1
[node name="HeightChannelLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"]
layout_mode = 2
text = " Source Channel: "
horizontal_alignment = 2
[node name="HeightChannelR" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_pressed = true
button_group = SubResource("ButtonGroup_wnxik")
text = "R"
[node name="HeightChannelB" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_wnxik")
text = "G"
[node name="HeightChannelG" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_wnxik")
text = "B"
[node name="HeightChannelA" type="Button" parent="Panel/MarginContainer/VBoxContainer/AlbedoHeightPanel/MarginContainer/HBoxContainer/HeightVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_wnxik")
text = "A"
[node name="NormalRoughnessPanel" type="Panel" parent="Panel/MarginContainer/VBoxContainer"]
custom_minimum_size = Vector2(0, 290)
layout_mode = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_lcvna")
[node name="MarginContainer" type="MarginContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 10
[node name="HBoxContainer" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer"]
layout_mode = 2
[node name="NormalVBox" type="VBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="NormalLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
text = "Normal texture"
[node name="NormalHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalHBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PickButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalHBox"]
layout_mode = 2
[node name="ClearButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalHBox"]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
size_flags_vertical = 4
theme_override_constants/margin_top = 10
[node name="Panel" type="Panel" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/MarginContainer"]
custom_minimum_size = Vector2(110, 110)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
theme_override_styles/panel = SubResource("StyleBoxFlat_cb0xf")
[node name="TextureRect" type="TextureRect" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -50.0
offset_top = -50.0
offset_right = 50.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
[node name="TextureButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas")
[node name="NormalWHHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
alignment = 1
[node name="NormalW" type="Label" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="NormalH" type="Label" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/NormalWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="HBoxContainer" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
alignment = 1
[node name="InvertGreenChannelCheckBox" type="CheckBox" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/HBoxContainer"]
layout_mode = 2
text = " Convert DirectX to OpenGL"
[node name="HBoxContainer2" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox"]
layout_mode = 2
alignment = 1
[node name="AlignNormalsCheckBox" type="CheckBox" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/NormalVBox/HBoxContainer2"]
layout_mode = 2
text = " Orthoganolise Normals"
[node name="RoughnessVBox" type="VBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="RoughnessLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
text = "Roughness texture"
[node name="RoughnessHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessHBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="PickButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessHBox"]
layout_mode = 2
[node name="ClearButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessHBox"]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
size_flags_vertical = 4
theme_override_constants/margin_top = 10
[node name="Panel" type="Panel" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/MarginContainer"]
custom_minimum_size = Vector2(110, 110)
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
theme_override_styles/panel = SubResource("StyleBoxFlat_cb0xf")
[node name="TextureRect" type="TextureRect" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -50.0
offset_top = -50.0
offset_right = 50.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
expand_mode = 1
[node name="TextureButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/MarginContainer/Panel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/normal = SubResource("StyleBoxEmpty_7qdas")
[node name="RoughnessWHHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
alignment = 1
[node name="RoughnessW" type="Label" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="RoughnessH" type="Label" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/RoughnessWHHBox"]
layout_mode = 2
horizontal_alignment = 1
[node name="HBoxContainer2" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
alignment = 1
[node name="InvertSmoothCheckBox" type="CheckBox" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer2"]
layout_mode = 2
text = " Convert Smoothness to Roughness"
[node name="HBoxContainer" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox"]
layout_mode = 2
alignment = 1
[node name="RoughnessChannelLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"]
layout_mode = 2
text = " Source Channel: "
horizontal_alignment = 2
[node name="RoughnessChannelR" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_pressed = true
button_group = SubResource("ButtonGroup_bs6ki")
text = "R"
[node name="RoughnessChannelG" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_bs6ki")
text = "G"
[node name="RoughnessChannelB" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_bs6ki")
text = "B"
[node name="RoughnessChannelA" type="Button" parent="Panel/MarginContainer/VBoxContainer/NormalRoughnessPanel/MarginContainer/HBoxContainer/RoughnessVBox/HBoxContainer"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_bs6ki")
text = "A"
[node name="GeneralOptionsLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer"]
layout_mode = 2
text = "General Options"
horizontal_alignment = 1
vertical_alignment = 1
[node name="GeneralOptionsHBox" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer"]
custom_minimum_size = Vector2(0, 35)
layout_mode = 2
alignment = 1
[node name="ResizeToggle" type="CheckBox" parent="Panel/MarginContainer/VBoxContainer/GeneralOptionsHBox"]
layout_mode = 2
text = " Resize Packed Image"
[node name="ResizeOptionButton" type="SpinBox" parent="Panel/MarginContainer/VBoxContainer/GeneralOptionsHBox"]
visible = false
layout_mode = 2
tooltip_text = "A value of 0 disables resizing."
min_value = 128.0
max_value = 4096.0
step = 128.0
value = 1024.0
[node name="VSeparator" type="VSeparator" parent="Panel/MarginContainer/VBoxContainer/GeneralOptionsHBox"]
layout_mode = 2
[node name="GenerateMipmapsCheckBox" type="CheckBox" parent="Panel/MarginContainer/VBoxContainer/GeneralOptionsHBox"]
layout_mode = 2
button_pressed = true
text = "Generate Mipmaps"
[node name="HighQualityCheckBox" type="CheckBox" parent="Panel/MarginContainer/VBoxContainer/GeneralOptionsHBox"]
layout_mode = 2
text = "Import High Quality"
[node name="PackButton" type="Button" parent="Panel/MarginContainer/VBoxContainer"]
layout_mode = 2
text = "Pack textures as..."
[node name="StatusLabel" type="Label" parent="Panel/MarginContainer/VBoxContainer"]
custom_minimum_size = Vector2(0, 60)
layout_mode = 2
horizontal_alignment = 1
autowrap_mode = 3
[node name="HBoxContainer" type="HBoxContainer" parent="Panel/MarginContainer/VBoxContainer"]
layout_mode = 2
alignment = 1
[node name="CloseButton" type="Button" parent="Panel/MarginContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "Close"
[connection signal="toggled" from="Panel/MarginContainer/VBoxContainer/GeneralOptionsHBox/ResizeToggle" to="Panel/MarginContainer/VBoxContainer/GeneralOptionsHBox/ResizeOptionButton" method="set_visible"]

View File

@@ -1 +0,0 @@
uid://r0eex2idvm56

View File

@@ -1 +0,0 @@
uid://dt80i8xsj8tun

View File

@@ -1,3 +1,6 @@
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
# DoubleSlider for Terrain3D
# Should work for other UIs
@tool
class_name DoubleSlider
extends Control
@@ -10,9 +13,23 @@ var min_value: float = 0.0
var max_value: float = 100.0
var step: float = 1.0
var range := Vector2(0, 100)
var display_scale: float = 1.
var position_x: float = 0.
var minimum_x: float = 60.
func _ready() -> void:
# Setup Display Scale
# 0 auto, 1 75%, 2 100%, 3 125%, 4 150%, 5 175%, 6 200%, 7 custom
var es: EditorSettings = EditorInterface.get_editor_settings()
var ds: int = es.get_setting("interface/editor/display_scale")
if ds == 0:
ds = 2
elif ds == 7:
display_scale = es.get_setting("interface/editor/custom_display_scale")
else:
display_scale = float(ds + 2) * .25
update_label()
@@ -68,11 +85,21 @@ func get_value() -> Vector2:
func update_label() -> void:
if label:
label.set_text(str(range.x) + suffix + "/" + str(range.y) + suffix)
if position_x == 0:
position_x = label.position.x
else:
label.position.x = position_x + 5 * display_scale
label.custom_minimum_size.x = minimum_x + 5 * display_scale
func _get_handle() -> int:
return 1
func _gui_input(p_event: InputEvent) -> void:
if p_event is InputEventMouseButton:
if p_event.get_button_index() == MOUSE_BUTTON_LEFT:
var button: int = p_event.get_button_index()
if button in [ MOUSE_BUTTON_LEFT, MOUSE_BUTTON_WHEEL_UP, MOUSE_BUTTON_WHEEL_DOWN ]:
if p_event.is_pressed():
var mid_point = (range.x + range.y) / 2.0
var xpos: float = p_event.get_position().x * 2.0
@@ -80,7 +107,13 @@ func _gui_input(p_event: InputEvent) -> void:
grabbed_handle = 1
else:
grabbed_handle = -1
set_slider(p_event.get_position().x)
match button:
MOUSE_BUTTON_LEFT:
set_slider(p_event.get_position().x)
MOUSE_BUTTON_WHEEL_DOWN:
set_slider(-1., true)
MOUSE_BUTTON_WHEEL_UP:
set_slider(1., true)
else:
grabbed_handle = 0
@@ -89,14 +122,20 @@ func _gui_input(p_event: InputEvent) -> void:
set_slider(p_event.get_position().x)
func set_slider(p_xpos: float) -> void:
func set_slider(p_xpos: float, p_relative: bool = false) -> void:
if grabbed_handle == 0:
return
var xpos_step: float = clamp(snappedf((p_xpos / size.x) * max_value, step), min_value, max_value)
if(grabbed_handle < 0):
range.x = xpos_step
if p_relative:
range.x += p_xpos
else:
range.x = xpos_step
else:
range.y = xpos_step
if p_relative:
range.y += p_xpos
else:
range.y = xpos_step
set_value(range)
@@ -111,14 +150,15 @@ func _notification(p_what: int) -> void:
# Draw foreground bar
var handle: Texture2D = get_theme_icon("grabber", "HSlider")
var area: StyleBox = get_theme_stylebox("grabber_area", "HSlider")
var h: float = size.y / 2 - handle.get_size().y / 2
var startx: float = (range.x / max_value) * size.x
var endx: float = (range.y / max_value) * size.x #- startx
var endx: float = (range.y / max_value) * size.x
draw_style_box(area, Rect2(Vector2(startx, mid_y), Vector2(endx - startx, bg_height)))
# Draw handles, slightly in so they don't get on the outside edges
var handle_pos: Vector2
handle_pos.x = clamp(startx - handle.get_size().x/2, -5, size.x)
handle_pos.x = clamp(startx - handle.get_size().x/2, -10, size.x)
handle_pos.y = clamp(endx - handle.get_size().x/2, 0, size.x - 10)
draw_texture(handle, Vector2(handle_pos.x, -mid_y))
draw_texture(handle, Vector2(handle_pos.y, -mid_y))
draw_texture(handle, Vector2(handle_pos.x, -mid_y - 10 * (display_scale - 1.)))
draw_texture(handle, Vector2(handle_pos.y, -mid_y - 10 * (display_scale - 1.)))
update_label()

View File

@@ -1 +1 @@
uid://cd6164a8pp48f
uid://stro0p1oawfb

View File

@@ -1,6 +1,7 @@
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
# Editor Plugin for Terrain3D
@tool
extends EditorPlugin
#class_name Terrain3DEditorPlugin Cannot be named until Godot #75388
# Includes
@@ -13,6 +14,7 @@ var modifier_alt: bool
var modifier_shift: bool
var _last_modifiers: int = 0
var _input_mode: int = 0 # -1: camera move, 0: none, 1: operating
var _use_meta: bool = false
var terrain: Terrain3D
var _last_terrain: Terrain3D
@@ -29,6 +31,9 @@ var godot_editor_window: Window # The Godot Editor window
func _init() -> void:
if OS.get_name() == "macOS":
_use_meta = true
# Get the Godot Editor window. Structure is root:Window/EditorNode/Base Control
godot_editor_window = EditorInterface.get_base_control().get_parent().get_parent()
godot_editor_window.focus_entered.connect(_on_godot_focus_entered)
@@ -113,10 +118,6 @@ func _edit(p_object: Object) -> void:
ui.set_visible(true)
terrain.set_meta("_edit_lock_", true)
# Deprecated 0.9.3 - Remove 1.0
if terrain.storage:
ui.terrain_menu.directory_setup.directory_setup_popup()
# Get alerted when a new asset list is loaded
if not terrain.assets_changed.is_connected(asset_dock.update_assets):
terrain.assets_changed.connect(asset_dock.update_assets)
@@ -153,40 +154,43 @@ func _clear() -> void:
func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) -> int:
if not is_terrain_valid():
return AFTER_GUI_INPUT_PASS
_read_input(p_event)
ui.update_decal()
## Setup active camera & viewport
# Always update this for all inputs, as the mouse position can move without
# necessarily being a InputEventMouseMotion object. get_intersection() also
# returns the last frame position, and should be updated more frequently.
# Snap terrain to current camera
terrain.set_camera(p_viewport_camera)
# Detect if viewport is set to half_resolution
# Structure is: Node3DEditorViewportContainer/Node3DEditorViewport(4)/SubViewportContainer/SubViewport/Camera3D
var editor_vpc: SubViewportContainer = p_viewport_camera.get_parent().get_parent()
var full_resolution: bool = false if editor_vpc.stretch_shrink == 2 else true
## Get mouse location on terrain
# Project 2D mouse position to 3D position and direction
var vp_mouse_pos: Vector2 = editor_vpc.get_local_mouse_position()
var mouse_pos: Vector2 = vp_mouse_pos if full_resolution else vp_mouse_pos / 2
var camera_pos: Vector3 = p_viewport_camera.project_ray_origin(mouse_pos)
var camera_dir: Vector3 = p_viewport_camera.project_ray_normal(mouse_pos)
# If region tool, grab mouse position without considering height
if editor.get_tool() == Terrain3DEditor.REGION:
var t = -Vector3(0, 1, 0).dot(camera_pos) / Vector3(0, 1, 0).dot(camera_dir)
mouse_global_position = (camera_pos + t * camera_dir)
else:
#Else look for intersection with terrain
var intersection_point: Vector3 = terrain.get_intersection(camera_pos, camera_dir, true)
if intersection_point.z > 3.4e38 or is_nan(intersection_point.y): # max double or nan
return AFTER_GUI_INPUT_PASS
mouse_global_position = intersection_point
## Handle mouse movement
if p_event is InputEventMouseMotion:
## Setup active camera & viewport
# Snap terrain to current camera
terrain.set_camera(p_viewport_camera)
# Detect if viewport is set to half_resolution
# Structure is: Node3DEditorViewportContainer/Node3DEditorViewport(4)/SubViewportContainer/SubViewport/Camera3D
var editor_vpc: SubViewportContainer = p_viewport_camera.get_parent().get_parent()
var full_resolution: bool = false if editor_vpc.stretch_shrink == 2 else true
## Get mouse location on terrain
# Project 2D mouse position to 3D position and direction
var mouse_pos: Vector2 = p_event.position if full_resolution else p_event.position/2
var camera_pos: Vector3 = p_viewport_camera.project_ray_origin(mouse_pos)
var camera_dir: Vector3 = p_viewport_camera.project_ray_normal(mouse_pos)
# If region tool, grab mouse position without considering height
if editor.get_tool() == Terrain3DEditor.REGION:
var t = -Vector3(0, 1, 0).dot(camera_pos) / Vector3(0, 1, 0).dot(camera_dir)
mouse_global_position = (camera_pos + t * camera_dir)
else:
# Else look for intersection with terrain
var intersection_point: Vector3 = terrain.get_intersection(camera_pos, camera_dir)
if intersection_point.z > 3.4e38 or is_nan(intersection_point.y): # max double or nan
return AFTER_GUI_INPUT_STOP
mouse_global_position = intersection_point
ui.update_decal()
if _input_mode != -1: # Not cam rotation
## Update region highlight
@@ -205,8 +209,6 @@ func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) ->
return AFTER_GUI_INPUT_PASS
ui.update_decal()
if p_event is InputEventMouseButton and _input_mode > 0:
if p_event.is_pressed():
# If picking
@@ -215,6 +217,11 @@ func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) ->
if not ui.operation_builder or not ui.operation_builder.is_ready():
return AFTER_GUI_INPUT_STOP
if modifier_ctrl and editor.get_tool() == Terrain3DEditor.HEIGHT:
var height: float = terrain.data.get_height(mouse_global_position)
ui.brush_data["height"] = height
ui.tool_settings.set_setting("height", height)
# If adjusting regions
if editor.get_tool() == Terrain3DEditor.REGION:
# Skip regions that already exist or don't
@@ -273,7 +280,7 @@ func _read_input(p_event: InputEvent = null) -> void:
## Determine modifiers pressed
modifier_shift = Input.is_key_pressed(KEY_SHIFT)
modifier_ctrl = Input.is_key_pressed(KEY_CTRL)
modifier_ctrl = Input.is_key_pressed(KEY_META) if _use_meta else Input.is_key_pressed(KEY_CTRL)
# Keybind enum: Alt,Space,Meta,Capslock
var alt_key: int
match get_setting("terrain3d/config/alt_key_bind", 0):
@@ -369,29 +376,6 @@ func setup_editor_settings() -> void:
"hint_string": "Alt,Space,Meta,Capslock"
}
editor_settings.add_property_info(property_info)
_cleanup_old_settings()
# Remove or rename old settings
func _cleanup_old_settings() -> void:
# Rename deprecated settings - Remove in 1.0
var value: Variant
var rename_arr := [ "terrain3d/config/dock_slot", "terrain3d/config/dock_tile_size",
"terrain3d/config/dock_floating", "terrain3d/config/dock_always_on_top",
"terrain3d/config/dock_window_size", "terrain3d/config/dock_window_position", ]
for es: String in rename_arr:
if editor_settings.has_setting(es):
value = editor_settings.get_setting(es)
editor_settings.erase(es)
editor_settings.set_setting(es.replace("/config/dock_", "/dock/"), value)
# Special handling
var es: String = "terrain3d/tool_settings/slope"
if editor_settings.has_setting(es):
value = editor_settings.get_setting(es)
if typeof(value) == TYPE_FLOAT:
editor_settings.erase(es)
func set_setting(p_str: String, p_value: Variant) -> void:

View File

@@ -0,0 +1 @@
uid://bsgxo1qywjdf3

View File

@@ -1,3 +1,5 @@
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
# Gradient Operation Builder for Terrain3D
extends "res://addons/terrain_3d/src/operation_builder.gd"
@@ -52,4 +54,3 @@ func apply_operation(p_editor: Terrain3DEditor, p_global_position: Vector3, p_ca
p_editor.stop_operation()
_get_point_picker().clear()

View File

@@ -1 +1 @@
uid://b1qt62vc3vnx7
uid://def7sych6dp8b

View File

@@ -1,3 +1,5 @@
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
# Multipicker for Terrain3D
extends HBoxContainer

View File

@@ -1 +1 @@
uid://dqwxbfvjra3p3
uid://dvdtoa32h6xdn

View File

@@ -1,3 +1,5 @@
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
# Operation Builder for Terrain3D
extends RefCounted

View File

@@ -1 +1 @@
uid://3rphgkvw2bi1
uid://bu5cm0eh052rm

View File

@@ -1,3 +1,5 @@
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
# Editor Region Gizmos for Terrain3D
extends EditorNode3DGizmo
var material: StandardMaterial3D

View File

@@ -1 +1 @@
uid://ddnxpafte8kb7
uid://bh6qwe1ok4cx3

View File

@@ -1 +0,0 @@
uid://cbtr86lrjls7a

View File

@@ -1,3 +1,5 @@
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
# Tool settings bar for Terrain3D
extends PanelContainer
signal picking(type, callback)
@@ -39,6 +41,7 @@ const NO_SAVE: int = 0x20 # Don't save this in EditorSettings
var plugin: EditorPlugin # Actually Terrain3DEditorPlugin, but Godot still has CRC errors
var brush_preview_material: ShaderMaterial
var select_brush_button: Button
var selected_brush_imgs: Array
var main_list: HFlowContainer
var advanced_list: VBoxContainer
var height_list: VBoxContainer
@@ -86,6 +89,9 @@ func _ready() -> void:
add_setting({ "name":"enable_texture", "label":"Texture", "type":SettingType.CHECKBOX,
"list":main_list, "default":true, "flags":ADD_SEPARATOR })
add_setting({ "name":"texture_filter", "label":"Texture Filter", "type":SettingType.CHECKBOX,
"list":main_list, "default":false, "flags":ADD_SEPARATOR })
add_setting({ "name":"margin", "type":SettingType.SLIDER, "list":main_list, "default":0,
"unit":"", "range":Vector3(-50, 50, 1), "flags":ALLOW_OUT_OF_BOUNDS })
@@ -171,6 +177,8 @@ func _ready() -> void:
"unit":"γ", "range":Vector3(0.1, 2.0, 0.01) })
add_setting({ "name":"jitter", "type":SettingType.SLIDER, "list":advanced_list, "default":50,
"unit":"%", "range":Vector3(0, 100, 1) })
add_setting({ "name":"crosshair_threshold", "type":SettingType.SLIDER, "list":advanced_list, "default":16.,
"unit":"m", "range":Vector3(0, 200, 1) })
func create_submenu(p_parent: Control, p_button_name: String, p_layout: Layout, p_hover_pop: bool = true) -> Container:
@@ -267,29 +275,33 @@ func add_brushes(p_parent: Control) -> void:
while file_name != "":
if !dir.current_is_dir() and file_name.ends_with(".exr"):
var img: Image = Image.load_from_file(BRUSH_PATH + "/" + file_name)
img = Terrain3DUtil.black_to_alpha(img)
if img.get_width() < 1024 and img.get_height() < 1024:
img.resize(1024, 1024, Image.INTERPOLATE_CUBIC)
var tex: ImageTexture = ImageTexture.create_from_image(img)
var thumbimg: Image = img.duplicate()
img.convert(Image.FORMAT_RF)
var btn: Button = Button.new()
btn.set_custom_minimum_size(Vector2.ONE * 100)
btn.set_button_icon(tex)
btn.set_meta("image", img)
btn.set_expand_icon(true)
btn.set_material(_get_brush_preview_material())
btn.set_toggle_mode(true)
btn.set_button_group(brush_button_group)
btn.mouse_entered.connect(_on_brush_hover.bind(true, btn))
btn.mouse_exited.connect(_on_brush_hover.bind(false, btn))
brush_list.add_child(btn, true)
if thumbimg.get_width() != 100 and thumbimg.get_height() != 100:
thumbimg.resize(100, 100, Image.INTERPOLATE_CUBIC)
thumbimg = Terrain3DUtil.black_to_alpha(thumbimg)
thumbimg.convert(Image.FORMAT_LA8)
var thumbtex: ImageTexture = ImageTexture.create_from_image(thumbimg)
var brush_btn: Button = Button.new()
brush_btn.set_custom_minimum_size(Vector2.ONE * 100)
brush_btn.set_button_icon(thumbtex)
brush_btn.set_meta("image", img)
brush_btn.set_expand_icon(true)
brush_btn.set_material(_get_brush_preview_material())
brush_btn.set_toggle_mode(true)
brush_btn.set_button_group(brush_button_group)
brush_btn.mouse_entered.connect(_on_brush_hover.bind(true, brush_btn))
brush_btn.mouse_exited.connect(_on_brush_hover.bind(false, brush_btn))
brush_list.add_child(brush_btn, true)
if file_name == DEFAULT_BRUSH:
default_brush_btn = btn
default_brush_btn = brush_btn
var lbl: Label = Label.new()
btn.name = file_name.get_basename().to_pascal_case()
btn.add_child(lbl, true)
lbl.text = btn.name
brush_btn.name = file_name.get_basename().to_pascal_case()
brush_btn.add_child(lbl, true)
lbl.text = brush_btn.name
lbl.visible = false
lbl.position.y = 70
lbl.add_theme_color_override("font_shadow_color", Color.BLACK)
@@ -304,6 +316,7 @@ func add_brushes(p_parent: Control) -> void:
if not default_brush_btn:
default_brush_btn = brush_button_group.get_buttons()[0]
default_brush_btn.set_pressed(true)
_generate_brush_texture(default_brush_btn)
settings["brush"] = brush_button_group
@@ -443,7 +456,7 @@ func add_setting(p_args: Dictionary) -> void:
pending_children.push_back(option)
control = option
SettingType.SLIDER, SettingType.DOUBLE_SLIDER:
SettingType.SLIDER, SettingType.DOUBLE_SLIDER:
var slider: Control
if p_type == SettingType.SLIDER:
# Create an editable value box
@@ -473,7 +486,7 @@ func add_setting(p_args: Dictionary) -> void:
else: # DOUBLE_SLIDER
var label := Label.new()
label.set_custom_minimum_size(Vector2(60, 0))
label.set_horizontal_alignment(HORIZONTAL_ALIGNMENT_RIGHT)
label.set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER)
slider = DoubleSlider.new()
slider.label = label
slider.suffix = p_suffix
@@ -560,9 +573,7 @@ func get_setting(p_setting: String) -> Variant:
elif object is DoubleSlider:
value = object.get_value()
elif object is ButtonGroup: # "brush"
var img: Image = object.get_pressed_button().get_meta("image")
var tex: Texture2D = object.get_pressed_button().get_button_icon()
value = [ img, tex ]
value = selected_brush_imgs
elif object is CheckBox:
value = object.is_pressed()
elif object is ColorPickerButton:
@@ -610,19 +621,30 @@ func show_settings(p_settings: PackedStringArray) -> void:
select_brush_button.show()
func _on_setting_changed(p_data: Variant = null) -> void:
# If a button was clicked on a submenu
if p_data is Button and p_data.get_parent().get_parent() is PopupPanel:
if p_data.get_parent().name == "BrushList":
# Optionally Set selected brush texture in main brush button
p_data.get_parent().get_parent().get_parent().set_button_icon(p_data.get_button_icon())
# Hide popup
p_data.get_parent().get_parent().set_visible(false)
# Hide label
if p_data.get_child_count() > 0:
p_data.get_child(0).visible = false
func _on_setting_changed(p_object: Variant = null) -> void:
# If a brush was selected
if p_object is Button and p_object.get_parent().name == "BrushList":
_generate_brush_texture(p_object)
# Optionally Set selected brush texture in main brush button
if select_brush_button:
select_brush_button.set_button_icon(p_object.get_button_icon())
# Hide popup
p_object.get_parent().get_parent().set_visible(false)
# Hide label
if p_object.get_child_count() > 0:
p_object.get_child(0).visible = false
emit_signal("setting_changed")
func _generate_brush_texture(p_btn: Button) -> void:
if p_btn is Button:
var img: Image = p_btn.get_meta("image")
if img.get_width() < 1024 and img.get_height() < 1024:
img = img.duplicate()
img.resize(1024, 1024, Image.INTERPOLATE_CUBIC)
var tex: ImageTexture = ImageTexture.create_from_image(img)
selected_brush_imgs = [ img, tex ]
func _on_drawable_toggled(p_button_pressed: bool) -> void:
if not p_button_pressed:

View File

@@ -1 +1 @@
uid://b0hs7rbtc8jfy
uid://ciskaaennrffu

View File

@@ -1,3 +1,5 @@
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
# Toolbar for Terrain3D
extends VFlowContainer
signal tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation)
@@ -26,8 +28,8 @@ func _init() -> void:
set_custom_minimum_size(Vector2(20, 0))
func _ready() -> void:
add_tool_group.connect("pressed", _on_tool_selected)
sub_tool_group.connect("pressed", _on_tool_selected)
add_tool_group.pressed.connect(_on_tool_selected)
sub_tool_group.pressed.connect(_on_tool_selected)
add_tool_button({ "tool":Terrain3DEditor.REGION,
"add_text":"Add Region", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_REGION_ADD,

View File

@@ -1 +1 @@
uid://bthdcp8t4awsk
uid://b1j37u6utjbom

View File

@@ -1,16 +1,17 @@
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
# UI for Terrain3D
extends Node
#class_name Terrain3DUI Cannot be named until Godot #75388
# Includes
const TerrainMenu: Script = preload("res://addons/terrain_3d/menu/terrain_menu.gd")
const Toolbar: Script = preload("res://addons/terrain_3d/src/toolbar.gd")
const ToolSettings: Script = preload("res://addons/terrain_3d/src/tool_settings.gd")
const TerrainMenu: Script = preload("res://addons/terrain_3d/src/terrain_menu.gd")
const OperationBuilder: Script = preload("res://addons/terrain_3d/src/operation_builder.gd")
const GradientOperationBuilder: Script = preload("res://addons/terrain_3d/src/gradient_operation_builder.gd")
const COLOR_RAISE := Color.WHITE
const COLOR_LOWER := Color(.02, .02, .02)
const COLOR_SMOOTH := Color(0.5, 0, .1)
const COLOR_LOWER := Color.BLACK
const COLOR_SMOOTH := Color(0.5, 0, .2)
const COLOR_LIFT := Color.ORANGE
const COLOR_FLATTEN := Color.BLUE_VIOLET
const COLOR_HEIGHT := Color(0., 0.32, .4)
@@ -20,7 +21,7 @@ const COLOR_SPRAY := Color.PALE_GREEN
const COLOR_ROUGHNESS := Color.ROYAL_BLUE
const COLOR_AUTOSHADER := Color.DODGER_BLUE
const COLOR_HOLES := Color.BLACK
const COLOR_NAVIGATION := Color(.15, .0, .255)
const COLOR_NAVIGATION := Color(.28, .0, .25)
const COLOR_INSTANCER := Color.CRIMSON
const COLOR_PICK_COLOR := Color.WHITE
const COLOR_PICK_HEIGHT := Color.DARK_RED
@@ -31,8 +32,13 @@ const OP_POSITIVE_ONLY: int = 0x01
const OP_NEGATIVE_ONLY: int = 0x02
const RING1: String = "res://addons/terrain_3d/brushes/ring1.exr"
@onready var ring_texture := ImageTexture.create_from_image(Terrain3DUtil.black_to_alpha(Image.load_from_file(RING1)))
var ring_texture : ImageTexture
@onready var region_texture := ImageTexture.new() :
set(value):
var image: Image = Image.create_empty(1, 1, false, Image.FORMAT_R8)
image.fill(Color.WHITE)
value.create_from_image(image)
region_texture = value
var plugin: EditorPlugin # Actually Terrain3DEditorPlugin, but Godot still has CRC errors
var toolbar: Toolbar
var tool_settings: ToolSettings
@@ -41,31 +47,42 @@ var setting_has_changed: bool = false
var visible: bool = false
var picking: int = Terrain3DEditor.TOOL_MAX
var picking_callback: Callable
var decal: Decal
var decal_timer: Timer
var gradient_decals: Array[Decal]
var brush_data: Dictionary
var operation_builder: OperationBuilder
var last_tool: Terrain3DEditor.Tool
var last_operation: Terrain3DEditor.Operation
var last_rmb_time: int = 0 # Set in editor.gd
# Compatibility decals, indices; 0 = main brush, 1 = slope point A, 2 = slope point B
var editor_decal_position: Array[Vector2]
var editor_decal_rotation: Array[float]
var editor_decal_size: Array[float]
var editor_decal_color: Array[Color]
var editor_decal_visible: Array[bool]
# Editor decals, indices; 0 = main brush, 1 = slope point A, 2 = slope point B
var mat_rid: RID
var editor_decal_position: Array[Vector2] = [Vector2(), Vector2(), Vector2()]
var editor_decal_rotation: Array[float] = [float(), float(), float()]
var editor_decal_size: Array[float] = [float(), float(), float()]
var editor_decal_color: Array[Color] = [Color(), Color(), Color()]
var editor_decal_visible: Array[bool] = [bool(), bool(), bool()]
var editor_brush_texture_rid: RID = RID()
var editor_decal_timer: Timer
var editor_decal_fade: float :
set(value):
editor_decal_fade = value
if editor_decal_color.size() > 0:
editor_decal_color[0].a = value
if is_shader_valid():
RenderingServer.material_set_param(mat_rid, "_editor_decal_color", editor_decal_color)
if value < 0.001:
var r_map: PackedInt32Array = plugin.terrain.data.get_region_map()
RenderingServer.material_set_param(mat_rid, "_region_map", r_map)
var editor_ring_texture_rid: RID
func _enter_tree() -> void:
toolbar = Toolbar.new()
toolbar.hide()
toolbar.connect("tool_changed", _on_tool_changed)
toolbar.tool_changed.connect(_on_tool_changed)
tool_settings = ToolSettings.new()
tool_settings.connect("setting_changed", _on_setting_changed)
tool_settings.connect("picking", _on_picking)
tool_settings.setting_changed.connect(_on_setting_changed)
tool_settings.picking.connect(_on_picking)
tool_settings.plugin = plugin
tool_settings.hide()
@@ -79,15 +96,19 @@ func _enter_tree() -> void:
_on_tool_changed(Terrain3DEditor.REGION, Terrain3DEditor.ADD)
decal = Decal.new()
add_child(decal)
decal_timer = Timer.new()
decal_timer.wait_time = .5
decal_timer.one_shot = true
decal_timer.timeout.connect(Callable(func(node):
if node:
get_tree().create_tween().tween_property(node, "albedo_mix", 0.0, 0.15)).bind(decal))
add_child(decal_timer)
editor_decal_timer = Timer.new()
editor_decal_timer.wait_time = .5
editor_decal_timer.one_shot = true
editor_decal_timer.timeout.connect(func():
get_tree().create_tween().tween_property(self, "editor_decal_fade", 0.0, 0.15))
add_child(editor_decal_timer)
func _ready() -> void:
var img: Image = Image.load_from_file(RING1)
img.convert(Image.FORMAT_R8)
ring_texture = ImageTexture.create_from_image(img)
editor_ring_texture_rid = ring_texture.get_rid()
func _exit_tree() -> void:
@@ -96,11 +117,7 @@ func _exit_tree() -> void:
toolbar.queue_free()
tool_settings.queue_free()
terrain_menu.queue_free()
decal.queue_free()
decal_timer.queue_free()
for gradient_decal in gradient_decals:
gradient_decal.queue_free()
gradient_decals.clear()
editor_decal_timer.queue_free()
func set_visible(p_visible: bool, p_menu_only: bool = false) -> void:
@@ -117,7 +134,7 @@ func set_visible(p_visible: bool, p_menu_only: bool = false) -> void:
if(plugin.editor):
if(p_visible):
await get_tree().create_timer(.01).timeout # Won't work, otherwise.
await get_tree().create_timer(.01).timeout # Won't work, otherwise
_on_tool_changed(last_tool, last_operation)
else:
plugin.editor.set_tool(Terrain3DEditor.TOOL_MAX)
@@ -185,7 +202,7 @@ func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor
to_show.push_back("color")
to_show.push_back("color_picker")
to_show.push_back("slope")
to_show.push_back("enable_texture")
to_show.push_back("texture_filter")
to_show.push_back("margin")
to_show.push_back("remove")
@@ -196,7 +213,7 @@ func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor
to_show.push_back("roughness")
to_show.push_back("roughness_picker")
to_show.push_back("slope")
to_show.push_back("enable_texture")
to_show.push_back("texture_filter")
to_show.push_back("margin")
to_show.push_back("remove")
@@ -236,6 +253,7 @@ func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor
to_show.push_back("show_cursor_while_painting")
to_show.push_back("gamma")
to_show.push_back("jitter")
to_show.push_back("crosshair_threshold")
tool_settings.show_settings(to_show)
operation_builder = null
@@ -258,9 +276,9 @@ func _on_setting_changed() -> void:
return
brush_data = tool_settings.get_settings()
brush_data["asset_id"] = plugin.asset_dock.get_current_list().get_selected_id()
update_decal()
plugin.editor.set_brush_data(brush_data)
plugin.editor.set_operation(_modify_operation(plugin.editor.get_operation()))
update_decal()
func update_modifiers() -> void:
@@ -299,195 +317,211 @@ func _invert_operation(p_operation: Terrain3DEditor.Operation, flags: int = OP_N
func update_decal() -> void:
if not plugin.terrain or brush_data.size() <= 3:
return
mat_rid = plugin.terrain.material.get_material_rid()
editor_decal_timer.start()
# If not a state that should show the decal, hide everything and return
if not visible or \
not plugin.terrain or \
plugin._input_mode < 0 or \
# Wait for cursor to recenter after moving camera before revealing
# See https://github.com/godotengine/godot/issues/70098
Time.get_ticks_msec() - last_rmb_time <= 30 or \
brush_data.is_empty() or \
plugin.editor.get_tool() == Terrain3DEditor.REGION or \
(plugin._input_mode > 0 and not brush_data["show_cursor_while_painting"]):
decal.visible = false
for gradient_decal in gradient_decals:
gradient_decal.visible = false
hide_decal()
return
decal.position = plugin.mouse_global_position
decal.visible = true
decal.size = Vector3.ONE * maxf(brush_data["size"], .5)
if brush_data["align_to_view"]:
var cam: Camera3D = plugin.terrain.get_camera();
if (cam):
decal.rotation.y = cam.rotation.y
else:
decal.rotation.y = 0
reset_decal_arrays()
editor_decal_position[0] = Vector2(plugin.mouse_global_position.x, plugin.mouse_global_position.z)
editor_decal_visible[0] = true
# Set region size, and modify region map for none background mode.
var r_map: PackedInt32Array = plugin.terrain.data.get_region_map()
if plugin.editor.get_tool() == Terrain3DEditor.REGION:
var r_size: float = float(plugin.terrain.get_region_size()) * plugin.terrain.get_vertex_spacing()
var map_size: int = plugin.terrain.data.REGION_MAP_SIZE
var half_r_size: float = r_size * 0.5
var pos: Vector2 = (Vector2(plugin.mouse_global_position.x, plugin.mouse_global_position.z) +
Vector2(half_r_size, half_r_size)).snappedf(r_size) - Vector2(half_r_size, half_r_size)
editor_brush_texture_rid = region_texture.get_rid()
editor_decal_position[0] = pos
editor_decal_size[0] = r_size
editor_decal_rotation[0] = 0.0
var loc: Vector2i = plugin.terrain.data.get_region_location(plugin.mouse_global_position)
loc += Vector2i(map_size / 2, map_size / 2)
if !(loc.x < 0 or loc.x > map_size - 1 or loc.y < 0 or loc.y > map_size - 1):
var index: int = clampi(loc.y * map_size + loc.x, 0, map_size * map_size - 1)
if plugin.terrain.material.get_world_background() == Terrain3DMaterial.WorldBackground.NONE:
if r_map[index] == 0 and plugin.editor.get_operation() == Terrain3DEditor.ADD:
r_map[index] = -index - 1
else:
r_map[index] = r_map[index]
match plugin.editor.get_operation():
Terrain3DEditor.ADD:
if r_map[index] <= 0:
editor_decal_color[0] = Color.WHITE
editor_decal_color[0].a = 0.25
else:
hide_decal()
Terrain3DEditor.SUBTRACT:
if r_map[index] > 0:
editor_decal_color[0] = Color.WHITE * .15
editor_decal_color[0].a = 0.75
else:
hide_decal()
else:
hide_decal()
# Set texture and color
if picking != Terrain3DEditor.TOOL_MAX:
decal.texture_albedo = ring_texture
decal.size = Vector3.ONE * 10. * plugin.terrain.get_vertex_spacing()
elif picking != Terrain3DEditor.TOOL_MAX:
editor_brush_texture_rid = ring_texture.get_rid()
editor_decal_size[0] = 10. * plugin.terrain.get_vertex_spacing()
match picking:
Terrain3DEditor.HEIGHT:
decal.modulate = COLOR_PICK_HEIGHT
editor_decal_color[0] = COLOR_PICK_HEIGHT
Terrain3DEditor.COLOR:
decal.modulate = COLOR_PICK_COLOR
editor_decal_color[0] = COLOR_PICK_COLOR
Terrain3DEditor.ROUGHNESS:
decal.modulate = COLOR_PICK_ROUGH
decal.modulate.a = 1.0
editor_decal_color[0] = COLOR_PICK_ROUGH
editor_decal_color[0].a = 1.0
else:
decal.texture_albedo = brush_data["brush"][1]
editor_brush_texture_rid = brush_data["brush"][1].get_rid()
editor_decal_size[0] = maxf(brush_data["size"], .5)
if brush_data["align_to_view"]:
var cam: Camera3D = plugin.terrain.get_camera();
if (cam):
editor_decal_rotation[0] = cam.rotation.y
else:
editor_decal_rotation[0] = 0.
match plugin.editor.get_tool():
Terrain3DEditor.SCULPT:
match plugin.editor.get_operation():
Terrain3DEditor.ADD:
if plugin.modifier_alt:
decal.modulate = COLOR_LIFT
decal.modulate.a = clamp(brush_data["strength"], .2, .5)
editor_decal_color[0] = COLOR_LIFT
editor_decal_color[0].a = clamp(brush_data["strength"], .2, .5)
else:
decal.modulate = COLOR_RAISE
decal.modulate.a = clamp(brush_data["strength"], .2, .5)
editor_decal_color[0] = COLOR_RAISE
editor_decal_color[0].a = clamp(brush_data["strength"], .25, .5)
Terrain3DEditor.SUBTRACT:
if plugin.modifier_alt:
decal.modulate = COLOR_FLATTEN
decal.modulate.a = clamp(brush_data["strength"], .2, .5)
editor_decal_color[0] = COLOR_FLATTEN
editor_decal_color[0].a = clamp(brush_data["strength"], .25, .5) + .1
else:
decal.modulate = COLOR_LOWER
decal.modulate.a = clamp(brush_data["strength"], .2, .5) + .5
editor_decal_color[0] = COLOR_LOWER
editor_decal_color[0].a = clamp(brush_data["strength"], .2, .5) + .25
Terrain3DEditor.AVERAGE:
decal.modulate = COLOR_SMOOTH
decal.modulate.a = clamp(brush_data["strength"], .2, .5) + .2
editor_decal_color[0] = COLOR_SMOOTH
editor_decal_color[0].a = clamp(brush_data["strength"], .2, .5) + .25
Terrain3DEditor.GRADIENT:
decal.modulate = COLOR_SLOPE
decal.modulate.a = clamp(brush_data["strength"], .2, .5)
editor_decal_color[0] = COLOR_SLOPE
editor_decal_color[0].a = clamp(brush_data["strength"], .2, .4)
Terrain3DEditor.HEIGHT:
decal.modulate = COLOR_HEIGHT
decal.modulate.a = clamp(brush_data["strength"], .2, .5)
editor_decal_color[0] = COLOR_HEIGHT
editor_decal_color[0].a = clamp(brush_data["strength"], .2, .5) + .25
Terrain3DEditor.TEXTURE:
match plugin.editor.get_operation():
Terrain3DEditor.REPLACE:
decal.modulate = COLOR_PAINT
decal.modulate.a = .7
editor_decal_color[0] = COLOR_PAINT
editor_decal_color[0].a = .6
Terrain3DEditor.SUBTRACT:
decal.modulate = COLOR_PAINT
decal.modulate.a = clamp(brush_data["strength"], .2, .5)
editor_decal_color[0] = COLOR_PAINT
editor_decal_color[0].a = clamp(brush_data["strength"], .2, .5) + .1
Terrain3DEditor.ADD:
decal.modulate = COLOR_SPRAY
decal.modulate.a = clamp(brush_data["strength"], .2, .5)
editor_decal_color[0] = COLOR_SPRAY
editor_decal_color[0].a = clamp(brush_data["strength"], .15, .4)
Terrain3DEditor.COLOR:
decal.modulate = brush_data["color"].srgb_to_linear()
decal.modulate.a *= clamp(brush_data["strength"], .2, .5)
editor_decal_color[0] = brush_data["color"].srgb_to_linear()
editor_decal_color[0].a *= clamp(brush_data["strength"], .2, .5)
Terrain3DEditor.ROUGHNESS:
decal.modulate = COLOR_ROUGHNESS
decal.modulate.a = clamp(brush_data["strength"], .2, .5)
editor_decal_color[0] = COLOR_ROUGHNESS
editor_decal_color[0].a = clamp(brush_data["strength"], .2, .5) + .1
Terrain3DEditor.AUTOSHADER:
decal.modulate = COLOR_AUTOSHADER
decal.modulate.a = .7
editor_decal_color[0] = COLOR_AUTOSHADER
editor_decal_color[0].a = .6
Terrain3DEditor.HOLES:
decal.modulate = COLOR_HOLES
decal.modulate.a = .85
editor_decal_color[0] = COLOR_HOLES
editor_decal_color[0].a = .75
Terrain3DEditor.NAVIGATION:
decal.modulate = COLOR_NAVIGATION
decal.modulate.a = .85
editor_decal_color[0] = COLOR_NAVIGATION
editor_decal_color[0].a = .80
Terrain3DEditor.INSTANCER:
decal.texture_albedo = ring_texture
decal.modulate = COLOR_INSTANCER
decal.modulate.a = 1.0
decal.size.y = max(1000, decal.size.y)
decal.albedo_mix = 1.0
decal.cull_mask = 1 << ( plugin.terrain.get_mouse_layer() - 1 )
decal_timer.start()
for gradient_decal in gradient_decals:
gradient_decal.visible = false
editor_brush_texture_rid = ring_texture.get_rid()
editor_decal_color[0] = COLOR_INSTANCER
editor_decal_color[0].a = .75
editor_decal_visible[1] = false
editor_decal_visible[2] = false
if plugin.editor.get_operation() == Terrain3DEditor.GRADIENT:
var index := 0
for point in brush_data["gradient_points"]:
if point != Vector3.ZERO:
var point_decal: Decal = _get_gradient_decal(index)
point_decal.visible = true
point_decal.position = point
index += 1
update_compatibility_decal()
func _get_gradient_decal(index: int) -> Decal:
if gradient_decals.size() > index:
return gradient_decals[index]
var point1: Vector3 = brush_data["gradient_points"][0]
if point1 != Vector3.ZERO:
editor_decal_color[1] = COLOR_SLOPE
editor_decal_size[1] = 10. * plugin.terrain.get_vertex_spacing()
editor_decal_visible[1] = true
editor_decal_position[1] = Vector2(point1.x, point1.z)
var point2: Vector3 = brush_data["gradient_points"][1]
if point2 != Vector3.ZERO:
editor_decal_color[2] = COLOR_SLOPE
editor_decal_size[2] = 10. * plugin.terrain.get_vertex_spacing()
editor_decal_visible[2] = true
editor_decal_position[2] = Vector2(point2.x, point2.z)
var gradient_decal := Decal.new()
gradient_decal = Decal.new()
gradient_decal.texture_albedo = ring_texture
gradient_decal.modulate = COLOR_SLOPE
gradient_decal.size = Vector3.ONE * 10. * plugin.terrain.get_vertex_spacing()
gradient_decal.size.y = 1000.
gradient_decal.cull_mask = decal.cull_mask
add_child(gradient_decal)
if RenderingServer.get_current_rendering_method().contains("gl_compatibility"):
for i in editor_decal_color.size():
editor_decal_color[i].a = maxf(0.1, editor_decal_color[i].a - .25)
gradient_decals.push_back(gradient_decal)
return gradient_decal
editor_decal_fade = editor_decal_color[0].a
# Update Shader params
if is_shader_valid():
RenderingServer.material_set_param(mat_rid, "_editor_brush_texture", editor_brush_texture_rid)
RenderingServer.material_set_param(mat_rid, "_editor_ring_texture", editor_ring_texture_rid)
RenderingServer.material_set_param(mat_rid, "_editor_decal_position", editor_decal_position)
RenderingServer.material_set_param(mat_rid, "_editor_decal_rotation", editor_decal_rotation)
RenderingServer.material_set_param(mat_rid, "_editor_decal_size", editor_decal_size)
RenderingServer.material_set_param(mat_rid, "_editor_decal_color", editor_decal_color)
RenderingServer.material_set_param(mat_rid, "_editor_decal_visible", editor_decal_visible)
RenderingServer.material_set_param(mat_rid, "_editor_crosshair_threshold", brush_data["crosshair_threshold"] + 0.1)
RenderingServer.material_set_param(mat_rid, "_region_map", r_map)
func update_compatibility_decal() -> void:
if not plugin.terrain.is_compatibility_mode():
return
func is_shader_valid() -> bool:
# As long as the compiled shader contains at least 1 uniform, we can use it to check
# if the shader compilation has failed, as this will then return an empty dictionary.
if not plugin.terrain:
return false
var params = RenderingServer.get_shader_parameter_list(plugin.terrain.material.get_shader_rid())
if params.is_empty():
return false
else:
return true
# Verify setup
if editor_decal_position.size() != 3:
editor_decal_position.resize(3)
editor_decal_rotation.resize(3)
editor_decal_size.resize(3)
editor_decal_color.resize(3)
editor_decal_visible.resize(3)
decal_timer.timeout.connect(func():
var mat_rid: RID = plugin.terrain.material.get_material_rid()
editor_decal_visible[0] = false
RenderingServer.material_set_param(mat_rid, "_editor_decal_visible", editor_decal_visible)
)
# Update compatibility decal
var mat_rid: RID = plugin.terrain.material.get_material_rid()
if decal.visible:
editor_decal_position[0] = Vector2(decal.global_position.x, decal.global_position.z)
editor_decal_rotation[0] = decal.rotation.y
editor_decal_size[0] = brush_data.get("size")
editor_decal_color[0] = decal.modulate
editor_decal_visible[0] = decal.visible
RenderingServer.material_set_param(
mat_rid, "_editor_decal_0", decal.texture_albedo.get_rid()
)
if gradient_decals.size() >= 1:
editor_decal_position[1] = Vector2(gradient_decals[0].global_position.x,
gradient_decals[0].global_position.z)
editor_decal_rotation[1] = gradient_decals[0].rotation.y
editor_decal_size[1] = 10.0
editor_decal_color[1] = gradient_decals[0].modulate
editor_decal_visible[1] = gradient_decals[0].visible
RenderingServer.material_set_param(
mat_rid, "_editor_decal_1", gradient_decals[0].texture_albedo.get_rid()
)
if gradient_decals.size() >= 2:
editor_decal_position[2] = Vector2(gradient_decals[1].global_position.x,
gradient_decals[1].global_position.z)
editor_decal_rotation[2] = gradient_decals[1].rotation.y
editor_decal_size[2] = 10.0
editor_decal_color[2] = gradient_decals[1].modulate
editor_decal_visible[2] = gradient_decals[1].visible
RenderingServer.material_set_param(
mat_rid, "_editor_decal_2", gradient_decals[1].texture_albedo.get_rid()
)
RenderingServer.material_set_param(mat_rid, "_editor_decal_position", editor_decal_position)
RenderingServer.material_set_param(mat_rid, "_editor_decal_rotation", editor_decal_rotation)
RenderingServer.material_set_param(mat_rid, "_editor_decal_size", editor_decal_size)
RenderingServer.material_set_param(mat_rid, "_editor_decal_color", editor_decal_color)
RenderingServer.material_set_param(mat_rid, "_editor_decal_visible", editor_decal_visible)
func hide_decal() -> void:
editor_decal_visible = [false, false, false]
if is_shader_valid():
var r_map: PackedInt32Array = plugin.terrain.data.get_region_map()
RenderingServer.material_set_param(mat_rid, "_editor_decal_visible", editor_decal_visible)
RenderingServer.material_set_param(mat_rid, "_region_map", r_map)
# These array sizes are reset to 0 when closing scenes for some unknown reason, so check and reset
func reset_decal_arrays() -> void:
if editor_decal_color.size() < 3:
editor_decal_position = [Vector2(), Vector2(), Vector2()]
editor_decal_rotation = [float(), float(), float()]
editor_decal_size = [float(), float(), float()]
editor_decal_color = [Color(), Color(), Color()]
editor_decal_visible = [false, false, false]
editor_brush_texture_rid = RID()
func set_decal_rotation(p_rot: float) -> void:
decal.rotation.y = p_rot
editor_decal_rotation[0] = p_rot
if is_shader_valid():
RenderingServer.material_set_param(mat_rid, "_editor_decal_rotation", editor_decal_rotation)
func _on_picking(p_type: int, p_callback: Callable) -> void:
@@ -515,7 +549,7 @@ func pick(p_global_position: Vector3) -> void:
var color: Color
match picking:
Terrain3DEditor.HEIGHT, Terrain3DEditor.SCULPT:
color = plugin.terrain.data.get_pixel(Terrain3DRegion.TYPE_HEIGHT, p_global_position)
color = Color(plugin.terrain.data.get_height(p_global_position), 0., 0., 1.)
Terrain3DEditor.ROUGHNESS:
color = plugin.terrain.data.get_pixel(Terrain3DRegion.TYPE_COLOR, p_global_position)
Terrain3DEditor.COLOR:
@@ -527,7 +561,9 @@ func pick(p_global_position: Vector3) -> void:
_:
push_error("Unsupported picking type: ", picking)
return
picking_callback.call(picking, color, p_global_position)
if picking_callback.is_valid():
picking_callback.call(picking, color, p_global_position)
picking_callback = Callable()
picking = Terrain3DEditor.TOOL_MAX
elif operation_builder and operation_builder.is_picking():

View File

@@ -1 +1 @@
uid://bv4lj2cvubl7p
uid://bpad72s36mwkx

View File

@@ -1,7 +1,7 @@
[configuration]
entry_symbol = "terrain_3d_init"
compatibility_minimum = 4.2
compatibility_minimum = 4.4
[icons]
@@ -26,4 +26,7 @@ android.debug.arm64 = "res://addons/terrain_3d/bin/libterrain.android.debug.arm6
android.release.arm64 = "res://addons/terrain_3d/bin/libterrain.android.release.arm64.so"
ios.debug = "res://addons/terrain_3d/bin/libterrain.ios.debug.universal.dylib"
ios.release = "res://addons/terrain_3d/bin/libterrain.ios.release.universal.dylib"
ios.release = "res://addons/terrain_3d/bin/libterrain.ios.release.universal.dylib"
web.debug = "res://addons/terrain_3d/bin/libterrain.web.debug.wasm32.wasm"
web.release = "res://addons/terrain_3d/bin/libterrain.web.release.wasm32.wasm"

View File

@@ -1 +1 @@
uid://chcl8jili5ggt
uid://bdn2ygldx56a8

View File

@@ -1,3 +1,5 @@
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
# Importer for Terrain3D
@tool
extends Terrain3D
@@ -20,10 +22,14 @@ func reset_settings(p_value) -> void:
r16_size = Vector2i(1024, 1024)
material = null
assets = null
reset_terrain(true)
func reset_terrain(p_value) -> void:
data_directory = ""
for region:Terrain3DRegion in data.get_regions_active():
data.remove_region(region, false)
data.update_maps(Terrain3DRegion.TYPE_MAX, true, false)
func update_heights(p_value) -> void:

View File

@@ -1 +1 @@
uid://co2gd0uhumg34
uid://cib2rig7vup10

View File

@@ -1,6 +1,6 @@
[gd_scene load_steps=9 format=3 uid="uid://blaieaqp413k7"]
[ext_resource type="Script" uid="uid://co2gd0uhumg34" path="res://addons/terrain_3d/tools/importer.gd" id="1_60b8f"]
[ext_resource type="Script" path="res://addons/terrain_3d/tools/importer.gd" id="1_60b8f"]
[sub_resource type="Gradient" id="Gradient_88f3t"]
offsets = PackedFloat32Array(0.2, 1)

View File

@@ -1,3 +1,6 @@
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
# Objects parent for Terrain3D
# Children nodes get transform updates on sculpting
@tool
extends Node3D
class_name Terrain3DObjects
@@ -97,7 +100,8 @@ func _on_child_exiting_tree(p_node: Node) -> void:
var helper: TransformChangedNotifier = p_node.get_node_or_null(CHILD_HELPER_PATH)
if helper:
helper.transform_changed.disconnect(_on_child_transform_changed)
if helper.transform_changed.is_connected(_on_child_transform_changed):
helper.transform_changed.disconnect(_on_child_transform_changed)
p_node.remove_child(helper)
helper.queue_free()

View File

@@ -1 +1 @@
uid://cl5vs8p47r15v
uid://dbndw8p05yam7

View File

@@ -1,3 +1,5 @@
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
# Transform Changed Notifier for Terrain3D
@tool
extends Node3D

View File

@@ -1 +1 @@
uid://drw381usdrd8b
uid://claxtgppe8keq