quake-rb 0.1.0 → 0.2.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/README.md +136 -0
- data/bin/quake +18 -1
- data/lib/quake/bsp/reader.rb +241 -38
- data/lib/quake/bsp/types.rb +49 -5
- data/lib/quake/bsp/vis.rb +2 -137
- data/lib/quake/camera.rb +73 -16
- data/lib/quake/entity.rb +413 -25
- data/lib/quake/game/brush_entities.rb +1814 -65
- data/lib/quake/game/engine.rb +4376 -57
- data/lib/quake/game/item_pickups.rb +584 -33
- data/lib/quake/game/player_state.rb +518 -21
- data/lib/quake/mdl/reader.rb +88 -7
- data/lib/quake/mdl/types.rb +2 -2
- data/lib/quake/pak/reader.rb +9 -3
- data/lib/quake/palette.rb +3 -4
- data/lib/quake/physics/hull_trace.rb +77 -4
- data/lib/quake/physics/player.rb +409 -112
- data/lib/quake/renderer/anorm_dots.rb +554 -0
- data/lib/quake/renderer/gl_alias_model.rb +418 -69
- data/lib/quake/renderer/gl_brush_model.rb +129 -17
- data/lib/quake/renderer/gl_hud.rb +384 -31
- data/lib/quake/renderer/gl_lightmap.rb +224 -48
- data/lib/quake/renderer/gl_particles.rb +390 -50
- data/lib/quake/renderer/gl_sky.rb +83 -10
- data/lib/quake/renderer/gl_texture_manager.rb +38 -4
- data/lib/quake/renderer/gl_textured.rb +53 -31
- data/lib/quake/renderer/gl_view_blend.rb +130 -0
- data/lib/quake/renderer/gl_viewmodel.rb +46 -11
- data/lib/quake/renderer/gl_warp_subdivision.rb +74 -0
- data/lib/quake/renderer/gl_water.rb +4 -76
- data/lib/quake/sound/events.rb +126 -2
- data/lib/quake/sound/mixer.rb +44 -9
- data/lib/quake/version.rb +1 -1
- data/lib/quake/wad/reader.rb +18 -8
- data/lib/quake/window.rb +3 -0
- metadata +5 -1
|
@@ -11,28 +11,115 @@ module Quake
|
|
|
11
11
|
class GLLightmap
|
|
12
12
|
LIGHTMAP_BLOCK_WIDTH = 128
|
|
13
13
|
LIGHTMAP_BLOCK_HEIGHT = 128
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
MAX_LIGHTSTYLES = 64
|
|
15
|
+
DEFAULT_LIGHTSTYLES = {
|
|
16
|
+
0 => "m",
|
|
17
|
+
1 => "mmnmmommommnonmmonqnmmo",
|
|
18
|
+
2 => "abcdefghijklmnopqrstuvwxyzyxwvutsrqponmlkjihgfedcba",
|
|
19
|
+
3 => "mmmmmaaaaammmmmaaaaaabcdefgabcdefg",
|
|
20
|
+
4 => "mamamamamama",
|
|
21
|
+
5 => "jklmnopqrstuvwxyzyxwvutsrqponmlkj",
|
|
22
|
+
6 => "nmonqnmomnmomomno",
|
|
23
|
+
7 => "mmmaaaabcdefgmmmmaaaammmaamm",
|
|
24
|
+
8 => "mmmaaammmaaammmabcdefaaaammmmabcdefmmmaaaa",
|
|
25
|
+
9 => "aaaaaaaazzzzzzzz",
|
|
26
|
+
10 => "mmamammmmammamamaaamammma",
|
|
27
|
+
11 => "abcdefghijklmnopqrrqponmlkjihgfedcba",
|
|
28
|
+
63 => "a"
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
LightmapInfo = Data.define(:gl_texture, :block_index, :s_offset, :t_offset, :width, :height)
|
|
32
|
+
|
|
33
|
+
attr_reader :face_lightmaps, :lightstyle_values # face_index -> LightmapInfo
|
|
34
|
+
|
|
35
|
+
def initialize(level, palette, lightstyles: nil)
|
|
20
36
|
@level = level
|
|
21
37
|
@palette = palette
|
|
22
38
|
@face_lightmaps = {}
|
|
23
39
|
@block_textures = [] # GL texture ids for each block
|
|
24
40
|
@block_allocated = [] # row allocation per block column
|
|
25
41
|
@block_pixels = [] # RGBA pixel data per block
|
|
42
|
+
@face_lightmap_faces = {}
|
|
43
|
+
@uploaded = false
|
|
44
|
+
@lightstyles = self.class.default_lightstyles
|
|
45
|
+
Array(lightstyles).each_with_index { |map, style| @lightstyles[style] = map if map }
|
|
46
|
+
animate_lightstyles(0.0)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.default_lightstyles
|
|
50
|
+
Array.new(MAX_LIGHTSTYLES) { |style| DEFAULT_LIGHTSTYLES[style] }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.lightstyle_values_for(time, lightstyles)
|
|
54
|
+
frame = (time.to_f * 10.0).to_i
|
|
55
|
+
values = Array.new(MAX_LIGHTSTYLES, 256)
|
|
56
|
+
MAX_LIGHTSTYLES.times do |style|
|
|
57
|
+
map = lightstyles[style]
|
|
58
|
+
next if map.nil? || map.empty?
|
|
59
|
+
|
|
60
|
+
values[style] = (map.getbyte(frame % map.length) - "a".ord) * 22
|
|
61
|
+
end
|
|
62
|
+
values
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def lightstyles=(lightstyles)
|
|
66
|
+
Array(lightstyles).each_with_index { |map, style| @lightstyles[style] = map if style < MAX_LIGHTSTYLES }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def set_lightstyle(style, map)
|
|
70
|
+
return if style.negative? || style >= MAX_LIGHTSTYLES
|
|
71
|
+
|
|
72
|
+
@lightstyles[style] = map
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def animate_lightstyles(time)
|
|
76
|
+
@lightstyle_values = self.class.lightstyle_values_for(time, @lightstyles)
|
|
77
|
+
@lightstyle_values
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def update(time, lightstyles: nil, dynamic_lights: nil)
|
|
81
|
+
self.lightstyles = lightstyles if lightstyles
|
|
82
|
+
old_values = @lightstyle_values || Array.new(MAX_LIGHTSTYLES, 256)
|
|
83
|
+
new_values = animate_lightstyles(time)
|
|
84
|
+
changed_styles = (0...MAX_LIGHTSTYLES).select { |style| old_values[style] != new_values[style] }
|
|
85
|
+
|
|
86
|
+
# R_MarkLights: faces touched by dynamic lights this frame, plus faces
|
|
87
|
+
# lit last frame (which must be re-baked back to their static light).
|
|
88
|
+
dlit_faces = mark_dynamic_faces(Array(dynamic_lights))
|
|
89
|
+
previously_dlit = @dlit_faces || {}
|
|
90
|
+
@dlit_faces = dlit_faces
|
|
91
|
+
return false if changed_styles.empty? && dlit_faces.empty? && previously_dlit.empty?
|
|
92
|
+
|
|
93
|
+
changed_blocks = {}
|
|
94
|
+
@face_lightmap_faces.each do |face_index, face|
|
|
95
|
+
lights = dlit_faces[face_index]
|
|
96
|
+
needs_rebake = lights ||
|
|
97
|
+
previously_dlit.key?(face_index) ||
|
|
98
|
+
(!changed_styles.empty? && face_uses_styles?(face, changed_styles))
|
|
99
|
+
next unless needs_rebake
|
|
100
|
+
|
|
101
|
+
info = @face_lightmaps[face_index]
|
|
102
|
+
next unless info
|
|
103
|
+
|
|
104
|
+
write_face_lightmap(face, info, face_index: face_index, dlights: lights)
|
|
105
|
+
changed_blocks[info.block_index] = true
|
|
106
|
+
end
|
|
107
|
+
upload_changed_blocks(changed_blocks.keys) if @uploaded
|
|
108
|
+
!changed_blocks.empty?
|
|
26
109
|
end
|
|
27
110
|
|
|
28
111
|
def build_all
|
|
112
|
+
animate_lightstyles(0.0) unless @lightstyle_values
|
|
29
113
|
allocate_block
|
|
30
114
|
|
|
31
115
|
@level.faces.each_with_index do |face, face_index|
|
|
32
116
|
texinfo = @level.texinfo[face.texinfo_index]
|
|
33
117
|
next if texinfo.nil?
|
|
34
118
|
next if texinfo.flags & 1 != 0 # TEX_SPECIAL (sky/turb)
|
|
35
|
-
|
|
119
|
+
# Faces with no samples (light_offset < 0) still get a lightmap: all
|
|
120
|
+
# zeros renders them black, matching R_BuildLightMap. Only a missing
|
|
121
|
+
# lighting lump means fullbright.
|
|
122
|
+
next if @level.lighting.nil? || @level.lighting.empty?
|
|
36
123
|
|
|
37
124
|
build_face_lightmap(face, face_index)
|
|
38
125
|
end
|
|
@@ -57,15 +144,16 @@ module Quake
|
|
|
57
144
|
s = vertex.dot(texinfo.s_vec) + texinfo.s_offset
|
|
58
145
|
t = vertex.dot(texinfo.t_vec) + texinfo.t_offset
|
|
59
146
|
|
|
60
|
-
# Convert to lightmap UV: subtract texture mins, scale by 1/16
|
|
61
|
-
#
|
|
147
|
+
# Convert to lightmap UV: subtract texture mins, scale by 1/16.
|
|
148
|
+
# The half-luxel centering is applied once, as the +0.5 texel below
|
|
149
|
+
# (C adds +8 texture units before the /16 -- same thing).
|
|
62
150
|
extents = face_extents(face_index)
|
|
63
151
|
return [0.0, 0.0] unless extents
|
|
64
152
|
|
|
65
153
|
tex_mins_s, tex_mins_t, _, _ = extents
|
|
66
154
|
|
|
67
|
-
ls = (s - tex_mins_s
|
|
68
|
-
lt = (t - tex_mins_t
|
|
155
|
+
ls = (s - tex_mins_s) / (info.width * 16.0)
|
|
156
|
+
lt = (t - tex_mins_t) / (info.height * 16.0)
|
|
69
157
|
|
|
70
158
|
# Map into atlas block coordinates
|
|
71
159
|
atlas_s = (info.s_offset + ls * info.width + 0.5) / LIGHTMAP_BLOCK_WIDTH
|
|
@@ -168,7 +256,7 @@ module Quake
|
|
|
168
256
|
extents = calc_extents(face)
|
|
169
257
|
return unless extents
|
|
170
258
|
|
|
171
|
-
|
|
259
|
+
_tex_min_s, _tex_min_t, extent_s, extent_t = extents
|
|
172
260
|
lm_width = (extent_s / 16) + 1
|
|
173
261
|
lm_height = (extent_t / 16) + 1
|
|
174
262
|
|
|
@@ -178,55 +266,141 @@ module Quake
|
|
|
178
266
|
@extents_cache ||= {}
|
|
179
267
|
@extents_cache[face_index] = extents
|
|
180
268
|
|
|
181
|
-
|
|
182
|
-
|
|
269
|
+
block_idx, s_off, t_off = find_space(lm_width, lm_height)
|
|
270
|
+
info = LightmapInfo.new(
|
|
271
|
+
gl_texture: nil,
|
|
272
|
+
block_index: block_idx,
|
|
273
|
+
s_offset: s_off, t_offset: t_off,
|
|
274
|
+
width: lm_width, height: lm_height
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
@face_lightmaps[face_index] = info
|
|
278
|
+
@face_lightmap_faces[face_index] = face
|
|
279
|
+
write_face_lightmap(face, info, face_index: face_index)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def write_face_lightmap(face, info, face_index: nil, dlights: nil)
|
|
283
|
+
lm_size = info.width * info.height
|
|
183
284
|
lighting = @level.lighting
|
|
184
285
|
offset = face.light_offset
|
|
185
|
-
|
|
186
|
-
# Accumulate light from all styles
|
|
187
286
|
blocklights = Array.new(lm_size, 0)
|
|
188
287
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
# Scale factor: normal light (style 0) = 264 (Quake default)
|
|
194
|
-
# Other styles animated, but default to full brightness
|
|
195
|
-
scale = 256
|
|
288
|
+
if offset >= 0
|
|
289
|
+
4.times do |style_idx|
|
|
290
|
+
style = face.styles[style_idx]
|
|
291
|
+
break if style == 255 # end of styles
|
|
196
292
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
293
|
+
scale = @lightstyle_values.fetch(style, 256)
|
|
294
|
+
lm_size.times do |j|
|
|
295
|
+
byte = lighting.getbyte(offset + j)
|
|
296
|
+
blocklights[j] += byte * scale if byte
|
|
297
|
+
end
|
|
298
|
+
offset += lm_size
|
|
200
299
|
end
|
|
201
|
-
offset += lm_size
|
|
202
300
|
end
|
|
203
301
|
|
|
204
|
-
|
|
205
|
-
block_idx, s_off, t_off = find_space(lm_width, lm_height)
|
|
302
|
+
add_dynamic_lights(face, face_index, info, blocklights, dlights) if dlights && face_index
|
|
206
303
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
# Shift right 7 (overbright by 2x) to match Quake's gamma
|
|
213
|
-
val = (blocklights[src_idx] >> 7).clamp(0, 255)
|
|
304
|
+
pixels = @block_pixels[info.block_index]
|
|
305
|
+
info.height.times do |row|
|
|
306
|
+
info.width.times do |col|
|
|
307
|
+
src_idx = row * info.width + col
|
|
308
|
+
val = 255 - (blocklights[src_idx] >> 7).clamp(0, 255)
|
|
214
309
|
|
|
215
|
-
dst_x =
|
|
216
|
-
dst_y =
|
|
310
|
+
dst_x = info.s_offset + col
|
|
311
|
+
dst_y = info.t_offset + row
|
|
217
312
|
dst_idx = (dst_y * LIGHTMAP_BLOCK_WIDTH + dst_x) * 4
|
|
218
|
-
pixels.setbyte(dst_idx, val)
|
|
219
|
-
pixels.setbyte(dst_idx + 1, val)
|
|
220
|
-
pixels.setbyte(dst_idx + 2, val)
|
|
221
|
-
pixels.setbyte(dst_idx + 3, 255)
|
|
313
|
+
pixels.setbyte(dst_idx + 3, val)
|
|
222
314
|
end
|
|
223
315
|
end
|
|
316
|
+
end
|
|
224
317
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
318
|
+
# R_MarkLights: walk the BSP tree per light, collecting faces on nodes
|
|
319
|
+
# the light sphere straddles. Returns face_index -> [lights].
|
|
320
|
+
def mark_dynamic_faces(dlights)
|
|
321
|
+
return {} if dlights.empty? || @level.nodes.empty? || @level.planes.empty?
|
|
322
|
+
|
|
323
|
+
marked = {}
|
|
324
|
+
dlights.each { |light| mark_light(light, 0, marked) }
|
|
325
|
+
marked
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def mark_light(light, node_index, marked)
|
|
329
|
+
return if node_index.negative? # leaf
|
|
330
|
+
|
|
331
|
+
node = @level.nodes[node_index]
|
|
332
|
+
return unless node
|
|
333
|
+
|
|
334
|
+
plane = @level.planes[node.plane_index]
|
|
335
|
+
return unless plane
|
|
336
|
+
|
|
337
|
+
radius = light.radius.to_f
|
|
338
|
+
dist = light.origin.dot(plane.normal) - plane.dist
|
|
339
|
+
return mark_light(light, node.children[0], marked) if dist > radius
|
|
340
|
+
return mark_light(light, node.children[1], marked) if dist < -radius
|
|
341
|
+
|
|
342
|
+
node.num_faces.times do |i|
|
|
343
|
+
face_index = node.first_face + i
|
|
344
|
+
(marked[face_index] ||= []) << light
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
mark_light(light, node.children[0], marked)
|
|
348
|
+
mark_light(light, node.children[1], marked)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# R_AddDynamicLights: project each light onto the face plane and add
|
|
352
|
+
# (rad - dist) * 256 per luxel inside the light's falloff.
|
|
353
|
+
def add_dynamic_lights(face, face_index, info, blocklights, dlights)
|
|
354
|
+
extents = @extents_cache&.[](face_index)
|
|
355
|
+
return unless extents
|
|
356
|
+
|
|
357
|
+
tex_mins_s, tex_mins_t, = extents
|
|
358
|
+
texinfo = @level.texinfo[face.texinfo_index]
|
|
359
|
+
plane = @level.planes[face.plane_index]
|
|
360
|
+
return unless texinfo && plane
|
|
361
|
+
|
|
362
|
+
smax = info.width
|
|
363
|
+
tmax = info.height
|
|
364
|
+
|
|
365
|
+
dlights.each do |light|
|
|
366
|
+
rad = light.radius.to_f
|
|
367
|
+
dist = light.origin.dot(plane.normal) - plane.dist
|
|
368
|
+
rad -= dist.abs
|
|
369
|
+
minlight = light.minlight.to_f
|
|
370
|
+
next if rad < minlight
|
|
371
|
+
|
|
372
|
+
minlight = rad - minlight
|
|
373
|
+
impact = light.origin - plane.normal * dist
|
|
374
|
+
local_s = impact.dot(texinfo.s_vec) + texinfo.s_offset - tex_mins_s
|
|
375
|
+
local_t = impact.dot(texinfo.t_vec) + texinfo.t_offset - tex_mins_t
|
|
376
|
+
|
|
377
|
+
tmax.times do |t|
|
|
378
|
+
td = (local_t - t * 16).abs
|
|
379
|
+
smax.times do |s|
|
|
380
|
+
sd = (local_s - s * 16).abs
|
|
381
|
+
d = sd > td ? sd + td / 2 : td + sd / 2
|
|
382
|
+
blocklights[t * smax + s] += ((rad - d) * 256).to_i if d < minlight
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def face_uses_styles?(face, changed_styles)
|
|
389
|
+
face.styles.any? { |style| style != 255 && changed_styles.include?(style) }
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def upload_changed_blocks(block_indices)
|
|
393
|
+
block_indices.each do |block_index|
|
|
394
|
+
info = @face_lightmaps.values.find { |face_info| face_info.block_index == block_index }
|
|
395
|
+
next unless info&.gl_texture
|
|
396
|
+
|
|
397
|
+
GL.BindTexture(GL::TEXTURE_2D, info.gl_texture)
|
|
398
|
+
GL.TexSubImage2D(
|
|
399
|
+
GL::TEXTURE_2D, 0, 0, 0,
|
|
400
|
+
LIGHTMAP_BLOCK_WIDTH, LIGHTMAP_BLOCK_HEIGHT,
|
|
401
|
+
GL::RGBA, GL::UNSIGNED_BYTE, @block_pixels[block_index]
|
|
402
|
+
)
|
|
403
|
+
end
|
|
230
404
|
end
|
|
231
405
|
|
|
232
406
|
def upload_blocks
|
|
@@ -250,11 +424,13 @@ module Quake
|
|
|
250
424
|
# Update face lightmap infos with actual GL texture ids
|
|
251
425
|
@face_lightmaps.each do |face_index, info|
|
|
252
426
|
@face_lightmaps[face_index] = LightmapInfo.new(
|
|
253
|
-
gl_texture: gl_ids[info.
|
|
427
|
+
gl_texture: gl_ids[info.block_index],
|
|
428
|
+
block_index: info.block_index,
|
|
254
429
|
s_offset: info.s_offset, t_offset: info.t_offset,
|
|
255
430
|
width: info.width, height: info.height
|
|
256
431
|
)
|
|
257
432
|
end
|
|
433
|
+
@uploaded = true
|
|
258
434
|
end
|
|
259
435
|
end
|
|
260
436
|
end
|