quake-rb 0.1.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 +7 -0
- data/bin/quake +143 -0
- data/bin/quake-debug +83 -0
- data/lib/quake/bsp/face_vertices.rb +63 -0
- data/lib/quake/bsp/reader.rb +264 -0
- data/lib/quake/bsp/types.rb +30 -0
- data/lib/quake/bsp/vis.rb +246 -0
- data/lib/quake/camera.rb +99 -0
- data/lib/quake/debug/png_writer.rb +58 -0
- data/lib/quake/debug/screenshot.rb +26 -0
- data/lib/quake/debug/script.rb +179 -0
- data/lib/quake/entity.rb +116 -0
- data/lib/quake/game/brush_entities.rb +361 -0
- data/lib/quake/game/engine.rb +300 -0
- data/lib/quake/game/item_pickups.rb +137 -0
- data/lib/quake/game/player_state.rb +158 -0
- data/lib/quake/math/vec3.rb +35 -0
- data/lib/quake/mdl/reader.rb +176 -0
- data/lib/quake/mdl/types.rb +30 -0
- data/lib/quake/pak/reader.rb +57 -0
- data/lib/quake/pak_downloader.rb +145 -0
- data/lib/quake/palette.rb +32 -0
- data/lib/quake/physics/hull_trace.rb +193 -0
- data/lib/quake/physics/player.rb +357 -0
- data/lib/quake/renderer/gl_alias_model.rb +122 -0
- data/lib/quake/renderer/gl_brush_model.rb +162 -0
- data/lib/quake/renderer/gl_hud.rb +226 -0
- data/lib/quake/renderer/gl_lightmap.rb +261 -0
- data/lib/quake/renderer/gl_particles.rb +173 -0
- data/lib/quake/renderer/gl_sky.rb +166 -0
- data/lib/quake/renderer/gl_texture_manager.rb +54 -0
- data/lib/quake/renderer/gl_textured.rb +224 -0
- data/lib/quake/renderer/gl_viewmodel.rb +109 -0
- data/lib/quake/renderer/gl_water.rb +200 -0
- data/lib/quake/renderer/gl_wireframe.rb +36 -0
- data/lib/quake/sound/events.rb +58 -0
- data/lib/quake/sound/mixer.rb +105 -0
- data/lib/quake/version.rb +5 -0
- data/lib/quake/wad/reader.rb +69 -0
- data/lib/quake/window.rb +74 -0
- data/lib/quake.rb +19 -0
- metadata +140 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quake
|
|
4
|
+
module Physics
|
|
5
|
+
# Full Quake player physics: gravity, friction, acceleration, jumping,
|
|
6
|
+
# ground detection, stair stepping, and swimming.
|
|
7
|
+
class Player
|
|
8
|
+
attr_accessor :position, :velocity, :on_ground, :water_level, :noclip
|
|
9
|
+
attr_reader :yaw, :pitch
|
|
10
|
+
|
|
11
|
+
# Quake constants
|
|
12
|
+
GRAVITY = 800.0 # units/sec^2
|
|
13
|
+
FRICTION = 4.0
|
|
14
|
+
STOP_SPEED = 100.0
|
|
15
|
+
MAX_SPEED = 320.0
|
|
16
|
+
ACCELERATE = 10.0
|
|
17
|
+
AIR_ACCELERATE = 0.7
|
|
18
|
+
JUMP_SPEED = 270.0
|
|
19
|
+
STEP_SIZE = 18.0
|
|
20
|
+
WATER_FRICTION = 1.0
|
|
21
|
+
WATER_ACCELERATE = 10.0
|
|
22
|
+
|
|
23
|
+
# Camera
|
|
24
|
+
SENSITIVITY = 0.15
|
|
25
|
+
MAX_MOUSE_DELTA = 50
|
|
26
|
+
VIEW_HEIGHT = 22.0 # eye offset from origin (origin is at player feet center)
|
|
27
|
+
|
|
28
|
+
# Ground: surface normal Z must be > 0.7 (roughly < 45 degrees from horizontal)
|
|
29
|
+
MIN_GROUND_NORMAL_Z = 0.7
|
|
30
|
+
|
|
31
|
+
def initialize(position:, yaw: 0.0)
|
|
32
|
+
@position = position
|
|
33
|
+
@velocity = Math::Vec3::ORIGIN
|
|
34
|
+
@yaw = yaw
|
|
35
|
+
@pitch = 0.0
|
|
36
|
+
@on_ground = false
|
|
37
|
+
@water_level = 0 # 0=dry, 1=feet, 2=waist, 3=head
|
|
38
|
+
@jump_held = false
|
|
39
|
+
@noclip = false
|
|
40
|
+
@ignore_mouse = 2
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Camera eye position (origin + view height)
|
|
44
|
+
def eye_position
|
|
45
|
+
Math::Vec3.new(@position.x, @position.y, @position.z + VIEW_HEIGHT)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def rotate(dx, dy)
|
|
49
|
+
if @ignore_mouse > 0
|
|
50
|
+
@ignore_mouse -= 1
|
|
51
|
+
return
|
|
52
|
+
end
|
|
53
|
+
dx = dx.clamp(-MAX_MOUSE_DELTA, MAX_MOUSE_DELTA)
|
|
54
|
+
dy = dy.clamp(-MAX_MOUSE_DELTA, MAX_MOUSE_DELTA)
|
|
55
|
+
@yaw -= dx * SENSITIVITY
|
|
56
|
+
@pitch += dy * SENSITIVITY
|
|
57
|
+
@pitch = @pitch.clamp(-89.0, 89.0)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def update(dt, level, keys, brush_entities: nil)
|
|
61
|
+
if @noclip
|
|
62
|
+
noclip_move(dt, keys)
|
|
63
|
+
return
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
@brush_entities = brush_entities
|
|
67
|
+
categorize_position(level)
|
|
68
|
+
|
|
69
|
+
# Calculate wish direction from input
|
|
70
|
+
wish_dir, wish_speed = compute_wish_velocity(keys)
|
|
71
|
+
|
|
72
|
+
if @water_level >= 2
|
|
73
|
+
water_move(dt, wish_dir, wish_speed, keys, level)
|
|
74
|
+
else
|
|
75
|
+
# Apply gravity if not on ground and not in water
|
|
76
|
+
unless @on_ground
|
|
77
|
+
@velocity = Math::Vec3.new(@velocity.x, @velocity.y,
|
|
78
|
+
@velocity.z - GRAVITY * dt)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Handle jumping (SET vz, don't add - matches Quake's PM_AirMove)
|
|
82
|
+
if @on_ground && keys[SDL::SCANCODE_SPACE] && !@jump_held
|
|
83
|
+
@velocity = Math::Vec3.new(@velocity.x, @velocity.y, JUMP_SPEED)
|
|
84
|
+
@on_ground = false
|
|
85
|
+
@jump_held = true
|
|
86
|
+
end
|
|
87
|
+
@jump_held = false unless keys[SDL::SCANCODE_SPACE]
|
|
88
|
+
|
|
89
|
+
if @on_ground
|
|
90
|
+
apply_friction(dt, level)
|
|
91
|
+
accelerate(wish_dir, wish_speed, ACCELERATE, dt)
|
|
92
|
+
else
|
|
93
|
+
air_accelerate(wish_dir, wish_speed, dt)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Move with collision
|
|
98
|
+
walk_move(dt, level)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Forward direction (horizontal only, for movement)
|
|
102
|
+
def forward_flat
|
|
103
|
+
ry = deg2rad(@yaw)
|
|
104
|
+
Math::Vec3.new(::Math.cos(ry), ::Math.sin(ry), 0.0)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def right_flat
|
|
108
|
+
ry = deg2rad(@yaw - 90.0)
|
|
109
|
+
Math::Vec3.new(::Math.cos(ry), ::Math.sin(ry), 0.0)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Full forward including pitch (for camera)
|
|
113
|
+
def forward
|
|
114
|
+
ry = deg2rad(@yaw)
|
|
115
|
+
rp = deg2rad(@pitch)
|
|
116
|
+
cp = ::Math.cos(rp)
|
|
117
|
+
Math::Vec3.new(::Math.cos(ry) * cp, ::Math.sin(ry) * cp, -::Math.sin(rp))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def right
|
|
121
|
+
ry = deg2rad(@yaw - 90.0)
|
|
122
|
+
Math::Vec3.new(::Math.cos(ry), ::Math.sin(ry), 0.0)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
def deg2rad(deg)
|
|
128
|
+
deg * ::Math::PI / 180.0
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def compute_wish_velocity(keys)
|
|
132
|
+
wish = Math::Vec3::ORIGIN
|
|
133
|
+
wish = wish + forward_flat if keys[SDL::SCANCODE_W]
|
|
134
|
+
wish = wish - forward_flat if keys[SDL::SCANCODE_S]
|
|
135
|
+
wish = wish + right_flat if keys[SDL::SCANCODE_D]
|
|
136
|
+
wish = wish - right_flat if keys[SDL::SCANCODE_A]
|
|
137
|
+
|
|
138
|
+
len = wish.length
|
|
139
|
+
return [Math::Vec3::ORIGIN, 0.0] if len < 0.001
|
|
140
|
+
|
|
141
|
+
[wish.normalize, MAX_SPEED]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def apply_friction(dt, level)
|
|
145
|
+
speed = ::Math.sqrt(@velocity.x**2 + @velocity.y**2)
|
|
146
|
+
return if speed < 1.0
|
|
147
|
+
|
|
148
|
+
control = [speed, STOP_SPEED].max
|
|
149
|
+
drop = control * FRICTION * dt
|
|
150
|
+
|
|
151
|
+
new_speed = [speed - drop, 0.0].max / speed
|
|
152
|
+
@velocity = Math::Vec3.new(
|
|
153
|
+
@velocity.x * new_speed,
|
|
154
|
+
@velocity.y * new_speed,
|
|
155
|
+
@velocity.z
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def accelerate(wish_dir, wish_speed, accel, dt)
|
|
160
|
+
current_speed = @velocity.dot(wish_dir)
|
|
161
|
+
add_speed = wish_speed - current_speed
|
|
162
|
+
return if add_speed <= 0
|
|
163
|
+
|
|
164
|
+
accel_speed = [accel * dt * wish_speed, add_speed].min
|
|
165
|
+
@velocity = @velocity + wish_dir * accel_speed
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def air_accelerate(wish_dir, wish_speed, dt)
|
|
169
|
+
current_speed = @velocity.dot(wish_dir)
|
|
170
|
+
wish_spd = [wish_speed, 30.0].min # air strafe cap
|
|
171
|
+
add_speed = wish_spd - current_speed
|
|
172
|
+
return if add_speed <= 0
|
|
173
|
+
|
|
174
|
+
accel_speed = [AIR_ACCELERATE * wish_speed * dt, add_speed].min
|
|
175
|
+
@velocity = @velocity + wish_dir * accel_speed
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def water_move(dt, wish_dir, wish_speed, keys, level)
|
|
179
|
+
# Water friction
|
|
180
|
+
speed = @velocity.length
|
|
181
|
+
if speed > 0
|
|
182
|
+
new_speed = [speed - dt * speed * WATER_FRICTION * @water_level, 0.0].max
|
|
183
|
+
@velocity = @velocity * (new_speed / speed)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Build wish velocity from horizontal input + vertical keys
|
|
187
|
+
has_horizontal = wish_speed > 0.001
|
|
188
|
+
has_up = keys[SDL::SCANCODE_SPACE]
|
|
189
|
+
has_down = keys[SDL::SCANCODE_C]
|
|
190
|
+
|
|
191
|
+
# Use forward direction including pitch when swimming
|
|
192
|
+
swim_wish = Math::Vec3::ORIGIN
|
|
193
|
+
if keys[SDL::SCANCODE_W]
|
|
194
|
+
swim_wish = swim_wish + forward
|
|
195
|
+
end
|
|
196
|
+
if keys[SDL::SCANCODE_S]
|
|
197
|
+
swim_wish = swim_wish - forward
|
|
198
|
+
end
|
|
199
|
+
if keys[SDL::SCANCODE_D]
|
|
200
|
+
swim_wish = swim_wish + right
|
|
201
|
+
end
|
|
202
|
+
if keys[SDL::SCANCODE_A]
|
|
203
|
+
swim_wish = swim_wish - right
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Vertical movement
|
|
207
|
+
if has_up
|
|
208
|
+
swim_wish = Math::Vec3.new(swim_wish.x, swim_wish.y, swim_wish.z + 1.0)
|
|
209
|
+
elsif has_down
|
|
210
|
+
swim_wish = Math::Vec3.new(swim_wish.x, swim_wish.y, swim_wish.z - 1.0)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
swim_len = swim_wish.length
|
|
214
|
+
if swim_len < 0.001
|
|
215
|
+
# No input at all: drift down slowly
|
|
216
|
+
@velocity = Math::Vec3.new(@velocity.x, @velocity.y, @velocity.z - 60.0 * dt)
|
|
217
|
+
return
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
wish_dir2 = swim_wish.normalize
|
|
221
|
+
wish_spd = MAX_SPEED * 0.7 # water reduces max speed to 70%
|
|
222
|
+
accelerate(wish_dir2, wish_spd, WATER_ACCELERATE, dt)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def categorize_position(level)
|
|
226
|
+
# Check ground: trace 1 unit down against world + brush entities
|
|
227
|
+
if @velocity.z > 180
|
|
228
|
+
@on_ground = false
|
|
229
|
+
else
|
|
230
|
+
trace_start = @position
|
|
231
|
+
trace_end = Math::Vec3.new(@position.x, @position.y, @position.z - 1.0)
|
|
232
|
+
result = HullTrace.trace_world_and_entities(
|
|
233
|
+
level, trace_start, trace_end, @brush_entities
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
if result.fraction < 1.0 && result.plane_normal &&
|
|
237
|
+
result.plane_normal.z >= MIN_GROUND_NORMAL_Z
|
|
238
|
+
@on_ground = true
|
|
239
|
+
@position = result.end_pos if !result.start_solid && !result.all_solid
|
|
240
|
+
# Zero out negative z velocity when grounded (matches Quake's
|
|
241
|
+
# PM_CategorizePosition: pmove.velocity[2] = 0)
|
|
242
|
+
if @velocity.z < 0
|
|
243
|
+
@velocity = Math::Vec3.new(@velocity.x, @velocity.y, 0.0)
|
|
244
|
+
end
|
|
245
|
+
else
|
|
246
|
+
@on_ground = false
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Check water level using BSP leaf contents (not clipnodes).
|
|
251
|
+
# Hull 1 clipnodes only track solid/empty; water content is in the
|
|
252
|
+
# BSP node/leaf tree which point_in_leaf walks.
|
|
253
|
+
@water_level = 0
|
|
254
|
+
@water_type = CONTENTS_EMPTY
|
|
255
|
+
|
|
256
|
+
# Feet check (player mins z = -24)
|
|
257
|
+
feet_pos = Math::Vec3.new(@position.x, @position.y, @position.z - 23)
|
|
258
|
+
contents = leaf_contents(level, feet_pos)
|
|
259
|
+
if contents <= CONTENTS_WATER
|
|
260
|
+
@water_type = contents
|
|
261
|
+
@water_level = 1
|
|
262
|
+
# Waist check
|
|
263
|
+
mid_pos = Math::Vec3.new(@position.x, @position.y, @position.z + 4)
|
|
264
|
+
if leaf_contents(level, mid_pos) <= CONTENTS_WATER
|
|
265
|
+
@water_level = 2
|
|
266
|
+
# Head check
|
|
267
|
+
head_pos = Math::Vec3.new(@position.x, @position.y, @position.z + VIEW_HEIGHT)
|
|
268
|
+
@water_level = 3 if leaf_contents(level, head_pos) <= CONTENTS_WATER
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Get the content type at a point using the BSP node/leaf tree.
|
|
274
|
+
# This correctly detects water, slime, lava (unlike clipnodes).
|
|
275
|
+
def leaf_contents(level, point)
|
|
276
|
+
leaf_idx = Bsp::Vis.point_in_leaf(level, point)
|
|
277
|
+
leaf = level.leafs[leaf_idx]
|
|
278
|
+
leaf ? leaf.contents : CONTENTS_EMPTY
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def walk_move(dt, level)
|
|
282
|
+
# Desired new position
|
|
283
|
+
desired = @position + @velocity * dt
|
|
284
|
+
|
|
285
|
+
# Trace against world + brush entities
|
|
286
|
+
result = trace_move(level, @position, desired)
|
|
287
|
+
|
|
288
|
+
return if result.all_solid # stuck
|
|
289
|
+
|
|
290
|
+
if result.fraction == 1.0
|
|
291
|
+
@position = desired
|
|
292
|
+
return
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Hit something. Try stair stepping.
|
|
296
|
+
contact = result.end_pos
|
|
297
|
+
|
|
298
|
+
# Step up
|
|
299
|
+
step_up = Math::Vec3.new(contact.x, contact.y, contact.z + STEP_SIZE)
|
|
300
|
+
up_trace = trace_move(level, contact, step_up)
|
|
301
|
+
raised = up_trace.end_pos
|
|
302
|
+
|
|
303
|
+
# Move forward from raised position
|
|
304
|
+
step_forward = Math::Vec3.new(desired.x, desired.y, raised.z)
|
|
305
|
+
forward_trace = trace_move(level, raised, step_forward)
|
|
306
|
+
forward_pos = forward_trace.end_pos
|
|
307
|
+
|
|
308
|
+
# Step back down
|
|
309
|
+
step_down = Math::Vec3.new(forward_pos.x, forward_pos.y, forward_pos.z - STEP_SIZE)
|
|
310
|
+
down_trace = trace_move(level, forward_pos, step_down)
|
|
311
|
+
|
|
312
|
+
if down_trace.fraction < 1.0 && down_trace.plane_normal &&
|
|
313
|
+
down_trace.plane_normal.z >= MIN_GROUND_NORMAL_Z
|
|
314
|
+
@position = down_trace.end_pos
|
|
315
|
+
return
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Step didn't work - slide along the wall
|
|
319
|
+
@position = contact
|
|
320
|
+
slide_along_wall(result, dt, level)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def trace_move(level, from, to)
|
|
324
|
+
HullTrace.trace_world_and_entities(level, from, to, @brush_entities)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def slide_along_wall(trace_result, dt, level)
|
|
328
|
+
normal = trace_result.plane_normal
|
|
329
|
+
return unless normal
|
|
330
|
+
|
|
331
|
+
backoff = @velocity.dot(normal)
|
|
332
|
+
@velocity = Math::Vec3.new(
|
|
333
|
+
@velocity.x - normal.x * backoff,
|
|
334
|
+
@velocity.y - normal.y * backoff,
|
|
335
|
+
@velocity.z - normal.z * backoff
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
remaining = @velocity * (dt * (1.0 - trace_result.fraction))
|
|
339
|
+
desired = @position + remaining
|
|
340
|
+
result = trace_move(level, @position, desired)
|
|
341
|
+
@position = result.end_pos
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def noclip_move(dt, keys)
|
|
345
|
+
wish = Math::Vec3::ORIGIN
|
|
346
|
+
wish = wish + forward if keys[SDL::SCANCODE_W]
|
|
347
|
+
wish = wish - forward if keys[SDL::SCANCODE_S]
|
|
348
|
+
wish = wish + right if keys[SDL::SCANCODE_D]
|
|
349
|
+
wish = wish - right if keys[SDL::SCANCODE_A]
|
|
350
|
+
wish = wish + Math::Vec3.new(0.0, 0.0, 1.0) if keys[SDL::SCANCODE_SPACE]
|
|
351
|
+
wish = wish - Math::Vec3.new(0.0, 0.0, 1.0) if keys[SDL::SCANCODE_C]
|
|
352
|
+
|
|
353
|
+
@position = @position + wish * (MAX_SPEED * dt)
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "opengl"
|
|
4
|
+
|
|
5
|
+
module Quake
|
|
6
|
+
module Renderer
|
|
7
|
+
# Renders Quake MDL (alias) models with texture mapping and frame animation.
|
|
8
|
+
class GLAliasModel
|
|
9
|
+
# Quake's pre-computed normal table (162 normals for lighting)
|
|
10
|
+
# Simplified to just the first few key directions for now
|
|
11
|
+
ANORMS = nil # Full table would be loaded from anorms.h
|
|
12
|
+
|
|
13
|
+
def initialize(model, palette)
|
|
14
|
+
@model = model
|
|
15
|
+
@palette = palette
|
|
16
|
+
@skin_textures = []
|
|
17
|
+
upload_skins
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Render the model at a given frame (interpolation between two frames).
|
|
21
|
+
# frame_index: current frame number
|
|
22
|
+
# lerp: 0.0-1.0 interpolation factor to next frame
|
|
23
|
+
# position: Vec3 world position
|
|
24
|
+
# yaw: rotation angle in degrees
|
|
25
|
+
def render(frame_index:, lerp: 0.0, position: Math::Vec3::ORIGIN, yaw: 0.0, pitch: 0.0, scale: 1.0)
|
|
26
|
+
frame = resolve_frame(frame_index)
|
|
27
|
+
next_frame = resolve_frame(frame_index + 1)
|
|
28
|
+
|
|
29
|
+
GL.PushMatrix
|
|
30
|
+
GL.Translatef(position.x, position.y, position.z)
|
|
31
|
+
GL.Rotatef(yaw, 0.0, 0.0, 1.0)
|
|
32
|
+
GL.Rotatef(pitch, 0.0, 1.0, 0.0)
|
|
33
|
+
|
|
34
|
+
GL.Enable(GL::TEXTURE_2D)
|
|
35
|
+
GL.BindTexture(GL::TEXTURE_2D, @skin_textures[0]) if @skin_textures.any?
|
|
36
|
+
|
|
37
|
+
GL.Begin(GL::TRIANGLES)
|
|
38
|
+
@model.triangles.each do |tri|
|
|
39
|
+
tri.vertex_indices.each_with_index do |vi, _ti|
|
|
40
|
+
# Texture coordinates
|
|
41
|
+
stvert = @model.stverts[vi]
|
|
42
|
+
s = stvert.s.to_f
|
|
43
|
+
t = stvert.t.to_f
|
|
44
|
+
|
|
45
|
+
# Adjust seam UVs for back-facing triangles
|
|
46
|
+
if stvert.on_seam != 0 && tri.faces_front == 0
|
|
47
|
+
s += @model.skin_width * 0.5
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
s = (s + 0.5) / @model.skin_width
|
|
51
|
+
t = (t + 0.5) / @model.skin_height
|
|
52
|
+
GL.TexCoord2f(s, t)
|
|
53
|
+
|
|
54
|
+
# Decompress vertex position
|
|
55
|
+
v1 = frame.vertices[vi]
|
|
56
|
+
x1 = v1.x * @model.scale.x + @model.scale_origin.x
|
|
57
|
+
y1 = v1.y * @model.scale.y + @model.scale_origin.y
|
|
58
|
+
z1 = v1.z * @model.scale.z + @model.scale_origin.z
|
|
59
|
+
|
|
60
|
+
if lerp > 0.0 && next_frame
|
|
61
|
+
v2 = next_frame.vertices[vi]
|
|
62
|
+
x2 = v2.x * @model.scale.x + @model.scale_origin.x
|
|
63
|
+
y2 = v2.y * @model.scale.y + @model.scale_origin.y
|
|
64
|
+
z2 = v2.z * @model.scale.z + @model.scale_origin.z
|
|
65
|
+
|
|
66
|
+
x = x1 + (x2 - x1) * lerp
|
|
67
|
+
y = y1 + (y2 - y1) * lerp
|
|
68
|
+
z = z1 + (z2 - z1) * lerp
|
|
69
|
+
else
|
|
70
|
+
x = x1
|
|
71
|
+
y = y1
|
|
72
|
+
z = z1
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
GL.Vertex3f(x * scale, y * scale, z * scale)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
GL.End
|
|
79
|
+
|
|
80
|
+
GL.PopMatrix
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def frame_count
|
|
84
|
+
@model.frames.size
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def resolve_frame(index)
|
|
90
|
+
return nil if @model.frames.empty?
|
|
91
|
+
entry = @model.frames[index % @model.frames.size]
|
|
92
|
+
case entry
|
|
93
|
+
when Mdl::Frame then entry
|
|
94
|
+
when Mdl::FrameGroup then entry.frames[0] # first frame of group
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def upload_skins
|
|
99
|
+
@model.skins.each do |skin_variants|
|
|
100
|
+
pixels = skin_variants[0] # use first variant
|
|
101
|
+
next if pixels.nil?
|
|
102
|
+
|
|
103
|
+
rgba = @palette.indexed_to_rgba(pixels)
|
|
104
|
+
|
|
105
|
+
buf = "\0" * 4
|
|
106
|
+
GL.GenTextures(1, buf)
|
|
107
|
+
tex_id = buf.unpack1("V")
|
|
108
|
+
|
|
109
|
+
GL.BindTexture(GL::TEXTURE_2D, tex_id)
|
|
110
|
+
GL.TexImage2D(GL::TEXTURE_2D, 0, GL::RGBA,
|
|
111
|
+
@model.skin_width, @model.skin_height, 0,
|
|
112
|
+
GL::RGBA, GL::UNSIGNED_BYTE, rgba)
|
|
113
|
+
GL.TexParameteri(GL::TEXTURE_2D, GL::TEXTURE_MIN_FILTER, GL::LINEAR_MIPMAP_LINEAR)
|
|
114
|
+
GL.TexParameteri(GL::TEXTURE_2D, GL::TEXTURE_MAG_FILTER, GL::LINEAR)
|
|
115
|
+
GL.GenerateMipmap(GL::TEXTURE_2D)
|
|
116
|
+
|
|
117
|
+
@skin_textures << tex_id
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "opengl"
|
|
4
|
+
|
|
5
|
+
module Quake
|
|
6
|
+
module Renderer
|
|
7
|
+
# Renders BSP sub-models (brush entities like doors, buttons, platforms).
|
|
8
|
+
# Each brush entity references a model in the BSP models array (models[1], [2], etc.)
|
|
9
|
+
# and has a position/angle from its entity definition.
|
|
10
|
+
class GLBrushModel
|
|
11
|
+
def initialize(level, texture_manager, lightmap)
|
|
12
|
+
@level = level
|
|
13
|
+
@texture_manager = texture_manager
|
|
14
|
+
@lightmap = lightmap
|
|
15
|
+
|
|
16
|
+
# Precompute surfaces for each sub-model (model index 1+)
|
|
17
|
+
@model_surfaces = {}
|
|
18
|
+
precompute_all_submodels
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Render all brush entities at their current positions.
|
|
22
|
+
# entities: array of Entity objects that have model_index set
|
|
23
|
+
def render(entities)
|
|
24
|
+
GL.Enable(GL::TEXTURE_2D)
|
|
25
|
+
GL.Color3f(1.0, 1.0, 1.0)
|
|
26
|
+
|
|
27
|
+
entities.each do |ent|
|
|
28
|
+
next unless ent.brush_entity?
|
|
29
|
+
surfaces = @model_surfaces[ent.model_index]
|
|
30
|
+
next unless surfaces
|
|
31
|
+
|
|
32
|
+
GL.PushMatrix
|
|
33
|
+
GL.Translatef(ent.position.x, ent.position.y, ent.position.z)
|
|
34
|
+
|
|
35
|
+
if ent.angle != 0.0 && ent.angles != Math::Vec3::ORIGIN
|
|
36
|
+
GL.Rotatef(ent.angles.x, 1.0, 0.0, 0.0)
|
|
37
|
+
GL.Rotatef(ent.angles.y, 0.0, 1.0, 0.0)
|
|
38
|
+
GL.Rotatef(ent.angles.z, 0.0, 0.0, 1.0)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
render_model_surfaces(surfaces)
|
|
42
|
+
|
|
43
|
+
GL.PopMatrix
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def precompute_all_submodels
|
|
50
|
+
@level.models.each_with_index do |model, model_idx|
|
|
51
|
+
next if model_idx == 0 # skip worldmodel, rendered by GLTextured
|
|
52
|
+
|
|
53
|
+
surfaces = []
|
|
54
|
+
model.num_faces.times do |i|
|
|
55
|
+
face_index = model.first_face + i
|
|
56
|
+
face = @level.faces[face_index]
|
|
57
|
+
next unless face
|
|
58
|
+
|
|
59
|
+
texinfo = @level.texinfo[face.texinfo_index]
|
|
60
|
+
next if texinfo.nil?
|
|
61
|
+
|
|
62
|
+
tex = @level.textures[texinfo.miptex_index]
|
|
63
|
+
next if tex.nil?
|
|
64
|
+
next if tex.name == "trigger" || tex.name == "clip"
|
|
65
|
+
next if tex.name.start_with?("sky")
|
|
66
|
+
|
|
67
|
+
surf = Bsp::FaceVertices.extract_surface(@level, face)
|
|
68
|
+
surfaces << {
|
|
69
|
+
vertices: surf.vertices,
|
|
70
|
+
texcoords: surf.texcoords,
|
|
71
|
+
miptex_index: texinfo.miptex_index,
|
|
72
|
+
face_index: face_index,
|
|
73
|
+
texinfo_index: face.texinfo_index
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
@model_surfaces[model_idx] = surfaces unless surfaces.empty?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
total = @model_surfaces.values.sum(&:size)
|
|
81
|
+
puts "Precomputed #{total} brush model surfaces across #{@model_surfaces.size} sub-models"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def render_model_surfaces(surfaces)
|
|
85
|
+
# Group by texture for fewer bind calls
|
|
86
|
+
current_miptex = -1
|
|
87
|
+
|
|
88
|
+
if @lightmap
|
|
89
|
+
render_surfaces_with_lightmaps(surfaces)
|
|
90
|
+
else
|
|
91
|
+
surfaces.each do |surf|
|
|
92
|
+
if surf[:miptex_index] != current_miptex
|
|
93
|
+
@texture_manager.bind(surf[:miptex_index])
|
|
94
|
+
current_miptex = surf[:miptex_index]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
GL.Begin(GL::TRIANGLE_FAN)
|
|
98
|
+
surf[:vertices].each_with_index do |v, i|
|
|
99
|
+
u, t = surf[:texcoords][i]
|
|
100
|
+
GL.TexCoord2f(u, t)
|
|
101
|
+
GL.Vertex3f(v.x, v.y, v.z)
|
|
102
|
+
end
|
|
103
|
+
GL.End
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def render_surfaces_with_lightmaps(surfaces)
|
|
109
|
+
# Pass 1: diffuse textures
|
|
110
|
+
GL.TexEnvi(GL::TEXTURE_ENV, GL::TEXTURE_ENV_MODE, GL::REPLACE)
|
|
111
|
+
current_miptex = -1
|
|
112
|
+
|
|
113
|
+
surfaces.each do |surf|
|
|
114
|
+
if surf[:miptex_index] != current_miptex
|
|
115
|
+
@texture_manager.bind(surf[:miptex_index])
|
|
116
|
+
current_miptex = surf[:miptex_index]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
GL.Begin(GL::TRIANGLE_FAN)
|
|
120
|
+
surf[:vertices].each_with_index do |v, i|
|
|
121
|
+
u, t = surf[:texcoords][i]
|
|
122
|
+
GL.TexCoord2f(u, t)
|
|
123
|
+
GL.Vertex3f(v.x, v.y, v.z)
|
|
124
|
+
end
|
|
125
|
+
GL.End
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Pass 2: lightmap multiply
|
|
129
|
+
GL.Enable(GL::BLEND)
|
|
130
|
+
GL.BlendFunc(GL::ZERO, GL::SRC_COLOR)
|
|
131
|
+
GL.DepthMask(GL::FALSE)
|
|
132
|
+
GL.DepthFunc(GL::LEQUAL)
|
|
133
|
+
|
|
134
|
+
last_lm_tex = -1
|
|
135
|
+
surfaces.each do |surf|
|
|
136
|
+
face_idx = surf[:face_index]
|
|
137
|
+
next unless @lightmap.face_lightmaps[face_idx]
|
|
138
|
+
|
|
139
|
+
lm_info = @lightmap.face_lightmaps[face_idx]
|
|
140
|
+
if lm_info.gl_texture != last_lm_tex
|
|
141
|
+
GL.BindTexture(GL::TEXTURE_2D, lm_info.gl_texture)
|
|
142
|
+
last_lm_tex = lm_info.gl_texture
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
texinfo = @level.texinfo[surf[:texinfo_index]]
|
|
146
|
+
GL.Begin(GL::TRIANGLE_FAN)
|
|
147
|
+
surf[:vertices].each_with_index do |v, i|
|
|
148
|
+
ls, lt = @lightmap.lightmap_texcoords(face_idx, v, texinfo)
|
|
149
|
+
GL.TexCoord2f(ls, lt)
|
|
150
|
+
GL.Vertex3f(v.x, v.y, v.z)
|
|
151
|
+
end
|
|
152
|
+
GL.End
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
GL.DepthMask(GL::TRUE)
|
|
156
|
+
GL.DepthFunc(GL::LESS)
|
|
157
|
+
GL.Disable(GL::BLEND)
|
|
158
|
+
GL.TexEnvi(GL::TEXTURE_ENV, GL::TEXTURE_ENV_MODE, GL::MODULATE)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|