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
|
@@ -1,20 +1,35 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "opengl"
|
|
4
|
+
require_relative "gl_alias_model"
|
|
4
5
|
|
|
5
6
|
module Quake
|
|
6
7
|
module Renderer
|
|
7
8
|
# Simple particle system for visual effects.
|
|
8
|
-
#
|
|
9
|
-
# Matches Quake's R_DrawParticles (gl_rpart.c).
|
|
9
|
+
# Matches Quake's GL R_DrawParticles (r_part.c).
|
|
10
10
|
class GLParticles
|
|
11
11
|
GRAVITY = 800.0
|
|
12
12
|
MAX_PARTICLES = 2048
|
|
13
|
+
PARTICLE_TEXTURE_SIZE = 8
|
|
14
|
+
PARTICLE_BILLBOARD_SIZE = 1.5
|
|
15
|
+
PARTICLE_SCALE_DISTANCE = 20.0
|
|
16
|
+
PARTICLE_SCALE_FACTOR = 0.004
|
|
17
|
+
|
|
18
|
+
DOT_TEXTURE = [
|
|
19
|
+
[0, 1, 1, 0, 0, 0, 0, 0],
|
|
20
|
+
[1, 1, 1, 1, 0, 0, 0, 0],
|
|
21
|
+
[1, 1, 1, 1, 0, 0, 0, 0],
|
|
22
|
+
[0, 1, 1, 0, 0, 0, 0, 0],
|
|
23
|
+
[0, 0, 0, 0, 0, 0, 0, 0],
|
|
24
|
+
[0, 0, 0, 0, 0, 0, 0, 0],
|
|
25
|
+
[0, 0, 0, 0, 0, 0, 0, 0],
|
|
26
|
+
[0, 0, 0, 0, 0, 0, 0, 0]
|
|
27
|
+
].freeze
|
|
13
28
|
|
|
14
29
|
# Quake ramp tables for color animation (palette indices)
|
|
15
30
|
RAMP1 = [0x6f, 0x6d, 0x6b, 0x69, 0x67, 0x65, 0x63, 0x61].freeze
|
|
16
31
|
RAMP2 = [0x6f, 0x6e, 0x6d, 0x6c, 0x6b, 0x6a, 0x68, 0x66].freeze
|
|
17
|
-
RAMP3 = [0x6d, 0x6b, 0x06, 0x05, 0x04, 0x03,
|
|
32
|
+
RAMP3 = [0x6d, 0x6b, 0x06, 0x05, 0x04, 0x03, 0x00, 0x00].freeze # explosion
|
|
18
33
|
|
|
19
34
|
Particle = Struct.new(:x, :y, :z, :vx, :vy, :vz,
|
|
20
35
|
:r, :g, :b, :a,
|
|
@@ -35,20 +50,12 @@ module Quake
|
|
|
35
50
|
p.y += p.vy * dt
|
|
36
51
|
p.z += p.vz * dt
|
|
37
52
|
|
|
38
|
-
# Apply gravity
|
|
39
|
-
p.vz -= GRAVITY * p.gravity_scale * dt
|
|
40
|
-
|
|
41
53
|
# Color ramp animation
|
|
42
|
-
if p.ramp_type
|
|
43
|
-
p.ramp += dt *
|
|
54
|
+
if p.ramp_type && (ramp = ramp_for_type(p.ramp_type))
|
|
55
|
+
p.ramp += dt * ramp_rate(p.ramp_type)
|
|
44
56
|
idx = p.ramp.to_i
|
|
45
|
-
ramp = case p.ramp_type
|
|
46
|
-
when :explosion then RAMP3
|
|
47
|
-
when :fire then RAMP1
|
|
48
|
-
else RAMP2
|
|
49
|
-
end
|
|
50
57
|
|
|
51
|
-
if idx >= ramp
|
|
58
|
+
if idx >= ramp_expiry_size(p.ramp_type, ramp)
|
|
52
59
|
next true # particle expired
|
|
53
60
|
end
|
|
54
61
|
|
|
@@ -58,51 +65,163 @@ module Quake
|
|
|
58
65
|
p.b = color[2] / 255.0
|
|
59
66
|
end
|
|
60
67
|
|
|
61
|
-
|
|
62
|
-
p.a = (p.life * 2.0).clamp(0.0, 1.0)
|
|
68
|
+
update_velocity(p, dt)
|
|
63
69
|
|
|
64
70
|
false
|
|
65
71
|
end
|
|
66
72
|
end
|
|
67
73
|
|
|
68
|
-
def render
|
|
74
|
+
def render(camera: nil)
|
|
69
75
|
return if @particles.empty?
|
|
70
76
|
|
|
71
|
-
|
|
77
|
+
upload_particle_texture unless @particle_texture
|
|
78
|
+
view_origin = camera&.position || Math::Vec3::ORIGIN
|
|
79
|
+
view_forward = camera&.forward || Math::Vec3.new(1.0, 0.0, 0.0)
|
|
80
|
+
up = (camera&.up || Math::Vec3.new(0.0, 0.0, 1.0)) * PARTICLE_BILLBOARD_SIZE
|
|
81
|
+
right = (camera&.right || Math::Vec3.new(0.0, -1.0, 0.0)) * PARTICLE_BILLBOARD_SIZE
|
|
82
|
+
|
|
83
|
+
GL.Enable(GL::TEXTURE_2D)
|
|
84
|
+
GL.BindTexture(GL::TEXTURE_2D, @particle_texture)
|
|
85
|
+
GL.Disable(GL::ALPHA_TEST)
|
|
72
86
|
GL.Enable(GL::BLEND)
|
|
87
|
+
GL.TexEnvi(GL::TEXTURE_ENV, GL::TEXTURE_ENV_MODE, GL::MODULATE)
|
|
73
88
|
GL.BlendFunc(GL::SRC_ALPHA, GL::ONE_MINUS_SRC_ALPHA)
|
|
74
|
-
GL.
|
|
89
|
+
GL.DepthMask(GL::FALSE)
|
|
75
90
|
|
|
76
|
-
GL.Begin(GL::
|
|
91
|
+
GL.Begin(GL::TRIANGLES)
|
|
77
92
|
@particles.each do |p|
|
|
78
|
-
|
|
93
|
+
scale = particle_scale(p, view_origin, view_forward)
|
|
94
|
+
GL.Color3f(p.r, p.g, p.b)
|
|
95
|
+
GL.TexCoord2f(0.0, 0.0)
|
|
79
96
|
GL.Vertex3f(p.x, p.y, p.z)
|
|
97
|
+
GL.TexCoord2f(1.0, 0.0)
|
|
98
|
+
GL.Vertex3f(p.x + up.x * scale, p.y + up.y * scale, p.z + up.z * scale)
|
|
99
|
+
GL.TexCoord2f(0.0, 1.0)
|
|
100
|
+
GL.Vertex3f(p.x + right.x * scale, p.y + right.y * scale, p.z + right.z * scale)
|
|
80
101
|
end
|
|
81
102
|
GL.End
|
|
82
103
|
|
|
83
|
-
GL.
|
|
104
|
+
GL.DepthMask(GL::TRUE)
|
|
84
105
|
GL.Disable(GL::BLEND)
|
|
85
|
-
GL.
|
|
106
|
+
GL.TexEnvi(GL::TEXTURE_ENV, GL::TEXTURE_ENV_MODE, GL::REPLACE)
|
|
107
|
+
GL.Color3f(1.0, 1.0, 1.0)
|
|
86
108
|
end
|
|
87
109
|
|
|
88
|
-
#
|
|
89
|
-
def
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
110
|
+
# Quake R_EntityParticles: EF_BRIGHTFIELD particles orbit each alias normal.
|
|
111
|
+
def entity_particles(pos, time:)
|
|
112
|
+
entity_particle_avelocities.each_with_index do |velocities, index|
|
|
113
|
+
yaw = time.to_f * velocities[0]
|
|
114
|
+
pitch = time.to_f * velocities[1]
|
|
115
|
+
roll = time.to_f * velocities[2]
|
|
116
|
+
sy = ::Math.sin(yaw)
|
|
117
|
+
cy = ::Math.cos(yaw)
|
|
118
|
+
sp = ::Math.sin(pitch)
|
|
119
|
+
cp = ::Math.cos(pitch)
|
|
120
|
+
sr = ::Math.sin(roll)
|
|
121
|
+
cr = ::Math.cos(roll)
|
|
122
|
+
forward = [cp * cy, cp * sy, -sp]
|
|
123
|
+
normal = GLAliasModel::ANORMS[index]
|
|
124
|
+
color = @palette.rgb(0x6f)
|
|
125
|
+
|
|
96
126
|
emit(
|
|
97
|
-
x: pos.x +
|
|
98
|
-
|
|
127
|
+
x: pos.x + normal[0] * 64.0 + forward[0] * 16.0,
|
|
128
|
+
y: pos.y + normal[1] * 64.0 + forward[1] * 16.0,
|
|
129
|
+
z: pos.z + normal[2] * 64.0 + forward[2] * 16.0,
|
|
130
|
+
vx: 0.0, vy: 0.0, vz: 0.0,
|
|
99
131
|
r: color[0] / 255.0, g: color[1] / 255.0, b: color[2] / 255.0,
|
|
100
|
-
life: 0.
|
|
101
|
-
|
|
132
|
+
life: 0.01,
|
|
133
|
+
ramp_type: :explosion
|
|
102
134
|
)
|
|
103
135
|
end
|
|
104
136
|
end
|
|
105
137
|
|
|
138
|
+
# Quake R_RocketTrail: projectile, blood, tracer, and voor trails.
|
|
139
|
+
def rocket_trail(start_pos, end_pos, type:)
|
|
140
|
+
dir_x = end_pos.x - start_pos.x
|
|
141
|
+
dir_y = end_pos.y - start_pos.y
|
|
142
|
+
dir_z = end_pos.z - start_pos.z
|
|
143
|
+
len = ::Math.sqrt(dir_x * dir_x + dir_y * dir_y + dir_z * dir_z)
|
|
144
|
+
return if len.zero?
|
|
145
|
+
|
|
146
|
+
dir_x /= len
|
|
147
|
+
dir_y /= len
|
|
148
|
+
dir_z /= len
|
|
149
|
+
|
|
150
|
+
dec = 3.0
|
|
151
|
+
if type >= 128
|
|
152
|
+
dec = 1.0
|
|
153
|
+
type -= 128
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
x = start_pos.x
|
|
157
|
+
y = start_pos.y
|
|
158
|
+
z = start_pos.z
|
|
159
|
+
while len.positive?
|
|
160
|
+
len -= dec
|
|
161
|
+
emit_rocket_trail_particle(x, y, z, dir_x, dir_y, dir_z, type)
|
|
162
|
+
len -= 3.0 if type == 4
|
|
163
|
+
x += dir_x
|
|
164
|
+
y += dir_y
|
|
165
|
+
z += dir_z
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Teleporter sparkle effect at a position
|
|
170
|
+
def teleport_splash(pos)
|
|
171
|
+
(-16...16).step(4) do |i|
|
|
172
|
+
(-16...16).step(4) do |j|
|
|
173
|
+
(-24...32).step(4) do |k|
|
|
174
|
+
color_idx = 7 + (rand * 8).to_i
|
|
175
|
+
color = @palette.rgb(color_idx)
|
|
176
|
+
dir_x = j * 8.0
|
|
177
|
+
dir_y = i * 8.0
|
|
178
|
+
dir_z = k * 8.0
|
|
179
|
+
length = ::Math.sqrt(dir_x * dir_x + dir_y * dir_y + dir_z * dir_z)
|
|
180
|
+
vel = 50.0 + (rand * 64).to_i
|
|
181
|
+
scale = length.zero? ? 0.0 : vel / length
|
|
182
|
+
emit(
|
|
183
|
+
x: pos.x + i + (rand * 4).to_i,
|
|
184
|
+
y: pos.y + j + (rand * 4).to_i,
|
|
185
|
+
z: pos.z + k + (rand * 4).to_i,
|
|
186
|
+
vx: dir_x * scale,
|
|
187
|
+
vy: dir_y * scale,
|
|
188
|
+
vz: dir_z * scale,
|
|
189
|
+
r: color[0] / 255.0, g: color[1] / 255.0, b: color[2] / 255.0,
|
|
190
|
+
life: 0.2 + (rand * 8).to_i * 0.02,
|
|
191
|
+
gravity_scale: 1.0
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Quake temp entity TE_LAVASPLASH.
|
|
199
|
+
def lava_splash(pos)
|
|
200
|
+
(-16...16).each do |i|
|
|
201
|
+
(-16...16).each do |j|
|
|
202
|
+
color_idx = 224 + (rand * 8).to_i
|
|
203
|
+
color = @palette.rgb(color_idx)
|
|
204
|
+
dir_x = j * 8.0 + (rand * 8).to_i
|
|
205
|
+
dir_y = i * 8.0 + (rand * 8).to_i
|
|
206
|
+
dir_z = 256.0
|
|
207
|
+
length = ::Math.sqrt(dir_x * dir_x + dir_y * dir_y + dir_z * dir_z)
|
|
208
|
+
vel = 50.0 + (rand * 64).to_i
|
|
209
|
+
scale = vel / length
|
|
210
|
+
emit(
|
|
211
|
+
x: pos.x + dir_x,
|
|
212
|
+
y: pos.y + dir_y,
|
|
213
|
+
z: pos.z + (rand * 64).to_i,
|
|
214
|
+
vx: dir_x * scale,
|
|
215
|
+
vy: dir_y * scale,
|
|
216
|
+
vz: dir_z * scale,
|
|
217
|
+
r: color[0] / 255.0, g: color[1] / 255.0, b: color[2] / 255.0,
|
|
218
|
+
life: 2.0 + (rand * 32).to_i * 0.02,
|
|
219
|
+
gravity_scale: 1.0
|
|
220
|
+
)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
106
225
|
# Item pickup sparkle
|
|
107
226
|
def pickup_effect(pos)
|
|
108
227
|
20.times do
|
|
@@ -124,48 +243,269 @@ module Quake
|
|
|
124
243
|
|
|
125
244
|
# Explosion burst
|
|
126
245
|
def explosion(pos)
|
|
127
|
-
|
|
128
|
-
|
|
246
|
+
1024.times do |i|
|
|
247
|
+
emit_explosion_particle(pos, index: i)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Quake temp entity TE_EXPLOSION2: color-mapped particle explosion.
|
|
252
|
+
def color_mapped_explosion(pos, color_start:, color_length:)
|
|
253
|
+
color_mod = 0
|
|
254
|
+
512.times do
|
|
255
|
+
color_idx = color_start + (color_mod % color_length)
|
|
256
|
+
color_mod += 1
|
|
257
|
+
color = @palette.rgb(color_idx)
|
|
129
258
|
emit(
|
|
130
|
-
x: pos.x + rand *
|
|
131
|
-
|
|
259
|
+
x: pos.x + (rand * 32).to_i - 16,
|
|
260
|
+
y: pos.y + (rand * 32).to_i - 16,
|
|
261
|
+
z: pos.z + (rand * 32).to_i - 16,
|
|
262
|
+
vx: (rand * 512).to_i - 256,
|
|
263
|
+
vy: (rand * 512).to_i - 256,
|
|
264
|
+
vz: (rand * 512).to_i - 256,
|
|
132
265
|
r: color[0] / 255.0, g: color[1] / 255.0, b: color[2] / 255.0,
|
|
133
|
-
life: 0.
|
|
134
|
-
|
|
135
|
-
ramp_type: :explosion
|
|
266
|
+
life: 0.3,
|
|
267
|
+
ramp_type: :blob
|
|
136
268
|
)
|
|
137
269
|
end
|
|
138
270
|
end
|
|
139
271
|
|
|
140
|
-
#
|
|
272
|
+
# Quake temp entity TE_TAREXPLOSION: tarbaby blob explosion.
|
|
273
|
+
def blob_explosion(pos)
|
|
274
|
+
1024.times do |i|
|
|
275
|
+
color_idx = i.odd? ? 66 + (rand * 6).to_i : 150 + (rand * 6).to_i
|
|
276
|
+
color = @palette.rgb(color_idx)
|
|
277
|
+
emit(
|
|
278
|
+
x: pos.x + (rand * 32).to_i - 16,
|
|
279
|
+
y: pos.y + (rand * 32).to_i - 16,
|
|
280
|
+
z: pos.z + (rand * 32).to_i - 16,
|
|
281
|
+
vx: (rand * 512).to_i - 256,
|
|
282
|
+
vy: (rand * 512).to_i - 256,
|
|
283
|
+
vz: (rand * 512).to_i - 256,
|
|
284
|
+
r: color[0] / 255.0, g: color[1] / 255.0, b: color[2] / 255.0,
|
|
285
|
+
life: 1.0 + (((rand * 256).to_i & 8) * 0.05),
|
|
286
|
+
ramp_type: i.odd? ? :blob : :blob2
|
|
287
|
+
)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# QuakeWorld TE_BLOOD uses R_RunParticleEffect with blood palette color 73.
|
|
141
292
|
def blood(pos, count: 20)
|
|
293
|
+
run_particle_effect(pos, color: 73, count: count)
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# QuakeWorld TE_LIGHTNINGBLOOD.
|
|
297
|
+
def lightning_blood(pos)
|
|
298
|
+
run_particle_effect(pos, color: 225, count: 50)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Quake R_RunParticleEffect: used by gunshots and spike impact puffs.
|
|
302
|
+
def run_particle_effect(pos, color:, count:, dir: nil)
|
|
303
|
+
if count == 1024
|
|
304
|
+
count.times { |i| emit_explosion_particle(pos, index: i) }
|
|
305
|
+
return
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
color_base = color & ~7
|
|
309
|
+
dir_x = dir ? dir.x : 0.0
|
|
310
|
+
dir_y = dir ? dir.y : 0.0
|
|
311
|
+
dir_z = dir ? dir.z : 0.0
|
|
312
|
+
|
|
142
313
|
count.times do
|
|
143
|
-
|
|
144
|
-
color = @palette.rgb(color_idx)
|
|
314
|
+
color = @palette.rgb(color_base + (rand * 8).to_i)
|
|
145
315
|
emit(
|
|
146
|
-
x: pos.x
|
|
147
|
-
|
|
316
|
+
x: pos.x + (rand * 16).to_i - 8,
|
|
317
|
+
y: pos.y + (rand * 16).to_i - 8,
|
|
318
|
+
z: pos.z + (rand * 16).to_i - 8,
|
|
319
|
+
vx: dir_x * 15.0, vy: dir_y * 15.0, vz: dir_z * 15.0,
|
|
148
320
|
r: color[0] / 255.0, g: color[1] / 255.0, b: color[2] / 255.0,
|
|
149
|
-
life:
|
|
321
|
+
life: (rand * 5).to_i * 0.1,
|
|
150
322
|
gravity_scale: 1.0
|
|
151
323
|
)
|
|
152
324
|
end
|
|
153
325
|
end
|
|
154
326
|
|
|
327
|
+
# Quake temp entity TE_GUNSHOT: a small wall puff at bullet impact.
|
|
328
|
+
def gunshot(pos, count: 20)
|
|
329
|
+
run_particle_effect(pos, color: 0, count: count)
|
|
330
|
+
end
|
|
331
|
+
|
|
155
332
|
def particle_count
|
|
156
333
|
@particles.size
|
|
157
334
|
end
|
|
158
335
|
|
|
159
336
|
private
|
|
160
337
|
|
|
338
|
+
def upload_particle_texture
|
|
339
|
+
buf = "\0" * 4
|
|
340
|
+
GL.GenTextures(1, buf)
|
|
341
|
+
@particle_texture = buf.unpack1("V")
|
|
342
|
+
|
|
343
|
+
GL.BindTexture(GL::TEXTURE_2D, @particle_texture)
|
|
344
|
+
GL.TexImage2D(GL::TEXTURE_2D, 0, GL::RGBA,
|
|
345
|
+
PARTICLE_TEXTURE_SIZE, PARTICLE_TEXTURE_SIZE, 0,
|
|
346
|
+
GL::RGBA, GL::UNSIGNED_BYTE, particle_texture_rgba)
|
|
347
|
+
GL.TexParameteri(GL::TEXTURE_2D, GL::TEXTURE_MIN_FILTER, GL::LINEAR)
|
|
348
|
+
GL.TexParameteri(GL::TEXTURE_2D, GL::TEXTURE_MAG_FILTER, GL::LINEAR)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def particle_texture_rgba
|
|
352
|
+
DOT_TEXTURE.flat_map.with_index do |row, y|
|
|
353
|
+
row.flat_map.with_index do |_value, x|
|
|
354
|
+
[255, 255, 255, DOT_TEXTURE[x][y] * 255]
|
|
355
|
+
end
|
|
356
|
+
end.pack("C*")
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def particle_scale(particle, view_origin, view_forward)
|
|
360
|
+
distance = (particle.x - view_origin.x) * view_forward.x +
|
|
361
|
+
(particle.y - view_origin.y) * view_forward.y +
|
|
362
|
+
(particle.z - view_origin.z) * view_forward.z
|
|
363
|
+
return 1.0 if distance < PARTICLE_SCALE_DISTANCE
|
|
364
|
+
|
|
365
|
+
1.0 + distance * PARTICLE_SCALE_FACTOR
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def emit_explosion_particle(pos, index:)
|
|
369
|
+
ramp = (rand * 4).to_i
|
|
370
|
+
color = @palette.rgb(RAMP1[0])
|
|
371
|
+
emit(
|
|
372
|
+
x: pos.x + (rand * 32).to_i - 16,
|
|
373
|
+
y: pos.y + (rand * 32).to_i - 16,
|
|
374
|
+
z: pos.z + (rand * 32).to_i - 16,
|
|
375
|
+
vx: (rand * 512).to_i - 256,
|
|
376
|
+
vy: (rand * 512).to_i - 256,
|
|
377
|
+
vz: (rand * 512).to_i - 256,
|
|
378
|
+
r: color[0] / 255.0, g: color[1] / 255.0, b: color[2] / 255.0,
|
|
379
|
+
life: 5.0,
|
|
380
|
+
ramp: ramp,
|
|
381
|
+
ramp_type: index.odd? ? :explosion : :explosion2
|
|
382
|
+
)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def update_velocity(p, dt)
|
|
386
|
+
grav = GRAVITY * 0.05 * dt
|
|
387
|
+
dvel = 4.0 * dt
|
|
388
|
+
|
|
389
|
+
case p.ramp_type
|
|
390
|
+
when :fire
|
|
391
|
+
p.vz += grav
|
|
392
|
+
when :explosion, :blob
|
|
393
|
+
p.vx += p.vx * dvel
|
|
394
|
+
p.vy += p.vy * dvel
|
|
395
|
+
p.vz += p.vz * dvel
|
|
396
|
+
p.vz -= grav
|
|
397
|
+
when :explosion2
|
|
398
|
+
p.vx -= p.vx * dt
|
|
399
|
+
p.vy -= p.vy * dt
|
|
400
|
+
p.vz -= p.vz * dt
|
|
401
|
+
p.vz -= grav
|
|
402
|
+
when :blob2
|
|
403
|
+
p.vx -= p.vx * dvel
|
|
404
|
+
p.vy -= p.vy * dvel
|
|
405
|
+
p.vz -= grav
|
|
406
|
+
else
|
|
407
|
+
p.vz -= grav * p.gravity_scale
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def emit_rocket_trail_particle(x, y, z, dir_x, dir_y, dir_z, type)
|
|
412
|
+
case type
|
|
413
|
+
when 0, 1
|
|
414
|
+
ramp = (rand * 4).to_i + (type == 1 ? 2 : 0)
|
|
415
|
+
color = @palette.rgb(RAMP3[ramp])
|
|
416
|
+
emit(
|
|
417
|
+
x: x + (rand * 6).to_i - 3,
|
|
418
|
+
y: y + (rand * 6).to_i - 3,
|
|
419
|
+
z: z + (rand * 6).to_i - 3,
|
|
420
|
+
vx: 0.0, vy: 0.0, vz: 0.0,
|
|
421
|
+
r: color[0] / 255.0, g: color[1] / 255.0, b: color[2] / 255.0,
|
|
422
|
+
life: 2.0,
|
|
423
|
+
ramp: ramp,
|
|
424
|
+
ramp_type: :fire
|
|
425
|
+
)
|
|
426
|
+
when 2, 4
|
|
427
|
+
color = @palette.rgb(67 + (rand * 4).to_i)
|
|
428
|
+
emit(
|
|
429
|
+
x: x + (rand * 6).to_i - 3,
|
|
430
|
+
y: y + (rand * 6).to_i - 3,
|
|
431
|
+
z: z + (rand * 6).to_i - 3,
|
|
432
|
+
vx: 0.0, vy: 0.0, vz: 0.0,
|
|
433
|
+
r: color[0] / 255.0, g: color[1] / 255.0, b: color[2] / 255.0,
|
|
434
|
+
life: 2.0,
|
|
435
|
+
gravity_scale: 1.0
|
|
436
|
+
)
|
|
437
|
+
when 3, 5
|
|
438
|
+
@tracercount ||= 0
|
|
439
|
+
color_idx = type == 3 ? 52 + ((@tracercount & 4) << 1) : 230 + ((@tracercount & 4) << 1)
|
|
440
|
+
@tracercount += 1
|
|
441
|
+
vx = if (@tracercount & 1) == 1
|
|
442
|
+
30.0 * dir_y
|
|
443
|
+
else
|
|
444
|
+
-30.0 * dir_y
|
|
445
|
+
end
|
|
446
|
+
vy = if (@tracercount & 1) == 1
|
|
447
|
+
30.0 * -dir_x
|
|
448
|
+
else
|
|
449
|
+
30.0 * dir_x
|
|
450
|
+
end
|
|
451
|
+
color = @palette.rgb(color_idx)
|
|
452
|
+
emit(
|
|
453
|
+
x: x, y: y, z: z,
|
|
454
|
+
vx: vx, vy: vy, vz: 0.0,
|
|
455
|
+
r: color[0] / 255.0, g: color[1] / 255.0, b: color[2] / 255.0,
|
|
456
|
+
life: 0.5
|
|
457
|
+
)
|
|
458
|
+
when 6
|
|
459
|
+
color = @palette.rgb((9 * 16) + 8 + (rand * 4).to_i)
|
|
460
|
+
emit(
|
|
461
|
+
x: x + (rand * 16).to_i - 8,
|
|
462
|
+
y: y + (rand * 16).to_i - 8,
|
|
463
|
+
z: z + (rand * 16).to_i - 8,
|
|
464
|
+
vx: 0.0, vy: 0.0, vz: 0.0,
|
|
465
|
+
r: color[0] / 255.0, g: color[1] / 255.0, b: color[2] / 255.0,
|
|
466
|
+
life: 0.3
|
|
467
|
+
)
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def entity_particle_avelocities
|
|
472
|
+
@entity_particle_avelocities ||= GLAliasModel::ANORMS.map do
|
|
473
|
+
[
|
|
474
|
+
(rand * 256.0).to_i * 0.01,
|
|
475
|
+
(rand * 256.0).to_i * 0.01,
|
|
476
|
+
(rand * 256.0).to_i * 0.01
|
|
477
|
+
]
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def ramp_for_type(type)
|
|
482
|
+
case type
|
|
483
|
+
when :fire then RAMP3
|
|
484
|
+
when :explosion then RAMP1
|
|
485
|
+
when :explosion2 then RAMP2
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def ramp_rate(type)
|
|
490
|
+
case type
|
|
491
|
+
when :fire then 5.0
|
|
492
|
+
when :explosion2 then 15.0
|
|
493
|
+
else 10.0
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def ramp_expiry_size(type, ramp)
|
|
498
|
+
type == :fire ? 6 : ramp.size
|
|
499
|
+
end
|
|
500
|
+
|
|
161
501
|
def emit(x:, y:, z:, vx:, vy:, vz:, r:, g:, b:, life:,
|
|
162
|
-
gravity_scale: 0.0, ramp_type: nil)
|
|
502
|
+
gravity_scale: 0.0, ramp: 0.0, ramp_type: nil)
|
|
163
503
|
return if @particles.size >= MAX_PARTICLES
|
|
164
504
|
|
|
165
505
|
@particles << Particle.new(
|
|
166
506
|
x, y, z, vx, vy, vz,
|
|
167
507
|
r, g, b, 1.0,
|
|
168
|
-
life,
|
|
508
|
+
life, ramp.to_f, ramp_type, gravity_scale
|
|
169
509
|
)
|
|
170
510
|
end
|
|
171
511
|
end
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "opengl"
|
|
4
|
+
require_relative "gl_warp_subdivision"
|
|
4
5
|
|
|
5
6
|
module Quake
|
|
6
7
|
module Renderer
|
|
7
8
|
# Renders Quake sky using the two-layer scrolling technique.
|
|
8
9
|
# Sky textures are 256x128, split into:
|
|
9
|
-
# -
|
|
10
|
-
# -
|
|
10
|
+
# - Right half (128x128): solid background layer
|
|
11
|
+
# - Left half (128x128): alpha foreground layer (index 0 = transparent)
|
|
11
12
|
class GLSky
|
|
13
|
+
include GLWarpSubdivision
|
|
14
|
+
|
|
12
15
|
def initialize(level, palette, texture_manager)
|
|
13
16
|
@level = level
|
|
14
17
|
@palette = palette
|
|
@@ -107,17 +110,17 @@ module Quake
|
|
|
107
110
|
h = miptex.height # 128
|
|
108
111
|
half_w = w / 2 # 128
|
|
109
112
|
|
|
110
|
-
#
|
|
113
|
+
# GLQuake uses the right half as the solid background layer.
|
|
111
114
|
solid_pixels = String.new(capacity: half_w * h)
|
|
112
115
|
h.times do |row|
|
|
113
|
-
src_offset = row * w
|
|
116
|
+
src_offset = row * w + half_w
|
|
114
117
|
solid_pixels << miptex.pixels[src_offset, half_w]
|
|
115
118
|
end
|
|
116
119
|
|
|
117
|
-
#
|
|
120
|
+
# GLQuake uses the left half as the alpha foreground layer.
|
|
118
121
|
alpha_pixels = String.new(capacity: half_w * h)
|
|
119
122
|
h.times do |row|
|
|
120
|
-
src_offset = row * w
|
|
123
|
+
src_offset = row * w
|
|
121
124
|
alpha_pixels << miptex.pixels[src_offset, half_w]
|
|
122
125
|
end
|
|
123
126
|
|
|
@@ -125,13 +128,81 @@ module Quake
|
|
|
125
128
|
@alpha_tex = upload_texture(alpha_pixels, half_w, h, transparent: true)
|
|
126
129
|
end
|
|
127
130
|
|
|
131
|
+
# Ruby port of tyrquake's QPic32_AlphaFix (qpic.c). Gives each fully
|
|
132
|
+
# transparent texel the average RGB of its non-transparent neighbours
|
|
133
|
+
# (wrapping around at the texture edges) to avoid wrong-colored fringes
|
|
134
|
+
# from GL_LINEAR blending. Alpha stays 0; texels with no non-transparent
|
|
135
|
+
# neighbours keep RGB 0.
|
|
136
|
+
def alpha_fix(rgba, width, height)
|
|
137
|
+
height.times do |y|
|
|
138
|
+
width.times do |x|
|
|
139
|
+
current = y * width + x
|
|
140
|
+
|
|
141
|
+
# Only modify completely transparent texels
|
|
142
|
+
next unless rgba.getbyte(current * 4 + 3) == 0
|
|
143
|
+
|
|
144
|
+
# Neighbour texel indexes are left to right:
|
|
145
|
+
# 0 1 2
|
|
146
|
+
# 3 * 4
|
|
147
|
+
# 5 6 7
|
|
148
|
+
neighbours = [
|
|
149
|
+
current - width - 1, current - width, current - width + 1,
|
|
150
|
+
current - 1, current + 1,
|
|
151
|
+
current + width - 1, current + width, current + width + 1
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
# Handle edge cases (wrap around)
|
|
155
|
+
if x == 0
|
|
156
|
+
neighbours[0] += width
|
|
157
|
+
neighbours[3] += width
|
|
158
|
+
neighbours[5] += width
|
|
159
|
+
elsif x == width - 1
|
|
160
|
+
neighbours[2] -= width
|
|
161
|
+
neighbours[4] -= width
|
|
162
|
+
neighbours[7] -= width
|
|
163
|
+
end
|
|
164
|
+
if y == 0
|
|
165
|
+
neighbours[0] += width * height
|
|
166
|
+
neighbours[1] += width * height
|
|
167
|
+
neighbours[2] += width * height
|
|
168
|
+
elsif y == height - 1
|
|
169
|
+
neighbours[5] -= width * height
|
|
170
|
+
neighbours[6] -= width * height
|
|
171
|
+
neighbours[7] -= width * height
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Find the average colour of non-transparent neighbours
|
|
175
|
+
r = g = b = count = 0
|
|
176
|
+
neighbours.each do |n|
|
|
177
|
+
next if rgba.getbyte(n * 4 + 3) == 0
|
|
178
|
+
|
|
179
|
+
r += rgba.getbyte(n * 4)
|
|
180
|
+
g += rgba.getbyte(n * 4 + 1)
|
|
181
|
+
b += rgba.getbyte(n * 4 + 2)
|
|
182
|
+
count += 1
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Skip if no non-transparent neighbours
|
|
186
|
+
next if count == 0
|
|
187
|
+
|
|
188
|
+
rgba.setbyte(current * 4, r / count)
|
|
189
|
+
rgba.setbyte(current * 4 + 1, g / count)
|
|
190
|
+
rgba.setbyte(current * 4 + 2, b / count)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
128
195
|
def upload_texture(pixels, width, height, transparent:)
|
|
129
196
|
rgba = String.new(capacity: width * height * 4)
|
|
130
197
|
pixels.each_byte do |idx|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
198
|
+
if transparent && idx == 0
|
|
199
|
+
rgba << 0 << 0 << 0 << 0
|
|
200
|
+
else
|
|
201
|
+
r, g, b = @palette.rgb(idx)
|
|
202
|
+
rgba << r << g << b << 255
|
|
203
|
+
end
|
|
134
204
|
end
|
|
205
|
+
alpha_fix(rgba, width, height) if transparent
|
|
135
206
|
|
|
136
207
|
buf = "\0" * 4
|
|
137
208
|
GL.GenTextures(1, buf)
|
|
@@ -157,7 +228,9 @@ module Quake
|
|
|
157
228
|
next unless tex.name.start_with?("sky")
|
|
158
229
|
|
|
159
230
|
verts = Bsp::FaceVertices.extract(@level, face)
|
|
160
|
-
|
|
231
|
+
subdivide_polygon(verts).each do |poly_verts|
|
|
232
|
+
@sky_surfaces << { vertices: poly_verts, face_index: face_index }
|
|
233
|
+
end
|
|
161
234
|
end
|
|
162
235
|
puts "Found #{@sky_surfaces.size} sky surfaces"
|
|
163
236
|
end
|