Class: Quake::Renderer::GLAliasModel

Inherits:
Object
  • Object
show all
Defined in:
lib/quake/renderer/gl_alias_model.rb

Overview

Renders Quake MDL (alias) models with texture mapping and frame animation.

Constant Summary collapse

SHADEDOT_QUANT =
16
TOP_RANGE =
16
BOTTOM_RANGE =
96
ANORMS =
[
  [-0.525731, 0.000000, 0.850651],
  [-0.442863, 0.238856, 0.864188],
  [-0.295242, 0.000000, 0.955423],
  [-0.309017, 0.500000, 0.809017],
  [-0.162460, 0.262866, 0.951056],
  [0.000000, 0.000000, 1.000000],
  [0.000000, 0.850651, 0.525731],
  [-0.147621, 0.716567, 0.681718],
  [0.147621, 0.716567, 0.681718],
  [0.000000, 0.525731, 0.850651],
  [0.309017, 0.500000, 0.809017],
  [0.525731, 0.000000, 0.850651],
  [0.295242, 0.000000, 0.955423],
  [0.442863, 0.238856, 0.864188],
  [0.162460, 0.262866, 0.951056],
  [-0.681718, 0.147621, 0.716567],
  [-0.809017, 0.309017, 0.500000],
  [-0.587785, 0.425325, 0.688191],
  [-0.850651, 0.525731, 0.000000],
  [-0.864188, 0.442863, 0.238856],
  [-0.716567, 0.681718, 0.147621],
  [-0.688191, 0.587785, 0.425325],
  [-0.500000, 0.809017, 0.309017],
  [-0.238856, 0.864188, 0.442863],
  [-0.425325, 0.688191, 0.587785],
  [-0.716567, 0.681718, -0.147621],
  [-0.500000, 0.809017, -0.309017],
  [-0.525731, 0.850651, 0.000000],
  [0.000000, 0.850651, -0.525731],
  [-0.238856, 0.864188, -0.442863],
  [0.000000, 0.955423, -0.295242],
  [-0.262866, 0.951056, -0.162460],
  [0.000000, 1.000000, 0.000000],
  [0.000000, 0.955423, 0.295242],
  [-0.262866, 0.951056, 0.162460],
  [0.238856, 0.864188, 0.442863],
  [0.262866, 0.951056, 0.162460],
  [0.500000, 0.809017, 0.309017],
  [0.238856, 0.864188, -0.442863],
  [0.262866, 0.951056, -0.162460],
  [0.500000, 0.809017, -0.309017],
  [0.850651, 0.525731, 0.000000],
  [0.716567, 0.681718, 0.147621],
  [0.716567, 0.681718, -0.147621],
  [0.525731, 0.850651, 0.000000],
  [0.425325, 0.688191, 0.587785],
  [0.864188, 0.442863, 0.238856],
  [0.688191, 0.587785, 0.425325],
  [0.809017, 0.309017, 0.500000],
  [0.681718, 0.147621, 0.716567],
  [0.587785, 0.425325, 0.688191],
  [0.955423, 0.295242, 0.000000],
  [1.000000, 0.000000, 0.000000],
  [0.951056, 0.162460, 0.262866],
  [0.850651, -0.525731, 0.000000],
  [0.955423, -0.295242, 0.000000],
  [0.864188, -0.442863, 0.238856],
  [0.951056, -0.162460, 0.262866],
  [0.809017, -0.309017, 0.500000],
  [0.681718, -0.147621, 0.716567],
  [0.850651, 0.000000, 0.525731],
  [0.864188, 0.442863, -0.238856],
  [0.809017, 0.309017, -0.500000],
  [0.951056, 0.162460, -0.262866],
  [0.525731, 0.000000, -0.850651],
  [0.681718, 0.147621, -0.716567],
  [0.681718, -0.147621, -0.716567],
  [0.850651, 0.000000, -0.525731],
  [0.809017, -0.309017, -0.500000],
  [0.864188, -0.442863, -0.238856],
  [0.951056, -0.162460, -0.262866],
  [0.147621, 0.716567, -0.681718],
  [0.309017, 0.500000, -0.809017],
  [0.425325, 0.688191, -0.587785],
  [0.442863, 0.238856, -0.864188],
  [0.587785, 0.425325, -0.688191],
  [0.688191, 0.587785, -0.425325],
  [-0.147621, 0.716567, -0.681718],
  [-0.309017, 0.500000, -0.809017],
  [0.000000, 0.525731, -0.850651],
  [-0.525731, 0.000000, -0.850651],
  [-0.442863, 0.238856, -0.864188],
  [-0.295242, 0.000000, -0.955423],
  [-0.162460, 0.262866, -0.951056],
  [0.000000, 0.000000, -1.000000],
  [0.295242, 0.000000, -0.955423],
  [0.162460, 0.262866, -0.951056],
  [-0.442863, -0.238856, -0.864188],
  [-0.309017, -0.500000, -0.809017],
  [-0.162460, -0.262866, -0.951056],
  [0.000000, -0.850651, -0.525731],
  [-0.147621, -0.716567, -0.681718],
  [0.147621, -0.716567, -0.681718],
  [0.000000, -0.525731, -0.850651],
  [0.309017, -0.500000, -0.809017],
  [0.442863, -0.238856, -0.864188],
  [0.162460, -0.262866, -0.951056],
  [0.238856, -0.864188, -0.442863],
  [0.500000, -0.809017, -0.309017],
  [0.425325, -0.688191, -0.587785],
  [0.716567, -0.681718, -0.147621],
  [0.688191, -0.587785, -0.425325],
  [0.587785, -0.425325, -0.688191],
  [0.000000, -0.955423, -0.295242],
  [0.000000, -1.000000, 0.000000],
  [0.262866, -0.951056, -0.162460],
  [0.000000, -0.850651, 0.525731],
  [0.000000, -0.955423, 0.295242],
  [0.238856, -0.864188, 0.442863],
  [0.262866, -0.951056, 0.162460],
  [0.500000, -0.809017, 0.309017],
  [0.716567, -0.681718, 0.147621],
  [0.525731, -0.850651, 0.000000],
  [-0.238856, -0.864188, -0.442863],
  [-0.500000, -0.809017, -0.309017],
  [-0.262866, -0.951056, -0.162460],
  [-0.850651, -0.525731, 0.000000],
  [-0.716567, -0.681718, -0.147621],
  [-0.716567, -0.681718, 0.147621],
  [-0.525731, -0.850651, 0.000000],
  [-0.500000, -0.809017, 0.309017],
  [-0.238856, -0.864188, 0.442863],
  [-0.262866, -0.951056, 0.162460],
  [-0.864188, -0.442863, 0.238856],
  [-0.809017, -0.309017, 0.500000],
  [-0.688191, -0.587785, 0.425325],
  [-0.681718, -0.147621, 0.716567],
  [-0.442863, -0.238856, 0.864188],
  [-0.587785, -0.425325, 0.688191],
  [-0.309017, -0.500000, 0.809017],
  [-0.147621, -0.716567, 0.681718],
  [-0.425325, -0.688191, 0.587785],
  [-0.162460, -0.262866, 0.951056],
  [0.442863, -0.238856, 0.864188],
  [0.162460, -0.262866, 0.951056],
  [0.309017, -0.500000, 0.809017],
  [0.147621, -0.716567, 0.681718],
  [0.000000, -0.525731, 0.850651],
  [0.425325, -0.688191, 0.587785],
  [0.587785, -0.425325, 0.688191],
  [0.688191, -0.587785, 0.425325],
  [-0.955423, 0.295242, 0.000000],
  [-0.951056, 0.162460, 0.262866],
  [-1.000000, 0.000000, 0.000000],
  [-0.850651, 0.000000, 0.525731],
  [-0.955423, -0.295242, 0.000000],
  [-0.951056, -0.162460, 0.262866],
  [-0.864188, 0.442863, -0.238856],
  [-0.951056, 0.162460, -0.262866],
  [-0.809017, 0.309017, -0.500000],
  [-0.864188, -0.442863, -0.238856],
  [-0.951056, -0.162460, -0.262866],
  [-0.809017, -0.309017, -0.500000],
  [-0.681718, 0.147621, -0.716567],
  [-0.681718, -0.147621, -0.716567],
  [-0.850651, 0.000000, -0.525731],
  [-0.688191, 0.587785, -0.425325],
  [-0.587785, 0.425325, -0.688191],
  [-0.425325, 0.688191, -0.587785],
  [-0.425325, -0.688191, -0.587785],
  [-0.587785, -0.425325, -0.688191],
  [-0.688191, -0.587785, -0.425325]
].freeze
ST_RAND =

sync_type: entities get a random animation phase

1

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model, palette, model_name: nil) ⇒ GLAliasModel

Returns a new instance of GLAliasModel.



181
182
183
184
185
186
187
188
# File 'lib/quake/renderer/gl_alias_model.rb', line 181

def initialize(model, palette, model_name: nil)
  @model = model
  @palette = palette
  @model_name = model_name
  @skin_textures = []
  @translated_skin_textures = {}
  upload_skins
end

Class Method Details

.alias_shade_color(vertex, shade_scale:, shadedot: 1.0) ⇒ Object



291
292
293
294
# File 'lib/quake/renderer/gl_alias_model.rb', line 291

def self.alias_shade_color(vertex, shade_scale:, shadedot: 1.0)
  light = shadedot.to_f * shade_scale.to_f
  [light, light, light]
end

.alias_shadedot(yaw:, normal_index:) ⇒ Object



296
297
298
299
# File 'lib/quake/renderer/gl_alias_model.rb', line 296

def self.alias_shadedot(yaw:, normal_index:)
  quantized = ((yaw.to_f * (SHADEDOT_QUANT / 360.0)).to_i) & (SHADEDOT_QUANT - 1)
  ANORM_DOTS[quantized].fetch(normal_index.to_i, 1.0)
end

.alias_texcoord(stvert, triangle, skin_width:, skin_height:) ⇒ Object



194
195
196
197
198
199
200
201
# File 'lib/quake/renderer/gl_alias_model.rb', line 194

def self.alias_texcoord(stvert, triangle, skin_width:, skin_height:)
  s = stvert.s.to_f
  t = stvert.t.to_f

  s += skin_width * 0.5 if stvert.on_seam != 0 && triangle.faces_front == 0

  [(s + 0.5) / skin_width, (t + 0.5) / skin_height]
end

.alias_vertex_position(model, vertex, model_name: nil) ⇒ Object



301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/quake/renderer/gl_alias_model.rb', line 301

def self.alias_vertex_position(model, vertex, model_name: nil)
  if model_name == "progs/eyes.mdl"
    return [
      vertex.x * model.scale.x * 2.0 + model.scale_origin.x,
      vertex.y * model.scale.y * 2.0 + model.scale_origin.y,
      vertex.z * model.scale.z * 2.0 + model.scale_origin.z - 30.0
    ]
  end

  [
    vertex.x * model.scale.x + model.scale_origin.x,
    vertex.y * model.scale.y + model.scale_origin.y,
    vertex.z * model.scale.z + model.scale_origin.z
  ]
end

.apply_alias_transform(position:, yaw:, pitch:, roll: 0.0) ⇒ Object



264
265
266
267
268
269
# File 'lib/quake/renderer/gl_alias_model.rb', line 264

def self.apply_alias_transform(position:, yaw:, pitch:, roll: 0.0)
  GL.Translatef(position.x, position.y, position.z)
  GL.Rotatef(yaw, 0.0, 0.0, 1.0)
  GL.Rotatef(pitch, 0.0, 1.0, 0.0)
  GL.Rotatef(roll, 1.0, 0.0, 0.0)
end

.find_interval_index(intervals, time) ⇒ Object

Mod_FindInterval: intervals are cumulative timestamps, so groups may hold poses for different durations.



214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/quake/renderer/gl_alias_model.rb', line 214

def self.find_interval_index(intervals, time)
  return 0 if intervals.nil? || intervals.empty?

  full = intervals.last.to_f
  return 0 if full <= 0.0

  target = time.to_f % full
  intervals.each_with_index do |value, i|
    return i if value.to_f > target
  end
  intervals.size - 1
end

.quake_alias_lighting(ambient_light:, shade_light:, model_name: nil, view_model: false, client_index: nil) ⇒ Object



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/quake/renderer/gl_alias_model.rb', line 271

def self.quake_alias_lighting(ambient_light:, shade_light:, model_name: nil, view_model: false, client_index: nil)
  ambient = ambient_light.to_f
  shade = shade_light.to_f

  ambient = shade = 24.0 if view_model && ambient < 24.0

  ambient = 128.0 if ambient > 128.0
  shade = 192.0 - ambient if ambient + shade > 192.0

  if client_index && client_index.between?(1, 32) && ambient < 8.0
    ambient = shade = 8.0
  end

  if %w[progs/flame2.mdl progs/flame.mdl].include?(model_name)
    ambient = shade = 256.0
  end

  [ambient, shade / 200.0]
end

.resolve_frame_entry(entry, time: 0.0) ⇒ Object



203
204
205
206
207
208
209
210
# File 'lib/quake/renderer/gl_alias_model.rb', line 203

def self.resolve_frame_entry(entry, time: 0.0)
  case entry
  when Mdl::Frame
    entry
  when Mdl::FrameGroup
    entry.frames[find_interval_index(entry.intervals, time)]
  end
end

.resolve_frame_index(num_frames, frame_index) ⇒ Object



227
228
229
230
231
232
# File 'lib/quake/renderer/gl_alias_model.rb', line 227

def self.resolve_frame_index(num_frames, frame_index)
  index = frame_index.to_i
  return 0 if index.negative? || index >= num_frames

  index
end

.resolve_skin_index(num_skins, skin_index) ⇒ Object



244
245
246
247
248
249
# File 'lib/quake/renderer/gl_alias_model.rb', line 244

def self.resolve_skin_index(num_skins, skin_index)
  index = skin_index.to_i
  return 0 if index.negative? || index >= num_skins

  index
end

.resolve_skin_variant_index(variant_count, time: 0.0, intervals: nil) ⇒ Object



234
235
236
237
238
239
240
241
242
# File 'lib/quake/renderer/gl_alias_model.rb', line 234

def self.resolve_skin_variant_index(variant_count, time: 0.0, intervals: nil)
  return 0 if variant_count <= 1

  if intervals && !intervals.empty?
    find_interval_index(intervals, time).clamp(0, variant_count - 1)
  else
    ((time.to_f * 10.0).to_i & 3) % variant_count
  end
end

.translate_player_skin(pixels, colors:) ⇒ Object



251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/quake/renderer/gl_alias_model.rb', line 251

def self.translate_player_skin(pixels, colors:)
  top = colors.to_i & 0xf0
  bottom = (colors.to_i & 15) << 4
  translate = (0..255).to_a

  16.times do |i|
    translate[TOP_RANGE + i] = top < 128 ? top + i : top + 15 - i
    translate[BOTTOM_RANGE + i] = bottom < 128 ? bottom + i : bottom + 15 - i
  end

  pixels.each_byte.map { |idx| translate[idx] }.pack("C*")
end

Instance Method Details

#frame_countObject



381
382
383
# File 'lib/quake/renderer/gl_alias_model.rb', line 381

def frame_count
  @model.frames.size
end

#pose_mesh(frame) ⇒ Object

Flat per-pose mesh [s, t, normal_index, x, y, z] per emitted vertex, precomputed once: texcoords and scale/scale_origin baking don't change between frames.



388
389
390
391
# File 'lib/quake/renderer/gl_alias_model.rb', line 388

def pose_mesh(frame)
  @pose_meshes ||= {}
  @pose_meshes[frame.object_id] ||= build_pose_mesh(frame)
end

#random_sync?Boolean

Returns:

  • (Boolean)


190
191
192
# File 'lib/quake/renderer/gl_alias_model.rb', line 190

def random_sync?
  @model.sync_type == ST_RAND
end

#render(frame_index:, lerp: 0.0, position: Math::Vec3::ORIGIN, yaw: 0.0, pitch: 0.0, roll: 0.0, scale: 1.0, skin_index: 0, time: 0.0, ambient_light: 200.0, shade_light: 200.0, view_model: false, client_index: nil, player_colors: nil) ⇒ Object

Render the model at a given frame (interpolation between two frames). frame_index: current frame number lerp: 0.0-1.0 interpolation factor to next frame position: Vec3 world position yaw: rotation angle in degrees



322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
# File 'lib/quake/renderer/gl_alias_model.rb', line 322

def render(frame_index:, lerp: 0.0, position: Math::Vec3::ORIGIN, yaw: 0.0, pitch: 0.0,
           roll: 0.0, scale: 1.0, skin_index: 0, time: 0.0, ambient_light: 200.0,
           shade_light: 200.0, view_model: false, client_index: nil, player_colors: nil)
  frame = resolve_frame(frame_index, time: time)
  next_frame = resolve_frame(frame_index + 1, time: time)
  _ambient, shade_scale = self.class.quake_alias_lighting(
    ambient_light: ambient_light,
    shade_light: shade_light,
    model_name: @model_name,
    view_model: view_model,
    client_index: client_index
  )

  GL.PushMatrix
  self.class.apply_alias_transform(position: position, yaw: yaw, pitch: pitch, roll: roll)

  GL.Enable(GL::TEXTURE_2D)
  resolved_skin_index = self.class.resolve_skin_index(@skin_textures.size, skin_index)
  skin_textures = @skin_textures[resolved_skin_index]
  variant = self.class.resolve_skin_variant_index(
    skin_textures.size, time: time, intervals: @model.skin_intervals[resolved_skin_index]
  )
  skin_texture = if player_colors
                   translated_player_skin_texture(resolved_skin_index, variant, player_colors)
                 else
                   skin_textures&.fetch(variant, skin_textures&.first)
                 end
  GL.BindTexture(GL::TEXTURE_2D, skin_texture) if skin_texture
  GL.ShadeModel(GL::SMOOTH)
  GL.TexEnvi(GL::TEXTURE_ENV, GL::TEXTURE_ENV_MODE, GL::MODULATE)

  quantized = ((yaw.to_f * (SHADEDOT_QUANT / 360.0)).to_i) & (SHADEDOT_QUANT - 1)
  dots = ANORM_DOTS[quantized]
  mesh = pose_mesh(frame)
  next_mesh = pose_mesh(next_frame) if lerp > 0.0 && next_frame

  GL.Begin(GL::TRIANGLES)
  i = 0
  size = mesh.size
  while i < size
    GL.TexCoord2f(mesh[i], mesh[i + 1])
    light = dots[mesh[i + 2]] * shade_scale
    GL.Color3f(light, light, light)
    if next_mesh
      GL.Vertex3f((mesh[i + 3] + (next_mesh[i + 3] - mesh[i + 3]) * lerp) * scale,
                  (mesh[i + 4] + (next_mesh[i + 4] - mesh[i + 4]) * lerp) * scale,
                  (mesh[i + 5] + (next_mesh[i + 5] - mesh[i + 5]) * lerp) * scale)
    else
      GL.Vertex3f(mesh[i + 3] * scale, mesh[i + 4] * scale, mesh[i + 5] * scale)
    end
    i += 6
  end
  GL.End
  GL.TexEnvi(GL::TEXTURE_ENV, GL::TEXTURE_ENV_MODE, GL::REPLACE)
  GL.ShadeModel(GL::FLAT)

  GL.PopMatrix
end