diff --git a/camera.gd b/camera.gd new file mode 100644 index 0000000..a75e667 --- /dev/null +++ b/camera.gd @@ -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 + diff --git a/camera.tscn b/camera.tscn new file mode 100644 index 0000000..ad15194 --- /dev/null +++ b/camera.tscn @@ -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) diff --git a/grass.gdshader b/grass.gdshader new file mode 100644 index 0000000..466eaa4 --- /dev/null +++ b/grass.gdshader @@ -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; +} diff --git a/grass_plane_mesh.tres b/grass_plane_mesh.tres new file mode 100644 index 0000000..5adb1d6 --- /dev/null +++ b/grass_plane_mesh.tres @@ -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 diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..b5aae58 Binary files /dev/null and b/icon.png differ diff --git a/icon.png.import b/icon.png.import new file mode 100644 index 0000000..7a34d26 --- /dev/null +++ b/icon.png.import @@ -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 diff --git a/main.tscn b/main.tscn new file mode 100644 index 0000000..74f8049 --- /dev/null +++ b/main.tscn @@ -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") diff --git a/multi_grass.gd b/multi_grass.gd new file mode 100644 index 0000000..be98f36 --- /dev/null +++ b/multi_grass.gd @@ -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) diff --git a/multi_grass.tscn b/multi_grass.tscn new file mode 100644 index 0000000..2ea84cc --- /dev/null +++ b/multi_grass.tscn @@ -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") diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..5d09d1f --- /dev/null +++ b/project.godot @@ -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) +] +}