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
|
@@ -23,15 +23,49 @@ module Quake
|
|
|
23
23
|
puts "Uploaded #{@gl_textures.size} textures to GL"
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
def bind(miptex_index)
|
|
27
|
-
tex_id = @gl_textures[miptex_index]
|
|
26
|
+
def bind(miptex_index, time: 0.0, alternate: false)
|
|
27
|
+
tex_id = @gl_textures[animated_miptex_index(miptex_index, time: time, alternate: alternate)]
|
|
28
28
|
GL.BindTexture(GL::TEXTURE_2D, tex_id || 0)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# Fence/grate textures ('{' prefix) have palette index 255 cut out
|
|
32
|
+
# and are drawn with alpha test (Quakespasm SURF_DRAWFENCE).
|
|
33
|
+
def fence?(miptex_index)
|
|
34
|
+
@level&.textures&.[](miptex_index)&.name&.start_with?("{") || false
|
|
35
|
+
end
|
|
36
|
+
|
|
31
37
|
private
|
|
32
38
|
|
|
39
|
+
def animated_miptex_index(miptex_index, time:, alternate:)
|
|
40
|
+
base_index = miptex_index
|
|
41
|
+
base = @level&.textures&.[](base_index)
|
|
42
|
+
return base_index unless base
|
|
43
|
+
|
|
44
|
+
if alternate && base.alternate_anims
|
|
45
|
+
base_index = base.alternate_anims
|
|
46
|
+
base = @level.textures[base_index]
|
|
47
|
+
return miptex_index unless base
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
return base_index if base.anim_total.to_i.zero?
|
|
51
|
+
|
|
52
|
+
relative = (time.to_f * 10).to_i % base.anim_total
|
|
53
|
+
count = 0
|
|
54
|
+
while base.anim_min > relative || base.anim_max <= relative
|
|
55
|
+
base_index = base.anim_next
|
|
56
|
+
raise "R_TextureAnimation: broken cycle" if base_index.nil?
|
|
57
|
+
|
|
58
|
+
base = @level.textures[base_index]
|
|
59
|
+
raise "R_TextureAnimation: broken cycle" unless base
|
|
60
|
+
raise "R_TextureAnimation: infinite cycle" if (count += 1) > 100
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
base_index
|
|
64
|
+
end
|
|
65
|
+
|
|
33
66
|
def upload_texture(miptex)
|
|
34
|
-
|
|
67
|
+
transparent = miptex.name.start_with?("{") ? 255 : nil
|
|
68
|
+
rgba = @palette.indexed_to_rgba(miptex.pixels, transparent_index: transparent)
|
|
35
69
|
|
|
36
70
|
buf = "\0" * 4
|
|
37
71
|
GL.GenTextures(1, buf)
|
|
@@ -43,7 +77,7 @@ module Quake
|
|
|
43
77
|
GL::RGBA, GL::UNSIGNED_BYTE, rgba)
|
|
44
78
|
GL.TexParameteri(GL::TEXTURE_2D, GL::TEXTURE_WRAP_S, GL::REPEAT)
|
|
45
79
|
GL.TexParameteri(GL::TEXTURE_2D, GL::TEXTURE_WRAP_T, GL::REPEAT)
|
|
46
|
-
GL.TexParameteri(GL::TEXTURE_2D, GL::TEXTURE_MIN_FILTER, GL::
|
|
80
|
+
GL.TexParameteri(GL::TEXTURE_2D, GL::TEXTURE_MIN_FILTER, GL::LINEAR_MIPMAP_NEAREST)
|
|
47
81
|
GL.TexParameteri(GL::TEXTURE_2D, GL::TEXTURE_MAG_FILTER, GL::LINEAR)
|
|
48
82
|
GL.GenerateMipmap(GL::TEXTURE_2D)
|
|
49
83
|
|
|
@@ -15,6 +15,7 @@ module Quake
|
|
|
15
15
|
@water = water
|
|
16
16
|
@last_leaf = -1
|
|
17
17
|
@visible_surface_cache = nil
|
|
18
|
+
@time = 0.0
|
|
18
19
|
|
|
19
20
|
# Precompute ALL surfaces grouped by texture (for non-VIS fallback
|
|
20
21
|
# and as source data for VIS filtering)
|
|
@@ -24,12 +25,17 @@ module Quake
|
|
|
24
25
|
puts "Precomputed #{@all_surfaces_by_texture.values.sum(&:size)} surfaces across #{@all_surfaces_by_texture.size} textures"
|
|
25
26
|
end
|
|
26
27
|
|
|
27
|
-
def render(camera, aspect)
|
|
28
|
+
def render(camera, aspect, time: @time || 0.0)
|
|
29
|
+
@time = time.to_f
|
|
30
|
+
|
|
28
31
|
GL.Clear(GL::COLOR_BUFFER_BIT | GL::DEPTH_BUFFER_BIT)
|
|
29
32
|
|
|
30
33
|
camera.apply_projection_gl(aspect)
|
|
31
34
|
camera.apply_gl
|
|
32
35
|
|
|
36
|
+
# 3D scene culls front faces (Quake winding); 2D passes disable this
|
|
37
|
+
GL.Enable(GL::CULL_FACE)
|
|
38
|
+
|
|
33
39
|
# Determine visible surfaces via PVS
|
|
34
40
|
surfaces_by_texture = visible_surfaces(camera)
|
|
35
41
|
|
|
@@ -42,15 +48,21 @@ module Quake
|
|
|
42
48
|
render_without_lightmaps(surfaces_by_texture)
|
|
43
49
|
end
|
|
44
50
|
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
# outdoor sky polys out, and TEXTURE_2D must stay enabled or sky
|
|
48
|
-
# polys render as untextured white shapes.
|
|
51
|
+
# PVS-cull keeps distant outdoor sky polys out, and TEXTURE_2D must
|
|
52
|
+
# stay enabled or sky polys render as untextured white shapes.
|
|
49
53
|
@sky&.render(camera, visible_face_set: @last_visible_face_set)
|
|
50
54
|
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
GL.Disable(GL::TEXTURE_2D)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Water is drawn after entities/viewmodel like R_DrawWaterSurfaces so
|
|
59
|
+
# translucent water blends over everything behind it.
|
|
60
|
+
# GLQuake's default r_wateralpha is 1.0.
|
|
61
|
+
def render_water(alpha: 1.0)
|
|
62
|
+
return unless @water
|
|
53
63
|
|
|
64
|
+
GL.Enable(GL::TEXTURE_2D)
|
|
65
|
+
@water.render(alpha: alpha)
|
|
54
66
|
GL.Disable(GL::TEXTURE_2D)
|
|
55
67
|
end
|
|
56
68
|
|
|
@@ -63,25 +75,15 @@ module Quake
|
|
|
63
75
|
|
|
64
76
|
def visible_surfaces(camera)
|
|
65
77
|
leaf_index = Bsp::Vis.point_in_leaf(@level, camera.position)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
# leaf-keyed cache is fine.
|
|
70
|
-
near_liquid = Bsp::Vis.near_liquid_portal?(@level, leaf)
|
|
71
|
-
|
|
72
|
-
if near_liquid
|
|
73
|
-
compute_visible_surfaces(leaf_index, point: camera.position)
|
|
74
|
-
else
|
|
75
|
-
if leaf_index != @last_leaf
|
|
76
|
-
@last_leaf = leaf_index
|
|
77
|
-
@visible_surface_cache = compute_visible_surfaces(leaf_index)
|
|
78
|
-
end
|
|
79
|
-
@visible_surface_cache || @all_surfaces_by_texture
|
|
78
|
+
if leaf_index != @last_leaf
|
|
79
|
+
@last_leaf = leaf_index
|
|
80
|
+
@visible_surface_cache = compute_visible_surfaces(leaf_index)
|
|
80
81
|
end
|
|
82
|
+
@visible_surface_cache || @all_surfaces_by_texture
|
|
81
83
|
end
|
|
82
84
|
|
|
83
|
-
def compute_visible_surfaces(leaf_index
|
|
84
|
-
visible_face_set = Bsp::Vis.visible_faces(@level, leaf_index
|
|
85
|
+
def compute_visible_surfaces(leaf_index)
|
|
86
|
+
visible_face_set = Bsp::Vis.visible_faces(@level, leaf_index)
|
|
85
87
|
# Stash so the sky pass can cull against the same set.
|
|
86
88
|
@last_visible_face_set = visible_face_set
|
|
87
89
|
|
|
@@ -102,7 +104,9 @@ module Quake
|
|
|
102
104
|
GL.Color3f(1.0, 1.0, 1.0)
|
|
103
105
|
|
|
104
106
|
surfaces_by_texture.each do |miptex_index, surfaces|
|
|
105
|
-
@texture_manager.bind(miptex_index)
|
|
107
|
+
@texture_manager.bind(miptex_index, time: (@time || 0.0).to_f)
|
|
108
|
+
fence = @texture_manager.fence?(miptex_index)
|
|
109
|
+
enable_fence_alpha_test if fence
|
|
106
110
|
|
|
107
111
|
surfaces.each do |surf|
|
|
108
112
|
GL.Begin(GL::TRIANGLE_FAN)
|
|
@@ -113,6 +117,8 @@ module Quake
|
|
|
113
117
|
end
|
|
114
118
|
GL.End
|
|
115
119
|
end
|
|
120
|
+
|
|
121
|
+
GL.Disable(GL::ALPHA_TEST) if fence
|
|
116
122
|
end
|
|
117
123
|
end
|
|
118
124
|
|
|
@@ -123,7 +129,9 @@ module Quake
|
|
|
123
129
|
GL.Color3f(1.0, 1.0, 1.0)
|
|
124
130
|
|
|
125
131
|
surfaces_by_texture.each do |miptex_index, surfaces|
|
|
126
|
-
@texture_manager.bind(miptex_index)
|
|
132
|
+
@texture_manager.bind(miptex_index, time: (@time || 0.0).to_f)
|
|
133
|
+
fence = @texture_manager.fence?(miptex_index)
|
|
134
|
+
enable_fence_alpha_test if fence
|
|
127
135
|
|
|
128
136
|
surfaces.each do |surf|
|
|
129
137
|
GL.Begin(GL::TRIANGLE_FAN)
|
|
@@ -134,16 +142,22 @@ module Quake
|
|
|
134
142
|
end
|
|
135
143
|
GL.End
|
|
136
144
|
end
|
|
145
|
+
|
|
146
|
+
GL.Disable(GL::ALPHA_TEST) if fence
|
|
137
147
|
end
|
|
138
148
|
|
|
139
149
|
# Pass 2: multiply lightmap on top
|
|
140
150
|
GL.Enable(GL::BLEND)
|
|
141
|
-
GL.BlendFunc(GL::
|
|
142
|
-
GL.
|
|
151
|
+
GL.BlendFunc(GL::SRC_ALPHA, GL::ONE_MINUS_SRC_ALPHA)
|
|
152
|
+
GL.DepthMask(GL::FALSE)
|
|
143
153
|
GL.TexEnvi(GL::TEXTURE_ENV, GL::TEXTURE_ENV_MODE, GL::REPLACE)
|
|
144
154
|
|
|
145
155
|
last_lm_tex = -1
|
|
146
|
-
surfaces_by_texture.each do |
|
|
156
|
+
surfaces_by_texture.each do |miptex_index, surfaces|
|
|
157
|
+
# Fence holes carry no diffuse/depth from pass 1; the lightmap quad
|
|
158
|
+
# would darken whatever shows through, so fences skip the light pass.
|
|
159
|
+
next if @texture_manager.fence?(miptex_index)
|
|
160
|
+
|
|
147
161
|
surfaces.each do |surf|
|
|
148
162
|
face_idx = surf[:face_index]
|
|
149
163
|
next unless face_idx && @lightmap.face_lightmaps[face_idx]
|
|
@@ -154,10 +168,13 @@ module Quake
|
|
|
154
168
|
last_lm_tex = lm_info.gl_texture
|
|
155
169
|
end
|
|
156
170
|
|
|
157
|
-
|
|
171
|
+
lm_coords = surf[:lm_texcoords] ||= begin
|
|
172
|
+
texinfo = @level.texinfo[surf[:texinfo_index]]
|
|
173
|
+
surf[:vertices].map { |v| @lightmap.lightmap_texcoords(face_idx, v, texinfo) }
|
|
174
|
+
end
|
|
158
175
|
GL.Begin(GL::TRIANGLE_FAN)
|
|
159
176
|
surf[:vertices].each_with_index do |v, i|
|
|
160
|
-
ls, lt =
|
|
177
|
+
ls, lt = lm_coords[i]
|
|
161
178
|
GL.TexCoord2f(ls, lt)
|
|
162
179
|
GL.Vertex3f(v.x, v.y, v.z)
|
|
163
180
|
end
|
|
@@ -165,7 +182,7 @@ module Quake
|
|
|
165
182
|
end
|
|
166
183
|
end
|
|
167
184
|
|
|
168
|
-
GL.
|
|
185
|
+
GL.DepthMask(GL::TRUE)
|
|
169
186
|
GL.Disable(GL::BLEND)
|
|
170
187
|
GL.TexEnvi(GL::TEXTURE_ENV, GL::TEXTURE_ENV_MODE, GL::MODULATE)
|
|
171
188
|
end
|
|
@@ -210,6 +227,11 @@ module Quake
|
|
|
210
227
|
grouped
|
|
211
228
|
end
|
|
212
229
|
|
|
230
|
+
def enable_fence_alpha_test
|
|
231
|
+
GL.AlphaFunc(GL::GREATER, 0.666)
|
|
232
|
+
GL.Enable(GL::ALPHA_TEST)
|
|
233
|
+
end
|
|
234
|
+
|
|
213
235
|
def build_face_index
|
|
214
236
|
index = {}
|
|
215
237
|
@all_surfaces_by_texture.each do |miptex_index, surfaces|
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "opengl"
|
|
4
|
+
require_relative "../physics/hull_trace"
|
|
5
|
+
|
|
6
|
+
module Quake
|
|
7
|
+
module Renderer
|
|
8
|
+
# Renders Quake GL polyblend overlays from view.c color shifts.
|
|
9
|
+
class GLViewBlend
|
|
10
|
+
CShift = Data.define(:color, :percent)
|
|
11
|
+
|
|
12
|
+
EMPTY_CSHIFT = CShift.new(color: [130, 80, 50], percent: 0)
|
|
13
|
+
WATER_CSHIFT = CShift.new(color: [130, 80, 50], percent: 128)
|
|
14
|
+
SLIME_CSHIFT = CShift.new(color: [0, 25, 5], percent: 150)
|
|
15
|
+
LAVA_CSHIFT = CShift.new(color: [255, 80, 0], percent: 150)
|
|
16
|
+
BONUS_COLOR = [215, 186, 69].freeze
|
|
17
|
+
DAMAGE_ARMOR_COLOR = [200, 100, 100].freeze
|
|
18
|
+
DAMAGE_MIXED_COLOR = [220, 50, 50].freeze
|
|
19
|
+
DAMAGE_BLOOD_COLOR = [255, 0, 0].freeze
|
|
20
|
+
|
|
21
|
+
def self.contents_cshift(contents)
|
|
22
|
+
case contents
|
|
23
|
+
when Physics::CONTENTS_EMPTY, Physics::CONTENTS_SOLID
|
|
24
|
+
EMPTY_CSHIFT
|
|
25
|
+
when Physics::CONTENTS_LAVA
|
|
26
|
+
LAVA_CSHIFT
|
|
27
|
+
when Physics::CONTENTS_SLIME
|
|
28
|
+
SLIME_CSHIFT
|
|
29
|
+
else
|
|
30
|
+
WATER_CSHIFT
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.powerup_cshift(player_state, game_time:)
|
|
35
|
+
if powerup_item_active?(player_state, :quad, game_time)
|
|
36
|
+
CShift.new(color: [0, 0, 255], percent: 30)
|
|
37
|
+
elsif powerup_item_active?(player_state, :biosuit, game_time)
|
|
38
|
+
CShift.new(color: [0, 255, 0], percent: 20)
|
|
39
|
+
elsif powerup_item_active?(player_state, :ring, game_time)
|
|
40
|
+
CShift.new(color: [100, 100, 100], percent: 100)
|
|
41
|
+
elsif powerup_item_active?(player_state, :pentagram, game_time)
|
|
42
|
+
CShift.new(color: [255, 255, 0], percent: 30)
|
|
43
|
+
else
|
|
44
|
+
CShift.new(color: [0, 0, 0], percent: 0)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.powerup_item_active?(player_state, powerup, game_time)
|
|
49
|
+
player_state&.powerup_item_active?(powerup, game_time: game_time)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.bonus_cshift(percent)
|
|
53
|
+
CShift.new(color: BONUS_COLOR, percent: percent.to_f.clamp(0.0, 50.0))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.damage_cshift(armor:, blood:, current_percent: 0.0)
|
|
57
|
+
armor = armor.to_f
|
|
58
|
+
blood = blood.to_f
|
|
59
|
+
count = blood * 0.5 + armor * 0.5
|
|
60
|
+
count = 10.0 if count < 10.0
|
|
61
|
+
percent = (current_percent.to_f + 3.0 * count).clamp(0.0, 150.0)
|
|
62
|
+
color = if armor > blood
|
|
63
|
+
DAMAGE_ARMOR_COLOR
|
|
64
|
+
elsif armor.positive?
|
|
65
|
+
DAMAGE_MIXED_COLOR
|
|
66
|
+
else
|
|
67
|
+
DAMAGE_BLOOD_COLOR
|
|
68
|
+
end
|
|
69
|
+
CShift.new(color: color, percent: percent)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.calc_blend(cshifts, cshift_percent: 100.0)
|
|
73
|
+
r = 0.0
|
|
74
|
+
g = 0.0
|
|
75
|
+
b = 0.0
|
|
76
|
+
a = 0.0
|
|
77
|
+
|
|
78
|
+
cshifts.each do |shift|
|
|
79
|
+
next if cshift_percent.zero?
|
|
80
|
+
|
|
81
|
+
a2 = ((shift.percent.to_f * cshift_percent.to_f) / 100.0) / 255.0
|
|
82
|
+
next if a2.zero?
|
|
83
|
+
|
|
84
|
+
a += a2 * (1.0 - a)
|
|
85
|
+
a2 /= a
|
|
86
|
+
r = r * (1.0 - a2) + shift.color[0] * a2
|
|
87
|
+
g = g * (1.0 - a2) + shift.color[1] * a2
|
|
88
|
+
b = b * (1.0 - a2) + shift.color[2] * a2
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
[r / 255.0, g / 255.0, b / 255.0, a.clamp(0.0, 1.0)]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def render(blend)
|
|
95
|
+
return if blend[3].to_f <= 0.0
|
|
96
|
+
|
|
97
|
+
GL.MatrixMode(GL::PROJECTION)
|
|
98
|
+
GL.PushMatrix
|
|
99
|
+
GL.LoadIdentity
|
|
100
|
+
GL.Ortho(0, 1, 0, 1, -1, 1)
|
|
101
|
+
GL.MatrixMode(GL::MODELVIEW)
|
|
102
|
+
GL.PushMatrix
|
|
103
|
+
GL.LoadIdentity
|
|
104
|
+
|
|
105
|
+
GL.Disable(GL::DEPTH_TEST)
|
|
106
|
+
GL.Disable(GL::CULL_FACE) # 2D pass; scene re-enables culling next frame
|
|
107
|
+
GL.Disable(GL::TEXTURE_2D)
|
|
108
|
+
GL.Enable(GL::BLEND)
|
|
109
|
+
GL.BlendFunc(GL::SRC_ALPHA, GL::ONE_MINUS_SRC_ALPHA)
|
|
110
|
+
GL.Color4f(*blend)
|
|
111
|
+
GL.Begin(GL::QUADS)
|
|
112
|
+
GL.Vertex2f(0.0, 0.0)
|
|
113
|
+
GL.Vertex2f(1.0, 0.0)
|
|
114
|
+
GL.Vertex2f(1.0, 1.0)
|
|
115
|
+
GL.Vertex2f(0.0, 1.0)
|
|
116
|
+
GL.End
|
|
117
|
+
|
|
118
|
+
GL.Color4f(1.0, 1.0, 1.0, 1.0)
|
|
119
|
+
GL.Disable(GL::BLEND)
|
|
120
|
+
GL.Enable(GL::TEXTURE_2D)
|
|
121
|
+
GL.Enable(GL::DEPTH_TEST)
|
|
122
|
+
GL.MatrixMode(GL::MODELVIEW)
|
|
123
|
+
GL.PopMatrix
|
|
124
|
+
GL.MatrixMode(GL::PROJECTION)
|
|
125
|
+
GL.PopMatrix
|
|
126
|
+
GL.MatrixMode(GL::MODELVIEW)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "opengl"
|
|
4
|
+
require_relative "../math/vec3"
|
|
4
5
|
|
|
5
6
|
module Quake
|
|
6
7
|
module Renderer
|
|
@@ -13,6 +14,7 @@ module Quake
|
|
|
13
14
|
CL_BOB = 0.02 # cl_bob: amplitude multiplier
|
|
14
15
|
CL_BOBCYCLE = 0.6 # cl_bobcycle: seconds per full bob cycle
|
|
15
16
|
CL_BOBUP = 0.5 # cl_bobup: fraction of cycle spent going up
|
|
17
|
+
VIEWMODEL_Z_FUDGE = 2.0 # scr_viewsize 100 in Quake's V_CalcRefdef
|
|
16
18
|
|
|
17
19
|
# Current bob value (applied to camera Z by the game loop)
|
|
18
20
|
attr_reader :bob
|
|
@@ -25,11 +27,15 @@ module Quake
|
|
|
25
27
|
@bob_speed = 0.0
|
|
26
28
|
@bob = 0.0
|
|
27
29
|
@current_model_path = nil
|
|
30
|
+
@animation_frames = []
|
|
31
|
+
@animation_time = 0.0
|
|
28
32
|
end
|
|
29
33
|
|
|
30
34
|
def set_weapon(model_path)
|
|
31
35
|
return if model_path == @current_model_path
|
|
32
36
|
@current_model_path = model_path
|
|
37
|
+
@animation_frames = []
|
|
38
|
+
@animation_time = 0.0
|
|
33
39
|
load_weapon(model_path) unless @weapon_renderers.key?(model_path)
|
|
34
40
|
end
|
|
35
41
|
|
|
@@ -37,9 +43,16 @@ module Quake
|
|
|
37
43
|
@time += dt
|
|
38
44
|
@bob_speed = speed
|
|
39
45
|
@bob = calc_bob
|
|
46
|
+
@animation_time += dt if @animation_frames.any?
|
|
40
47
|
end
|
|
41
48
|
|
|
42
|
-
def
|
|
49
|
+
def play_attack(frames)
|
|
50
|
+
@animation_frames = frames.to_a
|
|
51
|
+
@animation_time = 0.0
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def render(camera, aspect, visible: true, ambient_light: 200.0, shade_light: 200.0)
|
|
55
|
+
return unless visible
|
|
43
56
|
return unless @current_model_path
|
|
44
57
|
|
|
45
58
|
gl_model = @weapon_renderers[@current_model_path]
|
|
@@ -47,25 +60,35 @@ module Quake
|
|
|
47
60
|
|
|
48
61
|
# Compress depth range so weapon always appears in front of world
|
|
49
62
|
GL.DepthRange(0.0, 0.3)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
|
|
63
|
+
# Weapon origin = view origin + forward*bob*0.4 plus the default
|
|
64
|
+
# scr_viewsize 100 fudge from Quake's V_CalcRefdef. The camera
|
|
65
|
+
# position already carries the vertical bob, and in C the gun's
|
|
66
|
+
# own +bob cancels against the camera's, so none is added here.
|
|
67
|
+
# The fudge is applied along the view's up axis (not world Z as in
|
|
68
|
+
# the C source): a world-Z offset swings the gun down/away as the
|
|
69
|
+
# view pitches, while the reference's gun stays glued to the view.
|
|
70
|
+
pos = camera.position +
|
|
71
|
+
camera.forward * (@bob * 0.4) +
|
|
72
|
+
camera.up * VIEWMODEL_Z_FUDGE
|
|
56
73
|
|
|
57
74
|
GL.Enable(GL::TEXTURE_2D)
|
|
58
75
|
|
|
59
76
|
gl_model.render(
|
|
60
|
-
frame_index:
|
|
77
|
+
frame_index: current_frame,
|
|
61
78
|
lerp: 0.0,
|
|
62
79
|
position: pos,
|
|
80
|
+
# CalcGunAngle sets viewent pitch to -viewpitch and
|
|
81
|
+
# R_RotateForEntity negates it again, so the gun's world rotation
|
|
82
|
+
# is +viewpitch -- it pitches with the view.
|
|
63
83
|
yaw: camera.yaw,
|
|
64
|
-
pitch: camera.pitch
|
|
84
|
+
pitch: camera.pitch,
|
|
85
|
+
roll: camera.roll,
|
|
86
|
+
ambient_light: ambient_light,
|
|
87
|
+
shade_light: shade_light,
|
|
88
|
+
view_model: true
|
|
65
89
|
)
|
|
66
90
|
|
|
67
91
|
GL.DepthRange(0.0, 1.0)
|
|
68
|
-
GL.DepthFunc(GL::LESS)
|
|
69
92
|
end
|
|
70
93
|
|
|
71
94
|
private
|
|
@@ -73,7 +96,6 @@ module Quake
|
|
|
73
96
|
# Exact V_CalcBob from Quake's view.c
|
|
74
97
|
def calc_bob
|
|
75
98
|
speed = @bob_speed
|
|
76
|
-
return 0.0 if speed < 1.0
|
|
77
99
|
|
|
78
100
|
# Asymmetric cycle: spend cl_bobup fraction going up, rest going down
|
|
79
101
|
cycle = @time - (@time / CL_BOBCYCLE).floor * CL_BOBCYCLE
|
|
@@ -91,6 +113,19 @@ module Quake
|
|
|
91
113
|
bob.clamp(-7.0, 4.0)
|
|
92
114
|
end
|
|
93
115
|
|
|
116
|
+
def current_frame
|
|
117
|
+
return 0 if @animation_frames.empty?
|
|
118
|
+
|
|
119
|
+
idx = (@animation_time * 10.0).to_i
|
|
120
|
+
if idx >= @animation_frames.size
|
|
121
|
+
@animation_frames = []
|
|
122
|
+
@animation_time = 0.0
|
|
123
|
+
return 0
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
@animation_frames[idx]
|
|
127
|
+
end
|
|
128
|
+
|
|
94
129
|
def load_weapon(model_path)
|
|
95
130
|
begin
|
|
96
131
|
data = @pak.read(model_path)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Quake
|
|
4
|
+
module Renderer
|
|
5
|
+
module GLWarpSubdivision
|
|
6
|
+
SUBDIVIDE_SIZE = 128.0
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# Matches GLQuake's SubdividePolygon using default gl_subdivide_size.
|
|
11
|
+
def subdivide_polygon(verts)
|
|
12
|
+
result = []
|
|
13
|
+
subdivide_polygon_recursive(verts, result)
|
|
14
|
+
result
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def subdivide_polygon_recursive(verts, result)
|
|
18
|
+
3.times do |axis|
|
|
19
|
+
mins, maxs = polygon_axis_bounds(verts, axis)
|
|
20
|
+
mid = (mins + maxs) * 0.5
|
|
21
|
+
mid = SUBDIVIDE_SIZE * (mid / SUBDIVIDE_SIZE + 0.5).floor
|
|
22
|
+
next if maxs - mid < 8.0
|
|
23
|
+
next if mid - mins < 8.0
|
|
24
|
+
|
|
25
|
+
front, back = split_polygon_at_axis(verts, axis, mid)
|
|
26
|
+
subdivide_polygon_recursive(front, result)
|
|
27
|
+
subdivide_polygon_recursive(back, result)
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
result << verts
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def polygon_axis_bounds(verts, axis)
|
|
35
|
+
mins = Float::INFINITY
|
|
36
|
+
maxs = -Float::INFINITY
|
|
37
|
+
verts.each do |v|
|
|
38
|
+
val = v.to_a[axis]
|
|
39
|
+
mins = val if val < mins
|
|
40
|
+
maxs = val if val > maxs
|
|
41
|
+
end
|
|
42
|
+
[mins, maxs]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def split_polygon_at_axis(verts, axis, mid)
|
|
46
|
+
front = []
|
|
47
|
+
back = []
|
|
48
|
+
distances = verts.map { |v| v.to_a[axis] - mid }
|
|
49
|
+
|
|
50
|
+
verts.each_with_index do |v, i|
|
|
51
|
+
v_next = verts[(i + 1) % verts.size]
|
|
52
|
+
d1 = distances[i]
|
|
53
|
+
d2 = distances[(i + 1) % verts.size]
|
|
54
|
+
|
|
55
|
+
front << v if d1 >= 0.0
|
|
56
|
+
back << v if d1 <= 0.0
|
|
57
|
+
next if d1.zero? || d2.zero?
|
|
58
|
+
next if d1.positive? == d2.positive?
|
|
59
|
+
|
|
60
|
+
frac = d1 / (d1 - d2)
|
|
61
|
+
mid_vertex = Quake::Math::Vec3.new(
|
|
62
|
+
v.x + frac * (v_next.x - v.x),
|
|
63
|
+
v.y + frac * (v_next.y - v.y),
|
|
64
|
+
v.z + frac * (v_next.z - v.z)
|
|
65
|
+
)
|
|
66
|
+
front << mid_vertex
|
|
67
|
+
back << mid_vertex
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
[front, back]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -1,15 +1,16 @@
|
|
|
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 turbulent water/lava/slime surfaces with sine-wave warping.
|
|
8
9
|
# Quake liquid textures have names starting with '*'.
|
|
9
10
|
class GLWater
|
|
10
|
-
|
|
11
|
+
include GLWarpSubdivision
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
TURBSCALE = 256.0 / (2.0 * ::Math::PI) # ~40.74
|
|
13
14
|
|
|
14
15
|
def initialize(level, texture_manager)
|
|
15
16
|
@level = level
|
|
@@ -36,13 +37,6 @@ module Quake
|
|
|
36
37
|
return if @water_surfaces.empty?
|
|
37
38
|
|
|
38
39
|
GL.Enable(GL::TEXTURE_2D)
|
|
39
|
-
# Quake BSP stores liquid surfaces as front/back face pairs on the
|
|
40
|
-
# same plane so the surface is visible from above and below. Without
|
|
41
|
-
# culling both copies rasterize at the same depth, z-fight, and (with
|
|
42
|
-
# alpha < 1) double-blend, producing flicker. Match TyrQuake:
|
|
43
|
-
# glCullFace(GL_FRONT) with default CCW front face.
|
|
44
|
-
GL.Enable(GL::CULL_FACE)
|
|
45
|
-
GL.CullFace(GL::FRONT)
|
|
46
40
|
|
|
47
41
|
if alpha < 1.0
|
|
48
42
|
GL.Enable(GL::BLEND)
|
|
@@ -75,8 +69,6 @@ module Quake
|
|
|
75
69
|
end
|
|
76
70
|
end
|
|
77
71
|
|
|
78
|
-
GL.Disable(GL::CULL_FACE)
|
|
79
|
-
|
|
80
72
|
if alpha < 1.0
|
|
81
73
|
GL.Disable(GL::BLEND)
|
|
82
74
|
GL.Color4f(1.0, 1.0, 1.0, 1.0)
|
|
@@ -115,7 +107,7 @@ module Quake
|
|
|
115
107
|
|
|
116
108
|
subdivided.each do |poly_verts|
|
|
117
109
|
texcoords = poly_verts.map do |v|
|
|
118
|
-
# Raw dot product only, no offset (matches
|
|
110
|
+
# Raw dot product only, no offset (matches Quake SubdividePolygon)
|
|
119
111
|
s = v.dot(texinfo.s_vec)
|
|
120
112
|
t = v.dot(texinfo.t_vec)
|
|
121
113
|
[s, t]
|
|
@@ -131,70 +123,6 @@ module Quake
|
|
|
131
123
|
puts "Found #{@water_surfaces.sum { |ws| ws[:faces].size }} water surfaces"
|
|
132
124
|
end
|
|
133
125
|
|
|
134
|
-
# Subdivide polygon along axial 64-unit boundaries for smoother warp.
|
|
135
|
-
# Matches TyrQuake's SubdividePolygon.
|
|
136
|
-
def subdivide_polygon(verts)
|
|
137
|
-
polys = [verts]
|
|
138
|
-
|
|
139
|
-
3.times do |axis|
|
|
140
|
-
next_polys = []
|
|
141
|
-
polys.each do |poly|
|
|
142
|
-
split_polygon_axis(poly, axis, next_polys)
|
|
143
|
-
end
|
|
144
|
-
polys = next_polys
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
polys
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def split_polygon_axis(verts, axis, result)
|
|
151
|
-
# Find bounds on this axis
|
|
152
|
-
mins = Float::INFINITY
|
|
153
|
-
maxs = -Float::INFINITY
|
|
154
|
-
verts.each do |v|
|
|
155
|
-
val = v.to_a[axis]
|
|
156
|
-
mins = val if val < mins
|
|
157
|
-
maxs = val if val > maxs
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
# If small enough, no split needed
|
|
161
|
-
if maxs - mins <= SUBDIVIDE_SIZE
|
|
162
|
-
result << verts
|
|
163
|
-
return
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
# Find split plane (first 64-unit boundary)
|
|
167
|
-
dist = ((mins + SUBDIVIDE_SIZE) / SUBDIVIDE_SIZE).floor * SUBDIVIDE_SIZE
|
|
168
|
-
# Ensure we're actually splitting
|
|
169
|
-
dist = mins + SUBDIVIDE_SIZE if dist <= mins
|
|
170
|
-
|
|
171
|
-
front = []
|
|
172
|
-
back = []
|
|
173
|
-
|
|
174
|
-
verts.each_with_index do |v, i|
|
|
175
|
-
v_next = verts[(i + 1) % verts.size]
|
|
176
|
-
d1 = v.to_a[axis] - dist
|
|
177
|
-
d2 = v_next.to_a[axis] - dist
|
|
178
|
-
|
|
179
|
-
front << v if d1 >= 0
|
|
180
|
-
back << v if d1 < 0
|
|
181
|
-
|
|
182
|
-
# Edge crosses the split plane
|
|
183
|
-
if (d1 >= 0) != (d2 >= 0)
|
|
184
|
-
frac = d1 / (d1 - d2)
|
|
185
|
-
mid = Quake::Math::Vec3.new(
|
|
186
|
-
v.x + frac * (v_next.x - v.x),
|
|
187
|
-
v.y + frac * (v_next.y - v.y),
|
|
188
|
-
v.z + frac * (v_next.z - v.z)
|
|
189
|
-
)
|
|
190
|
-
front << mid
|
|
191
|
-
back << mid
|
|
192
|
-
end
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
split_polygon_axis(front, axis, result) if front.size >= 3
|
|
196
|
-
split_polygon_axis(back, axis, result) if back.size >= 3
|
|
197
|
-
end
|
|
198
126
|
end
|
|
199
127
|
end
|
|
200
128
|
end
|