new terrain plugin
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
[gd_scene load_steps=5 format=3 uid="uid://d3sr0a7dxfkr8"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bp7r4ppgq1m0g" path="res://addons/terrain_3d/extras/particle_example/terrain_3D_particles.gd" id="1_gl3qg"]
|
||||
[ext_resource type="Material" uid="uid://el5y10hnh13g" path="res://addons/terrain_3d/extras/particle_example/process_material.tres" id="2_2gon1"]
|
||||
[ext_resource type="Material" uid="uid://ljo1wt61kbkq" path="res://addons/terrain_3d/extras/particle_example/grass_material.tres" id="3_qyjnw"]
|
||||
|
||||
[sub_resource type="RibbonTrailMesh" id="RibbonTrailMesh_fwrtk"]
|
||||
shape = 0
|
||||
section_length = 0.18
|
||||
section_segments = 1
|
||||
|
||||
[node name="Terrain3DParticles" type="Node3D"]
|
||||
script = ExtResource("1_gl3qg")
|
||||
instance_spacing = 0.25
|
||||
cell_width = 24.0
|
||||
grid_width = 5
|
||||
rows = 96
|
||||
amount = 9216
|
||||
process_material = ExtResource("2_2gon1")
|
||||
mesh = SubResource("RibbonTrailMesh_fwrtk")
|
||||
shadow_mode = 0
|
||||
mesh_material_override = ExtResource("3_qyjnw")
|
||||
min_draw_distance = 60.0
|
||||
particle_count = 230400
|
||||
metadata/_edit_lock_ = true
|
||||
69
addons/terrain_3d/extras/particle_example/grass.gdshader
Normal file
69
addons/terrain_3d/extras/particle_example/grass.gdshader
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
|
||||
|
||||
shader_type spatial;
|
||||
|
||||
render_mode skip_vertex_transform, cull_disabled, blend_mix;
|
||||
|
||||
uniform vec2 wind_direction = vec2(1.0, 1.0);
|
||||
|
||||
varying vec4 data;
|
||||
|
||||
// A lot of hard coded things here atm.
|
||||
void vertex() {
|
||||
// Wind effect from model data, in this case no vertex colors,
|
||||
// so just use vertex Y component, including mesh offset
|
||||
data[2] = (VERTEX.y + 0.55);
|
||||
data[2] *= data[2]; // make non-linear
|
||||
|
||||
// Ribbon used as a grass mesh.. so pinch the top.
|
||||
VERTEX.xz *= (1.0 - data[2]);
|
||||
|
||||
// Brighten tips
|
||||
COLOR = mix(COLOR, vec4(1.0), smoothstep(0.9, 1.0, data[2]));
|
||||
// Darken base, skip is scale is less than threshold, as this means "grow in" is occuring.
|
||||
COLOR *= INSTANCE_CUSTOM[3] < 0.35 ? 1. : mix(1.0, 0.75, smoothstep(0.35, 0.0, data[2]));
|
||||
// Save red/green shift for fragment
|
||||
data.rg = INSTANCE_CUSTOM.rg;
|
||||
|
||||
// World space vertex
|
||||
vec3 w_vertex = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||
// Get wind force and scale from process()
|
||||
float scale = pow(INSTANCE_CUSTOM[3] * INSTANCE_CUSTOM[3], 0.707);
|
||||
float force = INSTANCE_CUSTOM[2] * data[2] * scale;
|
||||
// Add some cheap jitter at high wind values
|
||||
force -= fract(force * 256.0) * force * 0.05;
|
||||
// Curve the result
|
||||
force = pow(force, 0.707);
|
||||
|
||||
// These 2 combined result in a decent bend without resorting to matrices or pivot data.
|
||||
// Lateral move and wobble
|
||||
float lateral_wobble = sin(TIME * 2.0 * (1.0 + data.r + data.g)) * 0.25 * (1.0 - INSTANCE_CUSTOM[2]);
|
||||
vec2 direction = normalize(wind_direction);
|
||||
w_vertex.xz -= (vec2(-direction.y, direction.x) * lateral_wobble + direction) * force;
|
||||
// Flatten
|
||||
w_vertex.y -= INSTANCE_CUSTOM[2] * force * data[2];
|
||||
|
||||
// Save final wind force value for fragment.
|
||||
data[3] = force;
|
||||
|
||||
VERTEX = (VIEW_MATRIX * vec4(w_vertex, 1.0)).xyz;
|
||||
NORMAL = MODELVIEW_NORMAL_MATRIX * NORMAL;
|
||||
BINORMAL = MODELVIEW_NORMAL_MATRIX * BINORMAL;
|
||||
TANGENT = MODELVIEW_NORMAL_MATRIX * TANGENT;
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
// Hard coded color.
|
||||
ALBEDO = vec3(0.20, 0.22, 0.05) * (data[2] * 0.5 + 0.5);
|
||||
ALBEDO.rg *= (data.rg * 0.3 + 0.9);
|
||||
ALBEDO *= pow(COLOR.rgb, vec3(2.2));
|
||||
// Modify roughness / specular based on wind force for added detail
|
||||
float spec_rough = clamp(max(data[2], data[3]), 0., 1.);
|
||||
ROUGHNESS = 1. - spec_rough;
|
||||
SPECULAR = clamp(spec_rough * 0.25, 0., .15);
|
||||
|
||||
BACKLIGHT = vec3(0.33);
|
||||
#if CURRENT_RENDERER == RENDERER_COMPATIBILITY
|
||||
ALBEDO = pow(ALBEDO, vec3(0.4));
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dq3lfyp3u5oxt
|
||||
@@ -0,0 +1,8 @@
|
||||
[gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://ljo1wt61kbkq"]
|
||||
|
||||
[ext_resource type="Shader" uid="uid://dq3lfyp3u5oxt" path="res://addons/terrain_3d/extras/particle_example/grass.gdshader" id="1_nkru0"]
|
||||
|
||||
[resource]
|
||||
render_priority = 0
|
||||
shader = ExtResource("1_nkru0")
|
||||
shader_parameter/wind_direction = Vector2(1, 1)
|
||||
275
addons/terrain_3d/extras/particle_example/particles.gdshader
Normal file
275
addons/terrain_3d/extras/particle_example/particles.gdshader
Normal file
@@ -0,0 +1,275 @@
|
||||
// Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
|
||||
|
||||
// This is an example particle shader designed to procedurally place
|
||||
// particles like grass, small rocks, and other ground effects, on the terrain
|
||||
// surface by reading the Terrain3D data maps. It works in tandem with the
|
||||
// provided GDScript.
|
||||
|
||||
shader_type particles;
|
||||
render_mode disable_velocity, disable_force;
|
||||
|
||||
group_uniforms options;
|
||||
uniform sampler2D main_noise;
|
||||
uniform float main_noise_scale = 0.01;
|
||||
uniform vec3 position_offset = vec3(0.);
|
||||
uniform bool align_to_normal = true;
|
||||
uniform float normal_strength : hint_range(0.01, 1.0, 0.01) = 0.3;
|
||||
uniform bool random_rotation = true;
|
||||
uniform float random_spacing : hint_range(0.0, 1.0, 0.01) = 0.5;
|
||||
uniform vec3 min_scale = vec3(1.0);
|
||||
uniform vec3 max_scale = vec3(1.0);
|
||||
|
||||
group_uniforms wind;
|
||||
uniform float noise_scale = 0.0041;
|
||||
uniform float wind_speed = 0.025;
|
||||
uniform float wind_strength : hint_range(0.0, 1.0, 0.01) = 1.0;
|
||||
uniform float wind_dithering = 4.0;
|
||||
uniform vec2 wind_direction = vec2(1.0,1.0);
|
||||
|
||||
group_uniforms shapeing;
|
||||
uniform float clod_scale_boost = 3.0;
|
||||
uniform float clod_min_threshold : hint_range(0.0, 1.0, 0.001) = 0.2;
|
||||
uniform float clod_max_threshold : hint_range(0.0, 1.0, 0.001) = 0.5;
|
||||
uniform float patch_min_threshold : hint_range(0.0, 1.0, 0.001) = 0.025;
|
||||
uniform float patch_max_threshold : hint_range(0.0, 1.0, 0.001) = 0.2;
|
||||
|
||||
group_uniforms filtering;
|
||||
uniform float condition_dither_range : hint_range(0.0, 1.0, 0.01) = 0.15;
|
||||
uniform float surface_slope_min : hint_range(0.0, 1.0, 0.01) = 0.87;
|
||||
uniform float distance_fade_ammount : hint_range(0.0, 1.0, 0.01) = 0.5;
|
||||
|
||||
group_uniforms private;
|
||||
uniform float max_dist = 1.;
|
||||
uniform vec3 camera_position = vec3(0.);
|
||||
uniform uint instance_rows = 1;
|
||||
uniform float instance_spacing = 0.5;
|
||||
uniform uint _background_mode = 0u;
|
||||
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/REGION_SIZE
|
||||
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 : repeat_disable;
|
||||
|
||||
// Defined Constants
|
||||
#define SKIP_PASS 0
|
||||
#define VERTEX_PASS 1
|
||||
#define FRAGMENT_PASS 2
|
||||
|
||||
// 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 < 0; 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);
|
||||
}
|
||||
|
||||
#if CURRENT_RENDERER == RENDERER_COMPATIBILITY
|
||||
#define fma(a, b, c) ((a) * (b) + (c))
|
||||
#endif
|
||||
float random(vec2 v) {
|
||||
return fract(1e4 * sin(fma(17.0, v.x, v.y * 0.1)) * (0.1 + abs(sin(fma(v.y, 13.0, v.x)))));
|
||||
}
|
||||
|
||||
mat3 rotation_matrix(vec3 axis, float angle) {
|
||||
float c = cos(angle);
|
||||
float s = sin(angle);
|
||||
float t = 1.0 - c;
|
||||
vec3 n = normalize(axis);
|
||||
float x = n.x;
|
||||
float y = n.y;
|
||||
float z = n.z;
|
||||
|
||||
return mat3(
|
||||
vec3(t * x * x + c, t * x * y - z * s, t * x * z + y * s),
|
||||
vec3(t * x * y + z * s, t * y * y + c, t * y * z - x * s),
|
||||
vec3(t * x * z - y * s, t * y * z + x * s, t * z * z + c));
|
||||
}
|
||||
|
||||
mat3 align_to_vector(vec3 normal) {
|
||||
vec3 up = vec3(0.0, 1.0, 0.0);
|
||||
if (abs(dot(normal, up)) > 0.9999) { // Avoid singularity
|
||||
up = vec3(1.0, 0.0, 0.0);
|
||||
}
|
||||
vec3 tangent = normalize(cross(up, normal));
|
||||
vec3 bitangent = normalize(cross(tangent, normal));
|
||||
return mat3(tangent, normal, bitangent);
|
||||
}
|
||||
|
||||
void start() {
|
||||
// Create centered a grid
|
||||
vec3 pos = vec3(float(INDEX % instance_rows), 0.0, float(INDEX / instance_rows)) - float(instance_rows >> 1u);
|
||||
|
||||
// Apply spcaing
|
||||
pos *= instance_spacing;
|
||||
// Move the grid to the emitter, snapping is handled CPU side
|
||||
pos.xz += EMISSION_TRANSFORM[3].xz;
|
||||
|
||||
// Create random values per-instance, incorporating the seed, mask bits to avoid NAN/INF
|
||||
float seed = fract(uintBitsToFloat(RANDOM_SEED & 0x7EFFFFFFu));
|
||||
vec3 r = fract(vec3(random(pos.xz), random(pos.xz + vec2(0.5)), random(pos.xz - vec2(0.5))) + seed);
|
||||
// Randomize instance spacing
|
||||
pos.x += ((r.x * 2.0) - 1.0) * random_spacing * instance_spacing;
|
||||
pos.z += ((r.z * 2.0) - 1.0) * random_spacing * instance_spacing;
|
||||
|
||||
// Lookup offsets, ID and blend weight
|
||||
const vec3 offsets = vec3(0, 1, 2);
|
||||
vec2 index_id = floor(pos.xz * _vertex_density);
|
||||
vec2 weight = fract(pos.xz * _vertex_density);
|
||||
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
|
||||
);
|
||||
|
||||
ivec3 index[4];
|
||||
// Map lookups
|
||||
index[0] = get_index_coord(index_id + offsets.xy, VERTEX_PASS);
|
||||
index[1] = get_index_coord(index_id + offsets.yy, VERTEX_PASS);
|
||||
index[2] = get_index_coord(index_id + offsets.yx, VERTEX_PASS);
|
||||
index[3] = get_index_coord(index_id + offsets.xx, VERTEX_PASS);
|
||||
|
||||
highp float h[8];
|
||||
h[0] = texelFetch(_height_maps, index[0], 0).r; // 0 (0,1)
|
||||
h[1] = texelFetch(_height_maps, index[1], 0).r; // 1 (1,1)
|
||||
h[2] = texelFetch(_height_maps, index[2], 0).r; // 2 (1,0)
|
||||
h[3] = texelFetch(_height_maps, index[3], 0).r; // 3 (0,0)
|
||||
h[4] = texelFetch(_height_maps, get_index_coord(index_id + offsets.yz, VERTEX_PASS), 0).r; // 4 (1,2)
|
||||
h[5] = texelFetch(_height_maps, get_index_coord(index_id + offsets.zy, VERTEX_PASS), 0).r; // 5 (2,1)
|
||||
h[6] = texelFetch(_height_maps, get_index_coord(index_id + offsets.zx, VERTEX_PASS), 0).r; // 6 (2,0)
|
||||
h[7] = texelFetch(_height_maps, get_index_coord(index_id + offsets.xz, VERTEX_PASS), 0).r; // 7 (0,2)
|
||||
vec3 index_normal[4];
|
||||
index_normal[0] = vec3(h[0] - h[1], _vertex_spacing, h[0] - h[7]);
|
||||
index_normal[1] = vec3(h[1] - h[5], _vertex_spacing, h[1] - h[4]);
|
||||
index_normal[2] = vec3(h[2] - h[6], _vertex_spacing, h[2] - h[1]);
|
||||
index_normal[3] = vec3(h[3] - h[2], _vertex_spacing, h[3] - h[0]);
|
||||
vec3 w_normal = normalize(
|
||||
index_normal[0] * weights[0] +
|
||||
index_normal[1] * weights[1] +
|
||||
index_normal[2] * weights[2] +
|
||||
index_normal[3] * weights[3]);
|
||||
|
||||
// Set the height according to the heightmap data
|
||||
pos.y =
|
||||
h[0] * weights[0] +
|
||||
h[1] * weights[1] +
|
||||
h[2] * weights[2] +
|
||||
h[3] * weights[3] ;
|
||||
|
||||
// Offset, Rotation, Alignment.
|
||||
TRANSFORM = mat4(1.0);
|
||||
vec3 orientation = vec3(0., 1., 0.);
|
||||
|
||||
vec2 uv = (pos.xz) * main_noise_scale;
|
||||
float noise = textureLod(main_noise, uv, 0.0).r;
|
||||
float clods = smoothstep(clod_min_threshold, clod_max_threshold, noise) * clod_scale_boost;
|
||||
float patch = smoothstep(patch_min_threshold, patch_max_threshold, noise);
|
||||
float width_modifier = 1.0 + 3.0 * smoothstep(0., max_dist, length(camera_position - pos));
|
||||
|
||||
// Calculate scale
|
||||
vec3 scale = vec3(
|
||||
mix(min_scale.x, max_scale.x, r.x) * width_modifier,
|
||||
mix(min_scale.y, max_scale.y, r.y) + clods,
|
||||
mix(min_scale.z, max_scale.z, r.z) * width_modifier) * patch;
|
||||
|
||||
// Apply scale to offset
|
||||
vec3 offset = position_offset * scale;
|
||||
|
||||
// Apply normal orientation
|
||||
if (align_to_normal) {
|
||||
orientation = mix(orientation, w_normal, normal_strength);
|
||||
mat3 alignment = align_to_vector(orientation);
|
||||
offset = alignment * offset;
|
||||
TRANSFORM = mat4(alignment);
|
||||
}
|
||||
|
||||
// Apply rotation around orientation
|
||||
if (random_rotation) {
|
||||
mat3 rotation = rotation_matrix(orientation, r.x * TAU);
|
||||
TRANSFORM = mat4(rotation) * TRANSFORM;
|
||||
}
|
||||
|
||||
// Filtering - Causes some particles to be rendered as degenerate triangles
|
||||
// via 0./0. - Particles filtered this way are still processed by the GPU.
|
||||
// For compatibility it seems we must shift as well.
|
||||
// Surface slope filtering
|
||||
if (surface_slope_min > w_normal.y + (r.y - 0.5) * condition_dither_range) {
|
||||
pos.y = 0. / 0.;
|
||||
pos.xz = vec2(100000.0);
|
||||
}
|
||||
|
||||
// Read color map
|
||||
highp vec4 c[4];
|
||||
#define COLOR_MAP vec4(1., 1., 1., 0.5)
|
||||
c[0] = index[0].z >= 0 ? texelFetch(_color_maps, index[0], 0) : COLOR_MAP; // 0 (0,1)
|
||||
c[1] = index[1].z >= 0 ? texelFetch(_color_maps, index[1], 0) : COLOR_MAP; // 1 (1,1)
|
||||
c[2] = index[2].z >= 0 ? texelFetch(_color_maps, index[2], 0) : COLOR_MAP; // 2 (1,0)
|
||||
c[3] = index[3].z >= 0 ? texelFetch(_color_maps, index[3], 0) : COLOR_MAP; // 3 (0,0)
|
||||
vec4 color_map =
|
||||
c[0] * weights[0] +
|
||||
c[1] * weights[1] +
|
||||
c[2] * weights[2] +
|
||||
c[3] * weights[3] ;
|
||||
|
||||
COLOR = color_map;
|
||||
|
||||
// Read control map
|
||||
uint control = floatBitsToUint(texelFetch(_control_maps, index[3], 0).r);
|
||||
bool auto = bool(control & 0x1u);
|
||||
int base = int(control >>27u & 0x1Fu);
|
||||
int over = int(control >> 22u & 0x1Fu);
|
||||
float blend = float(control >> 14u & 0xFFu) * 0.003921568627450; // 1. / 255.
|
||||
|
||||
// Hardcoded example, hand painted texture id 0 is filtered out.
|
||||
if (!auto && ((base == 0 && blend < 0.7) || (over == 0 && blend >= 0.3))) {
|
||||
pos.y = 0. / 0.;
|
||||
pos.xz = vec2(100000.0);
|
||||
}
|
||||
|
||||
if (length(camera_position - pos) > max_dist) {
|
||||
pos.y = 0. / 0.;
|
||||
pos.xz = vec2(100000.0);
|
||||
} else {
|
||||
float fade_factor = 1.0 - smoothstep(max_dist * distance_fade_ammount, max_dist + 0.0001, length(camera_position - pos)) + 0.001;
|
||||
scale.y *= fade_factor;
|
||||
offset *= fade_factor;
|
||||
}
|
||||
|
||||
// Apply scale
|
||||
TRANSFORM[0] *= scale.x;
|
||||
TRANSFORM[1] *= scale.y;
|
||||
TRANSFORM[2] *= scale.z;
|
||||
|
||||
// Apply the position
|
||||
TRANSFORM[3].xyz = pos.xyz + offset;
|
||||
|
||||
// Save Fixed 2 Random values for Reg/Green color randomness
|
||||
CUSTOM.rg = r.rg;
|
||||
// Save Y component scale pre-rotation
|
||||
CUSTOM[3] = scale.y;
|
||||
}
|
||||
|
||||
void process() {
|
||||
// Extract world space UV from Transform Matrix
|
||||
vec2 uv = (TRANSFORM[3].xz + CUSTOM.rg * wind_dithering) * noise_scale;
|
||||
// Scaled wind noise, updated per instance, at process FPS. Passed to Vertex()
|
||||
CUSTOM[2] = textureLod(main_noise, uv + TIME * wind_speed * normalize(wind_direction), 0.0).r * wind_strength;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dce675i014xcn
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,237 @@
|
||||
# Copyright © 2025 Cory Petkovsek, Roope Palmroos, and Contributors.
|
||||
#
|
||||
# This is an example of using a particle shader with Terrain3D.
|
||||
# To use it, add `Terrain3DParticles.tscn` to your scene and assign the terrain.
|
||||
# Then customize the settings, materials and shader to extend it and make it your own.
|
||||
|
||||
@tool
|
||||
extends Node3D
|
||||
|
||||
|
||||
#region settings
|
||||
## Auto set if attached as a child of a Terrain3D node
|
||||
@export var terrain: Terrain3D:
|
||||
set(value):
|
||||
terrain = value
|
||||
_create_grid()
|
||||
|
||||
|
||||
## Distance between instances
|
||||
@export_range(0.125, 2.0, 0.015625) var instance_spacing: float = 0.5:
|
||||
set(value):
|
||||
instance_spacing = clamp(round(value * 64.0) * 0.015625, 0.125, 2.0)
|
||||
rows = maxi(int(cell_width / instance_spacing), 1)
|
||||
amount = rows * rows
|
||||
_set_offsets()
|
||||
|
||||
|
||||
## Width of an individual cell of the grid
|
||||
@export_range(8.0, 256.0, 1.0) var cell_width: float = 32.0:
|
||||
set(value):
|
||||
cell_width = clamp(value, 8.0, 256.0)
|
||||
rows = maxi(int(cell_width / instance_spacing), 1)
|
||||
amount = rows * rows
|
||||
min_draw_distance = 1.0
|
||||
# Have to update aabb
|
||||
if terrain and terrain.data:
|
||||
var height_range: Vector2 = terrain.data.get_height_range()
|
||||
var height: float = height_range[0] - height_range[1]
|
||||
var aabb: AABB = AABB()
|
||||
aabb.size = Vector3(cell_width, height, cell_width)
|
||||
aabb.position = aabb.size * -0.5
|
||||
aabb.position.y = height_range[1]
|
||||
for p in particle_nodes:
|
||||
p.custom_aabb = aabb
|
||||
_set_offsets()
|
||||
|
||||
|
||||
## Grid width. Must be odd.
|
||||
## Higher values cull slightly better, draw further out.
|
||||
@export_range(1, 15, 2) var grid_width: int = 9:
|
||||
set(value):
|
||||
grid_width = value
|
||||
particle_count = 1
|
||||
min_draw_distance = 1.0
|
||||
_create_grid()
|
||||
|
||||
|
||||
@export_storage var rows: int = 1
|
||||
|
||||
@export_storage var amount: int = 1:
|
||||
set(value):
|
||||
amount = value
|
||||
particle_count = value
|
||||
last_pos = Vector3.ZERO
|
||||
for p in particle_nodes:
|
||||
p.amount = amount
|
||||
|
||||
|
||||
@export_range(1, 256, 1) var process_fixed_fps: int = 30:
|
||||
set(value):
|
||||
process_fixed_fps = maxi(value, 1)
|
||||
for p in particle_nodes:
|
||||
p.fixed_fps = process_fixed_fps
|
||||
p.preprocess = 1.0 / float(process_fixed_fps)
|
||||
|
||||
|
||||
## Access to process material parameters
|
||||
@export var process_material: ShaderMaterial
|
||||
|
||||
## The mesh that each particle will render
|
||||
@export var mesh: Mesh
|
||||
|
||||
@export var shadow_mode: GeometryInstance3D.ShadowCastingSetting = (
|
||||
GeometryInstance3D.ShadowCastingSetting.SHADOW_CASTING_SETTING_ON):
|
||||
set(value):
|
||||
shadow_mode = value
|
||||
for p in particle_nodes:
|
||||
p.cast_shadow = value
|
||||
|
||||
|
||||
## Override material for the particle mesh
|
||||
@export_custom(
|
||||
PROPERTY_HINT_RESOURCE_TYPE,
|
||||
"BaseMaterial3D,ShaderMaterial") var mesh_material_override: Material:
|
||||
set(value):
|
||||
mesh_material_override = value
|
||||
for p in particle_nodes:
|
||||
p.material_override = mesh_material_override
|
||||
|
||||
|
||||
@export_group("Info")
|
||||
## The minimum distance that particles will be drawn upto
|
||||
## If using fade out effects like pixel alpha this is the limit to use.
|
||||
@export var min_draw_distance: float = 1.0:
|
||||
set(value):
|
||||
min_draw_distance = float(cell_width * grid_width) * 0.5
|
||||
|
||||
|
||||
## Displays current total particle count based on Cell Width and Instance Spacing
|
||||
@export var particle_count: int = 1:
|
||||
set(value):
|
||||
particle_count = amount * grid_width * grid_width
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
var offsets: Array[Vector3]
|
||||
var last_pos: Vector3 = Vector3.ZERO
|
||||
var particle_nodes: Array[GPUParticles3D]
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if not terrain:
|
||||
var parent: Node = get_parent()
|
||||
if parent is Terrain3D:
|
||||
terrain = parent
|
||||
_create_grid()
|
||||
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_PREDELETE:
|
||||
_destroy_grid()
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if terrain:
|
||||
var camera: Camera3D = terrain.get_camera()
|
||||
if camera:
|
||||
if last_pos.distance_squared_to(camera.global_position) > 1.0:
|
||||
var pos: Vector3 = camera.global_position.snapped(Vector3.ONE)
|
||||
_position_grid(pos)
|
||||
RenderingServer.material_set_param(process_material.get_rid(), "camera_position", pos )
|
||||
last_pos = camera.global_position
|
||||
_update_process_parameters()
|
||||
else:
|
||||
set_physics_process(false)
|
||||
|
||||
|
||||
func _create_grid() -> void:
|
||||
_destroy_grid()
|
||||
if not terrain:
|
||||
return
|
||||
set_physics_process(true)
|
||||
_set_offsets()
|
||||
var hr: Vector2 = terrain.data.get_height_range()
|
||||
var height: float = hr.x - hr.y
|
||||
var aabb: AABB = AABB()
|
||||
aabb.size = Vector3(cell_width, height, cell_width)
|
||||
aabb.position = aabb.size * -0.5
|
||||
aabb.position.y = hr.y
|
||||
var half_grid: int = grid_width / 2
|
||||
# Iterating the array like this allows identifying grid position, in case setting
|
||||
# different mesh or materials is desired for LODs etc.
|
||||
for x in range(-half_grid, half_grid + 1):
|
||||
for z in range(-half_grid, half_grid + 1):
|
||||
#var ring: int = maxi(maxi(absi(x), absi(z)), 0)
|
||||
var particle_node = GPUParticles3D.new()
|
||||
particle_node.lifetime = 600.0
|
||||
particle_node.amount = amount
|
||||
particle_node.explosiveness = 1.0
|
||||
particle_node.amount_ratio = 1.0
|
||||
particle_node.process_material = process_material
|
||||
particle_node.draw_pass_1 = mesh
|
||||
particle_node.speed_scale = 1.0
|
||||
particle_node.custom_aabb = aabb
|
||||
particle_node.cast_shadow = shadow_mode
|
||||
particle_node.fixed_fps = process_fixed_fps
|
||||
# This prevent minor grid alignment errors when the camera is moving very fast
|
||||
particle_node.preprocess = 1.0 / float(process_fixed_fps)
|
||||
if mesh_material_override:
|
||||
particle_node.material_override = mesh_material_override
|
||||
particle_node.use_fixed_seed = true
|
||||
if (x > -half_grid and z > -half_grid): # Use the same seed across all nodes
|
||||
particle_node.seed = particle_nodes[0].seed
|
||||
self.add_child(particle_node)
|
||||
particle_node.emitting = true
|
||||
particle_nodes.push_back(particle_node)
|
||||
last_pos = Vector3.ZERO
|
||||
|
||||
|
||||
func _set_offsets() -> void:
|
||||
var half_grid: int = grid_width / 2
|
||||
offsets.clear()
|
||||
for x in range(-half_grid, half_grid + 1):
|
||||
for z in range(-half_grid, half_grid + 1):
|
||||
var offset := Vector3(
|
||||
float(x * rows) * instance_spacing,
|
||||
0.0,
|
||||
float(z * rows) * instance_spacing
|
||||
)
|
||||
offsets.append(offset)
|
||||
|
||||
|
||||
func _destroy_grid() -> void:
|
||||
for node: GPUParticles3D in particle_nodes:
|
||||
if is_instance_valid(node):
|
||||
node.queue_free()
|
||||
particle_nodes.clear()
|
||||
|
||||
|
||||
func _position_grid(pos: Vector3) -> void:
|
||||
for i in particle_nodes.size():
|
||||
var node: GPUParticles3D = particle_nodes[i]
|
||||
var snap = Vector3(pos.x, 0, pos.z).snapped(Vector3.ONE) + offsets[i]
|
||||
node.global_position = (snap / instance_spacing).round() * instance_spacing
|
||||
node.reset_physics_interpolation()
|
||||
node.restart(true) # keep the same seed.
|
||||
|
||||
|
||||
func _update_process_parameters() -> void:
|
||||
if process_material:
|
||||
var process_rid: RID = process_material.get_rid()
|
||||
if terrain and process_rid.is_valid():
|
||||
RenderingServer.material_set_param(process_rid, "_background_mode", terrain.material.world_background)
|
||||
RenderingServer.material_set_param(process_rid, "_vertex_spacing", terrain.vertex_spacing)
|
||||
RenderingServer.material_set_param(process_rid, "_vertex_density", 1.0 / terrain.vertex_spacing)
|
||||
RenderingServer.material_set_param(process_rid, "_region_size", terrain.region_size)
|
||||
RenderingServer.material_set_param(process_rid, "_region_texel_size", 1.0 / terrain.region_size)
|
||||
RenderingServer.material_set_param(process_rid, "_region_map_size", 32)
|
||||
RenderingServer.material_set_param(process_rid, "_region_map", terrain.data.get_region_map())
|
||||
RenderingServer.material_set_param(process_rid, "_region_locations", terrain.data.get_region_locations())
|
||||
RenderingServer.material_set_param(process_rid, "_height_maps", terrain.data.get_height_maps_rid())
|
||||
RenderingServer.material_set_param(process_rid, "_control_maps", terrain.data.get_control_maps_rid())
|
||||
RenderingServer.material_set_param(process_rid, "_color_maps", terrain.data.get_color_maps_rid())
|
||||
RenderingServer.material_set_param(process_rid, "instance_spacing", instance_spacing)
|
||||
RenderingServer.material_set_param(process_rid, "instance_rows", rows)
|
||||
RenderingServer.material_set_param(process_rid, "max_dist", min_draw_distance)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bp7r4ppgq1m0g
|
||||
Reference in New Issue
Block a user