extends CanvasLayer class_name Dread ## The camera you are apparently holding. ## ## One full-screen shader doing four jobs, because they're all the same job — making the ## image feel like a recording of something rather than a view of it: ## ## VHS ......... grain, scanlines, a chroma split, and a tracking tear that wanders. ## Found-footage grammar. It also quietly hides the fact that the ## geometry is very simple. ## STATIC ...... Slender's interference. Driven by Entity.static_level, so it is a ## read-out of how long you've been looking at it — the HUD element that ## isn't a HUD element. ## SCARE ....... a white-hot bloom and a violent tear on the frame it takes you. ## PULSE ....... a slow breathing darkness at the edges when it's near but unseen, ## which is the only warning you get that it's behind you. ## ## Godot 4.7 GDScript 2.0. const SHADER := """ shader_type canvas_item; uniform float t = 0.0; uniform float grain : hint_range(0.0, 1.0) = 0.35; uniform float static_lv : hint_range(0.0, 1.0) = 0.0; uniform float scare : hint_range(0.0, 1.0) = 0.0; uniform float near : hint_range(0.0, 1.0) = 0.0; uniform sampler2D screen_tex : hint_screen_texture, filter_linear; float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); } void fragment() { vec2 uv = SCREEN_UV; // tracking tear: a band that slides down the frame and drags the image sideways float band = fract(uv.y * 1.4 - t * 0.11); float tear = smoothstep(0.982, 1.0, band) * (0.55 + static_lv + scare * 4.0); uv.x += tear * (hash(vec2(floor(uv.y * 200.0), floor(t * 12.0))) - 0.5) * 0.06; // static shoves the image around in blocks — legibility falls apart as it stares if (static_lv > 0.02) { vec2 blk = floor(uv * vec2(48.0, 90.0)); float j = hash(blk + floor(t * 20.0)); uv += (vec2(hash(blk.yx), j) - 0.5) * static_lv * 0.05 * step(0.72, j); } // chroma split, widening with dread float split = (0.0016 + static_lv * 0.010 + scare * 0.020); vec3 col; col.r = texture(screen_tex, uv + vec2(split, 0.0)).r; col.g = texture(screen_tex, uv).g; col.b = texture(screen_tex, uv - vec2(split, 0.0)).b; // grain + scanlines float g = hash(uv * vec2(1024.0, 768.0) + fract(t) * 91.0); col += (g - 0.5) * (grain * 0.13 + static_lv * 0.34); col *= 0.90 + 0.10 * sin(SCREEN_UV.y * 900.0); // the white blocks of real interference col = mix(col, vec3(g), static_lv * static_lv * 0.34); // edges close in when it's near but out of sight — your only warning vec2 d = (SCREEN_UV - vec2(0.5)) * vec2(1.0, 0.62); float r = length(d) * 2.0; float breathe = 0.5 + 0.5 * sin(t * 2.1); col *= 1.0 - smoothstep(0.42, 1.25, r) * (near * (0.45 + 0.25 * breathe)); // the frame it takes you col = mix(col, vec3(1.0), scare * 0.62); COLOR = vec4(col, 1.0); } """ var _rect: ColorRect var _t := 0.0 var _scare := 0.0 var _face: TextureRect var _face_left := 0.0 func setup() -> void: layer = 30 # above everything, including the rage vignette _rect = ColorRect.new() _rect.set_anchors_preset(Control.PRESET_FULL_RECT) _rect.mouse_filter = Control.MOUSE_FILTER_IGNORE var sh := Shader.new() sh.code = SHADER var m := ShaderMaterial.new() m.shader = sh _rect.material = m add_child(_rect) visible = false ## Driven every frame by Backrooms. func drive(dt: float, static_lv: float, near: float) -> void: _t += dt _scare = maxf(0.0, _scare - dt * 1.7) var m := _rect.material as ShaderMaterial m.set_shader_parameter("t", _t) m.set_shader_parameter("static_lv", static_lv) m.set_shader_parameter("near", near) m.set_shader_parameter("scare", _scare) func flash() -> void: _scare = 1.0 func set_active(on: bool) -> void: visible = on if not on: _scare = 0.0