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.
@@ -8,6 +8,8 @@ module Quake
8
8
  # Each brush entity references a model in the BSP models array (models[1], [2], etc.)
9
9
  # and has a position/angle from its entity definition.
10
10
  class GLBrushModel
11
+ BACKFACE_EPSILON = 0.01
12
+
11
13
  def initialize(level, texture_manager, lightmap)
12
14
  @level = level
13
15
  @texture_manager = texture_manager
@@ -20,25 +22,30 @@ module Quake
20
22
 
21
23
  # Render all brush entities at their current positions.
22
24
  # entities: array of Entity objects that have model_index set
23
- def render(entities)
25
+ def render(entities, view_origin: Math::Vec3::ORIGIN, time: 0.0, frustum: nil)
24
26
  GL.Enable(GL::TEXTURE_2D)
25
27
  GL.Color3f(1.0, 1.0, 1.0)
26
28
 
27
29
  entities.each do |ent|
28
30
  next unless ent.brush_entity?
31
+ next if ent.removed?
29
32
  surfaces = @model_surfaces[ent.model_index]
30
33
  next unless surfaces
34
+ next if frustum && culled_by_frustum?(ent, frustum)
31
35
 
32
36
  GL.PushMatrix
33
37
  GL.Translatef(ent.position.x, ent.position.y, ent.position.z)
34
38
 
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
+ if brush_model_rotated?(ent)
40
+ GL.Rotatef(ent.angles.y, 0.0, 0.0, 1.0)
41
+ # Net +pitch: R_DrawBrushModel negates pitch before
42
+ # R_RotateForEntity's -pitch rotation ("stupid quake bug")
43
+ GL.Rotatef(ent.angles.x, 0.0, 1.0, 0.0)
44
+ GL.Rotatef(ent.angles.z, 1.0, 0.0, 0.0)
39
45
  end
40
46
 
41
- render_model_surfaces(surfaces)
47
+ modelorg = brush_model_modelorg(view_origin, ent)
48
+ render_model_surfaces(surfaces, modelorg, time: time, alternate: ent.frame.to_i.positive?)
42
49
 
43
50
  GL.PopMatrix
44
51
  end
@@ -70,7 +77,9 @@ module Quake
70
77
  texcoords: surf.texcoords,
71
78
  miptex_index: texinfo.miptex_index,
72
79
  face_index: face_index,
73
- texinfo_index: face.texinfo_index
80
+ texinfo_index: face.texinfo_index,
81
+ plane_index: face.plane_index,
82
+ flags: face.flags
74
83
  }
75
84
  end
76
85
 
@@ -81,16 +90,18 @@ module Quake
81
90
  puts "Precomputed #{total} brush model surfaces across #{@model_surfaces.size} sub-models"
82
91
  end
83
92
 
84
- def render_model_surfaces(surfaces)
93
+ def render_model_surfaces(surfaces, modelorg, time:, alternate:)
94
+ surfaces = surfaces.select { |surf| brush_surface_visible?(surf, modelorg) }
95
+
85
96
  # Group by texture for fewer bind calls
86
97
  current_miptex = -1
87
98
 
88
99
  if @lightmap
89
- render_surfaces_with_lightmaps(surfaces)
100
+ render_surfaces_with_lightmaps(surfaces, time: time, alternate: alternate)
90
101
  else
91
102
  surfaces.each do |surf|
92
103
  if surf[:miptex_index] != current_miptex
93
- @texture_manager.bind(surf[:miptex_index])
104
+ @texture_manager.bind(surf[:miptex_index], time: time, alternate: alternate)
94
105
  current_miptex = surf[:miptex_index]
95
106
  end
96
107
 
@@ -105,15 +116,26 @@ module Quake
105
116
  end
106
117
  end
107
118
 
108
- def render_surfaces_with_lightmaps(surfaces)
119
+ def render_surfaces_with_lightmaps(surfaces, time:, alternate:)
109
120
  # Pass 1: diffuse textures
110
121
  GL.TexEnvi(GL::TEXTURE_ENV, GL::TEXTURE_ENV_MODE, GL::REPLACE)
111
122
  current_miptex = -1
112
123
 
124
+ fence = false
113
125
  surfaces.each do |surf|
114
126
  if surf[:miptex_index] != current_miptex
115
- @texture_manager.bind(surf[:miptex_index])
127
+ @texture_manager.bind(surf[:miptex_index], time: time, alternate: alternate)
116
128
  current_miptex = surf[:miptex_index]
129
+ surf_fence = @texture_manager.fence?(current_miptex)
130
+ if surf_fence != fence
131
+ fence = surf_fence
132
+ if fence
133
+ GL.AlphaFunc(GL::GREATER, 0.666)
134
+ GL.Enable(GL::ALPHA_TEST)
135
+ else
136
+ GL.Disable(GL::ALPHA_TEST)
137
+ end
138
+ end
117
139
  end
118
140
 
119
141
  GL.Begin(GL::TRIANGLE_FAN)
@@ -124,15 +146,18 @@ module Quake
124
146
  end
125
147
  GL.End
126
148
  end
149
+ GL.Disable(GL::ALPHA_TEST) if fence
127
150
 
128
151
  # Pass 2: lightmap multiply
129
152
  GL.Enable(GL::BLEND)
130
- GL.BlendFunc(GL::ZERO, GL::SRC_COLOR)
153
+ GL.BlendFunc(GL::SRC_ALPHA, GL::ONE_MINUS_SRC_ALPHA)
131
154
  GL.DepthMask(GL::FALSE)
132
- GL.DepthFunc(GL::LEQUAL)
133
155
 
134
156
  last_lm_tex = -1
135
157
  surfaces.each do |surf|
158
+ # Fences skip the light pass -- holes carry no pass-1 depth/diffuse
159
+ next if @texture_manager.fence?(surf[:miptex_index])
160
+
136
161
  face_idx = surf[:face_index]
137
162
  next unless @lightmap.face_lightmaps[face_idx]
138
163
 
@@ -142,10 +167,13 @@ module Quake
142
167
  last_lm_tex = lm_info.gl_texture
143
168
  end
144
169
 
145
- texinfo = @level.texinfo[surf[:texinfo_index]]
170
+ lm_coords = surf[:lm_texcoords] ||= begin
171
+ texinfo = @level.texinfo[surf[:texinfo_index]]
172
+ surf[:vertices].map { |v| @lightmap.lightmap_texcoords(face_idx, v, texinfo) }
173
+ end
146
174
  GL.Begin(GL::TRIANGLE_FAN)
147
175
  surf[:vertices].each_with_index do |v, i|
148
- ls, lt = @lightmap.lightmap_texcoords(face_idx, v, texinfo)
176
+ ls, lt = lm_coords[i]
149
177
  GL.TexCoord2f(ls, lt)
150
178
  GL.Vertex3f(v.x, v.y, v.z)
151
179
  end
@@ -153,10 +181,94 @@ module Quake
153
181
  end
154
182
 
155
183
  GL.DepthMask(GL::TRUE)
156
- GL.DepthFunc(GL::LESS)
157
184
  GL.Disable(GL::BLEND)
158
185
  GL.TexEnvi(GL::TEXTURE_ENV, GL::TEXTURE_ENV_MODE, GL::MODULATE)
159
186
  end
187
+
188
+ # R_CullBox against the view frustum (R_DrawBrushModel). Rotated
189
+ # entities use a conservative radius box like the C reference.
190
+ def culled_by_frustum?(ent, frustum)
191
+ model = @level.models[ent.model_index]
192
+ return false unless model&.mins && model.maxs
193
+
194
+ if brush_model_rotated?(ent)
195
+ radius = radius_from_bounds(model.mins, model.maxs)
196
+ extent = Math::Vec3.new(radius, radius, radius)
197
+ mins = ent.position - extent
198
+ maxs = ent.position + extent
199
+ else
200
+ mins = ent.position + model.mins
201
+ maxs = ent.position + model.maxs
202
+ end
203
+
204
+ cull_box?(mins, maxs, frustum)
205
+ end
206
+
207
+ def cull_box?(mins, maxs, frustum)
208
+ frustum.any? do |normal, dist|
209
+ px = normal.x >= 0 ? maxs.x : mins.x
210
+ py = normal.y >= 0 ? maxs.y : mins.y
211
+ pz = normal.z >= 0 ? maxs.z : mins.z
212
+ (normal.x * px) + (normal.y * py) + (normal.z * pz) - dist < 0
213
+ end
214
+ end
215
+
216
+ def radius_from_bounds(mins, maxs)
217
+ x = [mins.x.abs, maxs.x.abs].max
218
+ y = [mins.y.abs, maxs.y.abs].max
219
+ z = [mins.z.abs, maxs.z.abs].max
220
+ ::Math.sqrt((x * x) + (y * y) + (z * z))
221
+ end
222
+
223
+ def brush_surface_visible?(surf, modelorg)
224
+ plane = @level.planes[surf[:plane_index]]
225
+ return true unless plane
226
+
227
+ dot = modelorg.dot(plane.normal) - plane.dist
228
+ if (surf[:flags].to_i & Bsp::Face::SURF_PLANEBACK) != 0
229
+ dot < -BACKFACE_EPSILON
230
+ else
231
+ dot > BACKFACE_EPSILON
232
+ end
233
+ end
234
+
235
+ def brush_model_rotated?(ent)
236
+ ent.angles != Math::Vec3::ORIGIN
237
+ end
238
+
239
+ def brush_model_modelorg(view_origin, ent)
240
+ modelorg = view_origin - ent.position
241
+ return modelorg unless brush_model_rotated?(ent)
242
+
243
+ forward, right, up = quake_angle_vectors(ent.angles)
244
+ Math::Vec3.new(
245
+ modelorg.dot(forward),
246
+ -modelorg.dot(right),
247
+ modelorg.dot(up)
248
+ )
249
+ end
250
+
251
+ def quake_angle_vectors(angles)
252
+ sy = ::Math.sin(deg2rad(angles.y))
253
+ cy = ::Math.cos(deg2rad(angles.y))
254
+ sp = ::Math.sin(deg2rad(angles.x))
255
+ cp = ::Math.cos(deg2rad(angles.x))
256
+ sr = ::Math.sin(deg2rad(angles.z))
257
+ cr = ::Math.cos(deg2rad(angles.z))
258
+
259
+ forward = Math::Vec3.new(cp * cy, cp * sy, -sp)
260
+ right = Math::Vec3.new((-sr * sp * cy) + (cr * sy),
261
+ (-sr * sp * sy) - (cr * cy),
262
+ -sr * cp)
263
+ up = Math::Vec3.new((cr * sp * cy) + (sr * sy),
264
+ (cr * sp * sy) - (sr * cy),
265
+ cr * cp)
266
+ [forward, right, up]
267
+ end
268
+
269
+ def deg2rad(deg)
270
+ deg * ::Math::PI / 180.0
271
+ end
160
272
  end
161
273
  end
162
274
  end