Basics of washable 'gunk'
Some checks failed
linting & formatting / build (push) Failing after 42s

This commit is contained in:
Rob Kelly 2025-03-01 14:40:02 -07:00
parent 977ca741aa
commit b0078c1bab
17 changed files with 2637 additions and 3 deletions

Binary file not shown.

View File

@ -0,0 +1,50 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://bg5qmh1jufujy"
valid=false
[deps]
source_file="res://asset_dev/props/icosahedron.blend"
[params]
nodes/root_type=""
nodes/root_name=""
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
_subresources={}
blender/nodes/visible=0
blender/nodes/active_collection_only=false
blender/nodes/punctual_lights=true
blender/nodes/cameras=true
blender/nodes/custom_properties=true
blender/nodes/modifiers=1
blender/meshes/colors=false
blender/meshes/uvs=true
blender/meshes/normals=true
blender/meshes/tangents=true
blender/meshes/skins=2
blender/meshes/export_bones_deforming_mesh_only=false
blender/materials/unpack_enabled=true
blender/materials/export_materials=1
blender/animation/limit_playback=true
blender/animation/always_sample=true
blender/animation/group_tracks=true

Binary file not shown.

View File

@ -0,0 +1,50 @@
[remap]
importer="scene"
importer_version=1
type="PackedScene"
uid="uid://c43vybloao3v5"
valid=false
[deps]
source_file="res://asset_dev/props/monkey.blend"
[params]
nodes/root_type=""
nodes/root_name=""
nodes/apply_root_scale=true
nodes/root_scale=1.0
nodes/import_as_skeleton_bones=false
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
meshes/lightmap_texel_size=0.2
meshes/force_disable_compression=false
skins/use_named_skins=true
animation/import=true
animation/fps=30
animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
_subresources={}
blender/nodes/visible=0
blender/nodes/active_collection_only=false
blender/nodes/punctual_lights=true
blender/nodes/cameras=true
blender/nodes/custom_properties=true
blender/nodes/modifiers=1
blender/meshes/colors=false
blender/meshes/uvs=true
blender/meshes/normals=true
blender/meshes/tangents=true
blender/meshes/skins=2
blender/meshes/export_bones_deforming_mesh_only=false
blender/materials/unpack_enabled=true
blender/materials/export_materials=1
blender/animation/limit_playback=true
blender/animation/always_sample=true
blender/animation/group_tracks=true

View File

@ -0,0 +1,2 @@
# Blender 4.3.2 MTL File: 'icosahedron.blend'
# www.blender.org

View File

@ -0,0 +1,79 @@
# Blender 4.3.2
# www.blender.org
mtllib icosahedron.mtl
o Icosphere
v 0.000000 -1.000000 0.000000
v 0.723600 -0.447215 0.525720
v -0.276385 -0.447215 0.850640
v -0.894425 -0.447215 0.000000
v -0.276385 -0.447215 -0.850640
v 0.723600 -0.447215 -0.525720
v 0.276385 0.447215 0.850640
v -0.723600 0.447215 0.525720
v -0.723600 0.447215 -0.525720
v 0.276385 0.447215 -0.850640
v 0.894425 0.447215 0.000000
v 0.000000 1.000000 0.000000
vn 0.1876 -0.7947 0.5774
vn 0.6071 -0.7947 -0.0000
vn -0.4911 -0.7947 0.3568
vn -0.4911 -0.7947 -0.3568
vn 0.1876 -0.7947 -0.5774
vn 0.9822 -0.1876 -0.0000
vn 0.3035 -0.1876 0.9342
vn -0.7946 -0.1876 0.5774
vn -0.7946 -0.1876 -0.5774
vn 0.3035 -0.1876 -0.9342
vn 0.7946 0.1876 0.5774
vn -0.3035 0.1876 0.9342
vn -0.9822 0.1876 -0.0000
vn -0.3035 0.1876 -0.9342
vn 0.7946 0.1876 -0.5774
vn 0.4911 0.7947 0.3568
vn -0.1876 0.7947 0.5774
vn -0.6071 0.7947 -0.0000
vn -0.1876 0.7947 -0.5774
vn 0.4911 0.7947 -0.3568
vt 0.181819 0.000000
vt 0.272728 0.157461
vt 0.090910 0.157461
vt 0.363637 0.000000
vt 0.454546 0.157461
vt 0.909091 0.000000
vt 1.000000 0.157461
vt 0.818182 0.157461
vt 0.727273 0.000000
vt 0.636364 0.157461
vt 0.545455 0.000000
vt 0.363637 0.314921
vt 0.181819 0.314921
vt 0.909091 0.314921
vt 0.727273 0.314921
vt 0.545455 0.314921
vt 0.000000 0.314921
vt 0.272728 0.472382
vt 0.090910 0.472382
vt 0.818182 0.472382
vt 0.636364 0.472382
vt 0.454546 0.472382
s 0
f 1/1/1 2/2/1 3/3/1
f 2/2/2 1/4/2 6/5/2
f 1/6/3 3/7/3 4/8/3
f 1/9/4 4/8/4 5/10/4
f 1/11/5 5/10/5 6/5/5
f 2/2/6 6/5/6 11/12/6
f 3/3/7 2/2/7 7/13/7
f 4/8/8 3/7/8 8/14/8
f 5/10/9 4/8/9 9/15/9
f 6/5/10 5/10/10 10/16/10
f 2/2/11 11/12/11 7/13/11
f 3/3/12 7/13/12 8/17/12
f 4/8/13 8/14/13 9/15/13
f 5/10/14 9/15/14 10/16/14
f 6/5/15 10/16/15 11/12/15
f 7/13/16 11/12/16 12/18/16
f 8/17/17 7/13/17 12/19/17
f 9/15/18 8/14/18 12/20/18
f 10/16/19 9/15/19 12/21/19
f 11/12/20 10/16/20 12/22/20

View File

@ -0,0 +1,22 @@
[remap]
importer="wavefront_obj"
importer_version=1
type="Mesh"
uid="uid://lvats6ptrcxt"
path="res://.godot/imported/icosahedron.obj-8f3e6519933309a73eb5326f98a3de95.mesh"
[deps]
files=["res://.godot/imported/icosahedron.obj-8f3e6519933309a73eb5326f98a3de95.mesh"]
source_file="res://assets/props/icosahedron/icosahedron.obj"
dest_files=["res://.godot/imported/icosahedron.obj-8f3e6519933309a73eb5326f98a3de95.mesh", "res://.godot/imported/icosahedron.obj-8f3e6519933309a73eb5326f98a3de95.mesh"]
[params]
generate_tangents=true
scale_mesh=Vector3(1, 1, 1)
offset_mesh=Vector3(0, 0, 0)
optimize_mesh=true
force_disable_mesh_compression=false

View File

@ -0,0 +1,2 @@
# Blender 4.3.2 MTL File: 'monkey.blend'
# www.blender.org

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
[remap]
importer="wavefront_obj"
importer_version=1
type="Mesh"
uid="uid://e6syamfwiitk"
path="res://.godot/imported/monkey.obj-ee24e6fed272edb0c774a26f0a399a12.mesh"
[deps]
files=["res://.godot/imported/monkey.obj-ee24e6fed272edb0c774a26f0a399a12.mesh"]
source_file="res://assets/props/monkey/monkey.obj"
dest_files=["res://.godot/imported/monkey.obj-ee24e6fed272edb0c774a26f0a399a12.mesh", "res://.godot/imported/monkey.obj-ee24e6fed272edb0c774a26f0a399a12.mesh"]
[params]
generate_tangents=true
scale_mesh=Vector3(1, 1, 1)
offset_mesh=Vector3(0, 0, 0)
optimize_mesh=true
force_disable_mesh_compression=false

View File

@ -0,0 +1,28 @@
class_name DebugDraw extends Control
const COLOR := Color.ORANGE
const WIDTH := 4
var lines: Array = []
func _process(_delta: float) -> void:
if visible:
queue_redraw()
else:
lines = []
func draw_vector(vec: Vector3, origin: Vector3) -> void:
var camera := get_viewport().get_camera_3d()
var start := camera.unproject_position(origin)
var end := camera.unproject_position(origin + vec)
lines.push_back([start, end])
func _draw() -> void:
while lines:
var line: Array[Vector2] = []
line.assign(lines.pop_back() as Array)
draw_line(line[0], line[1], COLOR, WIDTH)

View File

@ -0,0 +1,34 @@
[gd_resource type="StandardMaterial3D" load_steps=8 format=3 uid="uid://byyjoruj8mwe0"]
[ext_resource type="Shader" path="res://src/shaders/gunk.gdshader" id="1_dot5s"]
[sub_resource type="PlaceholderTexture2D" id="PlaceholderTexture2D_pinpy"]
size = Vector2(256, 256)
[sub_resource type="FastNoiseLite" id="FastNoiseLite_f8h21"]
[sub_resource type="NoiseTexture2D" id="NoiseTexture2D_dj5ow"]
seamless = true
noise = SubResource("FastNoiseLite_f8h21")
[sub_resource type="FastNoiseLite" id="FastNoiseLite_1ennl"]
[sub_resource type="NoiseTexture2D" id="NoiseTexture2D_kkru7"]
seamless = true
as_normal_map = true
noise = SubResource("FastNoiseLite_1ennl")
[sub_resource type="ShaderMaterial" id="ShaderMaterial_0jkcp"]
render_priority = 0
shader = ExtResource("1_dot5s")
shader_parameter/color_1 = Color(0, 0.03, 0.1, 1)
shader_parameter/color_2 = Color(0, 0.1, 0.3, 1)
shader_parameter/roughness = 0.15
shader_parameter/specular_contribution = 0.2
shader_parameter/gunk_mask = SubResource("PlaceholderTexture2D_pinpy")
shader_parameter/gunk_noise = SubResource("NoiseTexture2D_dj5ow")
shader_parameter/gunk_normal_map = SubResource("NoiseTexture2D_kkru7")
[resource]
next_pass = SubResource("ShaderMaterial_0jkcp")
albedo_color = Color(1, 0.403922, 1, 1)

File diff suppressed because one or more lines are too long

View File

@ -2,11 +2,13 @@ class_name Player extends CharacterBody3D
const RUN_SPEED := 80.0 const RUN_SPEED := 80.0
const SPRINT_SPEED := 160.0 const SPRINT_SPEED := 160.0
const AIR_SPEED := 30.0 const AIR_SPEED := 10.0
const JUMP_FORCE := 4.5 const JUMP_FORCE := 4.5
const GROUND_FRICTION := 0.3 const GROUND_FRICTION := 0.3
const AIR_FRICTION := 0.03 const AIR_FRICTION := 0.03
const SPRAY_RADIUS := 15
var gravity: Vector3 = ( var gravity: Vector3 = (
ProjectSettings.get_setting("physics/3d/default_gravity") ProjectSettings.get_setting("physics/3d/default_gravity")
* ProjectSettings.get_setting("physics/3d/default_gravity_vector") * ProjectSettings.get_setting("physics/3d/default_gravity_vector")
@ -14,6 +16,8 @@ var gravity: Vector3 = (
@onready var camera_pivot: Node3D = %CameraPivot @onready var camera_pivot: Node3D = %CameraPivot
@onready var spray_effect: MeshInstance3D = %SprayEffect
func get_speed() -> float: func get_speed() -> float:
if is_on_floor(): if is_on_floor():
@ -29,7 +33,23 @@ func get_friction() -> float:
return AIR_FRICTION return AIR_FRICTION
func fire_spray() -> void:
for raycast: RayCast3D in get_tree().get_nodes_in_group("SprayCast"):
if raycast.is_colliding():
var collider := raycast.get_collider()
if collider is GunkBody:
(collider as GunkBody).paint_mask(
raycast.get_collision_point(), raycast.get_collision_normal(), SPRAY_RADIUS
)
func _physics_process(delta: float) -> void: func _physics_process(delta: float) -> void:
if Input.is_action_pressed("fire"):
fire_spray()
spray_effect.visible = true
else:
spray_effect.visible = false
# Gravity # Gravity
if not is_on_floor(): if not is_on_floor():
velocity += gravity * delta velocity += gravity * delta

View File

@ -1,8 +1,24 @@
[gd_scene load_steps=4 format=3 uid="uid://bwe2jdmvinhqd"] [gd_scene load_steps=8 format=3 uid="uid://bwe2jdmvinhqd"]
[ext_resource type="Script" path="res://src/player/player.gd" id="1_npueo"] [ext_resource type="Script" path="res://src/player/player.gd" id="1_npueo"]
[ext_resource type="Script" path="res://src/player/camera_controller.gd" id="2_veeqv"] [ext_resource type="Script" path="res://src/player/camera_controller.gd" id="2_veeqv"]
[sub_resource type="BoxMesh" id="BoxMesh_ua7a2"]
size = Vector3(0.05, 0.05, 0.3)
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ng43h"]
transparency = 1
shading_mode = 0
vertex_color_use_as_albedo = true
albedo_color = Color(0, 1, 0.301961, 0.254902)
[sub_resource type="PrismMesh" id="PrismMesh_ow0jh"]
material = SubResource("StandardMaterial3D_ng43h")
size = Vector3(1, 2, 0.1)
[sub_resource type="QuadMesh" id="QuadMesh_cn0yq"]
size = Vector2(1, 0.05)
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_s7f0r"] [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_s7f0r"]
[node name="Player" type="CharacterBody3D"] [node name="Player" type="CharacterBody3D"]
@ -14,8 +30,44 @@ unique_name_in_owner = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
script = ExtResource("2_veeqv") script = ExtResource("2_veeqv")
[node name="SprayNozzle" type="MeshInstance3D" parent="CameraPivot"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.15, -0.1, -0.1)
mesh = SubResource("BoxMesh_ua7a2")
[node name="SprayMuzzle" type="Marker3D" parent="CameraPivot/SprayNozzle"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -0.15)
[node name="RayCast3D" type="RayCast3D" parent="CameraPivot/SprayNozzle/SprayMuzzle" groups=["SprayCast"]]
enabled = false
target_position = Vector3(-0.4, 0.1, -2)
[node name="RayCast3D2" type="RayCast3D" parent="CameraPivot/SprayNozzle/SprayMuzzle" groups=["SprayCast"]]
target_position = Vector3(-0.2, 0.1, -2)
[node name="RayCast3D3" type="RayCast3D" parent="CameraPivot/SprayNozzle/SprayMuzzle" groups=["SprayCast"]]
target_position = Vector3(0, 0.1, -2)
[node name="RayCast3D4" type="RayCast3D" parent="CameraPivot/SprayNozzle/SprayMuzzle" groups=["SprayCast"]]
target_position = Vector3(0.2, 0.1, -2)
[node name="RayCast3D5" type="RayCast3D" parent="CameraPivot/SprayNozzle/SprayMuzzle" groups=["SprayCast"]]
enabled = false
target_position = Vector3(0.4, 0.1, -2)
[node name="SprayEffect" type="MeshInstance3D" parent="CameraPivot/SprayNozzle/SprayMuzzle"]
unique_name_in_owner = true
transform = Transform3D(1, 0, 0, 0, -0.0697565, -0.997564, 0, 0.997564, -0.0697565, 0, 0.0436133, -1)
visible = false
mesh = SubResource("PrismMesh_ow0jh")
skeleton = NodePath("../../..")
[node name="Camera3D" type="Camera3D" parent="CameraPivot"] [node name="Camera3D" type="Camera3D" parent="CameraPivot"]
current = true current = true
[node name="SprayZone" type="MeshInstance3D" parent="CameraPivot"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.166592, 0, -2.25)
visible = false
mesh = SubResource("QuadMesh_cn0yq")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."] [node name="CollisionShape3D" type="CollisionShape3D" parent="."]
shape = SubResource("CapsuleShape3D_s7f0r") shape = SubResource("CapsuleShape3D_s7f0r")

30
src/shaders/gunk.gdshader Normal file
View File

@ -0,0 +1,30 @@
shader_type spatial;
render_mode cull_back;
uniform vec4 color_1: source_color = vec4(0.0, 0.03, 0.1, 1.0);
uniform vec4 color_2: source_color = vec4(0.0, 0.1, 0.3, 1.0);
uniform float roughness: hint_range(0.0, 1.0) = 0.15;
uniform float specular_contribution = 0.2;
uniform sampler2D gunk_mask;
uniform highp sampler2D gunk_noise;
uniform highp sampler2D gunk_normal_map;
void fragment() {
float mask = texture(gunk_mask, UV).r;
if(mask < 0.1) {
float value = texture(gunk_noise, UV).r;
vec4 color = mix(color_1, color_2, value);
float roughness_mix = value * roughness;
ALBEDO = color.rgb;
ROUGHNESS = roughness_mix;
SPECULAR = 0.5 * inversesqrt(specular_contribution);
NORMAL_MAP = texture(gunk_normal_map, UV).xyz;
ALPHA = 1.0;
} else {
ALPHA = 0.0;
}
}

96
src/world/gunk_body.gd Normal file
View File

@ -0,0 +1,96 @@
class_name GunkBody extends StaticBody3D
## StaticBody3D with an associated "gunkable" mesh.
const MASK_DIM := 512
const FACE_EPSILON := 0.4
const MASK_COLOR := Color.RED
@export var mask_image: Image
var meshtool := MeshDataTool.new()
@onready var mesh_instance: MeshInstance3D = $MeshInstance3D
@onready var mesh: ArrayMesh = mesh_instance.mesh
@onready var gunk_mat: ShaderMaterial = mesh_instance.get_surface_override_material(0).next_pass
@onready var debug_draw: DebugDraw = %DebugDraw
func _ready() -> void:
if not mask_image:
mask_image = Image.create_empty(MASK_DIM, MASK_DIM, false, Image.FORMAT_L8)
_update_gunk_material()
meshtool.create_from_surface(mesh, 0)
## Must be called internally whenever `mask_image` is updated
func _update_gunk_material() -> void:
gunk_mat.set_shader_parameter("gunk_mask", ImageTexture.create_from_image(mask_image))
## Transform cartesian coordinates to barycentric wrt the given triangle
func _barycentric(p: Vector3, a: Vector3, b: Vector3, c: Vector3) -> Vector3:
var v0 := b - a
var v1 := c - a
var v2 := p - a
var d00 := v0.dot(v0)
var d01 := v0.dot(v1)
var d11 := v1.dot(v1)
var d20 := v2.dot(v0)
var d21 := v2.dot(v1)
var denom := d00 * d11 - d01 * d01
var v := (d11 * d20 - d01 * d21) / denom
var w := (d00 * d21 - d01 * d20) / denom
var u := 1.0 - v - w
return Vector3(u, v, w)
func _is_in_triangle(point: Vector3, v1: Vector3, v2: Vector3, v3: Vector3) -> bool:
var bc := _barycentric(point, v1, v2, v3)
return (bc.x > 0 and bc.x < 1) and (bc.y > 0 and bc.y < 1) and (bc.z > 0 and bc.z < 1)
func _get_face(point: Vector3, normal: Vector3) -> int:
for i in range(meshtool.get_face_count()):
if meshtool.get_face_normal(i).distance_squared_to(normal) > FACE_EPSILON:
continue
# Normals match, so check if the point is on this face
var v1 := meshtool.get_vertex(meshtool.get_face_vertex(i, 0))
var v2 := meshtool.get_vertex(meshtool.get_face_vertex(i, 1))
var v3 := meshtool.get_vertex(meshtool.get_face_vertex(i, 2))
if _is_in_triangle(point, v1, v2, v3):
return i
return -1
func _get_uv(point: Vector3, normal: Vector3) -> Vector2:
var face := _get_face(point, normal)
if face < 0:
return Vector2.INF
var fv0 := meshtool.get_face_vertex(face, 0)
var fv1 := meshtool.get_face_vertex(face, 1)
var fv2 := meshtool.get_face_vertex(face, 2)
var v1 := meshtool.get_vertex(fv0)
var v2 := meshtool.get_vertex(fv1)
var v3 := meshtool.get_vertex(fv2)
var bc := _barycentric(point, v1, v2, v3) # TODO memoize
var uv1 := meshtool.get_vertex_uv(fv0)
var uv2 := meshtool.get_vertex_uv(fv1)
var uv3 := meshtool.get_vertex_uv(fv2)
return (uv1 * bc.x) + (uv2 * bc.y) + (uv3 * bc.z)
## Paint a rectangle on the mask at a given point & normal on the mesh.
func paint_mask(point: Vector3, normal: Vector3, radius: int) -> void:
# debug_draw.draw_vector(normal, point)
var local_point := point * global_transform
var local_normal := normal * global_basis
var uv := _get_uv(local_point, local_normal)
if uv == Vector2.INF:
return
var px_center: Vector2i = Vector2i(uv * Vector2(mask_image.get_size()))
var rect := Rect2i(px_center.x - radius, px_center.y - radius, 2 * radius, 2 * radius)
mask_image.fill_rect(rect, MASK_COLOR)
# TODO: can we call this once after all painting in a single frame is finished?
_update_gunk_material()