Grass shell Texturing
Demo shell texturing for grass using shader and MultiMeshInstance.main
							parent
							
								
									16778381cb
								
							
						
					
					
						commit
						2b4746b77a
					
				@ -0,0 +1,46 @@
 | 
			
		||||
extends Node3D
 | 
			
		||||
class_name RotateCamera
 | 
			
		||||
 | 
			
		||||
@export var camera_rotate_sensitivity: float = 0.005
 | 
			
		||||
@export var camera_zoom_sensititivy: float = 0.01
 | 
			
		||||
@export var zoom_max: float = 10.
 | 
			
		||||
@export var zoom_min: float = 0.1
 | 
			
		||||
 | 
			
		||||
@onready var camera: Camera3D = $Camera3D
 | 
			
		||||
 | 
			
		||||
var camera_rotate_diff: Vector3 = Vector3()
 | 
			
		||||
var camera_zoom_weight: float = .15
 | 
			
		||||
 | 
			
		||||
var enable_rotate: bool = false
 | 
			
		||||
 | 
			
		||||
func _process(delta):
 | 
			
		||||
	self._update_rotation()
 | 
			
		||||
	self._update_zoom()
 | 
			
		||||
 | 
			
		||||
func _unhandled_input(event):
 | 
			
		||||
	if self.enable_rotate and event is InputEventMouseMotion:
 | 
			
		||||
		self.camera_rotate_diff.y -= event.relative.x
 | 
			
		||||
		self.camera_rotate_diff.x -= event.relative.y
 | 
			
		||||
	elif event is InputEventMouseButton:
 | 
			
		||||
		if event.is_action_pressed("camera_rotate_button"):
 | 
			
		||||
			self.enable_rotate = true
 | 
			
		||||
		elif event.is_action_released("camera_rotate_button"):
 | 
			
		||||
			self.enable_rotate = false
 | 
			
		||||
		elif event.is_action_pressed("camera_zoom_in"):
 | 
			
		||||
			self.camera_zoom_weight = clampf(self.camera_zoom_weight - camera_zoom_sensititivy, 0., 1.)
 | 
			
		||||
		elif event.is_action_pressed("camera_zoom_out"):
 | 
			
		||||
			self.camera_zoom_weight = clampf(self.camera_zoom_weight + camera_zoom_sensititivy, 0., 1.)
 | 
			
		||||
			
 | 
			
		||||
 | 
			
		||||
func _update_rotation():
 | 
			
		||||
	if self.enable_rotate:
 | 
			
		||||
		self.rotation += self.camera_rotate_diff * camera_rotate_sensitivity
 | 
			
		||||
	self.camera_rotate_diff = Vector3.ZERO
 | 
			
		||||
 | 
			
		||||
func _update_zoom():
 | 
			
		||||
	var from = self.global_position
 | 
			
		||||
	var to = self.global_position + self.camera.get_global_transform().basis.z * zoom_max
 | 
			
		||||
	
 | 
			
		||||
	var new_cam_pos = lerp(from, to, camera_zoom_weight)
 | 
			
		||||
	self.camera.global_position = new_cam_pos
 | 
			
		||||
	
 | 
			
		||||
@ -0,0 +1,10 @@
 | 
			
		||||
[gd_scene load_steps=2 format=3 uid="uid://bkavbjcx1vhaf"]
 | 
			
		||||
 | 
			
		||||
[ext_resource type="Script" path="res://camera.gd" id="1_bmlh2"]
 | 
			
		||||
 | 
			
		||||
[node name="camera_root" type="Node3D"]
 | 
			
		||||
transform = Transform3D(1, 0, 0, 0, 0.869029, 0.494761, 0, -0.494761, 0.869029, 0, 0, 0)
 | 
			
		||||
script = ExtResource("1_bmlh2")
 | 
			
		||||
 | 
			
		||||
[node name="Camera3D" type="Camera3D" parent="."]
 | 
			
		||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.306079, 2.53232)
 | 
			
		||||
@ -0,0 +1,61 @@
 | 
			
		||||
shader_type spatial;
 | 
			
		||||
render_mode cull_disabled;
 | 
			
		||||
 | 
			
		||||
//uniform int _ShellIndex;  // index of shell being worked on (0 -> _ShellCount)
 | 
			
		||||
uniform float _NoiseMin;  // minimum strand length
 | 
			
		||||
uniform float _NoiseMax;  // maxmimum strand length
 | 
			
		||||
uniform float _ShellLength;  // amount of distance that the shells cover, 1 means shells will span across 1 world space unit
 | 
			
		||||
uniform int _ShellCount;  // total number of shells (for normalizing the shell index)
 | 
			
		||||
uniform float _ShellDistanceAttenuation;  // exponent how far to push the shell outwards
 | 
			
		||||
uniform float _Thickness;  // how thick a strand shall be
 | 
			
		||||
uniform float _Attenuation;  // AO strength factor
 | 
			
		||||
uniform float _OcclusionBias;  // additive bias for AO
 | 
			
		||||
uniform vec3 _ShellColor;
 | 
			
		||||
 | 
			
		||||
uniform float _Density; // amout of strands to generate
 | 
			
		||||
 | 
			
		||||
varying flat int _ShellIndex;
 | 
			
		||||
 | 
			
		||||
float hash(uint n) {
 | 
			
		||||
	n = (n << 13U) ^ n;
 | 
			
		||||
	n = n * (n * n * 15731U + 1239221U) + 123376312589U;
 | 
			
		||||
	return float(n & uint(0x7fffffffU)) / float(0x7fffffff);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void vertex() {
 | 
			
		||||
	_ShellIndex = int(INSTANCE_CUSTOM.x);
 | 
			
		||||
	float shell_height = float(_ShellIndex) / float(_ShellCount);
 | 
			
		||||
	shell_height = pow(shell_height, _ShellDistanceAttenuation);
 | 
			
		||||
	
 | 
			
		||||
	VERTEX.xyz += NORMAL.xyz * _ShellLength * shell_height;
 | 
			
		||||
	
 | 
			
		||||
	//NORMAL = normalize(NORMAL);
 | 
			
		||||
	UV = UV;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void fragment() {
 | 
			
		||||
	vec2 newUV = UV * _Density;
 | 
			
		||||
	vec2 localUV = fract(newUV) * 2. - 1.;
 | 
			
		||||
	
 | 
			
		||||
	float localDistanceFromCenter = length(localUV);
 | 
			
		||||
	float h = float(_ShellIndex) / float(_ShellCount);  // normalized height
 | 
			
		||||
	
 | 
			
		||||
	uint seed = uint(int(newUV.x) + 100 * int(newUV.y) + 100 * 10);
 | 
			
		||||
	
 | 
			
		||||
	float rand = mix(_NoiseMin, _NoiseMax, hash(seed));  // getting random value for strand
 | 
			
		||||
	
 | 
			
		||||
	if (localDistanceFromCenter > _Thickness * (rand - h) && _ShellIndex > 0) discard;  // discarding pixels outside of thickness
 | 
			
		||||
	
 | 
			
		||||
	//float ndotl = dot(NORMAL, WORLD)  // TODO do light later?
 | 
			
		||||
	float ambientOcclusion = pow(h, _Attenuation);  // fake Ambient Occlusion
 | 
			
		||||
	ambientOcclusion += _OcclusionBias;
 | 
			
		||||
	// Called for every pixel the material is visible on.
 | 
			
		||||
	ALBEDO.rgb = _ShellColor * ambientOcclusion;
 | 
			
		||||
	//ALPHA = 1.0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void light() {
 | 
			
		||||
	float ndotl = clamp(dot(NORMAL, LIGHT), 0., 1.) * .5 + .5;
 | 
			
		||||
	
 | 
			
		||||
	DIFFUSE_LIGHT += ndotl * ndotl;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,22 @@
 | 
			
		||||
[gd_resource type="QuadMesh" load_steps=3 format=3 uid="uid://obd7wi7g27sb"]
 | 
			
		||||
 | 
			
		||||
[ext_resource type="Shader" path="res://grass.gdshader" id="1_5707v"]
 | 
			
		||||
 | 
			
		||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_hgeuk"]
 | 
			
		||||
render_priority = 0
 | 
			
		||||
shader = ExtResource("1_5707v")
 | 
			
		||||
shader_parameter/_NoiseMin = 0.4
 | 
			
		||||
shader_parameter/_NoiseMax = 1.0
 | 
			
		||||
shader_parameter/_ShellLength = 0.03
 | 
			
		||||
shader_parameter/_ShellCount = 16
 | 
			
		||||
shader_parameter/_ShellDistanceAttenuation = 1.0
 | 
			
		||||
shader_parameter/_Thickness = 3.4
 | 
			
		||||
shader_parameter/_Attenuation = null
 | 
			
		||||
shader_parameter/_OcclusionBias = null
 | 
			
		||||
shader_parameter/_ShellColor = Vector3(0.2, 0.7, 0.2)
 | 
			
		||||
shader_parameter/_Density = 10.0
 | 
			
		||||
 | 
			
		||||
[resource]
 | 
			
		||||
material = SubResource("ShaderMaterial_hgeuk")
 | 
			
		||||
subdivide_width = 32
 | 
			
		||||
subdivide_depth = 32
 | 
			
		||||
@ -0,0 +1,34 @@
 | 
			
		||||
[remap]
 | 
			
		||||
 | 
			
		||||
importer="texture"
 | 
			
		||||
type="CompressedTexture2D"
 | 
			
		||||
uid="uid://yafv1eyajxv0"
 | 
			
		||||
path="res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex"
 | 
			
		||||
metadata={
 | 
			
		||||
"vram_texture": false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
[deps]
 | 
			
		||||
 | 
			
		||||
source_file="res://icon.png"
 | 
			
		||||
dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex"]
 | 
			
		||||
 | 
			
		||||
[params]
 | 
			
		||||
 | 
			
		||||
compress/mode=0
 | 
			
		||||
compress/high_quality=false
 | 
			
		||||
compress/lossy_quality=0.7
 | 
			
		||||
compress/hdr_compression=1
 | 
			
		||||
compress/normal_map=0
 | 
			
		||||
compress/channel_pack=0
 | 
			
		||||
mipmaps/generate=false
 | 
			
		||||
mipmaps/limit=-1
 | 
			
		||||
roughness/mode=0
 | 
			
		||||
roughness/src_normal=""
 | 
			
		||||
process/fix_alpha_border=true
 | 
			
		||||
process/premult_alpha=false
 | 
			
		||||
process/normal_map_invert_y=false
 | 
			
		||||
process/hdr_as_srgb=false
 | 
			
		||||
process/hdr_clamp_exposure=false
 | 
			
		||||
process/size_limit=0
 | 
			
		||||
detect_3d/compress_to=1
 | 
			
		||||
@ -0,0 +1,23 @@
 | 
			
		||||
[gd_scene load_steps=5 format=3 uid="uid://b45p5o8obc4rb"]
 | 
			
		||||
 | 
			
		||||
[ext_resource type="PackedScene" uid="uid://bkavbjcx1vhaf" path="res://camera.tscn" id="1_fql2y"]
 | 
			
		||||
[ext_resource type="PackedScene" uid="uid://gbmju3x1gp7y" path="res://multi_grass.tscn" id="3_lkk6e"]
 | 
			
		||||
 | 
			
		||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_sivjy"]
 | 
			
		||||
albedo_color = Color(0.2, 0.137255, 0.0431373, 1)
 | 
			
		||||
 | 
			
		||||
[sub_resource type="PlaneMesh" id="PlaneMesh_vpqsi"]
 | 
			
		||||
material = SubResource("StandardMaterial3D_sivjy")
 | 
			
		||||
 | 
			
		||||
[node name="main" type="Node3D"]
 | 
			
		||||
 | 
			
		||||
[node name="camera" parent="." instance=ExtResource("1_fql2y")]
 | 
			
		||||
 | 
			
		||||
[node name="light" type="DirectionalLight3D" parent="."]
 | 
			
		||||
transform = Transform3D(0.758372, 0.524229, 0.38737, -0.457532, 0.00484201, 0.88918, 0.464258, -0.851564, 0.243524, 1.21403, 2.38572, 0.933982)
 | 
			
		||||
 | 
			
		||||
[node name="multi_grass" parent="." instance=ExtResource("3_lkk6e")]
 | 
			
		||||
 | 
			
		||||
[node name="ground" type="MeshInstance3D" parent="."]
 | 
			
		||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.149795, 0)
 | 
			
		||||
mesh = SubResource("PlaneMesh_vpqsi")
 | 
			
		||||
@ -0,0 +1,72 @@
 | 
			
		||||
extends MultiMeshInstance3D
 | 
			
		||||
 | 
			
		||||
@export_range(1, 126) var shell_count = 64
 | 
			
		||||
@export var noise_min: float = 0.4
 | 
			
		||||
@export var noise_max: float = 1.0
 | 
			
		||||
@export var shell_length: float = .1
 | 
			
		||||
@export var shell_distance_attenuation: float = 1.
 | 
			
		||||
@export var thickness: float = 3.4
 | 
			
		||||
@export var attenuation: float = 1.98
 | 
			
		||||
@export var occlusion_bias: float = .04
 | 
			
		||||
@export var shell_color: Vector3 = Vector3(0.2, 0.7, 0.2)
 | 
			
		||||
@export var density: int = 150
 | 
			
		||||
 | 
			
		||||
func _generate_mmi(layers: int, mmi: MultiMeshInstance3D, mesh: Mesh, material: Material, cast_shadow: bool):
 | 
			
		||||
	var mdt = MeshDataTool.new()
 | 
			
		||||
	
 | 
			
		||||
	if mmi.multimesh == null:
 | 
			
		||||
		mmi.multimesh = MultiMesh.new()
 | 
			
		||||
		mmi.multimesh.transform_format = MultiMesh.TRANSFORM_3D
 | 
			
		||||
	
 | 
			
		||||
	var new_mesh: Mesh = mesh.duplicate(true) as Mesh
 | 
			
		||||
	#new_mesh = _normals_to_vertex_color(new_mesh, material)  # saves normal data as vertex color to be used by the shader (in the orig code)
 | 
			
		||||
	mmi.multimesh.mesh = new_mesh
 | 
			
		||||
	mmi.multimesh.instance_count = layers
 | 
			
		||||
	mmi.multimesh.visible_instance_count = layers
 | 
			
		||||
	for surface in new_mesh.get_surface_count():
 | 
			
		||||
		material = ShaderMaterial.new()
 | 
			
		||||
		material.set_shader(preload("res://grass.gdshader"))
 | 
			
		||||
		mmi.multimesh.mesh.surface_set_material(surface, material.duplicate(true))
 | 
			
		||||
	
 | 
			
		||||
	for i in range(layers):
 | 
			
		||||
		#mmi.multimesh.set_instance_transform(i, Transform3D(Basis(), Vector3(0., .1 * i, 0.)))
 | 
			
		||||
		mmi.multimesh.set_instance_transform(i, Transform3D(Basis(), Vector3()))
 | 
			
		||||
		var grey = float(i) / float(layers)
 | 
			
		||||
		#mmi.multimesh.set_instance_color(i, Color(1., 1., 1., 1.))
 | 
			
		||||
		
 | 
			
		||||
		var mat = mmi.multimesh.mesh.surface_get_material(0)
 | 
			
		||||
		if mat is ShaderMaterial:
 | 
			
		||||
			#print("Setting shader params (", i, ")")
 | 
			
		||||
			#mat.set_shader_parameter("_ShellIndex", i)
 | 
			
		||||
			mmi.multimesh.set_instance_custom_data(i, Color(i, 0., 0., 0.))  # passes shell index for each instance
 | 
			
		||||
			mat.set_shader_parameter("_NoiseMin", noise_min)
 | 
			
		||||
			mat.set_shader_parameter("_NoiseMax", noise_max)
 | 
			
		||||
			mat.set_shader_parameter("_ShellLength", shell_length)
 | 
			
		||||
			mat.set_shader_parameter("_ShellCount", layers)
 | 
			
		||||
			mat.set_shader_parameter("_ShellDistanceAttenuation", shell_distance_attenuation)
 | 
			
		||||
			mat.set_shader_parameter("_Thickness", thickness)
 | 
			
		||||
			mat.set_shader_parameter("_Attenuation", attenuation)
 | 
			
		||||
			mat.set_shader_parameter("_OcclusionBias", occlusion_bias)
 | 
			
		||||
			mat.set_shader_parameter("_ShellColor", shell_color)
 | 
			
		||||
			mat.set_shader_parameter("_Density", density)
 | 
			
		||||
	
 | 
			
		||||
	mmi.cast_shadow = 1 if cast_shadow else 0
 | 
			
		||||
 | 
			
		||||
func _ready():
 | 
			
		||||
	var mat = ShaderMaterial.new()
 | 
			
		||||
	#var mat2 = StandardMaterial3D.new()
 | 
			
		||||
	mat.set_shader(preload("res://grass.gdshader"))
 | 
			
		||||
	self._generate_mmi(self.shell_count, self, self.get_multimesh().mesh, mat, false)
 | 
			
		||||
	
 | 
			
		||||
	print("===== Shell Texturing =====")
 | 
			
		||||
	print("Layers: ", self.shell_count)
 | 
			
		||||
	print("Noise (min): ", self.noise_min)
 | 
			
		||||
	print("Noise (max): ", self.noise_max)
 | 
			
		||||
	print("Shell length: ", self.shell_length)
 | 
			
		||||
	print("Shell count: ", self.shell_count)
 | 
			
		||||
	print("Shell distance attenuation: ", self.shell_distance_attenuation)
 | 
			
		||||
	print("Thickness: ", self.thickness)
 | 
			
		||||
	print("Attenuation: ", self.attenuation)
 | 
			
		||||
	print("Occlusion bias: ", self.occlusion_bias)
 | 
			
		||||
	print("Shell color: ", self.shell_color)
 | 
			
		||||
	print("Density: ", self.density)
 | 
			
		||||
@ -0,0 +1,15 @@
 | 
			
		||||
[gd_scene load_steps=4 format=3 uid="uid://gbmju3x1gp7y"]
 | 
			
		||||
 | 
			
		||||
[ext_resource type="Script" path="res://multi_grass.gd" id="1_f7cbg"]
 | 
			
		||||
 | 
			
		||||
[sub_resource type="QuadMesh" id="QuadMesh_h8afx"]
 | 
			
		||||
orientation = 1
 | 
			
		||||
 | 
			
		||||
[sub_resource type="MultiMesh" id="MultiMesh_j6hjp"]
 | 
			
		||||
transform_format = 1
 | 
			
		||||
use_custom_data = true
 | 
			
		||||
mesh = SubResource("QuadMesh_h8afx")
 | 
			
		||||
 | 
			
		||||
[node name="multi_grass" type="MultiMeshInstance3D"]
 | 
			
		||||
multimesh = SubResource("MultiMesh_j6hjp")
 | 
			
		||||
script = ExtResource("1_f7cbg")
 | 
			
		||||
@ -0,0 +1,34 @@
 | 
			
		||||
; 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="Shell Texturing"
 | 
			
		||||
run/main_scene="res://main.tscn"
 | 
			
		||||
config/features=PackedStringArray("4.2", "Forward Plus")
 | 
			
		||||
config/icon="res://icon.png"
 | 
			
		||||
 | 
			
		||||
[input]
 | 
			
		||||
 | 
			
		||||
camera_rotate_button={
 | 
			
		||||
"deadzone": 0.5,
 | 
			
		||||
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":3,"canceled":false,"pressed":false,"double_click":false,"script":null)
 | 
			
		||||
]
 | 
			
		||||
}
 | 
			
		||||
camera_zoom_in={
 | 
			
		||||
"deadzone": 0.5,
 | 
			
		||||
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":4,"canceled":false,"pressed":false,"double_click":false,"script":null)
 | 
			
		||||
]
 | 
			
		||||
}
 | 
			
		||||
camera_zoom_out={
 | 
			
		||||
"deadzone": 0.5,
 | 
			
		||||
"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":5,"canceled":false,"pressed":false,"double_click":false,"script":null)
 | 
			
		||||
]
 | 
			
		||||
}
 | 
			
		||||
					Loading…
					
					
				
		Reference in New Issue