Reorganize into role-based folders and consolidate art
Move every script and scene into role-based directories (actors/, systems/, ui/, levels/, props/) and all raw art under art/ (grouped by role, snake_case, Extract-Textures GLBs), dropping Blender version-suffix noise. Filenames are snake_case (PascalCase reserved for class_name) to avoid case-insensitive filesystem aliasing on the web/macOS targets. Routes the bull's hits through the typed DamageEvent pipeline and wires the bull's abilities to the Fx library. Updates CLAUDE.md's folder-layout overview to match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CCyaJPTHCH9EDthpf4A5Sj
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
extends ColorRect
|
||||
## Drives the fullscreen PS1/CRT post-process (ps1_filter.gdshader) from the DP
|
||||
## registry so every uniform is live-tunable in the console. `show_ps1` toggles the
|
||||
## whole overlay; the ps1_* floats feed the matching shader uniforms 1:1.
|
||||
|
||||
# DP param key → shader uniform name. Keys are registered in debug_params.gd with
|
||||
# ranges that mirror the shader's hint_range, so nothing here can go out of bounds.
|
||||
const _UNIFORMS := {
|
||||
"ps1_pixel_scale": "pixel_scale",
|
||||
"ps1_color_depth": "color_depth",
|
||||
"ps1_dither_strength": "dither_strength",
|
||||
"ps1_scanline_strength": "scanline_strength",
|
||||
"ps1_roll_speed": "roll_speed",
|
||||
"ps1_roll_strength": "roll_strength",
|
||||
}
|
||||
|
||||
@onready var _mat: ShaderMaterial = material as ShaderMaterial
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
DP.any_changed.connect(_on_param_changed)
|
||||
_apply_all()
|
||||
|
||||
|
||||
# reset_all() fires with key "__all__"; a single edit fires with its own key. Either
|
||||
# way just re-push everything — it's a handful of cheap uniform writes.
|
||||
func _on_param_changed(_key: String, _value: Variant) -> void:
|
||||
_apply_all()
|
||||
|
||||
|
||||
func _apply_all() -> void:
|
||||
visible = DP.b("show_ps1")
|
||||
if _mat == null:
|
||||
return
|
||||
for key: String in _UNIFORMS:
|
||||
_mat.set_shader_parameter(_UNIFORMS[key], DP.f(key))
|
||||
@@ -0,0 +1 @@
|
||||
uid://ce5n4qu88mhyx
|
||||
@@ -0,0 +1,47 @@
|
||||
shader_type canvas_item;
|
||||
|
||||
uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_nearest;
|
||||
uniform float pixel_scale : hint_range(1.0, 8.0, 1.0) = 4.0;
|
||||
uniform float color_depth : hint_range(2.0, 8.0, 1.0) = 5.0;
|
||||
uniform float dither_strength : hint_range(0.0, 2.0, 0.05) = 0.8;
|
||||
uniform float scanline_strength : hint_range(0.0, 1.0, 0.05) = 0.2;
|
||||
uniform float roll_speed : hint_range(0.0, 10.0, 0.1) = 3.0;
|
||||
uniform float roll_strength : hint_range(0.0, 1.0, 0.05) = 1.0;
|
||||
|
||||
float bayer4(ivec2 p) {
|
||||
int x = p.x & 3;
|
||||
int y = p.y & 3;
|
||||
float m[16] = float[16](
|
||||
0.0, 8.0, 2.0, 10.0,
|
||||
12.0, 4.0, 14.0, 6.0,
|
||||
3.0, 11.0, 1.0, 9.0,
|
||||
15.0, 7.0, 13.0, 5.0
|
||||
);
|
||||
return m[y * 4 + x] / 16.0 - 0.5;
|
||||
}
|
||||
|
||||
void fragment() {
|
||||
vec2 pixel_size = SCREEN_PIXEL_SIZE * pixel_scale;
|
||||
vec2 snapped = (floor(SCREEN_UV / pixel_size) + 0.5) * pixel_size;
|
||||
vec3 col = texture(SCREEN_TEXTURE, snapped).rgb;
|
||||
|
||||
ivec2 dpos = ivec2(FRAGCOORD.xy / pixel_scale);
|
||||
float t = bayer4(dpos) * dither_strength;
|
||||
|
||||
float levels = exp2(color_depth) - 1.0;
|
||||
col = clamp(floor(col * levels + t + 0.5) / levels, 0.0, 1.0);
|
||||
|
||||
if (scanline_strength > 0.0) {
|
||||
// Rolling gradient mask scrolling top-to-bottom
|
||||
float screen_y = FRAGCOORD.y * SCREEN_PIXEL_SIZE.y;
|
||||
float roll_mask = sin(screen_y * TAU - TIME * roll_speed) * 0.5 + 0.5;
|
||||
|
||||
// Scanlines modulated by the roll: strong at peak, faint at trough
|
||||
float effective_strength = scanline_strength * mix(1.0 - roll_strength, 1.0, roll_mask);
|
||||
float row_frac = fract(FRAGCOORD.y / pixel_scale);
|
||||
float scan = smoothstep(0.0, 0.3, row_frac) * (1.0 - smoothstep(0.7, 1.0, row_frac));
|
||||
col *= 1.0 - effective_strength * (1.0 - scan);
|
||||
}
|
||||
|
||||
COLOR = vec4(col, 1.0);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://d3xyyy5bh0ma
|
||||
@@ -0,0 +1,151 @@
|
||||
extends RefCounted
|
||||
## Converts Skeleton3D-skinned meshes into NON-skinned mesh pieces parented to
|
||||
## BoneAttachment3D nodes, so rendering never uses GPU vertex-skinning (transform
|
||||
## feedback). Required on ANGLE/Vulkan/Mali mobile GPUs (e.g. Mali-G720 in mobile
|
||||
## Chrome/Brave), where Godot's Compatibility skinning path renders skinned meshes
|
||||
## invisible while non-skinned meshes draw fine.
|
||||
##
|
||||
## Each triangle is assigned wholesale to its dominant bone (highest summed weight),
|
||||
## so segments stay watertight; joints become rigid (no smooth bending), which suits
|
||||
## the low-poly look. Call RigidSkin.convert_tree(character_root) once after the scene
|
||||
## is instanced (bones don't need to be posed yet — pieces are baked in bind space).
|
||||
|
||||
static func convert_tree(root: Node) -> int:
|
||||
var converted := 0
|
||||
for skel: Skeleton3D in _find(root, "Skeleton3D", []):
|
||||
for mi: Node in skel.get_children():
|
||||
if mi is MeshInstance3D and (mi as MeshInstance3D).skin != null \
|
||||
and (mi as MeshInstance3D).mesh is ArrayMesh:
|
||||
if _convert(skel, mi as MeshInstance3D):
|
||||
converted += 1
|
||||
return converted
|
||||
|
||||
|
||||
static func _find(n: Node, klass: String, acc: Array) -> Array:
|
||||
if n.is_class(klass):
|
||||
acc.append(n)
|
||||
for c: Node in n.get_children():
|
||||
_find(c, klass, acc)
|
||||
return acc
|
||||
|
||||
|
||||
static func _convert(skel: Skeleton3D, mi: MeshInstance3D) -> bool:
|
||||
var mesh := mi.mesh as ArrayMesh
|
||||
var skin := mi.skin
|
||||
|
||||
# bind index -> (bone index, bind pose = mesh-space -> bone-local)
|
||||
var bind_bone: PackedInt32Array = []
|
||||
var bind_pose: Array[Transform3D] = []
|
||||
for b: int in skin.get_bind_count():
|
||||
var bone := skin.get_bind_bone(b)
|
||||
if bone < 0:
|
||||
bone = skel.find_bone(skin.get_bind_name(b))
|
||||
bind_bone.append(bone)
|
||||
bind_pose.append(skin.get_bind_pose(b))
|
||||
|
||||
# Accumulate SurfaceTool geometry per (destination bone, source surface). Keying on the
|
||||
# surface too — not just the bone — is what preserves materials: a head bone that carries
|
||||
# both the skin and hair surfaces must stay two pieces with two materials, otherwise every
|
||||
# surface funnelled to a bone collapses onto one material (bald matadors, wrong uniforms).
|
||||
var groups: Dictionary = {} # "bone:surface" -> {"st": SurfaceTool, "mat": Material, "bone": int}
|
||||
|
||||
for s: int in mesh.get_surface_count():
|
||||
var arr := mesh.surface_get_arrays(s)
|
||||
var verts: PackedVector3Array = arr[Mesh.ARRAY_VERTEX]
|
||||
var norms: PackedVector3Array = arr[Mesh.ARRAY_NORMAL]
|
||||
var uvs: PackedVector2Array = arr[Mesh.ARRAY_TEX_UV] if arr[Mesh.ARRAY_TEX_UV] != null else PackedVector2Array()
|
||||
var cols: PackedColorArray = arr[Mesh.ARRAY_COLOR] if arr[Mesh.ARRAY_COLOR] != null else PackedColorArray()
|
||||
var bones: PackedInt32Array = arr[Mesh.ARRAY_BONES]
|
||||
var weights: PackedFloat32Array = arr[Mesh.ARRAY_WEIGHTS]
|
||||
var idx: PackedInt32Array = arr[Mesh.ARRAY_INDEX]
|
||||
# Honour a per-instance override (whole mesh) or per-surface override the scene set,
|
||||
# falling back to the material baked into the mesh surface.
|
||||
var mat: Material = mi.material_override
|
||||
if mat == null:
|
||||
mat = mi.get_surface_override_material(s)
|
||||
if mat == null:
|
||||
mat = mesh.surface_get_material(s)
|
||||
var infl := 8 if (mesh.surface_get_format(s) & Mesh.ARRAY_FLAG_USE_8_BONE_WEIGHTS) else 4
|
||||
|
||||
var tri := PackedInt32Array()
|
||||
if idx != null and idx.size() > 0:
|
||||
tri = idx
|
||||
else:
|
||||
tri.resize(verts.size())
|
||||
for i: int in verts.size():
|
||||
tri[i] = i
|
||||
|
||||
for t: int in range(0, tri.size(), 3):
|
||||
var a := tri[t]
|
||||
var b := tri[t + 1]
|
||||
var c := tri[t + 2]
|
||||
var bind := _dominant_bind([a, b, c], bones, weights, infl)
|
||||
var bone := bind_bone[bind]
|
||||
var pose := bind_pose[bind]
|
||||
var key := "%d:%d" % [bone, s]
|
||||
var group: Dictionary = groups.get(key, {})
|
||||
if group.is_empty():
|
||||
var new_st := SurfaceTool.new()
|
||||
new_st.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||
group = {"st": new_st, "mat": mat, "bone": bone}
|
||||
groups[key] = group
|
||||
var st: SurfaceTool = group["st"]
|
||||
for v: int in [a, b, c]:
|
||||
if cols.size() > v:
|
||||
st.set_color(cols[v])
|
||||
if uvs.size() > v:
|
||||
st.set_uv(uvs[v])
|
||||
if norms.size() > v:
|
||||
st.set_normal((pose.basis * norms[v]).normalized())
|
||||
st.add_vertex(pose * verts[v])
|
||||
|
||||
if groups.is_empty():
|
||||
return false
|
||||
|
||||
# Combine all of a bone's surface-groups into ONE mesh under ONE BoneAttachment (each group
|
||||
# stays its own surface + material). Same draw calls as before, but roughly half the nodes to
|
||||
# transform/cull every frame — which is what a phone feels while ragdolls drive the skeleton.
|
||||
var by_bone: Dictionary = {} # bone:int -> Array[Dictionary]
|
||||
for key: String in groups:
|
||||
var group: Dictionary = groups[key]
|
||||
var bone: int = group["bone"]
|
||||
if not by_bone.has(bone):
|
||||
by_bone[bone] = []
|
||||
(by_bone[bone] as Array).append(group)
|
||||
|
||||
for bone: int in by_bone:
|
||||
var att := BoneAttachment3D.new()
|
||||
att.bone_name = skel.get_bone_name(bone)
|
||||
skel.add_child(att)
|
||||
var piece := MeshInstance3D.new()
|
||||
var combined := ArrayMesh.new()
|
||||
for group: Dictionary in by_bone[bone]:
|
||||
var st: SurfaceTool = group["st"]
|
||||
st.commit(combined)
|
||||
if group["mat"] != null:
|
||||
combined.surface_set_material(combined.get_surface_count() - 1, group["mat"])
|
||||
piece.mesh = combined
|
||||
att.add_child(piece)
|
||||
|
||||
# Hide (don't free) the original: keeps the node for any gameplay code that references
|
||||
# it, and a hidden skinned mesh isn't drawn so it never hits the transform-feedback path.
|
||||
mi.visible = false
|
||||
return true
|
||||
|
||||
|
||||
static func _dominant_bind(vs: Array, bones: PackedInt32Array, weights: PackedFloat32Array, infl: int) -> int:
|
||||
var acc: Dictionary = {}
|
||||
for v: int in vs:
|
||||
for k: int in infl:
|
||||
var w := weights[v * infl + k]
|
||||
if w <= 0.0:
|
||||
continue
|
||||
var bind := bones[v * infl + k]
|
||||
acc[bind] = float(acc.get(bind, 0.0)) + w
|
||||
var best := 0
|
||||
var best_w := -1.0
|
||||
for bind: int in acc:
|
||||
if acc[bind] > best_w:
|
||||
best_w = acc[bind]
|
||||
best = bind
|
||||
return best
|
||||
@@ -0,0 +1 @@
|
||||
uid://dymd8vcfmtjts
|
||||
Reference in New Issue
Block a user