Add project files

This commit is contained in:
ChaoticByte 2024-09-06 17:11:48 +02:00
commit b10fbca719
No known key found for this signature in database
14 changed files with 588 additions and 0 deletions

3
.gitattributes vendored Normal file
View file

@ -0,0 +1,3 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf

19
.gitignore vendored Normal file
View 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
View 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
View file

@ -0,0 +1,7 @@
# godot-procedural-grass
Procedural grass with wind and translucency in Godot 4.3
![](example/screenshots/screenshot.png)
https://raw.githubusercontent.com/ChaoticByte/godot-procedural-grass/main/example/screenshots/example-video.mkv

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

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

65
global/Wind.gd Normal file
View 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)

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

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

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