doom 0.6.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +17 -0
  3. data/README.md +65 -4
  4. data/bin/doom +138 -25
  5. data/lib/doom/benchmark.rb +282 -0
  6. data/lib/doom/game/animations.rb +9 -2
  7. data/lib/doom/game/combat.rb +377 -105
  8. data/lib/doom/game/framebuffer_blitter.rb +38 -0
  9. data/lib/doom/game/geometry.rb +68 -0
  10. data/lib/doom/game/intermission.rb +231 -0
  11. data/lib/doom/game/item_pickup.rb +108 -60
  12. data/lib/doom/game/menu.rb +347 -0
  13. data/lib/doom/game/monster_ai.rb +304 -91
  14. data/lib/doom/game/player.rb +105 -0
  15. data/lib/doom/game/player_physics.rb +384 -0
  16. data/lib/doom/game/player_state.rb +47 -64
  17. data/lib/doom/game/random.rb +82 -0
  18. data/lib/doom/game/sector_actions.rb +466 -29
  19. data/lib/doom/game/sector_effects.rb +25 -18
  20. data/lib/doom/game/snapshot.rb +584 -0
  21. data/lib/doom/game/sound_engine.rb +176 -0
  22. data/lib/doom/game/state_hash.rb +125 -0
  23. data/lib/doom/game/ticcmd.rb +61 -0
  24. data/lib/doom/game/world.rb +420 -0
  25. data/lib/doom/map/data.rb +95 -0
  26. data/lib/doom/net/client.rb +204 -0
  27. data/lib/doom/net/desync_monitor.rb +101 -0
  28. data/lib/doom/net/game_server.rb +232 -0
  29. data/lib/doom/net/lockstep.rb +170 -0
  30. data/lib/doom/net/protocol.rb +350 -0
  31. data/lib/doom/net/session.rb +282 -0
  32. data/lib/doom/net/transport.rb +110 -0
  33. data/lib/doom/platform/gosu_window.rb +820 -640
  34. data/lib/doom/platform/sdl.rb +74 -0
  35. data/lib/doom/platform/window_logic.rb +61 -0
  36. data/lib/doom/render/font.rb +80 -0
  37. data/lib/doom/render/hardware_renderer.rb +547 -0
  38. data/lib/doom/render/ray_tracing/bvh.rb +103 -0
  39. data/lib/doom/render/ray_tracing/material_state.rb +66 -0
  40. data/lib/doom/render/ray_tracing/texture_atlas.rb +78 -0
  41. data/lib/doom/render/ray_tracing_renderer.rb +940 -0
  42. data/lib/doom/render/renderer.rb +684 -330
  43. data/lib/doom/render/renderer_factory.rb +50 -0
  44. data/lib/doom/render/screen_melt.rb +71 -0
  45. data/lib/doom/render/spinel_native/kernel.rb +780 -0
  46. data/lib/doom/render/spinel_native_renderer.rb +162 -0
  47. data/lib/doom/render/status_bar.rb +14 -10
  48. data/lib/doom/render/weapon_renderer.rb +15 -17
  49. data/lib/doom/render/world_mesh.rb +270 -0
  50. data/lib/doom/render/zbuffer_renderer.rb +151 -0
  51. data/lib/doom/version.rb +1 -1
  52. data/lib/doom/wad/flat.rb +1 -1
  53. data/lib/doom/wad/hud_graphics.rb +7 -3
  54. data/lib/doom/wad/palette.rb +3 -3
  55. data/lib/doom/wad/patch.rb +1 -1
  56. data/lib/doom/wad/reader.rb +59 -10
  57. data/lib/doom/wad/sound.rb +93 -0
  58. data/lib/doom/wad/sprite.rb +60 -34
  59. data/lib/doom/wad/texture.rb +5 -5
  60. data/lib/doom/wad_downloader.rb +38 -56
  61. data/lib/doom.rb +235 -12
  62. metadata +100 -7
@@ -0,0 +1,940 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doom
4
+ module Render
5
+ # GPU ray tracer hosted in Gosu's OpenGL context. World triangles are kept
6
+ # in a floating-point data texture and intersected by a fragment shader;
7
+ # the old hardware renderer is inherited only for texture/sprite/UI glue.
8
+ class RayTracingRenderer < HardwareRenderer
9
+ DATA_WIDTH = 1024
10
+ NODE_DATA_WIDTH = 1024
11
+ TEXELS_PER_TRIANGLE = 7
12
+ TEXELS_PER_NODE = 3
13
+ BVH_LEAF_SIZE = 8
14
+ MAX_RAY_LIGHTS = 8
15
+ # Soft shadows: each shadow test fires this many rays at jittered points on
16
+ # a small light disc (SHADOW_SOFTNESS map units across) and averages them,
17
+ # so edges get a penumbra instead of a hard cut. More samples = smoother
18
+ # but costs a BVH ray each; radius widens the penumbra.
19
+ SHADOW_SAMPLES = 5
20
+ SHADOW_SOFTNESS = 12.0
21
+ # Atmosphere: how much the sector light level fills unlit surfaces, and how
22
+ # bright the sky reads. Both kept low so shadows stay deep and the
23
+ # flashlight carries the scene -- lower is darker/moodier, higher is flatter.
24
+ AMBIENT_LEVEL = 0.18 # was 0.32
25
+ SKY_BRIGHTNESS = 0.28 # was 0.45
26
+ # Distance fog gives depth: without it a bright sector reads evenly lit all
27
+ # the way down a corridor. Higher density swallows distance sooner; FOG_MAX
28
+ # is how completely far surfaces fade into the dark fog colour.
29
+ FOG_DENSITY = 0.0011 # was 0.00075
30
+ FOG_MAX = 0.94 # was 0.82
31
+ RAY_WIDTH = 640
32
+ RAY_HEIGHT = 480
33
+ MAX_TRIANGLES = 4096
34
+ ATLAS_SIZE = 2048
35
+
36
+ VERTEX_SHADER = <<~GLSL
37
+ #version 120
38
+ varying vec2 screen_uv;
39
+ void main() {
40
+ screen_uv = gl_MultiTexCoord0.xy;
41
+ gl_Position = gl_Vertex;
42
+ }
43
+ GLSL
44
+
45
+ FRAGMENT_SHADER = <<~GLSL
46
+ #version 120
47
+ varying vec2 screen_uv;
48
+ uniform sampler2D triangle_data;
49
+ uniform sampler2D bvh_data;
50
+ uniform sampler2D material_atlas;
51
+ uniform sampler2D sky_texture;
52
+ uniform float data_height;
53
+ uniform float bvh_height;
54
+ uniform int node_count;
55
+ uniform vec3 camera_position;
56
+ uniform vec3 camera_forward;
57
+ uniform vec3 camera_right;
58
+ uniform vec3 camera_up;
59
+ uniform float aspect_ratio;
60
+ uniform int light_count;
61
+ uniform vec4 light_positions[#{MAX_RAY_LIGHTS}];
62
+ uniform vec4 light_colors[#{MAX_RAY_LIGHTS}];
63
+ uniform int fog_enabled;
64
+ uniform int flashlight_enabled;
65
+ uniform int bounces_enabled;
66
+
67
+ vec4 datum(float index) {
68
+ float x = mod(index, #{DATA_WIDTH}.0);
69
+ float y = floor(index / #{DATA_WIDTH}.0);
70
+ return texture2D(triangle_data,
71
+ vec2((x + 0.5) / #{DATA_WIDTH}.0, (y + 0.5) / data_height));
72
+ }
73
+
74
+ vec4 node_datum(float index) {
75
+ float x = mod(index, #{NODE_DATA_WIDTH}.0);
76
+ float y = floor(index / #{NODE_DATA_WIDTH}.0);
77
+ return texture2D(bvh_data,
78
+ vec2((x + 0.5) / #{NODE_DATA_WIDTH}.0, (y + 0.5) / bvh_height));
79
+ }
80
+
81
+ bool intersect_box(vec3 origin, vec3 inverse_direction, vec3 minimum,
82
+ vec3 maximum, float distance_limit) {
83
+ vec3 near_values = (minimum - origin) * inverse_direction;
84
+ vec3 far_values = (maximum - origin) * inverse_direction;
85
+ vec3 low = min(near_values, far_values);
86
+ vec3 high = max(near_values, far_values);
87
+ float near_distance = max(max(low.x, low.y), max(low.z, 0.0));
88
+ float far_distance = min(min(high.x, high.y), high.z);
89
+ return near_distance <= far_distance && near_distance < distance_limit;
90
+ }
91
+
92
+ bool intersect_triangle(vec3 origin, vec3 direction, float base,
93
+ out float distance, out vec2 barycentric) {
94
+ vec3 a = datum(base).xyz;
95
+ vec3 b = datum(base + 1.0).xyz;
96
+ vec3 c = datum(base + 2.0).xyz;
97
+ vec3 edge1 = b - a;
98
+ vec3 edge2 = c - a;
99
+ vec3 p = cross(direction, edge2);
100
+ float determinant = dot(edge1, p);
101
+ if (abs(determinant) < 0.00001) return false;
102
+ float inverse = 1.0 / determinant;
103
+ vec3 t = origin - a;
104
+ float u = dot(t, p) * inverse;
105
+ if (u < 0.0 || u > 1.0) return false;
106
+ vec3 q = cross(t, edge1);
107
+ float v = dot(direction, q) * inverse;
108
+ if (v < 0.0 || u + v > 1.0) return false;
109
+ distance = dot(edge2, q) * inverse;
110
+ barycentric = vec2(u, v);
111
+ return distance > 0.01;
112
+ }
113
+
114
+ bool accepts_surface(float base, vec2 barycentric) {
115
+ float flags = datum(base + 3.0).w;
116
+ float masked = floor(mod(flags, 4096.0) / 2048.0);
117
+ if (masked < 0.5) return true;
118
+ vec4 uv0_uv1 = datum(base + 4.0);
119
+ vec4 uv2_rect = datum(base + 5.0);
120
+ vec4 rect_size = datum(base + 6.0);
121
+ float w = 1.0 - barycentric.x - barycentric.y;
122
+ vec2 uv = uv0_uv1.xy * w + uv0_uv1.zw * barycentric.x +
123
+ uv2_rect.xy * barycentric.y;
124
+ vec2 wrapped = fract(uv / rect_size.zw);
125
+ vec2 atlas_uv = rect_size.xy + wrapped * uv2_rect.zw;
126
+ return texture2D(material_atlas, atlas_uv).a > 0.5;
127
+ }
128
+
129
+ bool shadowed(vec3 origin, vec3 direction, float maximum) {
130
+ vec3 inverse_direction = 1.0 / direction;
131
+ int node_index = 0;
132
+ while (node_index < node_count) {
133
+ float node_base = float(node_index * #{TEXELS_PER_NODE});
134
+ vec4 minimum_escape = node_datum(node_base);
135
+ vec4 maximum_start = node_datum(node_base + 1.0);
136
+ int escape = int(minimum_escape.w + 0.5);
137
+ if (!intersect_box(origin, inverse_direction, minimum_escape.xyz,
138
+ maximum_start.xyz, maximum)) {
139
+ node_index = escape;
140
+ continue;
141
+ }
142
+ if (maximum_start.w >= 0.0) {
143
+ int start = int(maximum_start.w + 0.5);
144
+ int count = int(node_datum(node_base + 2.0).x + 0.5);
145
+ for (int offset = 0; offset < #{BVH_LEAF_SIZE}; ++offset) {
146
+ if (offset >= count) break;
147
+ float distance;
148
+ vec2 barycentric;
149
+ float triangle_base = float((start + offset) * #{TEXELS_PER_TRIANGLE});
150
+ if (intersect_triangle(origin, direction, triangle_base,
151
+ distance, barycentric) && distance < maximum &&
152
+ accepts_surface(triangle_base, barycentric)) return true;
153
+ }
154
+ node_index = escape;
155
+ } else {
156
+ node_index += 1;
157
+ }
158
+ }
159
+ return false;
160
+ }
161
+
162
+ // Average several shadow rays fired at jittered points on a small light
163
+ // disc so shadow edges soften into a penumbra. Returns visibility [0,1].
164
+ float shadow_visibility(vec3 origin, vec3 light_position) {
165
+ float lit = 0.0;
166
+ for (int s = 0; s < #{SHADOW_SAMPLES}; ++s) {
167
+ float angle = float(s) * 2.39996323; // golden-angle spread
168
+ float disc_radius = #{SHADOW_SOFTNESS} * sqrt((float(s) + 0.5) / float(#{SHADOW_SAMPLES}));
169
+ vec3 jittered = light_position + camera_right * (cos(angle) * disc_radius) +
170
+ camera_up * (sin(angle) * disc_radius);
171
+ vec3 delta = jittered - origin;
172
+ float light_dist = length(delta);
173
+ if (!shadowed(origin, delta / max(light_dist, 0.001), light_dist - 0.1)) lit += 1.0;
174
+ }
175
+ return lit / float(#{SHADOW_SAMPLES});
176
+ }
177
+
178
+ bool trace_scene(vec3 origin, vec3 direction, float distance_limit,
179
+ int ignored_triangle,
180
+ out int hit, out float nearest, out vec2 hit_barycentric) {
181
+ nearest = distance_limit;
182
+ hit = -1;
183
+ hit_barycentric = vec2(0.0);
184
+ vec3 inverse_direction = 1.0 / direction;
185
+ int node_index = 0;
186
+ while (node_index < node_count) {
187
+ float node_base = float(node_index * #{TEXELS_PER_NODE});
188
+ vec4 minimum_escape = node_datum(node_base);
189
+ vec4 maximum_start = node_datum(node_base + 1.0);
190
+ int escape = int(minimum_escape.w + 0.5);
191
+ if (!intersect_box(origin, inverse_direction, minimum_escape.xyz,
192
+ maximum_start.xyz, nearest)) {
193
+ node_index = escape;
194
+ continue;
195
+ }
196
+ if (maximum_start.w >= 0.0) {
197
+ int start = int(maximum_start.w + 0.5);
198
+ int count = int(node_datum(node_base + 2.0).x + 0.5);
199
+ for (int offset = 0; offset < #{BVH_LEAF_SIZE}; ++offset) {
200
+ if (offset >= count) break;
201
+ int triangle_index = start + offset;
202
+ if (triangle_index == ignored_triangle) continue;
203
+ float distance;
204
+ vec2 barycentric;
205
+ float triangle_base = float(triangle_index * #{TEXELS_PER_TRIANGLE});
206
+ if (intersect_triangle(origin, direction, triangle_base,
207
+ distance, barycentric) && distance < nearest &&
208
+ accepts_surface(triangle_base, barycentric)) {
209
+ nearest = distance;
210
+ hit = triangle_index;
211
+ hit_barycentric = barycentric;
212
+ }
213
+ }
214
+ node_index = escape;
215
+ } else {
216
+ node_index += 1;
217
+ }
218
+ }
219
+ return hit >= 0;
220
+ }
221
+
222
+ vec3 sky_radiance(vec3 direction) {
223
+ float sky_u = atan(direction.y, direction.x) * 2.0 / 3.14159265;
224
+ float sky_v = 0.5 - asin(clamp(direction.z, -1.0, 1.0)) / 3.14159265;
225
+ return texture2D(sky_texture, vec2(sky_u, sky_v * (200.0 / 128.0))).rgb * #{SKY_BRIGHTNESS};
226
+ }
227
+
228
+ vec3 secondary_radiance(vec3 origin, vec3 direction, int source_triangle) {
229
+ int secondary_hit;
230
+ float secondary_distance;
231
+ vec2 barycentric;
232
+ if (!trace_scene(origin, direction, 1.0e20, source_triangle, secondary_hit,
233
+ secondary_distance, barycentric))
234
+ return sky_radiance(direction);
235
+
236
+ float base = float(secondary_hit * #{TEXELS_PER_TRIANGLE});
237
+ vec4 normal_light = datum(base + 3.0);
238
+ vec4 uv0_uv1 = datum(base + 4.0);
239
+ vec4 uv2_rect = datum(base + 5.0);
240
+ vec4 rect_size = datum(base + 6.0);
241
+ float w = 1.0 - barycentric.x - barycentric.y;
242
+ vec2 uv = uv0_uv1.xy * w + uv0_uv1.zw * barycentric.x + uv2_rect.xy * barycentric.y;
243
+ vec2 atlas_uv = rect_size.xy + fract(uv / rect_size.zw) * uv2_rect.zw;
244
+ vec3 albedo = texture2D(material_atlas, atlas_uv).rgb;
245
+ float emission = floor(mod(normal_light.w, 2048.0) / 1024.0);
246
+ float sector = clamp(mod(normal_light.w, 1024.0) / 255.0, 0.10, 1.0);
247
+ return albedo * (vec3(sector * #{AMBIENT_LEVEL}) + emission * vec3(0.18, 0.62, 0.12));
248
+ }
249
+
250
+ float noise(vec3 point) {
251
+ return fract(sin(dot(point, vec3(12.9898, 78.233, 37.719))) * 43758.5453);
252
+ }
253
+
254
+ vec3 diffuse_bounce_direction(vec3 normal, float surface_id) {
255
+ vec3 helper = abs(normal.z) < 0.9 ? vec3(0.0, 0.0, 1.0) : vec3(0.0, 1.0, 0.0);
256
+ vec3 tangent = normalize(cross(helper, normal));
257
+ vec3 bitangent = cross(normal, tangent);
258
+ // Keep the sample fixed to the triangle. Hashing the continuously
259
+ // moving hit point makes indirect light sparkle as the camera moves.
260
+ float angle = noise(vec3(surface_id, surface_id * 0.37, 1.0)) * 6.2831853;
261
+ float radius = 0.65;
262
+ return normalize(normal * sqrt(1.0 - radius * radius) +
263
+ tangent * cos(angle) * radius + bitangent * sin(angle) * radius);
264
+ }
265
+
266
+ void main() {
267
+ vec2 plane = screen_uv * 2.0 - 1.0;
268
+ plane.y /= aspect_ratio;
269
+ vec3 direction = normalize(camera_forward + camera_right * plane.x + camera_up * plane.y);
270
+ float nearest = 1.0e20;
271
+ int hit = -1;
272
+ vec2 hit_barycentric = vec2(0.0);
273
+ vec3 inverse_direction = 1.0 / direction;
274
+ int node_index = 0;
275
+ while (node_index < node_count) {
276
+ float node_base = float(node_index * #{TEXELS_PER_NODE});
277
+ vec4 minimum_escape = node_datum(node_base);
278
+ vec4 maximum_start = node_datum(node_base + 1.0);
279
+ int escape = int(minimum_escape.w + 0.5);
280
+ if (!intersect_box(camera_position, inverse_direction, minimum_escape.xyz,
281
+ maximum_start.xyz, nearest)) {
282
+ node_index = escape;
283
+ continue;
284
+ }
285
+ if (maximum_start.w >= 0.0) {
286
+ int start = int(maximum_start.w + 0.5);
287
+ int count = int(node_datum(node_base + 2.0).x + 0.5);
288
+ for (int offset = 0; offset < #{BVH_LEAF_SIZE}; ++offset) {
289
+ if (offset >= count) break;
290
+ int triangle_index = start + offset;
291
+ float distance;
292
+ vec2 barycentric;
293
+ float triangle_base = float(triangle_index * #{TEXELS_PER_TRIANGLE});
294
+ if (intersect_triangle(camera_position, direction, triangle_base,
295
+ distance, barycentric) && distance < nearest &&
296
+ accepts_surface(triangle_base, barycentric)) {
297
+ nearest = distance;
298
+ hit = triangle_index;
299
+ hit_barycentric = barycentric;
300
+ }
301
+ }
302
+ node_index = escape;
303
+ } else {
304
+ node_index += 1;
305
+ }
306
+ }
307
+ if (hit < 0) {
308
+ // Match HardwareRenderer::draw_sky and Doom's SKY1 density:
309
+ // repeat the 256px panorama four times around the player and map
310
+ // 200 sky texels over the full view height.
311
+ float sky_u = atan(direction.y, direction.x) * 2.0 / 3.14159265;
312
+ float sky_v = (1.0 - screen_uv.y) * (200.0 / 128.0);
313
+ vec3 sky = texture2D(sky_texture, vec2(sky_u, sky_v)).rgb;
314
+ gl_FragColor = vec4(sky * #{SKY_BRIGHTNESS}, 1.0);
315
+ return;
316
+ }
317
+ float base = float(hit * #{TEXELS_PER_TRIANGLE});
318
+ vec4 normal_light = datum(base + 3.0);
319
+ vec4 uv0_uv1 = datum(base + 4.0);
320
+ vec4 uv2_rect = datum(base + 5.0);
321
+ vec4 rect_size = datum(base + 6.0);
322
+ float w = 1.0 - hit_barycentric.x - hit_barycentric.y;
323
+ vec2 uv = uv0_uv1.xy * w + uv0_uv1.zw * hit_barycentric.x + uv2_rect.xy * hit_barycentric.y;
324
+ vec2 wrapped = fract(uv / rect_size.zw);
325
+ vec2 atlas_uv = rect_size.xy + wrapped * uv2_rect.zw;
326
+ vec3 albedo = texture2D(material_atlas, atlas_uv).rgb;
327
+ vec3 normal = normalize(normal_light.xyz);
328
+ if (dot(normal, direction) > 0.0) normal = -normal;
329
+ vec3 point = camera_position + direction * nearest;
330
+ float emission = floor(mod(normal_light.w, 2048.0) / 1024.0);
331
+ float sector = clamp(mod(normal_light.w, 1024.0) / 255.0, 0.10, 1.0);
332
+ vec3 ambient = albedo * sector * #{AMBIENT_LEVEL};
333
+ vec3 direct = vec3(0.0);
334
+ float strongest_score = 0.0;
335
+ float second_score = 0.0;
336
+ vec3 strongest_direct = vec3(0.0);
337
+ vec3 second_direct = vec3(0.0);
338
+ vec3 strongest_direction = vec3(0.0);
339
+ vec3 second_direction = vec3(0.0);
340
+ float strongest_distance = 0.0;
341
+ float second_distance = 0.0;
342
+ for (int light_index = 0; light_index < #{MAX_RAY_LIGHTS}; ++light_index) {
343
+ if (light_index >= light_count) break;
344
+ vec3 to_light = light_positions[light_index].xyz - point;
345
+ float light_distance = length(to_light);
346
+ vec3 light_direction = to_light / max(light_distance, 0.001);
347
+ float diffuse = max(dot(normal, light_direction), 0.0);
348
+ // A gentler physically-shaped falloff keeps distant visible lamps
349
+ // contributing instead of crossing an apparent hard threshold.
350
+ float attenuation = 1.0 / (1.0 + light_distance * 0.0015 +
351
+ light_distance * light_distance * 0.000004);
352
+ float contribution = diffuse * attenuation;
353
+ if (contribution < 0.003) continue;
354
+ vec3 light_direct = albedo * light_colors[light_index].rgb * contribution * 1.8;
355
+ direct += light_direct;
356
+ float score = contribution * dot(light_colors[light_index].rgb, vec3(0.30, 0.59, 0.11));
357
+ if (score > strongest_score) {
358
+ second_score = strongest_score;
359
+ second_direct = strongest_direct;
360
+ second_direction = strongest_direction;
361
+ second_distance = strongest_distance;
362
+ strongest_score = score;
363
+ strongest_direct = light_direct;
364
+ strongest_direction = light_direction;
365
+ strongest_distance = light_distance;
366
+ } else if (score > second_score) {
367
+ second_score = score;
368
+ second_direct = light_direct;
369
+ second_direction = light_direction;
370
+ second_distance = light_distance;
371
+ }
372
+ }
373
+ // Flashlight beam coverage for this point: wide and feathered so the
374
+ // beam edge is soft, not a hard disc.
375
+ float flashlight_beam = 0.0;
376
+ if (flashlight_enabled != 0) {
377
+ flashlight_beam = smoothstep(0.65, 0.95,
378
+ dot(normalize(point - camera_position), camera_forward));
379
+ }
380
+
381
+ // All lights illuminate, but only the two dominant contributors launch
382
+ // expensive BVH shadow rays for this surface.
383
+ if (strongest_score > 0.003) {
384
+ float strongest_visibility = shadow_visibility(point + normal * 0.08,
385
+ point + strongest_direction * strongest_distance);
386
+ direct -= strongest_direct * 0.92 * (1.0 - strongest_visibility);
387
+ }
388
+ if (second_score > 0.003) {
389
+ float second_visibility = shadow_visibility(point + normal * 0.08,
390
+ point + second_direction * second_distance);
391
+ direct -= second_direct * 0.92 * (1.0 - second_visibility);
392
+ }
393
+
394
+ if (flashlight_enabled != 0) {
395
+ // The flashlight sits slightly to the side of and below the eye, not
396
+ // exactly at it. A light co-located with the camera can never cast a
397
+ // visible shadow: the shadow of any caster falls directly behind it,
398
+ // hidden from the eye, and the shadow ray back to the camera retraces
399
+ // the (empty by construction) primary ray. Offsetting it like a
400
+ // handheld lantern throws shadows to the side where they show.
401
+ vec3 flashlight_position = camera_position + camera_right * 14.0 - camera_up * 10.0;
402
+
403
+ // The beam still points where you look; reuse the wide coverage.
404
+ float cone = flashlight_beam;
405
+
406
+ // Lighting and shadowing come from the offset lantern position.
407
+ vec3 to_flashlight = flashlight_position - point;
408
+ float flashlight_distance = length(to_flashlight);
409
+ vec3 flashlight_direction = to_flashlight / max(flashlight_distance, 0.001);
410
+ float facing = max(dot(normal, flashlight_direction), 0.0);
411
+ float flashlight_attenuation = 1.0 / (1.0 + flashlight_distance * 0.0015 +
412
+ flashlight_distance * flashlight_distance * 0.000002);
413
+ float flashlight_strength = cone * facing * flashlight_attenuation;
414
+ if (flashlight_strength > 0.004) {
415
+ float flashlight_visible = mix(0.06, 1.0,
416
+ shadow_visibility(point + normal * 0.08, flashlight_position));
417
+ direct += albedo * vec3(1.0, 0.88, 0.68) * flashlight_strength *
418
+ flashlight_visible * 2.2;
419
+ }
420
+ }
421
+
422
+ vec3 shaded = ambient + direct;
423
+ if (emission > 0.5)
424
+ shaded += albedo * vec3(0.18, 0.62, 0.12);
425
+ if (bounces_enabled != 0) {
426
+ float reflective = floor(mod(normal_light.w, 8192.0) / 4096.0);
427
+ float refractive = floor(mod(normal_light.w, 16384.0) / 8192.0);
428
+ if (reflective > 0.5) {
429
+ vec3 reflected = secondary_radiance(point + normal * 0.12,
430
+ reflect(direction, normal), hit);
431
+ float fresnel = pow(1.0 - max(dot(-direction, normal), 0.0), 5.0);
432
+ float reflection_mix = refractive > 0.5
433
+ ? mix(0.34, 0.72, fresnel)
434
+ : mix(0.42, 0.68, fresnel);
435
+ shaded = mix(shaded, reflected, reflection_mix);
436
+ } else {
437
+ vec3 bounced = secondary_radiance(point + normal * 0.12,
438
+ diffuse_bounce_direction(normal, float(hit)), hit);
439
+ shaded += albedo * bounced * 0.18;
440
+ }
441
+ if (refractive > 0.5) {
442
+ vec3 transmitted_direction = refract(direction, normal, 1.0 / 1.33);
443
+ if (length(transmitted_direction) > 0.01) {
444
+ vec3 transmitted = secondary_radiance(point - normal * 0.12,
445
+ transmitted_direction, hit);
446
+ shaded = mix(shaded, transmitted * vec3(0.72, 0.92, 0.74), 0.08);
447
+ }
448
+ }
449
+ }
450
+ if (fog_enabled != 0) {
451
+ float fog = clamp(1.0 - exp(-nearest * #{FOG_DENSITY}), 0.0, #{FOG_MAX});
452
+ shaded = mix(shaded, vec3(0.020, 0.025, 0.035), fog);
453
+ }
454
+ gl_FragColor = vec4(shaded, 1.0);
455
+ }
456
+ GLSL
457
+
458
+ SPRITE_VERTEX_SHADER = <<~GLSL
459
+ #version 120
460
+ varying vec2 sprite_uv;
461
+ varying vec3 world_position;
462
+ void main() {
463
+ sprite_uv = gl_MultiTexCoord0.xy;
464
+ world_position = gl_Vertex.xyz;
465
+ gl_Position = ftransform();
466
+ }
467
+ GLSL
468
+
469
+ SPRITE_FRAGMENT_SHADER = <<~GLSL
470
+ #version 120
471
+ varying vec2 sprite_uv;
472
+ varying vec3 world_position;
473
+ uniform sampler2D sprite_texture;
474
+ uniform float sector_light;
475
+ uniform int light_count;
476
+ uniform vec4 light_positions[#{MAX_RAY_LIGHTS}];
477
+ uniform vec4 light_colors[#{MAX_RAY_LIGHTS}];
478
+ uniform vec3 camera_position;
479
+ uniform vec3 camera_forward;
480
+ uniform int fog_enabled;
481
+ uniform int flashlight_enabled;
482
+
483
+ void main() {
484
+ vec4 texel = texture2D(sprite_texture, sprite_uv);
485
+ if (texel.a < 0.01) discard;
486
+ vec3 shaded = texel.rgb * clamp(sector_light, 0.10, 1.0) * #{AMBIENT_LEVEL};
487
+ for (int light_index = 0; light_index < #{MAX_RAY_LIGHTS}; ++light_index) {
488
+ if (light_index >= light_count) break;
489
+ float distance_to_light = length(light_positions[light_index].xyz - world_position);
490
+ float attenuation = 1.0 / (1.0 + distance_to_light * 0.0015 +
491
+ distance_to_light * distance_to_light * 0.000004);
492
+ shaded += texel.rgb * light_colors[light_index].rgb * attenuation * 0.75;
493
+ }
494
+ vec3 camera_to_point = world_position - camera_position;
495
+ float distance_to_camera = length(camera_to_point);
496
+ if (flashlight_enabled != 0 && distance_to_camera > 0.001) {
497
+ float cone = smoothstep(0.80, 0.96,
498
+ dot(camera_to_point / distance_to_camera, camera_forward));
499
+ float attenuation = 1.0 / (1.0 + distance_to_camera * 0.0015 +
500
+ distance_to_camera * distance_to_camera * 0.000002);
501
+ shaded += texel.rgb * vec3(1.0, 0.88, 0.68) * cone * attenuation * 1.5;
502
+ }
503
+ if (fog_enabled != 0) {
504
+ float fog = clamp(1.0 - exp(-distance_to_camera * #{FOG_DENSITY}), 0.0, #{FOG_MAX});
505
+ shaded = mix(shaded, vec3(0.020, 0.025, 0.035), fog);
506
+ }
507
+ gl_FragColor = vec4(shaded, texel.a);
508
+ }
509
+ GLSL
510
+
511
+ def ray_tracing?
512
+ true
513
+ end
514
+
515
+ attr_accessor :fog_enabled, :flashlight_enabled, :bounces_enabled
516
+
517
+ def initialize(...)
518
+ super
519
+ @ray_materials = RayTracing::MaterialState.new(@flats, @animations)
520
+ @fog_enabled = true
521
+ @flashlight_enabled = true
522
+ @bounces_enabled = true
523
+ end
524
+
525
+ def render_frame
526
+ signature = geometry_signature
527
+ if signature != @geometry_signature
528
+ @mesh = WorldMesh.new(@map, @textures)
529
+ @geometry_signature = signature
530
+ @ray_scene_dirty = true
531
+ @gpu_batches_dirty = true
532
+ end
533
+ @framebuffer.fill(0)
534
+ end
535
+
536
+ def draw_hardware(viewport_width, viewport_height)
537
+ Gosu.gl do
538
+ load_opengl_library
539
+ current_animation = @ray_materials.animation_signature
540
+ if @ray_animation_signature != current_animation
541
+ @ray_animation_signature = current_animation
542
+ @ray_materials_dirty = true if @ray_data_texture
543
+ end
544
+ build_ray_scene if @ray_program.nil? || @ray_scene_dirty
545
+ if @ray_materials_dirty
546
+ upload_triangle_data
547
+ @ray_materials_dirty = false
548
+ end
549
+ ensure_ray_target
550
+ viewport = [0, 0, 0, 0].pack('l4')
551
+ glGetIntegerv(GL_VIEWPORT, viewport)
552
+ viewport_x, viewport_y, physical_width, physical_height = viewport.unpack('l4')
553
+ glDisable(GL_DEPTH_TEST)
554
+ glDisable(GL_CULL_FACE)
555
+ glDisable(GL_LIGHTING)
556
+ glClearColor(0.0, 0.0, 0.0, 1.0)
557
+ glBindFramebuffer(GL_FRAMEBUFFER, @ray_framebuffer)
558
+ glViewport(0, 0, RAY_WIDTH, RAY_HEIGHT)
559
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
560
+ draw_ray_pass(RAY_WIDTH, RAY_HEIGHT)
561
+ glBindFramebuffer(GL_FRAMEBUFFER, 0)
562
+ glViewport(viewport_x, viewport_y, physical_width, physical_height)
563
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
564
+ draw_ray_target
565
+ setup_camera(viewport_width, viewport_height)
566
+ rebuild_gpu_batches if @gpu_batches.nil? || @gpu_batches_dirty
567
+ glClear(GL_DEPTH_BUFFER_BIT)
568
+ draw_occluder_depth_prepass(include_ceilings: true)
569
+ draw_sprites
570
+ capture_frame if ENV['DOOM_GL_CAPTURE'] && !@frame_captured
571
+ # Gosu draws the weapon, HUD and pause menu immediately after this
572
+ # block. Do not leak sprite-shader or modulation state into its 2D
573
+ # pipeline, otherwise menu text inherits scene lighting.
574
+ glUseProgram(0)
575
+ glActiveTexture(GL_TEXTURE0)
576
+ glBindTexture(GL_TEXTURE_2D, 0)
577
+ glColor4f(1.0, 1.0, 1.0, 1.0)
578
+ glDisable(GL_LIGHTING)
579
+ glDisable(GL_ALPHA_TEST)
580
+ glDisable(GL_BLEND)
581
+ glDisable(GL_TEXTURE_2D)
582
+ glDisable(GL_DEPTH_TEST)
583
+ end
584
+ end
585
+
586
+ private
587
+
588
+ def draw_sprites
589
+ @sprite_program ||= create_program(SPRITE_VERTEX_SHADER, SPRITE_FRAGMENT_SHADER)
590
+ glUseProgram(@sprite_program)
591
+ uniform1i('sprite_texture', 0, program: @sprite_program)
592
+ uniform1i('fog_enabled', @fog_enabled ? 1 : 0, program: @sprite_program)
593
+ uniform1i('flashlight_enabled', @flashlight_enabled ? 1 : 0, program: @sprite_program)
594
+ uniform3f('camera_position', @player_x, @player_y, @player_z, program: @sprite_program)
595
+ uniform3f('camera_forward', @cos_angle, @sin_angle, 0.0, program: @sprite_program)
596
+ lights = ray_lights.first(MAX_RAY_LIGHTS)
597
+ uniform1i('light_count', lights.size, program: @sprite_program)
598
+ positions = lights.flat_map { |light| [light[:x].to_f, light[:y].to_f, light[:z].to_f, 1.0] }
599
+ colors = lights.flat_map { |light| [*light[:color].map(&:to_f), 1.0] }
600
+ positions.concat(Array.new((MAX_RAY_LIGHTS - lights.size) * 4, 0.0))
601
+ colors.concat(Array.new((MAX_RAY_LIGHTS - lights.size) * 4, 0.0))
602
+ uniform4fv('light_positions', MAX_RAY_LIGHTS, positions, program: @sprite_program)
603
+ uniform4fv('light_colors', MAX_RAY_LIGHTS, colors, program: @sprite_program)
604
+ super
605
+ ensure
606
+ glUseProgram(0) if @sprite_program
607
+ end
608
+
609
+ def before_sprite_draw(_thing, _sprite, sector)
610
+ uniform1f('sector_light', (sector&.light_level || 128).to_f / 255.0,
611
+ program: @sprite_program)
612
+ end
613
+
614
+ def ensure_ray_target
615
+ return if @ray_framebuffer
616
+
617
+ @ray_target_texture = replace_texture(nil, RAY_WIDTH, RAY_HEIGHT,
618
+ GL_RGBA, GL_UNSIGNED_BYTE, nil)
619
+ ids = [0].pack('L')
620
+ glGenFramebuffers(1, ids)
621
+ @ray_framebuffer = ids.unpack1('L')
622
+ glBindFramebuffer(GL_FRAMEBUFFER, @ray_framebuffer)
623
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
624
+ GL_TEXTURE_2D, @ray_target_texture, 0)
625
+ status = glCheckFramebufferStatus(GL_FRAMEBUFFER)
626
+ raise "ray framebuffer incomplete: 0x#{status.to_s(16)}" unless status == GL_FRAMEBUFFER_COMPLETE
627
+
628
+ glBindFramebuffer(GL_FRAMEBUFFER, 0)
629
+ end
630
+
631
+ def draw_ray_target
632
+ glUseProgram(0)
633
+ glActiveTexture(GL_TEXTURE0)
634
+ glEnable(GL_TEXTURE_2D)
635
+ glBindTexture(GL_TEXTURE_2D, @ray_target_texture)
636
+ glColor3f(1.0, 1.0, 1.0)
637
+ glMatrixMode(GL_PROJECTION)
638
+ glLoadIdentity
639
+ glMatrixMode(GL_MODELVIEW)
640
+ glLoadIdentity
641
+ glBegin(GL_QUADS)
642
+ glTexCoord2f(0.0, 0.0)
643
+ glVertex2f(-1.0, -1.0)
644
+ glTexCoord2f(1.0, 0.0)
645
+ glVertex2f(1.0, -1.0)
646
+ glTexCoord2f(1.0, 1.0)
647
+ glVertex2f(1.0, 1.0)
648
+ glTexCoord2f(0.0, 1.0)
649
+ glVertex2f(-1.0, 1.0)
650
+ glEnd
651
+ end
652
+
653
+ def setup_camera(width, height)
654
+ glEnable(GL_DEPTH_TEST)
655
+ glDepthFunc(GL_LEQUAL)
656
+ glMatrixMode(GL_PROJECTION)
657
+ glLoadIdentity
658
+ aspect = width.to_f / height
659
+ glFrustum(-1.0, 1.0, -1.0 / aspect, 1.0 / aspect, 1.0, 16_384.0)
660
+ glMatrixMode(GL_MODELVIEW)
661
+ glLoadMatrixf(view_matrix.pack('f16'))
662
+ end
663
+
664
+ def draw_ray_pass(width, height)
665
+ # texture_for binds as a side effect. Resolve the sky before assigning
666
+ # fixed sampler units so it cannot replace the material atlas.
667
+ glActiveTexture(GL_TEXTURE0)
668
+ sky_texture = texture_for('SKY1')
669
+ glUseProgram(@ray_program)
670
+ bind_ray_texture(GL_TEXTURE0, @ray_data_texture, 'triangle_data', 0)
671
+ bind_ray_texture(GL_TEXTURE1, @ray_bvh_texture, 'bvh_data', 1)
672
+ bind_ray_texture(GL_TEXTURE2, @ray_atlas_texture, 'material_atlas', 2)
673
+ bind_ray_texture(GL_TEXTURE3, sky_texture, 'sky_texture', 3)
674
+ uniform1f('data_height', @ray_data_height)
675
+ uniform1f('bvh_height', @ray_bvh_height)
676
+ uniform1i('node_count', @ray_bvh.nodes.size)
677
+ uniform1i('fog_enabled', @fog_enabled ? 1 : 0)
678
+ uniform1i('flashlight_enabled', @flashlight_enabled ? 1 : 0)
679
+ uniform1i('bounces_enabled', @bounces_enabled ? 1 : 0)
680
+ uniform3f('camera_position', @player_x, @player_y, @player_z)
681
+ uniform3f('camera_forward', @cos_angle, @sin_angle, 0.0)
682
+ uniform3f('camera_right', @sin_angle, -@cos_angle, 0.0)
683
+ uniform3f('camera_up', 0.0, 0.0, 1.0)
684
+ uniform1f('aspect_ratio', width.to_f / height)
685
+ lights = ray_lights.first(MAX_RAY_LIGHTS)
686
+ uniform1i('light_count', lights.size)
687
+ positions = lights.flat_map { |light| [light[:x].to_f, light[:y].to_f, light[:z].to_f, 1.0] }
688
+ colors = lights.flat_map { |light| [*light[:color].map(&:to_f), 1.0] }
689
+ positions.concat(Array.new((MAX_RAY_LIGHTS - lights.size) * 4, 0.0))
690
+ colors.concat(Array.new((MAX_RAY_LIGHTS - lights.size) * 4, 0.0))
691
+ uniform4fv('light_positions', MAX_RAY_LIGHTS, positions)
692
+ uniform4fv('light_colors', MAX_RAY_LIGHTS, colors)
693
+ glMatrixMode(GL_PROJECTION)
694
+ glLoadIdentity
695
+ glMatrixMode(GL_MODELVIEW)
696
+ glLoadIdentity
697
+ glBegin(GL_QUADS)
698
+ glTexCoord2f(0.0, 0.0)
699
+ glVertex2f(-1.0, -1.0)
700
+ glTexCoord2f(1.0, 0.0)
701
+ glVertex2f(1.0, -1.0)
702
+ glTexCoord2f(1.0, 1.0)
703
+ glVertex2f(1.0, 1.0)
704
+ glTexCoord2f(0.0, 1.0)
705
+ glVertex2f(-1.0, 1.0)
706
+ glEnd
707
+ glUseProgram(0)
708
+ glActiveTexture(GL_TEXTURE0)
709
+ end
710
+
711
+ def build_ray_scene
712
+ @ray_program ||= create_program(VERTEX_SHADER, FRAGMENT_SHADER)
713
+ unless @ray_atlas_texture
714
+ materials = @mesh.triangles.map(&:material).compact
715
+ materials.concat(@ray_materials.animation_names) if @animations
716
+ atlas_builder = RayTracing::TextureAtlas.new(
717
+ size: ATLAS_SIZE, textures: @textures, flats: @flats, palette: @palette
718
+ )
719
+ atlas, @ray_material_rectangles = atlas_builder.build(materials)
720
+ @ray_atlas_texture = replace_texture(nil, ATLAS_SIZE, ATLAS_SIZE,
721
+ GL_RGBA, GL_UNSIGNED_BYTE, atlas)
722
+ end
723
+ source_triangles = @mesh.triangles.first(MAX_TRIANGLES)
724
+ if @ray_bvh&.compatible?(source_triangles)
725
+ @ray_bvh.refit(source_triangles)
726
+ else
727
+ @ray_bvh = RayTracing::Bvh.new(source_triangles, leaf_size: BVH_LEAF_SIZE)
728
+ end
729
+ upload_triangle_data
730
+ upload_bvh
731
+ @ray_scene_dirty = false
732
+ @ray_materials_dirty = false
733
+ end
734
+
735
+ def upload_triangle_data
736
+ rectangles = @ray_material_rectangles
737
+ floats = []
738
+ @ray_bvh.triangles.each do |triangle|
739
+ triangle.vertices.each { |vertex| floats.concat([*vertex, 0.0]) }
740
+ floats.concat([*triangle.normal, @ray_materials.encoded_light(triangle)])
741
+ floats.concat([*triangle.uvs[0], *triangle.uvs[1]])
742
+ material = @ray_materials.resolve(triangle.material)
743
+ rect = rectangles.fetch(material, [0.0, 0.0, 1.0 / ATLAS_SIZE, 1.0 / ATLAS_SIZE, 1.0, 1.0])
744
+ floats.concat([*triangle.uvs[2], rect[2], rect[3]])
745
+ floats.concat([rect[0], rect[1], rect[4], rect[5]])
746
+ end
747
+ texel_count = floats.size / 4
748
+ @ray_data_height = [(texel_count.to_f / DATA_WIDTH).ceil, 1].max
749
+ floats.concat(Array.new((DATA_WIDTH * @ray_data_height * 4) - floats.size, 0.0))
750
+ @ray_data_texture = replace_texture(@ray_data_texture, DATA_WIDTH, @ray_data_height, GL_RGBA32F, GL_FLOAT,
751
+ floats.pack('f*'))
752
+ end
753
+
754
+ def upload_bvh
755
+ floats = @ray_bvh.packed_floats
756
+ texels = floats.size / 4
757
+ @ray_bvh_height = [(texels.to_f / NODE_DATA_WIDTH).ceil, 1].max
758
+ floats.concat(Array.new((NODE_DATA_WIDTH * @ray_bvh_height * 4) - floats.size, 0.0))
759
+ @ray_bvh_texture = replace_texture(@ray_bvh_texture, NODE_DATA_WIDTH, @ray_bvh_height,
760
+ GL_RGBA32F, GL_FLOAT, floats.pack('f*'))
761
+ end
762
+
763
+ # Ceiling flats that are light fixtures rather than plain ceilings. These
764
+ # glow as warm point lights so the lamps dotting the map actually cast
765
+ # light and shadows instead of just being bright textures.
766
+ CEILING_LAMP_PATTERN = /\ATLITE/
767
+
768
+ def ray_lights
769
+ lights = active_lights + acid_lights + ceiling_lamp_lights
770
+ lights.sort_by { |light| ((light[:x] - @player_x)**2) + ((light[:y] - @player_y)**2) }
771
+ end
772
+
773
+ # Approximate RGB light reaching a point: sector ambient, the nearest point
774
+ # lights by distance falloff, and the flashlight (always on a held object
775
+ # right in front of the eye). Used to tint the weapon so lamps and the beam
776
+ # colour the player's hand. Values may exceed 1.0; the caller clamps.
777
+ def light_color_at(x, y, z)
778
+ sector = @map.sector_at(x, y)
779
+ ambient = ((sector&.light_level || 128).to_f / 255.0) * AMBIENT_LEVEL
780
+ color = [ambient, ambient, ambient]
781
+
782
+ ray_lights.first(MAX_RAY_LIGHTS).each do |light|
783
+ dx = light[:x] - x
784
+ dy = light[:y] - y
785
+ dz = light[:z] - z
786
+ distance_sq = (dx * dx) + (dy * dy) + (dz * dz)
787
+ attenuation = 1.0 / (1.0 + (Math.sqrt(distance_sq) * 0.0015) + (distance_sq * 0.000004))
788
+ light[:color].each_with_index { |channel, index| color[index] += channel * attenuation }
789
+ end
790
+
791
+ if @flashlight_enabled
792
+ color[0] += 0.70
793
+ color[1] += 0.62
794
+ color[2] += 0.48
795
+ end
796
+
797
+ color
798
+ end
799
+ # Called from the window to tint the weapon; must be reachable despite
800
+ # sitting among the private light helpers.
801
+ public :light_color_at
802
+
803
+ def acid_lights
804
+ @acid_lights ||= @map.sectors.each_with_index.filter_map do |sector, sector_index|
805
+ next unless @ray_materials.emissive?(sector.floor_texture)
806
+
807
+ points = sector_perimeter_points(sector_index)
808
+ next if points.empty?
809
+
810
+ { x: points.sum(&:x).to_f / points.size,
811
+ y: points.sum(&:y).to_f / points.size,
812
+ z: sector.floor_height.to_f + 18.0,
813
+ color: [0.22, 1.0, 0.18] }
814
+ end
815
+ end
816
+
817
+ def ceiling_lamp_lights
818
+ @ceiling_lamp_lights ||= @map.sectors.each_with_index.filter_map do |sector, sector_index|
819
+ next unless sector.ceiling_texture&.match?(CEILING_LAMP_PATTERN)
820
+
821
+ points = sector_perimeter_points(sector_index)
822
+ next if points.empty?
823
+
824
+ { x: points.sum(&:x).to_f / points.size,
825
+ y: points.sum(&:y).to_f / points.size,
826
+ z: sector.ceiling_height.to_f - 12.0,
827
+ color: lamp_color(sector.ceiling_texture) }
828
+ end
829
+ end
830
+
831
+ # A lamp glows in the hue of its own fixture flat: average the flat's
832
+ # colours and normalise to full brightness, so a red light flat throws red
833
+ # light and a warm one throws warm light. Falls back to warm white.
834
+ def lamp_color(flat_name)
835
+ @lamp_colors ||= {}
836
+ @lamp_colors[flat_name] ||= begin
837
+ flat = @flats[flat_name]
838
+ pixels = flat.respond_to?(:pixels) ? flat.pixels : nil
839
+ if pixels && !pixels.empty?
840
+ sums = pixels.each_with_object([0, 0, 0]) do |index, acc|
841
+ rgb = @palette.colors[index]
842
+ acc[0] += rgb[0]; acc[1] += rgb[1]; acc[2] += rgb[2]
843
+ end
844
+ average = sums.map { |channel| channel.to_f / pixels.size }
845
+ peak = average.max
846
+ peak.positive? ? average.map { |channel| channel / peak } : [1.0, 0.93, 0.78]
847
+ else
848
+ [1.0, 0.93, 0.78]
849
+ end
850
+ end
851
+ end
852
+
853
+ # The distinct vertices of every linedef bordering a sector -- used to find
854
+ # a sector's rough centre for placing a light in it.
855
+ def sector_perimeter_points(sector_index)
856
+ @map.linedefs.flat_map do |line|
857
+ touches = [line.sidedef_right, line.sidedef_left].compact.any? do |side_index|
858
+ side_index >= 0 && @map.sidedefs[side_index]&.sector == sector_index
859
+ end
860
+ touches ? [@map.vertices[line.v1], @map.vertices[line.v2]] : []
861
+ end.uniq { |point| [point.x, point.y] }
862
+ end
863
+
864
+ def replace_texture(old_id, width, height, internal, type, data)
865
+ glDeleteTextures(1, [old_id].pack('L')) if old_id
866
+ ids = [0].pack('L')
867
+ glGenTextures(1, ids)
868
+ id = ids.unpack1('L')
869
+ glBindTexture(GL_TEXTURE_2D, id)
870
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
871
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
872
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
873
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
874
+ glTexImage2D(GL_TEXTURE_2D, 0, internal, width, height, 0, GL_RGBA, type, data)
875
+ id
876
+ end
877
+
878
+ def create_program(vertex_source, fragment_source)
879
+ vertex = compile_shader(GL_VERTEX_SHADER, vertex_source)
880
+ fragment = compile_shader(GL_FRAGMENT_SHADER, fragment_source)
881
+ program = glCreateProgram
882
+ glAttachShader(program, vertex)
883
+ glAttachShader(program, fragment)
884
+ glLinkProgram(program)
885
+ status = [0].pack('L')
886
+ glGetProgramiv(program, GL_LINK_STATUS, status)
887
+ raise "ray shader link failed: #{program_log(program)}" if status.unpack1('L').zero?
888
+
889
+ glDeleteShader(vertex)
890
+ glDeleteShader(fragment)
891
+ program
892
+ end
893
+
894
+ def compile_shader(type, source)
895
+ shader = glCreateShader(type)
896
+ source_pointer = Fiddle::Pointer[source]
897
+ pointer_pointer = Fiddle::Pointer[[source_pointer.to_i].pack('J')]
898
+ length_pointer = Fiddle::Pointer[[source.bytesize].pack('l')]
899
+ glShaderSource(shader, 1, pointer_pointer, length_pointer)
900
+ glCompileShader(shader)
901
+ status = [0].pack('L')
902
+ glGetShaderiv(shader, GL_COMPILE_STATUS, status)
903
+ raise "ray shader compile failed: #{shader_log(shader)}" if status.unpack1('L').zero?
904
+
905
+ shader
906
+ end
907
+
908
+ def shader_log(shader)
909
+ buffer = Fiddle::Pointer.malloc(4096)
910
+ length = Fiddle::Pointer.malloc(4)
911
+ glGetShaderInfoLog(shader, 4096, length, buffer)
912
+ buffer[0, length[0, 4].unpack1('l')]
913
+ end
914
+
915
+ def program_log(program)
916
+ buffer = Fiddle::Pointer.malloc(4096)
917
+ length = Fiddle::Pointer.malloc(4)
918
+ glGetProgramInfoLog(program, 4096, length, buffer)
919
+ buffer[0, length[0, 4].unpack1('l')]
920
+ end
921
+
922
+ def bind_ray_texture(unit, texture, uniform, index)
923
+ glActiveTexture(unit)
924
+ glBindTexture(GL_TEXTURE_2D, texture || 0)
925
+ uniform1i(uniform, index)
926
+ end
927
+
928
+ def uniform1i(name, value, program: @ray_program) = glUniform1i(glGetUniformLocation(program, name), value)
929
+ def uniform1f(name, value, program: @ray_program) = glUniform1f(glGetUniformLocation(program, name), value.to_f)
930
+
931
+ def uniform3f(name, x, y, z, program: @ray_program)
932
+ glUniform3f(glGetUniformLocation(program, name), x.to_f, y.to_f, z.to_f)
933
+ end
934
+
935
+ def uniform4fv(name, count, values, program: @ray_program)
936
+ glUniform4fv(glGetUniformLocation(program, name), count, values.pack('f*'))
937
+ end
938
+ end
939
+ end
940
+ end