stagecraft 1.0.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.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +207 -0
  4. data/Rakefile +8 -0
  5. data/docs/api.md +85 -0
  6. data/docs/tutorial.md +344 -0
  7. data/examples/01_spinning_cube.rb +13 -0
  8. data/examples/02_primitives.rb +21 -0
  9. data/examples/03_pbr_materials.rb +20 -0
  10. data/examples/04_lights.rb +22 -0
  11. data/examples/05_directional_shadow.rb +23 -0
  12. data/examples/06_transparency.rb +19 -0
  13. data/examples/07_texture.rb +22 -0
  14. data/examples/08_shader_material.rb +30 -0
  15. data/examples/09_gltf_loader.rb +24 -0
  16. data/examples/10_animation.rb +21 -0
  17. data/examples/11_scene_graph.rb +20 -0
  18. data/examples/12_orbit_controls.rb +13 -0
  19. data/examples/13_orthographic_camera.rb +18 -0
  20. data/examples/14_raycaster.rb +14 -0
  21. data/examples/15_offscreen.rb +22 -0
  22. data/examples/16_msaa.rb +13 -0
  23. data/examples/17_physics_binding.rb +22 -0
  24. data/examples/18_custom_geometry.rb +22 -0
  25. data/examples/19_imgui_stats.rb +24 -0
  26. data/examples/20_window_adapters.rb +15 -0
  27. data/examples/README.md +11 -0
  28. data/examples/support/demo.rb +49 -0
  29. data/lib/stagecraft/animation/action.rb +79 -0
  30. data/lib/stagecraft/animation/clip.rb +15 -0
  31. data/lib/stagecraft/animation/mixer.rb +97 -0
  32. data/lib/stagecraft/animation/track.rb +122 -0
  33. data/lib/stagecraft/app.rb +61 -0
  34. data/lib/stagecraft/cameras/camera.rb +15 -0
  35. data/lib/stagecraft/cameras/orthographic_camera.rb +60 -0
  36. data/lib/stagecraft/cameras/perspective_camera.rb +69 -0
  37. data/lib/stagecraft/color.rb +56 -0
  38. data/lib/stagecraft/controls/orbit_controls.rb +119 -0
  39. data/lib/stagecraft/core/attribute.rb +91 -0
  40. data/lib/stagecraft/core/bounding.rb +131 -0
  41. data/lib/stagecraft/core/geometry.rb +155 -0
  42. data/lib/stagecraft/core/mesh.rb +26 -0
  43. data/lib/stagecraft/core/node.rb +163 -0
  44. data/lib/stagecraft/core/observed_value.rb +182 -0
  45. data/lib/stagecraft/core/raycaster.rb +62 -0
  46. data/lib/stagecraft/core/scene.rb +11 -0
  47. data/lib/stagecraft/core/skin.rb +23 -0
  48. data/lib/stagecraft/geometries.rb +200 -0
  49. data/lib/stagecraft/lights/ambient_light.rb +10 -0
  50. data/lib/stagecraft/lights/directional_light.rb +31 -0
  51. data/lib/stagecraft/lights/light.rb +30 -0
  52. data/lib/stagecraft/lights/point_light.rb +23 -0
  53. data/lib/stagecraft/lights/spot_light.rb +36 -0
  54. data/lib/stagecraft/loaders/gltf_loader.rb +387 -0
  55. data/lib/stagecraft/materials/material.rb +58 -0
  56. data/lib/stagecraft/materials/pbr_material.rb +75 -0
  57. data/lib/stagecraft/materials/shader_material.rb +21 -0
  58. data/lib/stagecraft/materials/unlit_material.rb +26 -0
  59. data/lib/stagecraft/physics_binding.rb +63 -0
  60. data/lib/stagecraft/renderer/features.rb +58 -0
  61. data/lib/stagecraft/renderer/frame.rb +45 -0
  62. data/lib/stagecraft/renderer/frame_resources.rb +467 -0
  63. data/lib/stagecraft/renderer/gpu_context.rb +120 -0
  64. data/lib/stagecraft/renderer/pipeline_cache.rb +59 -0
  65. data/lib/stagecraft/renderer/pipeline_factory.rb +470 -0
  66. data/lib/stagecraft/renderer/render_list.rb +80 -0
  67. data/lib/stagecraft/renderer/renderer.rb +336 -0
  68. data/lib/stagecraft/renderer/resource_cache.rb +176 -0
  69. data/lib/stagecraft/renderer/shaders/common.wgsl +48 -0
  70. data/lib/stagecraft/renderer/shaders/pbr.wgsl +251 -0
  71. data/lib/stagecraft/renderer/shaders/shadow.wgsl +37 -0
  72. data/lib/stagecraft/renderer/shaders/skinning.wgsl +6 -0
  73. data/lib/stagecraft/renderer/shaders/tonemap.wgsl +37 -0
  74. data/lib/stagecraft/renderer/shaders/unlit.wgsl +89 -0
  75. data/lib/stagecraft/renderer/shaders.rb +64 -0
  76. data/lib/stagecraft/renderer/stats.rb +41 -0
  77. data/lib/stagecraft/renderer/uniform_packer.rb +91 -0
  78. data/lib/stagecraft/renderer/wgpu_compatibility.rb +41 -0
  79. data/lib/stagecraft/textures/cube_texture.rb +16 -0
  80. data/lib/stagecraft/textures/sampler_state.rb +58 -0
  81. data/lib/stagecraft/textures/texture.rb +71 -0
  82. data/lib/stagecraft/version.rb +5 -0
  83. data/lib/stagecraft/window/adapter.rb +38 -0
  84. data/lib/stagecraft/window/glfw.rb +67 -0
  85. data/lib/stagecraft/window/sdl3.rb +57 -0
  86. data/lib/stagecraft.rb +60 -0
  87. metadata +187 -0
@@ -0,0 +1,470 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stagecraft
4
+ class Renderer
5
+ class PipelineFactory
6
+ MaterialBinding = Struct.new(
7
+ :owner, :version, :texture_versions, :value_signature,
8
+ :buffer, :bind_group, :signature
9
+ )
10
+ ATTRIBUTE_LOCATIONS = {
11
+ position: 0, normal: 1, uv: 2, tangent: 3, color: 4, joints: 5, weights: 6,
12
+ uv1: 7
13
+ }.freeze
14
+ FORMAT_SIZES = {
15
+ float32: 4, float32x2: 8, float32x3: 12, float32x4: 16,
16
+ sint8x2: 2, sint8x4: 4, snorm8x2: 2, snorm8x4: 4,
17
+ sint16x2: 4, sint16x4: 8, snorm16x2: 4, snorm16x4: 8,
18
+ uint16: 2, uint16x2: 4, uint16x4: 8,
19
+ unorm16x2: 4, unorm16x4: 8,
20
+ uint32: 4, uint32x2: 8, uint32x3: 12, uint32x4: 16,
21
+ unorm8x2: 2, unorm8x4: 4, uint8x2: 2, uint8x4: 4
22
+ }.freeze
23
+
24
+ def initialize(device:, queue:, resources:, resource_cache:, stats:, sample_count:)
25
+ @device = device
26
+ @queue = queue
27
+ @resources = resources
28
+ @resource_cache = resource_cache
29
+ @sample_count = sample_count
30
+ @stats = stats
31
+ @cache = PipelineCache.new(on_change: ->(amount) { stats.increment(:pipelines, amount) })
32
+ @material_layouts = {}
33
+ @material_bindings = {}
34
+ end
35
+
36
+ def pipeline_for(item, shadow: false)
37
+ material = item.mesh.material
38
+ layout_signature = material_layout_signature(material)
39
+ key = PipelineCache::Key.new(
40
+ material_class: material_pipeline_signature(material, layout_signature),
41
+ feature_bits: Features.bits(item.features),
42
+ vertex_layout_id: Features.vertex_layout_id(item.mesh.geometry),
43
+ blend_state: material.blend,
44
+ depth_state: [material.depth_test, material.depth_write],
45
+ cull_mode: material.side,
46
+ sample_count: shadow ? 1 : @sample_count,
47
+ color_format: shadow ? :depth32_float : :rgba16_float,
48
+ shadow_pass: shadow
49
+ )
50
+ @cache.fetch(key) { build_pipeline(item, shadow:) }
51
+ end
52
+
53
+ def material_binding(material, features)
54
+ signature = material_layout_signature(material)
55
+ key = [material.object_id, signature]
56
+ purge_obsolete_material_bindings(material, key)
57
+ texture_versions = material_texture_versions(material)
58
+ value_signature = material_value_signature(material)
59
+ existing = @material_bindings[key]
60
+ if existing&.owner&.equal?(material) &&
61
+ existing.version == material.version &&
62
+ existing.texture_versions == texture_versions &&
63
+ existing.value_signature == value_signature
64
+ return existing.bind_group
65
+ end
66
+
67
+ bytes, textures = material_bytes_and_textures(material)
68
+ if existing&.owner&.equal?(material) &&
69
+ existing.buffer.size == bytes.bytesize &&
70
+ existing.texture_versions == texture_versions
71
+ @queue.write_buffer(existing.buffer, 0, bytes)
72
+ existing.version = material.version
73
+ existing.value_signature = value_signature
74
+ return existing.bind_group
75
+ end
76
+
77
+ release_material_binding(existing)
78
+ buffer = @device.create_buffer_with_data(
79
+ label: "stagecraft material",
80
+ data: bytes.empty? ? "\0".b * 16 : bytes,
81
+ usage: %i[uniform copy_dst]
82
+ )
83
+ @stats.increment(:buffers)
84
+ layout = material_layout(material)
85
+ bind_group = @device.create_bind_group(
86
+ layout:,
87
+ entries: material_entries(buffer, textures, material)
88
+ )
89
+ @material_bindings[key] = MaterialBinding.new(
90
+ material, material.version, texture_versions, value_signature,
91
+ buffer, bind_group, signature
92
+ )
93
+ bind_group
94
+ end
95
+
96
+ def post_pipeline(format)
97
+ defines = format.to_s.end_with?("_srgb") ? Set.new : Set[:ENCODE_SRGB]
98
+ key = [:post, format, defines.hash]
99
+ @cache.fetch(key) do
100
+ shader = @device.create_shader_module(code: Shaders.compose("tonemap.wgsl", defines:))
101
+ layout = @device.create_pipeline_layout(bind_group_layouts: [post_layout])
102
+ @device.create_render_pipeline(
103
+ label: "stagecraft tonemap",
104
+ layout:,
105
+ vertex: { module: shader, entry_point: "vs_main" },
106
+ primitive: { topology: :triangle_list, cull_mode: :none },
107
+ fragment: {
108
+ module: shader,
109
+ entry_point: "fs_main",
110
+ targets: [{ format: }]
111
+ }
112
+ )
113
+ end
114
+ end
115
+
116
+ def post_layout
117
+ @post_layout ||= @device.create_bind_group_layout(
118
+ entries: [
119
+ { binding: 0, visibility: :fragment, texture: { sample_type: :float } },
120
+ { binding: 1, visibility: :fragment, sampler: { type: :filtering } }
121
+ ]
122
+ )
123
+ end
124
+
125
+ def dispose
126
+ @cache.clear
127
+ @material_bindings.each_value { |binding| release_material_binding(binding) }
128
+ @material_bindings.clear
129
+ @material_layouts.each_value { |layout| layout.release if layout.respond_to?(:release) }
130
+ @material_layouts.clear
131
+ @post_layout&.release
132
+ end
133
+
134
+ private
135
+
136
+ def build_pipeline(item, shadow:)
137
+ material = item.mesh.material
138
+ source = if shadow
139
+ Shaders.compose("shadow.wgsl", defines: item.features)
140
+ else
141
+ material_shader_source(material, item.features)
142
+ end
143
+ shader = @device.create_shader_module(code: source)
144
+ groups = if shadow
145
+ [@resources.shadow_frame_layout, @resources.empty_layout, @resources.object_layout]
146
+ else
147
+ [@resources.frame_layout, material_layout(material), @resources.object_layout]
148
+ end
149
+ layout = @device.create_pipeline_layout(bind_group_layouts: groups)
150
+ descriptor = {
151
+ label: shadow ? "stagecraft shadow pipeline" : "stagecraft material pipeline",
152
+ layout:,
153
+ vertex: {
154
+ module: shader,
155
+ entry_point: "vs_main",
156
+ buffers: vertex_buffers(item.mesh.geometry, material, shadow:)
157
+ },
158
+ primitive: {
159
+ topology: topology(item.mesh.geometry),
160
+ cull_mode: shadow ? :front : cull_mode(material)
161
+ },
162
+ depth_stencil: {
163
+ format: shadow ? :depth32_float : :depth24_plus,
164
+ depth_write_enabled: shadow || material.depth_write,
165
+ depth_compare: material.depth_test || shadow ? :less : :always,
166
+ depth_bias: shadow ? 2 : 0,
167
+ depth_bias_slope_scale: shadow ? 2.0 : 0.0
168
+ },
169
+ multisample: { count: shadow ? 1 : @sample_count }
170
+ }
171
+ if %i[triangle_strip line_strip].include?(item.mesh.geometry.topology) && item.mesh.geometry.index
172
+ descriptor[:primitive][:strip_index_format] = item.mesh.geometry.index.format
173
+ end
174
+ unless shadow
175
+ descriptor[:fragment] = {
176
+ module: shader,
177
+ entry_point: "fs_main",
178
+ targets: [{ format: :rgba16_float, blend: blend_state(material) }]
179
+ }
180
+ end
181
+ @device.create_render_pipeline(**descriptor)
182
+ end
183
+
184
+ def material_shader_source(material, features)
185
+ case material
186
+ when Materials::PBR then Shaders.compose("pbr.wgsl", defines: features)
187
+ when Materials::Unlit then Shaders.compose("unlit.wgsl", defines: features)
188
+ when Materials::Shader then custom_shader_source(material)
189
+ else
190
+ raise Error, "unsupported material #{material.class}"
191
+ end
192
+ end
193
+
194
+ def custom_shader_source(material)
195
+ packed = UniformPacker.new.pack(material.uniforms)
196
+ common = Shaders.read("common.wgsl")
197
+ source = material.wgsl.gsub(%r{^\s*//#include\s+["']common\.wgsl["']\s*$}, common)
198
+ source = "#{common}\n#{source}" unless source.include?("struct FrameUniforms")
199
+ declarations = [packed.struct_source, "@group(1) @binding(0) var<uniform> material: MaterialUniforms;"]
200
+ packed.textures.each_with_index do |(name, _texture), index|
201
+ binding = 1 + (index * 2)
202
+ declarations << "@group(1) @binding(#{binding}) var #{name}_sampler: sampler;"
203
+ declarations << "@group(1) @binding(#{binding + 1}) var #{name}: texture_2d<f32>;"
204
+ end
205
+ "#{declarations.join("\n")}\n#{source}"
206
+ end
207
+
208
+ def material_layout(material)
209
+ signature = material_layout_signature(material)
210
+ @material_layouts[signature] ||= @device.create_bind_group_layout(
211
+ label: "stagecraft material layout",
212
+ entries: material_layout_entries(material)
213
+ )
214
+ end
215
+
216
+ def material_layout_signature(material)
217
+ case material
218
+ when Materials::PBR then :pbr
219
+ when Materials::Unlit then :unlit
220
+ when Materials::Shader
221
+ packed = UniformPacker.new.pack(material.uniforms)
222
+ [
223
+ :shader,
224
+ packed.fields.map { |field| [field.name, field.type] },
225
+ packed.textures.map(&:first)
226
+ ]
227
+ else
228
+ material.class.name
229
+ end
230
+ end
231
+
232
+ def material_pipeline_signature(material, layout_signature)
233
+ return [material.class, layout_signature, material.wgsl] if material.is_a?(Materials::Shader)
234
+
235
+ [material.class, layout_signature]
236
+ end
237
+
238
+ def material_layout_entries(material)
239
+ base = [{ binding: 0, visibility: %i[vertex fragment], buffer: { type: :uniform } }]
240
+ case material
241
+ when Materials::PBR
242
+ 5.times do |index|
243
+ binding = 1 + (index * 2)
244
+ base << { binding:, visibility: :fragment, sampler: { type: :filtering } }
245
+ base << {
246
+ binding: binding + 1,
247
+ visibility: :fragment,
248
+ texture: { sample_type: :float }
249
+ }
250
+ end
251
+ when Materials::Unlit
252
+ base << { binding: 1, visibility: :fragment, sampler: { type: :filtering } }
253
+ base << { binding: 2, visibility: :fragment, texture: { sample_type: :float } }
254
+ when Materials::Shader
255
+ UniformPacker.new.pack(material.uniforms).textures.each_index do |index|
256
+ binding = 1 + (index * 2)
257
+ base << {
258
+ binding:,
259
+ visibility: %i[vertex fragment],
260
+ sampler: { type: :filtering }
261
+ }
262
+ base << {
263
+ binding: binding + 1,
264
+ visibility: %i[vertex fragment],
265
+ texture: { sample_type: :float }
266
+ }
267
+ end
268
+ end
269
+ base
270
+ end
271
+
272
+ def material_bytes_and_textures(material)
273
+ case material
274
+ when Materials::PBR then pbr_bytes_and_textures(material)
275
+ when Materials::Unlit then unlit_bytes_and_textures(material)
276
+ when Materials::Shader
277
+ packed = UniformPacker.new.pack(material.uniforms)
278
+ [packed.bytes, packed.textures.map(&:last)]
279
+ end
280
+ end
281
+
282
+ def pbr_bytes_and_textures(material)
283
+ maps = [
284
+ material.base_color_map,
285
+ material.metallic_roughness_map,
286
+ material.normal_map,
287
+ material.occlusion_map,
288
+ material.emissive_map
289
+ ]
290
+ color = material.base_color.to_a
291
+ color[3] *= material.opacity
292
+ bytes = color.pack("e*")
293
+ bytes << [material.metallic, material.roughness, material.normal_scale,
294
+ material.occlusion_strength].pack("e*")
295
+ bytes << [*material.emissive.to_a.first(3), material.emissive_strength].pack("e*")
296
+ bytes << maps.first(4).map { |map| map ? 1 : 0 }.pack("L<*")
297
+ bytes << [maps[4] ? 1 : 0].pack("L<")
298
+ bytes << [material.alpha_cutoff].pack("e")
299
+ bytes << "\0".b * 8
300
+ bytes << Materials::PBR::UV_SET_ATTRIBUTES.map { |name| material.public_send(name) }
301
+ .first(4).pack("L<*")
302
+ bytes << [material.emissive_uv_set].pack("L<")
303
+ bytes << "\0".b * 12
304
+ Materials::PBR::TRANSFORM_ATTRIBUTES.each do |name|
305
+ bytes << pack_mat3(material.public_send(name))
306
+ end
307
+ fallbacks = [
308
+ @resources.fallback_white,
309
+ @resources.fallback_white,
310
+ @resources.fallback_normal,
311
+ @resources.fallback_white,
312
+ @resources.fallback_black
313
+ ]
314
+ [bytes, resolve_textures(maps, fallbacks)]
315
+ end
316
+
317
+ def unlit_bytes_and_textures(material)
318
+ color = material.color.to_a
319
+ color[3] *= material.opacity
320
+ bytes = color.pack("e*")
321
+ bytes << [material.map ? 1 : 0].pack("L<")
322
+ bytes << [material.alpha_cutoff].pack("e")
323
+ bytes << [material.uv_set].pack("L<")
324
+ bytes << "\0".b * 4
325
+ bytes << pack_mat3(material.uv_transform)
326
+ [bytes, resolve_textures([material.map], [@resources.fallback_white])]
327
+ end
328
+
329
+ def resolve_textures(textures, fallbacks)
330
+ textures.each_with_index.map do |texture, index|
331
+ texture ? @resource_cache.gpu_texture(texture) : fallbacks[index]
332
+ end
333
+ end
334
+
335
+ def material_entries(buffer, textures, material)
336
+ entries = [{ binding: 0, buffer: }]
337
+ return entries if textures.empty?
338
+
339
+ if material.is_a?(Materials::PBR)
340
+ textures.each_with_index do |texture, index|
341
+ binding = 1 + (index * 2)
342
+ entries << { binding:, sampler: texture.sampler }
343
+ entries << { binding: binding + 1, texture_view: texture.view }
344
+ end
345
+ elsif material.is_a?(Materials::Unlit)
346
+ entries << { binding: 1, sampler: textures.first.sampler }
347
+ textures.each_with_index { |texture, index| entries << { binding: index + 2, texture_view: texture.view } }
348
+ else
349
+ textures.each_with_index do |texture, index|
350
+ binding = 1 + (index * 2)
351
+ entries << { binding:, sampler: texture.sampler }
352
+ entries << { binding: binding + 1, texture_view: texture.view }
353
+ end
354
+ end
355
+ entries
356
+ end
357
+
358
+ def material_texture_versions(material)
359
+ case material
360
+ when Materials::PBR
361
+ Materials::PBR::TEXTURE_ATTRIBUTES.map do |name|
362
+ texture_version(name, material.public_send(name))
363
+ end
364
+ when Materials::Unlit
365
+ [texture_version(:map, material.map)]
366
+ when Materials::Shader
367
+ UniformPacker.new.pack(material.uniforms).textures.map do |name, texture|
368
+ texture_version(name, texture)
369
+ end
370
+ else
371
+ []
372
+ end
373
+ end
374
+
375
+ def texture_version(name, texture)
376
+ [name, texture&.object_id, texture&.version]
377
+ end
378
+
379
+ def material_value_signature(material)
380
+ return unless material.is_a?(Materials::Shader)
381
+
382
+ material.uniforms.map do |name, value|
383
+ next [name, :texture] if value.is_a?(Textures::Texture)
384
+
385
+ normalized = value.respond_to?(:to_a) ? value.to_a : value
386
+ [name, normalized]
387
+ end
388
+ end
389
+
390
+ def purge_obsolete_material_bindings(material, current_key)
391
+ @material_bindings.keys.each do |key|
392
+ next unless key.first == material.object_id && key != current_key
393
+
394
+ release_material_binding(@material_bindings.delete(key))
395
+ end
396
+ end
397
+
398
+ def vertex_buffers(geometry, material, shadow:)
399
+ names = if shadow
400
+ %i[position joints weights]
401
+ elsif material.is_a?(Materials::Unlit)
402
+ %i[position uv uv1 color joints weights]
403
+ else
404
+ ATTRIBUTE_LOCATIONS.keys
405
+ end
406
+ names.filter_map do |name|
407
+ attribute = geometry.attribute(name)
408
+ next unless attribute
409
+
410
+ {
411
+ array_stride: FORMAT_SIZES.fetch(attribute.format),
412
+ step_mode: :vertex,
413
+ attributes: [{
414
+ format: attribute.format,
415
+ offset: 0,
416
+ shader_location: ATTRIBUTE_LOCATIONS.fetch(name)
417
+ }]
418
+ }
419
+ end
420
+ end
421
+
422
+ def topology(geometry)
423
+ geometry.topology
424
+ end
425
+
426
+ def pack_mat3(matrix)
427
+ values = matrix.to_a
428
+ [values[0], values[1], values[2], 0.0,
429
+ values[3], values[4], values[5], 0.0,
430
+ values[6], values[7], values[8], 0.0].pack("e*")
431
+ end
432
+
433
+ def cull_mode(material)
434
+ { front: :back, back: :front, double: :none }.fetch(material.side)
435
+ end
436
+
437
+ def blend_state(material)
438
+ return nil unless material.transparent?
439
+ return material.blend if material.blend.is_a?(Hash)
440
+
441
+ case material.blend
442
+ when :additive
443
+ {
444
+ color: { src_factor: :src_alpha, dst_factor: :one },
445
+ alpha: { src_factor: :one, dst_factor: :one }
446
+ }
447
+ when :multiply
448
+ {
449
+ color: { src_factor: :dst, dst_factor: :zero },
450
+ alpha: { src_factor: :one, dst_factor: :one_minus_src_alpha }
451
+ }
452
+ else
453
+ {
454
+ color: { src_factor: :src_alpha, dst_factor: :one_minus_src_alpha },
455
+ alpha: { src_factor: :one, dst_factor: :one_minus_src_alpha }
456
+ }
457
+ end
458
+ end
459
+
460
+ def release_material_binding(binding)
461
+ return unless binding
462
+
463
+ binding.bind_group.release if binding.bind_group.respond_to?(:release)
464
+ binding.buffer.destroy if binding.buffer.respond_to?(:destroy)
465
+ binding.buffer.release if binding.buffer.respond_to?(:release)
466
+ @stats.decrement(:buffers)
467
+ end
468
+ end
469
+ end
470
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stagecraft
4
+ class Renderer
5
+ class RenderList
6
+ Item = Data.define(:mesh, :depth, :features)
7
+
8
+ attr_reader :opaque, :transparent, :lights, :ambient, :culled_count
9
+
10
+ def initialize(scene, camera)
11
+ @opaque = []
12
+ @transparent = []
13
+ @lights = []
14
+ @ambient = Larb::Vec3.new
15
+ @culled_count = 0
16
+ @camera = camera
17
+ @frustum = Bounding::Frustum.from_matrix(camera.view_projection_matrix)
18
+ collect(scene, true)
19
+ sort!
20
+ end
21
+
22
+ def items
23
+ [*opaque, *transparent]
24
+ end
25
+
26
+ private
27
+
28
+ def collect(node, inherited_visibility)
29
+ visible = inherited_visibility && node.visible
30
+ node.world_matrix
31
+ collect_visible(node) if visible
32
+ node.children.each { |child| collect(child, visible) }
33
+ end
34
+
35
+ def collect_visible(node)
36
+ case node
37
+ when Mesh then collect_mesh(node)
38
+ when Lights::Ambient then collect_ambient(node)
39
+ when Lights::Light then lights << node
40
+ end
41
+ end
42
+
43
+ def collect_mesh(mesh)
44
+ if mesh.frustum_culled && !visible_in_frustum?(mesh)
45
+ @culled_count += 1
46
+ return
47
+ end
48
+
49
+ camera_position = @camera.view_matrix * mesh.world_position.to_vec4(1.0)
50
+ item = Item.new(mesh:, depth: -camera_position.z, features: Features.for(mesh))
51
+ mesh.material.transparent? ? transparent << item : opaque << item
52
+ end
53
+
54
+ def collect_ambient(light)
55
+ color = light.color.to_larb.to_vec3 * light.intensity
56
+ @ambient = ambient + color
57
+ end
58
+
59
+ def visible_in_frustum?(mesh)
60
+ sphere = mesh.geometry.bounding_sphere.transform(mesh.world_matrix)
61
+ @frustum.intersects_sphere?(sphere)
62
+ end
63
+
64
+ def sort!
65
+ opaque.sort_by! do |item|
66
+ [pipeline_signature(item), item.mesh.material.object_id, item.depth]
67
+ end
68
+ transparent.sort_by! { |item| [item.mesh.render_order, -item.depth] }
69
+ end
70
+
71
+ def pipeline_signature(item)
72
+ [
73
+ item.mesh.material.class.name,
74
+ Features.bits(item.features),
75
+ Features.vertex_layout_id(item.mesh.geometry)
76
+ ].hash
77
+ end
78
+ end
79
+ end
80
+ end