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.
@@ -2,80 +2,378 @@
2
2
 
3
3
  require "opengl"
4
4
 
5
+ require_relative "anorm_dots"
6
+
5
7
  module Quake
6
8
  module Renderer
7
9
  # Renders Quake MDL (alias) models with texture mapping and frame animation.
8
10
  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
11
+ SHADEDOT_QUANT = 16
12
+ TOP_RANGE = 16
13
+ BOTTOM_RANGE = 96
14
+ ANORMS = [
15
+ [-0.525731, 0.000000, 0.850651],
16
+ [-0.442863, 0.238856, 0.864188],
17
+ [-0.295242, 0.000000, 0.955423],
18
+ [-0.309017, 0.500000, 0.809017],
19
+ [-0.162460, 0.262866, 0.951056],
20
+ [0.000000, 0.000000, 1.000000],
21
+ [0.000000, 0.850651, 0.525731],
22
+ [-0.147621, 0.716567, 0.681718],
23
+ [0.147621, 0.716567, 0.681718],
24
+ [0.000000, 0.525731, 0.850651],
25
+ [0.309017, 0.500000, 0.809017],
26
+ [0.525731, 0.000000, 0.850651],
27
+ [0.295242, 0.000000, 0.955423],
28
+ [0.442863, 0.238856, 0.864188],
29
+ [0.162460, 0.262866, 0.951056],
30
+ [-0.681718, 0.147621, 0.716567],
31
+ [-0.809017, 0.309017, 0.500000],
32
+ [-0.587785, 0.425325, 0.688191],
33
+ [-0.850651, 0.525731, 0.000000],
34
+ [-0.864188, 0.442863, 0.238856],
35
+ [-0.716567, 0.681718, 0.147621],
36
+ [-0.688191, 0.587785, 0.425325],
37
+ [-0.500000, 0.809017, 0.309017],
38
+ [-0.238856, 0.864188, 0.442863],
39
+ [-0.425325, 0.688191, 0.587785],
40
+ [-0.716567, 0.681718, -0.147621],
41
+ [-0.500000, 0.809017, -0.309017],
42
+ [-0.525731, 0.850651, 0.000000],
43
+ [0.000000, 0.850651, -0.525731],
44
+ [-0.238856, 0.864188, -0.442863],
45
+ [0.000000, 0.955423, -0.295242],
46
+ [-0.262866, 0.951056, -0.162460],
47
+ [0.000000, 1.000000, 0.000000],
48
+ [0.000000, 0.955423, 0.295242],
49
+ [-0.262866, 0.951056, 0.162460],
50
+ [0.238856, 0.864188, 0.442863],
51
+ [0.262866, 0.951056, 0.162460],
52
+ [0.500000, 0.809017, 0.309017],
53
+ [0.238856, 0.864188, -0.442863],
54
+ [0.262866, 0.951056, -0.162460],
55
+ [0.500000, 0.809017, -0.309017],
56
+ [0.850651, 0.525731, 0.000000],
57
+ [0.716567, 0.681718, 0.147621],
58
+ [0.716567, 0.681718, -0.147621],
59
+ [0.525731, 0.850651, 0.000000],
60
+ [0.425325, 0.688191, 0.587785],
61
+ [0.864188, 0.442863, 0.238856],
62
+ [0.688191, 0.587785, 0.425325],
63
+ [0.809017, 0.309017, 0.500000],
64
+ [0.681718, 0.147621, 0.716567],
65
+ [0.587785, 0.425325, 0.688191],
66
+ [0.955423, 0.295242, 0.000000],
67
+ [1.000000, 0.000000, 0.000000],
68
+ [0.951056, 0.162460, 0.262866],
69
+ [0.850651, -0.525731, 0.000000],
70
+ [0.955423, -0.295242, 0.000000],
71
+ [0.864188, -0.442863, 0.238856],
72
+ [0.951056, -0.162460, 0.262866],
73
+ [0.809017, -0.309017, 0.500000],
74
+ [0.681718, -0.147621, 0.716567],
75
+ [0.850651, 0.000000, 0.525731],
76
+ [0.864188, 0.442863, -0.238856],
77
+ [0.809017, 0.309017, -0.500000],
78
+ [0.951056, 0.162460, -0.262866],
79
+ [0.525731, 0.000000, -0.850651],
80
+ [0.681718, 0.147621, -0.716567],
81
+ [0.681718, -0.147621, -0.716567],
82
+ [0.850651, 0.000000, -0.525731],
83
+ [0.809017, -0.309017, -0.500000],
84
+ [0.864188, -0.442863, -0.238856],
85
+ [0.951056, -0.162460, -0.262866],
86
+ [0.147621, 0.716567, -0.681718],
87
+ [0.309017, 0.500000, -0.809017],
88
+ [0.425325, 0.688191, -0.587785],
89
+ [0.442863, 0.238856, -0.864188],
90
+ [0.587785, 0.425325, -0.688191],
91
+ [0.688191, 0.587785, -0.425325],
92
+ [-0.147621, 0.716567, -0.681718],
93
+ [-0.309017, 0.500000, -0.809017],
94
+ [0.000000, 0.525731, -0.850651],
95
+ [-0.525731, 0.000000, -0.850651],
96
+ [-0.442863, 0.238856, -0.864188],
97
+ [-0.295242, 0.000000, -0.955423],
98
+ [-0.162460, 0.262866, -0.951056],
99
+ [0.000000, 0.000000, -1.000000],
100
+ [0.295242, 0.000000, -0.955423],
101
+ [0.162460, 0.262866, -0.951056],
102
+ [-0.442863, -0.238856, -0.864188],
103
+ [-0.309017, -0.500000, -0.809017],
104
+ [-0.162460, -0.262866, -0.951056],
105
+ [0.000000, -0.850651, -0.525731],
106
+ [-0.147621, -0.716567, -0.681718],
107
+ [0.147621, -0.716567, -0.681718],
108
+ [0.000000, -0.525731, -0.850651],
109
+ [0.309017, -0.500000, -0.809017],
110
+ [0.442863, -0.238856, -0.864188],
111
+ [0.162460, -0.262866, -0.951056],
112
+ [0.238856, -0.864188, -0.442863],
113
+ [0.500000, -0.809017, -0.309017],
114
+ [0.425325, -0.688191, -0.587785],
115
+ [0.716567, -0.681718, -0.147621],
116
+ [0.688191, -0.587785, -0.425325],
117
+ [0.587785, -0.425325, -0.688191],
118
+ [0.000000, -0.955423, -0.295242],
119
+ [0.000000, -1.000000, 0.000000],
120
+ [0.262866, -0.951056, -0.162460],
121
+ [0.000000, -0.850651, 0.525731],
122
+ [0.000000, -0.955423, 0.295242],
123
+ [0.238856, -0.864188, 0.442863],
124
+ [0.262866, -0.951056, 0.162460],
125
+ [0.500000, -0.809017, 0.309017],
126
+ [0.716567, -0.681718, 0.147621],
127
+ [0.525731, -0.850651, 0.000000],
128
+ [-0.238856, -0.864188, -0.442863],
129
+ [-0.500000, -0.809017, -0.309017],
130
+ [-0.262866, -0.951056, -0.162460],
131
+ [-0.850651, -0.525731, 0.000000],
132
+ [-0.716567, -0.681718, -0.147621],
133
+ [-0.716567, -0.681718, 0.147621],
134
+ [-0.525731, -0.850651, 0.000000],
135
+ [-0.500000, -0.809017, 0.309017],
136
+ [-0.238856, -0.864188, 0.442863],
137
+ [-0.262866, -0.951056, 0.162460],
138
+ [-0.864188, -0.442863, 0.238856],
139
+ [-0.809017, -0.309017, 0.500000],
140
+ [-0.688191, -0.587785, 0.425325],
141
+ [-0.681718, -0.147621, 0.716567],
142
+ [-0.442863, -0.238856, 0.864188],
143
+ [-0.587785, -0.425325, 0.688191],
144
+ [-0.309017, -0.500000, 0.809017],
145
+ [-0.147621, -0.716567, 0.681718],
146
+ [-0.425325, -0.688191, 0.587785],
147
+ [-0.162460, -0.262866, 0.951056],
148
+ [0.442863, -0.238856, 0.864188],
149
+ [0.162460, -0.262866, 0.951056],
150
+ [0.309017, -0.500000, 0.809017],
151
+ [0.147621, -0.716567, 0.681718],
152
+ [0.000000, -0.525731, 0.850651],
153
+ [0.425325, -0.688191, 0.587785],
154
+ [0.587785, -0.425325, 0.688191],
155
+ [0.688191, -0.587785, 0.425325],
156
+ [-0.955423, 0.295242, 0.000000],
157
+ [-0.951056, 0.162460, 0.262866],
158
+ [-1.000000, 0.000000, 0.000000],
159
+ [-0.850651, 0.000000, 0.525731],
160
+ [-0.955423, -0.295242, 0.000000],
161
+ [-0.951056, -0.162460, 0.262866],
162
+ [-0.864188, 0.442863, -0.238856],
163
+ [-0.951056, 0.162460, -0.262866],
164
+ [-0.809017, 0.309017, -0.500000],
165
+ [-0.864188, -0.442863, -0.238856],
166
+ [-0.951056, -0.162460, -0.262866],
167
+ [-0.809017, -0.309017, -0.500000],
168
+ [-0.681718, 0.147621, -0.716567],
169
+ [-0.681718, -0.147621, -0.716567],
170
+ [-0.850651, 0.000000, -0.525731],
171
+ [-0.688191, 0.587785, -0.425325],
172
+ [-0.587785, 0.425325, -0.688191],
173
+ [-0.425325, 0.688191, -0.587785],
174
+ [-0.425325, -0.688191, -0.587785],
175
+ [-0.587785, -0.425325, -0.688191],
176
+ [-0.688191, -0.587785, -0.425325]
177
+ ].freeze
178
+
179
+ ST_RAND = 1 # sync_type: entities get a random animation phase
12
180
 
13
- def initialize(model, palette)
181
+ def initialize(model, palette, model_name: nil)
14
182
  @model = model
15
183
  @palette = palette
184
+ @model_name = model_name
16
185
  @skin_textures = []
186
+ @translated_skin_textures = {}
17
187
  upload_skins
18
188
  end
19
189
 
190
+ def random_sync?
191
+ @model.sync_type == ST_RAND
192
+ end
193
+
194
+ def self.alias_texcoord(stvert, triangle, skin_width:, skin_height:)
195
+ s = stvert.s.to_f
196
+ t = stvert.t.to_f
197
+
198
+ s += skin_width * 0.5 if stvert.on_seam != 0 && triangle.faces_front == 0
199
+
200
+ [(s + 0.5) / skin_width, (t + 0.5) / skin_height]
201
+ end
202
+
203
+ def self.resolve_frame_entry(entry, time: 0.0)
204
+ case entry
205
+ when Mdl::Frame
206
+ entry
207
+ when Mdl::FrameGroup
208
+ entry.frames[find_interval_index(entry.intervals, time)]
209
+ end
210
+ end
211
+
212
+ # Mod_FindInterval: intervals are cumulative timestamps, so groups may
213
+ # hold poses for different durations.
214
+ def self.find_interval_index(intervals, time)
215
+ return 0 if intervals.nil? || intervals.empty?
216
+
217
+ full = intervals.last.to_f
218
+ return 0 if full <= 0.0
219
+
220
+ target = time.to_f % full
221
+ intervals.each_with_index do |value, i|
222
+ return i if value.to_f > target
223
+ end
224
+ intervals.size - 1
225
+ end
226
+
227
+ def self.resolve_frame_index(num_frames, frame_index)
228
+ index = frame_index.to_i
229
+ return 0 if index.negative? || index >= num_frames
230
+
231
+ index
232
+ end
233
+
234
+ def self.resolve_skin_variant_index(variant_count, time: 0.0, intervals: nil)
235
+ return 0 if variant_count <= 1
236
+
237
+ if intervals && !intervals.empty?
238
+ find_interval_index(intervals, time).clamp(0, variant_count - 1)
239
+ else
240
+ ((time.to_f * 10.0).to_i & 3) % variant_count
241
+ end
242
+ end
243
+
244
+ def self.resolve_skin_index(num_skins, skin_index)
245
+ index = skin_index.to_i
246
+ return 0 if index.negative? || index >= num_skins
247
+
248
+ index
249
+ end
250
+
251
+ def self.translate_player_skin(pixels, colors:)
252
+ top = colors.to_i & 0xf0
253
+ bottom = (colors.to_i & 15) << 4
254
+ translate = (0..255).to_a
255
+
256
+ 16.times do |i|
257
+ translate[TOP_RANGE + i] = top < 128 ? top + i : top + 15 - i
258
+ translate[BOTTOM_RANGE + i] = bottom < 128 ? bottom + i : bottom + 15 - i
259
+ end
260
+
261
+ pixels.each_byte.map { |idx| translate[idx] }.pack("C*")
262
+ end
263
+
264
+ def self.apply_alias_transform(position:, yaw:, pitch:, roll: 0.0)
265
+ GL.Translatef(position.x, position.y, position.z)
266
+ GL.Rotatef(yaw, 0.0, 0.0, 1.0)
267
+ GL.Rotatef(pitch, 0.0, 1.0, 0.0)
268
+ GL.Rotatef(roll, 1.0, 0.0, 0.0)
269
+ end
270
+
271
+ def self.quake_alias_lighting(ambient_light:, shade_light:, model_name: nil, view_model: false, client_index: nil)
272
+ ambient = ambient_light.to_f
273
+ shade = shade_light.to_f
274
+
275
+ ambient = shade = 24.0 if view_model && ambient < 24.0
276
+
277
+ ambient = 128.0 if ambient > 128.0
278
+ shade = 192.0 - ambient if ambient + shade > 192.0
279
+
280
+ if client_index && client_index.between?(1, 32) && ambient < 8.0
281
+ ambient = shade = 8.0
282
+ end
283
+
284
+ if %w[progs/flame2.mdl progs/flame.mdl].include?(model_name)
285
+ ambient = shade = 256.0
286
+ end
287
+
288
+ [ambient, shade / 200.0]
289
+ end
290
+
291
+ def self.alias_shade_color(vertex, shade_scale:, shadedot: 1.0)
292
+ light = shadedot.to_f * shade_scale.to_f
293
+ [light, light, light]
294
+ end
295
+
296
+ def self.alias_shadedot(yaw:, normal_index:)
297
+ quantized = ((yaw.to_f * (SHADEDOT_QUANT / 360.0)).to_i) & (SHADEDOT_QUANT - 1)
298
+ ANORM_DOTS[quantized].fetch(normal_index.to_i, 1.0)
299
+ end
300
+
301
+ def self.alias_vertex_position(model, vertex, model_name: nil)
302
+ if model_name == "progs/eyes.mdl"
303
+ return [
304
+ vertex.x * model.scale.x * 2.0 + model.scale_origin.x,
305
+ vertex.y * model.scale.y * 2.0 + model.scale_origin.y,
306
+ vertex.z * model.scale.z * 2.0 + model.scale_origin.z - 30.0
307
+ ]
308
+ end
309
+
310
+ [
311
+ vertex.x * model.scale.x + model.scale_origin.x,
312
+ vertex.y * model.scale.y + model.scale_origin.y,
313
+ vertex.z * model.scale.z + model.scale_origin.z
314
+ ]
315
+ end
316
+
20
317
  # Render the model at a given frame (interpolation between two frames).
21
318
  # frame_index: current frame number
22
319
  # lerp: 0.0-1.0 interpolation factor to next frame
23
320
  # position: Vec3 world position
24
321
  # 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)
322
+ def render(frame_index:, lerp: 0.0, position: Math::Vec3::ORIGIN, yaw: 0.0, pitch: 0.0,
323
+ roll: 0.0, scale: 1.0, skin_index: 0, time: 0.0, ambient_light: 200.0,
324
+ shade_light: 200.0, view_model: false, client_index: nil, player_colors: nil)
325
+ frame = resolve_frame(frame_index, time: time)
326
+ next_frame = resolve_frame(frame_index + 1, time: time)
327
+ _ambient, shade_scale = self.class.quake_alias_lighting(
328
+ ambient_light: ambient_light,
329
+ shade_light: shade_light,
330
+ model_name: @model_name,
331
+ view_model: view_model,
332
+ client_index: client_index
333
+ )
28
334
 
29
335
  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)
336
+ self.class.apply_alias_transform(position: position, yaw: yaw, pitch: pitch, roll: roll)
33
337
 
34
338
  GL.Enable(GL::TEXTURE_2D)
35
- GL.BindTexture(GL::TEXTURE_2D, @skin_textures[0]) if @skin_textures.any?
339
+ resolved_skin_index = self.class.resolve_skin_index(@skin_textures.size, skin_index)
340
+ skin_textures = @skin_textures[resolved_skin_index]
341
+ variant = self.class.resolve_skin_variant_index(
342
+ skin_textures.size, time: time, intervals: @model.skin_intervals[resolved_skin_index]
343
+ )
344
+ skin_texture = if player_colors
345
+ translated_player_skin_texture(resolved_skin_index, variant, player_colors)
346
+ else
347
+ skin_textures&.fetch(variant, skin_textures&.first)
348
+ end
349
+ GL.BindTexture(GL::TEXTURE_2D, skin_texture) if skin_texture
350
+ GL.ShadeModel(GL::SMOOTH)
351
+ GL.TexEnvi(GL::TEXTURE_ENV, GL::TEXTURE_ENV_MODE, GL::MODULATE)
352
+
353
+ quantized = ((yaw.to_f * (SHADEDOT_QUANT / 360.0)).to_i) & (SHADEDOT_QUANT - 1)
354
+ dots = ANORM_DOTS[quantized]
355
+ mesh = pose_mesh(frame)
356
+ next_mesh = pose_mesh(next_frame) if lerp > 0.0 && next_frame
36
357
 
37
358
  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)
359
+ i = 0
360
+ size = mesh.size
361
+ while i < size
362
+ GL.TexCoord2f(mesh[i], mesh[i + 1])
363
+ light = dots[mesh[i + 2]] * shade_scale
364
+ GL.Color3f(light, light, light)
365
+ if next_mesh
366
+ GL.Vertex3f((mesh[i + 3] + (next_mesh[i + 3] - mesh[i + 3]) * lerp) * scale,
367
+ (mesh[i + 4] + (next_mesh[i + 4] - mesh[i + 4]) * lerp) * scale,
368
+ (mesh[i + 5] + (next_mesh[i + 5] - mesh[i + 5]) * lerp) * scale)
369
+ else
370
+ GL.Vertex3f(mesh[i + 3] * scale, mesh[i + 4] * scale, mesh[i + 5] * scale)
76
371
  end
372
+ i += 6
77
373
  end
78
374
  GL.End
375
+ GL.TexEnvi(GL::TEXTURE_ENV, GL::TEXTURE_ENV_MODE, GL::REPLACE)
376
+ GL.ShadeModel(GL::FLAT)
79
377
 
80
378
  GL.PopMatrix
81
379
  end
@@ -84,39 +382,90 @@ module Quake
84
382
  @model.frames.size
85
383
  end
86
384
 
385
+ # Flat per-pose mesh [s, t, normal_index, x, y, z] per emitted vertex,
386
+ # precomputed once: texcoords and scale/scale_origin baking don't
387
+ # change between frames.
388
+ def pose_mesh(frame)
389
+ @pose_meshes ||= {}
390
+ @pose_meshes[frame.object_id] ||= build_pose_mesh(frame)
391
+ end
392
+
87
393
  private
88
394
 
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
395
+ def build_pose_mesh(frame)
396
+ mesh = []
397
+ @model.triangles.each do |tri|
398
+ # Original MDL winding, like BuildTris' strip emission -- the
399
+ # visible side is GL-back-facing under Quake's axis flip, same
400
+ # as world polys, so glCullFace(GL_FRONT) keeps the outside.
401
+ tri.vertex_indices.each do |vi|
402
+ stvert = @model.stverts[vi]
403
+ s, t = self.class.alias_texcoord(
404
+ stvert, tri, skin_width: @model.skin_width, skin_height: @model.skin_height
405
+ )
406
+ vertex = frame.vertices[vi]
407
+ x, y, z = self.class.alias_vertex_position(@model, vertex, model_name: @model_name)
408
+ mesh << s << t << vertex.normal_index << x << y << z
409
+ end
95
410
  end
411
+ mesh
412
+ end
413
+
414
+ def resolve_frame(index, time: 0.0)
415
+ return nil if @model.frames.empty?
416
+ entry = @model.frames[self.class.resolve_frame_index(@model.frames.size, index)]
417
+ self.class.resolve_frame_entry(entry, time: time)
96
418
  end
97
419
 
98
420
  def upload_skins
99
421
  @model.skins.each do |skin_variants|
100
- pixels = skin_variants[0] # use first variant
101
- next if pixels.nil?
422
+ textures = skin_variants.filter_map do |pixels|
423
+ next if pixels.nil?
102
424
 
103
- rgba = @palette.indexed_to_rgba(pixels)
425
+ rgba = @palette.indexed_to_rgba(pixels, transparent_index: nil)
104
426
 
105
- buf = "\0" * 4
106
- GL.GenTextures(1, buf)
107
- tex_id = buf.unpack1("V")
427
+ buf = "\0" * 4
428
+ GL.GenTextures(1, buf)
429
+ tex_id = buf.unpack1("V")
108
430
 
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)
431
+ GL.BindTexture(GL::TEXTURE_2D, tex_id)
432
+ GL.TexImage2D(GL::TEXTURE_2D, 0, GL::RGBA,
433
+ @model.skin_width, @model.skin_height, 0,
434
+ GL::RGBA, GL::UNSIGNED_BYTE, rgba)
435
+ GL.TexParameteri(GL::TEXTURE_2D, GL::TEXTURE_MIN_FILTER, GL::LINEAR_MIPMAP_NEAREST)
436
+ GL.TexParameteri(GL::TEXTURE_2D, GL::TEXTURE_MAG_FILTER, GL::LINEAR)
437
+ GL.GenerateMipmap(GL::TEXTURE_2D)
116
438
 
117
- @skin_textures << tex_id
439
+ tex_id
440
+ end
441
+
442
+ @skin_textures << textures
118
443
  end
119
444
  end
445
+
446
+ def translated_player_skin_texture(skin_index, variant, colors)
447
+ key = [skin_index, variant, colors.to_i]
448
+ return @translated_skin_textures[key] if @translated_skin_textures.key?(key)
449
+
450
+ pixels = @model.skins.dig(skin_index, variant) || @model.skins.dig(skin_index, 0)
451
+ return nil unless pixels
452
+
453
+ translated = self.class.translate_player_skin(pixels, colors: colors)
454
+ rgba = @palette.indexed_to_rgba(translated, transparent_index: nil)
455
+
456
+ buf = "\0" * 4
457
+ GL.GenTextures(1, buf)
458
+ tex_id = buf.unpack1("V")
459
+
460
+ GL.BindTexture(GL::TEXTURE_2D, tex_id)
461
+ GL.TexImage2D(GL::TEXTURE_2D, 0, GL::RGBA,
462
+ @model.skin_width, @model.skin_height, 0,
463
+ GL::RGBA, GL::UNSIGNED_BYTE, rgba)
464
+ GL.TexParameteri(GL::TEXTURE_2D, GL::TEXTURE_MIN_FILTER, GL::LINEAR)
465
+ GL.TexParameteri(GL::TEXTURE_2D, GL::TEXTURE_MAG_FILTER, GL::LINEAR)
466
+
467
+ @translated_skin_textures[key] = tex_id
468
+ end
120
469
  end
121
470
  end
122
471
  end