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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +207 -0
- data/Rakefile +8 -0
- data/docs/api.md +85 -0
- data/docs/tutorial.md +344 -0
- data/examples/01_spinning_cube.rb +13 -0
- data/examples/02_primitives.rb +21 -0
- data/examples/03_pbr_materials.rb +20 -0
- data/examples/04_lights.rb +22 -0
- data/examples/05_directional_shadow.rb +23 -0
- data/examples/06_transparency.rb +19 -0
- data/examples/07_texture.rb +22 -0
- data/examples/08_shader_material.rb +30 -0
- data/examples/09_gltf_loader.rb +24 -0
- data/examples/10_animation.rb +21 -0
- data/examples/11_scene_graph.rb +20 -0
- data/examples/12_orbit_controls.rb +13 -0
- data/examples/13_orthographic_camera.rb +18 -0
- data/examples/14_raycaster.rb +14 -0
- data/examples/15_offscreen.rb +22 -0
- data/examples/16_msaa.rb +13 -0
- data/examples/17_physics_binding.rb +22 -0
- data/examples/18_custom_geometry.rb +22 -0
- data/examples/19_imgui_stats.rb +24 -0
- data/examples/20_window_adapters.rb +15 -0
- data/examples/README.md +11 -0
- data/examples/support/demo.rb +49 -0
- data/lib/stagecraft/animation/action.rb +79 -0
- data/lib/stagecraft/animation/clip.rb +15 -0
- data/lib/stagecraft/animation/mixer.rb +97 -0
- data/lib/stagecraft/animation/track.rb +122 -0
- data/lib/stagecraft/app.rb +61 -0
- data/lib/stagecraft/cameras/camera.rb +15 -0
- data/lib/stagecraft/cameras/orthographic_camera.rb +60 -0
- data/lib/stagecraft/cameras/perspective_camera.rb +69 -0
- data/lib/stagecraft/color.rb +56 -0
- data/lib/stagecraft/controls/orbit_controls.rb +119 -0
- data/lib/stagecraft/core/attribute.rb +91 -0
- data/lib/stagecraft/core/bounding.rb +131 -0
- data/lib/stagecraft/core/geometry.rb +155 -0
- data/lib/stagecraft/core/mesh.rb +26 -0
- data/lib/stagecraft/core/node.rb +163 -0
- data/lib/stagecraft/core/observed_value.rb +182 -0
- data/lib/stagecraft/core/raycaster.rb +62 -0
- data/lib/stagecraft/core/scene.rb +11 -0
- data/lib/stagecraft/core/skin.rb +23 -0
- data/lib/stagecraft/geometries.rb +200 -0
- data/lib/stagecraft/lights/ambient_light.rb +10 -0
- data/lib/stagecraft/lights/directional_light.rb +31 -0
- data/lib/stagecraft/lights/light.rb +30 -0
- data/lib/stagecraft/lights/point_light.rb +23 -0
- data/lib/stagecraft/lights/spot_light.rb +36 -0
- data/lib/stagecraft/loaders/gltf_loader.rb +387 -0
- data/lib/stagecraft/materials/material.rb +58 -0
- data/lib/stagecraft/materials/pbr_material.rb +75 -0
- data/lib/stagecraft/materials/shader_material.rb +21 -0
- data/lib/stagecraft/materials/unlit_material.rb +26 -0
- data/lib/stagecraft/physics_binding.rb +63 -0
- data/lib/stagecraft/renderer/features.rb +58 -0
- data/lib/stagecraft/renderer/frame.rb +45 -0
- data/lib/stagecraft/renderer/frame_resources.rb +467 -0
- data/lib/stagecraft/renderer/gpu_context.rb +120 -0
- data/lib/stagecraft/renderer/pipeline_cache.rb +59 -0
- data/lib/stagecraft/renderer/pipeline_factory.rb +470 -0
- data/lib/stagecraft/renderer/render_list.rb +80 -0
- data/lib/stagecraft/renderer/renderer.rb +336 -0
- data/lib/stagecraft/renderer/resource_cache.rb +176 -0
- data/lib/stagecraft/renderer/shaders/common.wgsl +48 -0
- data/lib/stagecraft/renderer/shaders/pbr.wgsl +251 -0
- data/lib/stagecraft/renderer/shaders/shadow.wgsl +37 -0
- data/lib/stagecraft/renderer/shaders/skinning.wgsl +6 -0
- data/lib/stagecraft/renderer/shaders/tonemap.wgsl +37 -0
- data/lib/stagecraft/renderer/shaders/unlit.wgsl +89 -0
- data/lib/stagecraft/renderer/shaders.rb +64 -0
- data/lib/stagecraft/renderer/stats.rb +41 -0
- data/lib/stagecraft/renderer/uniform_packer.rb +91 -0
- data/lib/stagecraft/renderer/wgpu_compatibility.rb +41 -0
- data/lib/stagecraft/textures/cube_texture.rb +16 -0
- data/lib/stagecraft/textures/sampler_state.rb +58 -0
- data/lib/stagecraft/textures/texture.rb +71 -0
- data/lib/stagecraft/version.rb +5 -0
- data/lib/stagecraft/window/adapter.rb +38 -0
- data/lib/stagecraft/window/glfw.rb +67 -0
- data/lib/stagecraft/window/sdl3.rb +57 -0
- data/lib/stagecraft.rb +60 -0
- 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
|