Add project files
This commit is contained in:
commit
b10fbca719
14 changed files with 588 additions and 0 deletions
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Normalize EOL for all files that Git considers text files.
|
||||
* text=auto eol=lf
|
||||
|
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Godot 4+ specific ignores
|
||||
.godot/
|
||||
/android/
|
||||
|
||||
# Godot-specific ignores
|
||||
.import/
|
||||
export.cfg
|
||||
export_presets.cfg
|
||||
|
||||
# Imported translations (automatically generated from CSV files)
|
||||
*.translation
|
||||
|
||||
# Mono-specific ignores
|
||||
.mono/
|
||||
data_*/
|
||||
mono_crash.*.json
|
||||
|
||||
example/screenshots/*.import
|
||||
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 Julian Müller (ChaoticByte)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
7
README.md
Normal file
7
README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# godot-procedural-grass
|
||||
|
||||
Procedural grass with wind and translucency in Godot 4.3
|
||||
|
||||

|
||||
|
||||
https://raw.githubusercontent.com/ChaoticByte/godot-procedural-grass/main/example/screenshots/example-video.mkv
|
42
example/SimpeFirstPersonController.gd
Normal file
42
example/SimpeFirstPersonController.gd
Normal file
|
@ -0,0 +1,42 @@
|
|||
extends Camera3D
|
||||
|
||||
# Copyright (c) 2024 Julian Müller (ChaoticByte)
|
||||
|
||||
@export var movement_speed = 3.0
|
||||
@export var mouse_sensitivity = 0.01
|
||||
@export var mouse_x_min: float = -PI/2
|
||||
@export var mouse_x_max: float = PI/2
|
||||
|
||||
func _ready() -> void:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
func toggle_mouse_mode():
|
||||
if Input.mouse_mode == Input.MOUSE_MODE_VISIBLE:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
else:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||||
rotate_y(-event.relative.x * mouse_sensitivity)
|
||||
rotation.x = clamp(
|
||||
rotation.x - event.relative.y * mouse_sensitivity,
|
||||
mouse_x_min, mouse_x_max
|
||||
)
|
||||
elif event is InputEventKey:
|
||||
if event.is_action_released("ui_cancel"):
|
||||
toggle_mouse_mode()
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
var dir = Vector3()
|
||||
if Input.is_action_pressed("forward"):
|
||||
dir.z -= 1
|
||||
if Input.is_action_pressed("backward"):
|
||||
dir.z += 1
|
||||
if Input.is_action_pressed("left"):
|
||||
dir.x -= 1
|
||||
if Input.is_action_pressed("right"):
|
||||
dir.x += 1
|
||||
position += (
|
||||
dir.normalized() * delta * movement_speed
|
||||
).rotated(Vector3.UP, rotation.y)
|
11
example/example.gd
Normal file
11
example/example.gd
Normal file
|
@ -0,0 +1,11 @@
|
|||
extends Node3D
|
||||
|
||||
@export var wind_strength = 0.2;
|
||||
|
||||
@onready var fps_label: Label = $UI/FPS
|
||||
|
||||
func _ready() -> void:
|
||||
Wind.wind_strength = wind_strength;
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
fps_label.text = "FPS: %d" % Engine.get_frames_per_second()
|
160
example/example.tscn
Normal file
160
example/example.tscn
Normal file
File diff suppressed because one or more lines are too long
BIN
example/screenshots/example-video.mkv
Normal file
BIN
example/screenshots/example-video.mkv
Normal file
Binary file not shown.
BIN
example/screenshots/screenshot.png
Normal file
BIN
example/screenshots/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 MiB |
65
global/Wind.gd
Normal file
65
global/Wind.gd
Normal file
|
@ -0,0 +1,65 @@
|
|||
extends Node
|
||||
|
||||
# Copyright (c) 2024 Julian Müller (ChaoticByte)
|
||||
|
||||
var wind_strength: float:
|
||||
set(value):
|
||||
wind_strength = value
|
||||
_update_shader_param("wind_strength", value)
|
||||
|
||||
var wind_turbulence: float:
|
||||
set(value):
|
||||
wind_turbulence = value
|
||||
_update_shader_param("wind_turbulence", value)
|
||||
|
||||
var wind_direction: Vector2:
|
||||
set(value):
|
||||
value = value.normalized()
|
||||
wind_direction = value
|
||||
_update_shader_param("wind_direction", value)
|
||||
|
||||
var wind_noise_texture_size: int:
|
||||
set(value):
|
||||
wind_noise_texture_size = value
|
||||
_update_wind_noise()
|
||||
|
||||
var wind_noise: Noise:
|
||||
set(value):
|
||||
wind_noise = value
|
||||
_update_wind_noise()
|
||||
|
||||
func _update_wind_noise():
|
||||
var tex = ImageTexture.new()
|
||||
if wind_noise != null:
|
||||
var img = wind_noise.get_seamless_image(
|
||||
wind_noise_texture_size, wind_noise_texture_size)
|
||||
tex.set_image(img)
|
||||
_update_shader_param("wind_noise", tex)
|
||||
|
||||
var wind_noise_scale: float:
|
||||
set(value):
|
||||
wind_noise_scale = value
|
||||
_update_shader_param("wind_noise_scale", value)
|
||||
|
||||
var wind_noise_strength: float:
|
||||
set(value):
|
||||
wind_noise_strength = value
|
||||
_update_shader_param("wind_noise_strength", value)
|
||||
|
||||
# reset all params to default
|
||||
func reset():
|
||||
wind_strength = 0.2
|
||||
wind_turbulence = 0.5
|
||||
wind_direction = Vector2(1, 1)
|
||||
wind_noise_texture_size = 2048
|
||||
wind_noise = FastNoiseLite.new()
|
||||
wind_noise_scale = 8.0
|
||||
wind_noise_strength = 1.0
|
||||
|
||||
# internal stuff
|
||||
|
||||
func _ready():
|
||||
reset() # init with defaults
|
||||
|
||||
func _update_shader_param(varname: String, value):
|
||||
RenderingServer.global_shader_parameter_set(varname, value)
|
122
procedural_grass/procedural_grass.gd
Normal file
122
procedural_grass/procedural_grass.gd
Normal file
|
@ -0,0 +1,122 @@
|
|||
@tool
|
||||
extends MeshInstance3D
|
||||
|
||||
# Copyright (c) 2024 Julian Müller (ChaoticByte)
|
||||
|
||||
|
||||
# points of a leaf
|
||||
const VERTS: Array[Vector3] = [
|
||||
Vector3(-0.5, 0.0, -0.5), # 0: left base back
|
||||
Vector3(0.5, 0.0, -0.5), # 1: right base back
|
||||
Vector3(-0.5, 0.5, -0.5), # 2: left middle back
|
||||
Vector3(0.5, 0.5, -0.5), # 3: right middle back
|
||||
Vector3(-0.5, 0.0, 0.5), # 4: left base front
|
||||
Vector3(0.5, 0.0, 0.5), # 5: right base front
|
||||
Vector3(-0.5, 0.5, 0.5), # 6: left middle front
|
||||
Vector3(0.5, 0.5, 0.5), # 7: right middle front
|
||||
Vector3(0.0, 1.0, 0.0) # 8: tip
|
||||
]
|
||||
|
||||
|
||||
# triangles of a leaf
|
||||
var TRIS: Array[PackedVector3Array] = [
|
||||
([VERTS[2], VERTS[8], VERTS[6]]), # tip left
|
||||
([VERTS[8], VERTS[3], VERTS[7]]), # tip right
|
||||
([VERTS[6], VERTS[8], VERTS[7]]), # tip front
|
||||
([VERTS[2], VERTS[3], VERTS[8]]), # tip back
|
||||
([VERTS[0], VERTS[4], VERTS[5]]), # base abc
|
||||
([VERTS[0], VERTS[5], VERTS[1]]), # base acd
|
||||
([VERTS[6], VERTS[4], VERTS[0]]), # left abc
|
||||
([VERTS[6], VERTS[0], VERTS[2]]), # left acd
|
||||
([VERTS[3], VERTS[1], VERTS[5]]), # right abc
|
||||
([VERTS[3], VERTS[5], VERTS[7]]), # right acd
|
||||
([VERTS[7], VERTS[5], VERTS[4]]), # front abc
|
||||
([VERTS[7], VERTS[4], VERTS[6]]), # front acd
|
||||
([VERTS[2], VERTS[0], VERTS[1]]), # back abc
|
||||
([VERTS[2], VERTS[1], VERTS[3]]), # back acd
|
||||
]
|
||||
|
||||
|
||||
@export_category("Procedural Grass")
|
||||
|
||||
@export var click_to_update: bool:
|
||||
set(_val):
|
||||
generate_grass()
|
||||
|
||||
@export var color_base: Color
|
||||
@export var color_tip: Color
|
||||
|
||||
@export var leaf_width: float = 0.03
|
||||
@export var leaf_height_min: float = 0.1
|
||||
@export var leaf_height_max: float = 1.0
|
||||
@export var leaf_height_add: float = 0.0
|
||||
@export var leaf_height_mult: float = 0.75
|
||||
|
||||
@export var offset_mult: float = 0.15
|
||||
|
||||
@export var num_leafs: Vector2 = Vector2(40, 40)
|
||||
@export var leafs_gap: float = 0.1
|
||||
|
||||
# I wanna use noise directly instead of an texture
|
||||
# - If using FastNoiseLite: use a frequency around 0.1 and SimplexSmooth
|
||||
@export var height_noise_abs: bool = true
|
||||
@export var height_noise: Noise
|
||||
# - If using FastNoiseLite: use a frequency around 0.2 and SimplexSmooth
|
||||
@export var offset_noise: Noise
|
||||
|
||||
var st = SurfaceTool.new()
|
||||
var shader = preload("res://procedural_grass/procedural_grass.gdshader")
|
||||
var shader_mat = ShaderMaterial.new()
|
||||
var rotation_rng = RandomNumberGenerator.new()
|
||||
|
||||
func generate_leaf(leaf_height: float, offset_xz: Vector2) -> void:
|
||||
# create leaf
|
||||
var rot = rotation_rng.randf_range(0.0, 2*PI)
|
||||
var leaf_size = Vector3(leaf_width, leaf_height, leaf_width)
|
||||
var leaf_offset = Vector3(offset_xz.x, 0.0, offset_xz.y)
|
||||
for tri_ in TRIS:
|
||||
var tri = PackedVector3Array()
|
||||
var colors = PackedColorArray()
|
||||
for v in tri_:
|
||||
tri.append((
|
||||
v.rotated(Vector3.UP, rot)
|
||||
* leaf_size
|
||||
) + leaf_offset
|
||||
)
|
||||
if v.y < 0:
|
||||
colors.append(color_base)
|
||||
else:
|
||||
colors.append(color_tip)
|
||||
st.add_triangle_fan(tri, PackedVector2Array(), colors)
|
||||
|
||||
func generate_grass() -> void:
|
||||
assert(
|
||||
height_noise != null and offset_noise != null,
|
||||
"generate_grass was called, but height_noise or offset_noise is null"
|
||||
)
|
||||
var m = ArrayMesh.new()
|
||||
st.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||
st.set_smooth_group(-1) # !
|
||||
for x in range(-num_leafs.x/2, num_leafs.x/2):
|
||||
for y in range(-num_leafs.y/2, num_leafs.y/2):
|
||||
var h = height_noise.get_noise_2d(x, y) + leaf_height_add
|
||||
if height_noise_abs:
|
||||
h = abs(h)
|
||||
if h > 0:
|
||||
h = clamp(h, leaf_height_min, leaf_height_max)
|
||||
var o = Vector2(
|
||||
offset_noise.get_noise_2d(x, y),
|
||||
offset_noise.get_noise_2d(x + num_leafs.x, y + num_leafs.y) # reuse
|
||||
) * offset_mult
|
||||
generate_leaf(
|
||||
leaf_height_mult * h,
|
||||
o + (Vector2(x, y) * leafs_gap)
|
||||
)
|
||||
st.generate_normals(false)
|
||||
st.commit(m)
|
||||
# set material
|
||||
if shader_mat.shader == null:
|
||||
shader_mat.shader = shader
|
||||
m.surface_set_material(0, shader_mat)
|
||||
# set mesh
|
||||
self.set_mesh(m)
|
54
procedural_grass/procedural_grass.gdshader
Normal file
54
procedural_grass/procedural_grass.gdshader
Normal file
|
@ -0,0 +1,54 @@
|
|||
shader_type spatial;
|
||||
render_mode cull_disabled;
|
||||
|
||||
// Copyright (c) 2024 Julian Mueller (ChaoticByte)
|
||||
|
||||
global uniform float wind_strength;
|
||||
global uniform float wind_turbulence;
|
||||
global uniform vec2 wind_direction;
|
||||
global uniform sampler2D wind_noise;
|
||||
global uniform float wind_noise_scale;
|
||||
global uniform float wind_noise_strength;
|
||||
|
||||
const float TRANSLUCENCY = 0.15;
|
||||
|
||||
void vertex() {
|
||||
// scale down grass where the camera is
|
||||
vec3 camera_pos_relative = (CAMERA_POSITION_WORLD - (NODE_POSITION_WORLD + VERTEX));
|
||||
VERTEX.y *= min(1.0, length(camera_pos_relative.xz));
|
||||
// waving according to wind direction, strength, noise, etc.
|
||||
ivec2 wind_noise_texsize = textureSize(wind_noise, 0);
|
||||
// calculate a substitute for the uv
|
||||
vec2 uv = (NODE_POSITION_WORLD.xz + VERTEX.xz)
|
||||
/ vec2(wind_noise_texsize)
|
||||
* -wind_direction
|
||||
* wind_noise_scale
|
||||
+ (TIME * 0.01 * wind_turbulence);
|
||||
// get the noise at this position
|
||||
float noise = (textureLod(wind_noise, uv, 0.0).g - 0.5) * wind_noise_strength;
|
||||
VERTEX.xz += pow(VERTEX.y, 1.2) * (
|
||||
(wind_strength * wind_direction)
|
||||
+ (noise * wind_direction)
|
||||
);
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
ALBEDO = COLOR.rgb;
|
||||
}
|
||||
|
||||
void light() {
|
||||
// Adapted from https://godotshaders.com/shader/shoji-shader-translucency-sun-spot/
|
||||
// The normal between the object/fragment and the light source
|
||||
float nl = clamp(dot(NORMAL, LIGHT), -1.0, 1.0);
|
||||
if (nl <= 0.0) {
|
||||
// the normal is facing away from the light source
|
||||
float light_through = clamp(-nl, 0.0, 1.0) * TRANSLUCENCY;
|
||||
float attenuation = max(0.2, ATTENUATION); // softer shadows on this side
|
||||
DIFFUSE_LIGHT += clamp(light_through, 0.0, 1.0) * LIGHT_COLOR/PI * attenuation;
|
||||
}
|
||||
else {
|
||||
// The normal is facing toward the light source
|
||||
// -> Diffuse (Lambert)
|
||||
DIFFUSE_LIGHT += clamp(dot(NORMAL, LIGHT), 0.0, 1.0) * LIGHT_COLOR/PI * ATTENUATION;
|
||||
}
|
||||
}
|
8
procedural_grass/procedural_grass.tscn
Normal file
8
procedural_grass/procedural_grass.tscn
Normal file
|
@ -0,0 +1,8 @@
|
|||
[gd_scene load_steps=2 format=3 uid="uid://dq4xt6tl25c4v"]
|
||||
|
||||
[ext_resource type="Script" path="res://procedural_grass/procedural_grass.gd" id="2_e58by"]
|
||||
|
||||
[node name="ProceduralGrass" type="MeshInstance3D"]
|
||||
script = ExtResource("2_e58by")
|
||||
color_base = Color(0.415686, 0.431373, 0.133333, 1)
|
||||
color_tip = Color(0.378045, 0.596905, 0.184511, 1)
|
76
project.godot
Normal file
76
project.godot
Normal file
|
@ -0,0 +1,76 @@
|
|||
; Engine configuration file.
|
||||
; It's best edited using the editor UI and not directly,
|
||||
; since the parameters that go here are not all obvious.
|
||||
;
|
||||
; Format:
|
||||
; [section] ; section goes between []
|
||||
; param=value ; assign values to parameters
|
||||
|
||||
config_version=5
|
||||
|
||||
[application]
|
||||
|
||||
config/name="godot-procedural-grass"
|
||||
run/main_scene="res://example/example.tscn"
|
||||
config/features=PackedStringArray("4.3", "Forward Plus")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[autoload]
|
||||
|
||||
Wind="*res://global/Wind.gd"
|
||||
|
||||
[input]
|
||||
|
||||
forward={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
backward={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
left={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
right={
|
||||
"deadzone": 0.5,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
[rendering]
|
||||
|
||||
lights_and_shadows/directional_shadow/size=8192
|
||||
global_illumination/sdfgi/probe_ray_count=2
|
||||
anti_aliasing/quality/use_taa=true
|
||||
|
||||
[shader_globals]
|
||||
|
||||
wind_noise={
|
||||
"type": "sampler2D",
|
||||
"value": ""
|
||||
}
|
||||
wind_strength={
|
||||
"type": "float",
|
||||
"value": 0.0
|
||||
}
|
||||
wind_direction={
|
||||
"type": "vec2",
|
||||
"value": Vector2(0, 0)
|
||||
}
|
||||
wind_turbulence={
|
||||
"type": "float",
|
||||
"value": 0.0
|
||||
}
|
||||
wind_noise_scale={
|
||||
"type": "float",
|
||||
"value": 0.0
|
||||
}
|
||||
wind_noise_strength={
|
||||
"type": "float",
|
||||
"value": 0.0
|
||||
}
|
Reference in a new issue