extends CanvasLayer class_name Gore ## Blood on the lens, and the death card. ## ## All procedural — a shader stamping seeded blobs that grow and then bleed downward in ## streaks, plus a burst of red at the camera. No textures, same as everything else here. ## ## The death sequence is deliberately SLOW. A jumpscare is a spike; a death should take ## its time, because the four seconds you spend watching the camera settle onto the ## carpet while blood runs down the glass is where the dread actually lands. Then a card ## that names what got you, so the three deaths stay distinguishable. ## ## Godot 4.7 GDScript 2.0. const SHADER := """ shader_type canvas_item; uniform float amount : hint_range(0.0, 1.0) = 0.0; uniform float t = 0.0; uniform vec3 tint = vec3(0.42, 0.02, 0.02); float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } void fragment() { vec2 uv = UV; float a = 0.0; // splatter: a fixed set of blobs that swell with `amount` for (int i = 0; i < 14; i++) { float fi = float(i); vec2 c = vec2(hash(vec2(fi, 3.1)), hash(vec2(fi, 7.7))); float rad = (0.05 + 0.13 * hash(vec2(fi, 11.3))) * amount; float d = length((uv - c) * vec2(1.0, 0.62)); a += smoothstep(rad, rad * 0.35, d); // and each blob bleeds downward once it's big enough float run = clamp((amount - 0.35) * 1.6, 0.0, 1.0) * (0.10 + 0.22 * hash(vec2(fi, 5.5))); if (uv.y > c.y && uv.y < c.y + run) { float w = rad * 0.30 * (1.0 - (uv.y - c.y) / max(run, 0.001)); a += smoothstep(w, w * 0.2, abs(uv.x - c.x)) * 0.9; } } // heavier at the edges — it's on the housing as well as the glass vec2 d2 = (uv - vec2(0.5)) * vec2(1.0, 0.62); a += smoothstep(0.30, 0.95, length(d2) * 2.0) * amount * 1.2; // clotted texture so it isn't flat a *= 0.80 + 0.20 * hash(floor(uv * 220.0)); a = clamp(a, 0.0, 1.0) * clamp(amount * 1.35, 0.0, 1.0); vec3 col = mix(tint, vec3(0.16, 0.0, 0.0), 0.5 + 0.5 * sin(t * 0.7)); COLOR = vec4(col, a); } """ var _blood: ColorRect var _black: ColorRect var _card: Control var _title: Label var _epitaph: Label var _hint: Label var _t := 0.0 var _amount := 0.0 func setup() -> void: layer = 40 # above the tape and everything else _blood = ColorRect.new() _blood.set_anchors_preset(Control.PRESET_FULL_RECT) _blood.mouse_filter = Control.MOUSE_FILTER_IGNORE var sh := Shader.new() sh.code = SHADER var m := ShaderMaterial.new() m.shader = sh _blood.material = m add_child(_blood) _black = ColorRect.new() _black.set_anchors_preset(Control.PRESET_FULL_RECT) _black.mouse_filter = Control.MOUSE_FILTER_IGNORE _black.color = Color(0, 0, 0, 0) add_child(_black) _card = Control.new() _card.set_anchors_preset(Control.PRESET_FULL_RECT) _card.mouse_filter = Control.MOUSE_FILTER_IGNORE _card.visible = false add_child(_card) _title = _label(_card, 46, Color(0.78, 0.06, 0.07), -70) _epitaph = _label(_card, 20, Color(0.72, 0.70, 0.66), 10) _hint = _label(_card, 15, Color(0.45, 0.44, 0.42), 74) set_process(true) func _label(parent: Control, size: int, col: Color, y: int) -> Label: var l := Label.new() l.add_theme_font_size_override("font_size", size) l.add_theme_color_override("font_color", col) l.anchor_left = 0.5 l.anchor_right = 0.5 l.anchor_top = 0.5 l.offset_left = -420 l.offset_right = 420 l.offset_top = y l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART parent.add_child(l) return l ## 0..1. Backrooms drives this up over the death sequence. func set_blood(v: float) -> void: _amount = clampf(v, 0.0, 1.0) func set_fade(v: float) -> void: _black.color = Color(0, 0, 0, clampf(v, 0.0, 1.0)) func show_card(title: String, epitaph: String) -> void: _card.visible = true _title.text = title _epitaph.text = epitaph _hint.text = "R — wake up" func clear() -> void: _amount = 0.0 _card.visible = false _black.color = Color(0, 0, 0, 0) visible = false func arm() -> void: visible = true func _process(dt: float) -> void: _t += dt if _blood != null: var m := _blood.material as ShaderMaterial m.set_shader_parameter("amount", _amount) m.set_shader_parameter("t", _t) ## A wet burst at the camera. Called once, on the kill. static func splatter(host: Node3D, at: Vector3, dir: Vector3) -> void: var p := GPUParticles3D.new() p.one_shot = true p.explosiveness = 1.0 p.amount = 220 p.lifetime = 2.4 p.local_coords = false var pm := ParticleProcessMaterial.new() pm.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_SPHERE pm.emission_sphere_radius = 0.12 pm.direction = dir pm.spread = 62.0 pm.initial_velocity_min = 2.5 pm.initial_velocity_max = 9.5 pm.gravity = Vector3(0, -11.0, 0) pm.scale_min = 0.5 pm.scale_max = 1.9 pm.angular_velocity_min = -600.0 pm.angular_velocity_max = 600.0 p.process_material = pm var mesh := BoxMesh.new() mesh.size = Vector3(0.035, 0.035, 0.035) var mat := StandardMaterial3D.new() mat.albedo_color = Color(0.36, 0.015, 0.02) mat.roughness = 0.35 mesh.material = mat p.draw_pass_1 = mesh host.add_child(p) p.global_position = at p.emitting = true p.finished.connect(p.queue_free)