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.
- checksums.yaml +4 -4
- data/LICENSE +17 -0
- data/README.md +65 -4
- data/bin/doom +138 -25
- data/lib/doom/benchmark.rb +282 -0
- data/lib/doom/game/animations.rb +9 -2
- data/lib/doom/game/combat.rb +377 -105
- data/lib/doom/game/framebuffer_blitter.rb +38 -0
- data/lib/doom/game/geometry.rb +68 -0
- data/lib/doom/game/intermission.rb +231 -0
- data/lib/doom/game/item_pickup.rb +108 -60
- data/lib/doom/game/menu.rb +347 -0
- data/lib/doom/game/monster_ai.rb +304 -91
- data/lib/doom/game/player.rb +105 -0
- data/lib/doom/game/player_physics.rb +384 -0
- data/lib/doom/game/player_state.rb +47 -64
- data/lib/doom/game/random.rb +82 -0
- data/lib/doom/game/sector_actions.rb +466 -29
- data/lib/doom/game/sector_effects.rb +25 -18
- data/lib/doom/game/snapshot.rb +584 -0
- data/lib/doom/game/sound_engine.rb +176 -0
- data/lib/doom/game/state_hash.rb +125 -0
- data/lib/doom/game/ticcmd.rb +61 -0
- data/lib/doom/game/world.rb +420 -0
- data/lib/doom/map/data.rb +95 -0
- data/lib/doom/net/client.rb +204 -0
- data/lib/doom/net/desync_monitor.rb +101 -0
- data/lib/doom/net/game_server.rb +232 -0
- data/lib/doom/net/lockstep.rb +170 -0
- data/lib/doom/net/protocol.rb +350 -0
- data/lib/doom/net/session.rb +282 -0
- data/lib/doom/net/transport.rb +110 -0
- data/lib/doom/platform/gosu_window.rb +820 -640
- data/lib/doom/platform/sdl.rb +74 -0
- data/lib/doom/platform/window_logic.rb +61 -0
- data/lib/doom/render/font.rb +80 -0
- data/lib/doom/render/hardware_renderer.rb +547 -0
- data/lib/doom/render/ray_tracing/bvh.rb +103 -0
- data/lib/doom/render/ray_tracing/material_state.rb +66 -0
- data/lib/doom/render/ray_tracing/texture_atlas.rb +78 -0
- data/lib/doom/render/ray_tracing_renderer.rb +940 -0
- data/lib/doom/render/renderer.rb +684 -330
- data/lib/doom/render/renderer_factory.rb +50 -0
- data/lib/doom/render/screen_melt.rb +71 -0
- data/lib/doom/render/spinel_native/kernel.rb +780 -0
- data/lib/doom/render/spinel_native_renderer.rb +162 -0
- data/lib/doom/render/status_bar.rb +14 -10
- data/lib/doom/render/weapon_renderer.rb +15 -17
- data/lib/doom/render/world_mesh.rb +270 -0
- data/lib/doom/render/zbuffer_renderer.rb +151 -0
- data/lib/doom/version.rb +1 -1
- data/lib/doom/wad/flat.rb +1 -1
- data/lib/doom/wad/hud_graphics.rb +7 -3
- data/lib/doom/wad/palette.rb +3 -3
- data/lib/doom/wad/patch.rb +1 -1
- data/lib/doom/wad/reader.rb +59 -10
- data/lib/doom/wad/sound.rb +93 -0
- data/lib/doom/wad/sprite.rb +60 -34
- data/lib/doom/wad/texture.rb +5 -5
- data/lib/doom/wad_downloader.rb +38 -56
- data/lib/doom.rb +235 -12
- 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
|