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,387 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stagecraft
|
|
4
|
+
module Loaders
|
|
5
|
+
class GLTF
|
|
6
|
+
Result = Data.define(:scene, :scenes, :animations, :cameras)
|
|
7
|
+
|
|
8
|
+
ATTRIBUTE_NAMES = {
|
|
9
|
+
POSITION: :position,
|
|
10
|
+
NORMAL: :normal,
|
|
11
|
+
TEXCOORD_0: :uv,
|
|
12
|
+
TEXCOORD_1: :uv1,
|
|
13
|
+
TANGENT: :tangent,
|
|
14
|
+
COLOR_0: :color,
|
|
15
|
+
JOINTS_0: :joints,
|
|
16
|
+
WEIGHTS_0: :weights
|
|
17
|
+
}.freeze
|
|
18
|
+
TOPOLOGIES = {
|
|
19
|
+
points: :point_list,
|
|
20
|
+
lines: :line_list,
|
|
21
|
+
line_strip: :line_strip,
|
|
22
|
+
triangles: :triangle_list,
|
|
23
|
+
triangle_strip: :triangle_strip
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
def self.load(source)
|
|
27
|
+
require "rgltf"
|
|
28
|
+
require "texel"
|
|
29
|
+
|
|
30
|
+
document = source.is_a?(Rgltf::Document) ? source : Rgltf.load(source)
|
|
31
|
+
new(document).convert
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize(document)
|
|
35
|
+
@document = document
|
|
36
|
+
@textures = {}
|
|
37
|
+
@materials = {}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def convert
|
|
41
|
+
source_scenes = @document.scenes
|
|
42
|
+
converted = source_scenes.map { |scene| convert_scene(scene) }
|
|
43
|
+
default_index = @document.default_scene && source_scenes.index(@document.default_scene)
|
|
44
|
+
default_index ||= 0
|
|
45
|
+
default_scene = converted[default_index] || convert_scene(nil)
|
|
46
|
+
mapping = @scene_mappings.fetch(default_scene.object_id)
|
|
47
|
+
Result.new(
|
|
48
|
+
scene: default_scene,
|
|
49
|
+
scenes: converted.empty? ? [default_scene] : converted,
|
|
50
|
+
animations: convert_animations(mapping),
|
|
51
|
+
cameras: converted_cameras(mapping)
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def convert_scene(source_scene)
|
|
58
|
+
@scene_mappings ||= {}
|
|
59
|
+
mapping = {}
|
|
60
|
+
stage_scene = Scene.new(name: source_scene&.name)
|
|
61
|
+
@document.nodes.each { |node| build_node(node, mapping) }
|
|
62
|
+
roots = source_scene ? source_scene.nodes : inferred_roots
|
|
63
|
+
roots.each { |root| stage_scene.add(mapping.fetch(root)) }
|
|
64
|
+
@document.nodes.each { |node| decorate_node(node, mapping) }
|
|
65
|
+
@scene_mappings[stage_scene.object_id] = mapping
|
|
66
|
+
stage_scene
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def inferred_roots
|
|
70
|
+
children = @document.nodes.flat_map(&:children)
|
|
71
|
+
@document.nodes.reject { |node| children.include?(node) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def build_node(source, mapping)
|
|
75
|
+
return mapping[source] if mapping.key?(source)
|
|
76
|
+
|
|
77
|
+
node = transform_node(source)
|
|
78
|
+
mapping[source] = node
|
|
79
|
+
source.children.each { |child| node.add(build_node(child, mapping)) }
|
|
80
|
+
node
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def transform_node(source)
|
|
84
|
+
return trs_node(source) if source.trs?
|
|
85
|
+
|
|
86
|
+
matrix = Larb::Mat4.new(source.matrix)
|
|
87
|
+
position = matrix.extract_translation
|
|
88
|
+
rotation = matrix.extract_rotation
|
|
89
|
+
scale = matrix.extract_scale
|
|
90
|
+
recomposed = Larb::Mat4.translation(*position.to_a) *
|
|
91
|
+
rotation.to_mat4 *
|
|
92
|
+
Larb::Mat4.scaling(*scale.to_a)
|
|
93
|
+
return MatrixNode.new(matrix, name: source.name) unless matrix.near?(recomposed)
|
|
94
|
+
|
|
95
|
+
Node.new(name: source.name).tap do |node|
|
|
96
|
+
node.position.set(position)
|
|
97
|
+
node.rotation.set(rotation)
|
|
98
|
+
node.scale.set(scale)
|
|
99
|
+
end
|
|
100
|
+
rescue StandardError
|
|
101
|
+
MatrixNode.new(matrix, name: source.name)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def trs_node(source)
|
|
105
|
+
Node.new(name: source.name).tap do |node|
|
|
106
|
+
node.position.set(source.translation)
|
|
107
|
+
node.rotation.set(source.rotation)
|
|
108
|
+
node.scale.set(source.scale)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def decorate_node(source, mapping)
|
|
113
|
+
target = mapping.fetch(source)
|
|
114
|
+
source.mesh&.primitives&.each_with_index do |primitive, index|
|
|
115
|
+
target.add(convert_primitive(source, primitive, index, mapping))
|
|
116
|
+
end
|
|
117
|
+
target.add(convert_camera(source.camera)) if source.camera
|
|
118
|
+
light = source.extension("KHR_lights_punctual")
|
|
119
|
+
target.add(convert_light(light.light)) if light
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def convert_primitive(source_node, primitive, index, mapping)
|
|
123
|
+
geometry = convert_geometry(primitive)
|
|
124
|
+
material = convert_material(primitive.material)
|
|
125
|
+
name = source_node.mesh.name
|
|
126
|
+
name = "#{source_node.name || name || "mesh"} primitive #{index}" if source_node.mesh.primitives.length > 1
|
|
127
|
+
Mesh.new(geometry, material, name:).tap do |mesh|
|
|
128
|
+
weights = source_node.weights || source_node.mesh.weights
|
|
129
|
+
mesh.morph_weights = weights.dup if weights
|
|
130
|
+
mesh.skin = convert_skin(source_node.skin, mapping) if source_node.skin
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def convert_geometry(primitive)
|
|
135
|
+
topology = TOPOLOGIES.fetch(primitive.mode, :triangle_list)
|
|
136
|
+
geometry = Geometry.new(topology:)
|
|
137
|
+
primitive.attributes.each do |semantic, accessor|
|
|
138
|
+
name = ATTRIBUTE_NAMES[semantic]
|
|
139
|
+
next unless name
|
|
140
|
+
|
|
141
|
+
data, format = attribute_payload(semantic, accessor)
|
|
142
|
+
raise Error, "glTF attribute #{semantic} has no WebGPU vertex format" unless format
|
|
143
|
+
|
|
144
|
+
geometry.set_attribute(name, data:, format:, count: accessor.count)
|
|
145
|
+
end
|
|
146
|
+
raise Error, "glTF primitive has no POSITION attribute" unless geometry.attribute(:position)
|
|
147
|
+
if %i[line_loop triangle_fan].include?(primitive.mode)
|
|
148
|
+
geometry.topology = primitive.mode == :line_loop ? :line_list : :triangle_list
|
|
149
|
+
geometry.set_index(data: expanded_indices(primitive), format: :uint32)
|
|
150
|
+
elsif primitive.indices
|
|
151
|
+
data, format = index_data(primitive.indices)
|
|
152
|
+
geometry.set_index(data:, format:)
|
|
153
|
+
end
|
|
154
|
+
geometry
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def attribute_payload(semantic, accessor)
|
|
158
|
+
return expanded_color(accessor) if semantic == :COLOR_0 &&
|
|
159
|
+
accessor.type == "VEC3" &&
|
|
160
|
+
accessor.vertex_format.nil?
|
|
161
|
+
|
|
162
|
+
[accessor.packed, accessor.vertex_format]
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def expanded_color(accessor)
|
|
166
|
+
case accessor.component_type
|
|
167
|
+
when :u8
|
|
168
|
+
values = accessor.packed.unpack("C*").each_slice(3).flat_map { |color| [*color, 255] }
|
|
169
|
+
[values.pack("C*"), :unorm8x4]
|
|
170
|
+
when :u16
|
|
171
|
+
values = accessor.packed.unpack("S<*").each_slice(3).flat_map { |color| [*color, 65_535] }
|
|
172
|
+
[values.pack("S<*"), :unorm16x4]
|
|
173
|
+
else
|
|
174
|
+
[accessor.packed, accessor.vertex_format]
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def expanded_indices(primitive)
|
|
179
|
+
values = if primitive.indices
|
|
180
|
+
primitive.indices.to_a.map { |value| value.is_a?(Array) ? value.first : value }
|
|
181
|
+
else
|
|
182
|
+
(0...primitive.attributes.fetch(:POSITION).count).to_a
|
|
183
|
+
end
|
|
184
|
+
expanded = if primitive.mode == :line_loop
|
|
185
|
+
return "".b if values.empty?
|
|
186
|
+
|
|
187
|
+
values.each_with_index.flat_map { |value, index| [value, values[(index + 1) % values.length]] }
|
|
188
|
+
else
|
|
189
|
+
(1...(values.length - 1)).flat_map { |index| [values.first, values[index], values[index + 1]] }
|
|
190
|
+
end
|
|
191
|
+
expanded.pack("L<*")
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def index_data(accessor)
|
|
195
|
+
case accessor.component_type
|
|
196
|
+
when :u8 then [accessor.packed_as_u32, :uint32]
|
|
197
|
+
when :u16 then [accessor.packed, :uint16]
|
|
198
|
+
when :u32 then [accessor.packed, :uint32]
|
|
199
|
+
else raise Error, "unsupported glTF index component #{accessor.component_type}"
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def convert_material(source)
|
|
204
|
+
return Materials::PBR.new unless source
|
|
205
|
+
|
|
206
|
+
@materials[source] ||= begin
|
|
207
|
+
pbr = source.pbr
|
|
208
|
+
common = {
|
|
209
|
+
side: source.double_sided ? :double : :front,
|
|
210
|
+
alpha_mode: source.alpha_mode,
|
|
211
|
+
alpha_cutoff: source.alpha_cutoff,
|
|
212
|
+
transparent: source.alpha_mode == :blend
|
|
213
|
+
}
|
|
214
|
+
if source.unlit?
|
|
215
|
+
Materials::Unlit.new(
|
|
216
|
+
**common,
|
|
217
|
+
color: pbr.base_color_factor,
|
|
218
|
+
map: texture_from_info(pbr.base_color_texture, :srgb),
|
|
219
|
+
uv_transform: uv_transform(pbr.base_color_texture),
|
|
220
|
+
uv_set: uv_set(pbr.base_color_texture)
|
|
221
|
+
)
|
|
222
|
+
else
|
|
223
|
+
Materials::PBR.new(
|
|
224
|
+
**common,
|
|
225
|
+
base_color: pbr.base_color_factor,
|
|
226
|
+
base_color_map: texture_from_info(pbr.base_color_texture, :srgb),
|
|
227
|
+
base_color_uv_transform: uv_transform(pbr.base_color_texture),
|
|
228
|
+
base_color_uv_set: uv_set(pbr.base_color_texture),
|
|
229
|
+
metallic: pbr.metallic_factor,
|
|
230
|
+
roughness: pbr.roughness_factor,
|
|
231
|
+
metallic_roughness_map: texture_from_info(pbr.metallic_roughness_texture, :linear),
|
|
232
|
+
metallic_roughness_uv_transform: uv_transform(pbr.metallic_roughness_texture),
|
|
233
|
+
metallic_roughness_uv_set: uv_set(pbr.metallic_roughness_texture),
|
|
234
|
+
normal_map: texture_from_info(source.normal_texture, :linear),
|
|
235
|
+
normal_scale: source.normal_texture&.scale || 1.0,
|
|
236
|
+
normal_uv_transform: uv_transform(source.normal_texture),
|
|
237
|
+
normal_uv_set: uv_set(source.normal_texture),
|
|
238
|
+
occlusion_map: texture_from_info(source.occlusion_texture, :linear),
|
|
239
|
+
occlusion_strength: source.occlusion_texture&.strength || 1.0,
|
|
240
|
+
occlusion_uv_transform: uv_transform(source.occlusion_texture),
|
|
241
|
+
occlusion_uv_set: uv_set(source.occlusion_texture),
|
|
242
|
+
emissive: source.emissive_factor,
|
|
243
|
+
emissive_map: texture_from_info(source.emissive_texture, :srgb),
|
|
244
|
+
emissive_strength: source.extension("KHR_materials_emissive_strength")&.emissive_strength || 1.0,
|
|
245
|
+
emissive_uv_transform: uv_transform(source.emissive_texture),
|
|
246
|
+
emissive_uv_set: uv_set(source.emissive_texture)
|
|
247
|
+
)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def texture_from_info(info, color_space)
|
|
253
|
+
return unless info
|
|
254
|
+
|
|
255
|
+
source = info.texture
|
|
256
|
+
@textures[[source, color_space]] ||= begin
|
|
257
|
+
raise Error, "glTF texture has no image source" unless source.source
|
|
258
|
+
|
|
259
|
+
image = Texel.load(source.source.bytes, channels: 4, color_space:)
|
|
260
|
+
Textures::Texture.new(image, sampler: convert_sampler(source.sampler), color_space:)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def convert_sampler(source)
|
|
265
|
+
return Textures::SamplerState.new unless source
|
|
266
|
+
|
|
267
|
+
min_filter, mipmap_filter = split_min_filter(source.min_filter)
|
|
268
|
+
Textures::SamplerState.new(
|
|
269
|
+
wrap_u: source.wrap_s,
|
|
270
|
+
wrap_v: source.wrap_t,
|
|
271
|
+
mag_filter: source.mag_filter || :linear,
|
|
272
|
+
min_filter:,
|
|
273
|
+
mipmap_filter:
|
|
274
|
+
)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def split_min_filter(filter)
|
|
278
|
+
{
|
|
279
|
+
nil => %i[linear linear],
|
|
280
|
+
nearest: %i[nearest nearest],
|
|
281
|
+
linear: %i[linear linear],
|
|
282
|
+
nearest_mipmap_nearest: %i[nearest nearest],
|
|
283
|
+
linear_mipmap_nearest: %i[linear nearest],
|
|
284
|
+
nearest_mipmap_linear: %i[nearest linear],
|
|
285
|
+
linear_mipmap_linear: %i[linear linear]
|
|
286
|
+
}.fetch(filter)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def uv_transform(info)
|
|
290
|
+
matrix = info&.transform&.uv_matrix
|
|
291
|
+
matrix ? Larb::Mat3.new(matrix) : Larb::Mat3.new
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def uv_set(info)
|
|
295
|
+
return 0 unless info
|
|
296
|
+
|
|
297
|
+
Integer(info.transform&.tex_coord || info.tex_coord)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def convert_skin(source, mapping)
|
|
301
|
+
matrices = if source.inverse_bind_matrices
|
|
302
|
+
source.inverse_bind_matrices.to_a.map { |values| Larb::Mat4.new(values) }
|
|
303
|
+
end
|
|
304
|
+
Skin.new(
|
|
305
|
+
joints: source.joints.map { |joint| mapping.fetch(joint) },
|
|
306
|
+
inverse_bind_matrices: matrices,
|
|
307
|
+
skeleton: source.skeleton && mapping.fetch(source.skeleton)
|
|
308
|
+
)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def convert_camera(source)
|
|
312
|
+
camera = case source.type
|
|
313
|
+
when :perspective
|
|
314
|
+
value = source.perspective
|
|
315
|
+
Cameras::Perspective.new(
|
|
316
|
+
fov: value.yfov * 180.0 / Math::PI,
|
|
317
|
+
aspect: value.aspect_ratio || 1.0,
|
|
318
|
+
near: value.znear,
|
|
319
|
+
far: value.zfar || 1.0e9
|
|
320
|
+
)
|
|
321
|
+
when :orthographic
|
|
322
|
+
value = source.orthographic
|
|
323
|
+
Cameras::Orthographic.new(
|
|
324
|
+
left: -value.xmag,
|
|
325
|
+
right: value.xmag,
|
|
326
|
+
top: value.ymag,
|
|
327
|
+
bottom: -value.ymag,
|
|
328
|
+
near: value.znear,
|
|
329
|
+
far: value.zfar
|
|
330
|
+
)
|
|
331
|
+
end
|
|
332
|
+
camera.name = source.name
|
|
333
|
+
camera
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def convert_light(source)
|
|
337
|
+
options = { name: source.name, color: [*source.color, 1.0], intensity: source.intensity }
|
|
338
|
+
case source.type
|
|
339
|
+
when :directional then Lights::Directional.new(**options)
|
|
340
|
+
when :point then Lights::Point.new(**options, range: source.range || Float::INFINITY)
|
|
341
|
+
when :spot
|
|
342
|
+
Lights::Spot.new(
|
|
343
|
+
**options,
|
|
344
|
+
range: source.range || Float::INFINITY,
|
|
345
|
+
inner_angle: source.spot.inner_cone_angle,
|
|
346
|
+
outer_angle: source.spot.outer_cone_angle
|
|
347
|
+
)
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def convert_animations(mapping)
|
|
352
|
+
@document.animations.map do |animation|
|
|
353
|
+
tracks = animation.channels.filter_map do |channel|
|
|
354
|
+
next unless channel.target_node
|
|
355
|
+
|
|
356
|
+
sampler = channel.sampler
|
|
357
|
+
Animation::Track.new(
|
|
358
|
+
times: sampler.input.packed,
|
|
359
|
+
values: sampler.output.packed,
|
|
360
|
+
target: mapping.fetch(channel.target_node),
|
|
361
|
+
target_path: channel.target_path,
|
|
362
|
+
interpolation: sampler.interpolation,
|
|
363
|
+
value_size: animation_value_size(channel)
|
|
364
|
+
)
|
|
365
|
+
end
|
|
366
|
+
Animation::Clip.new(name: animation.name, tracks:)
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def animation_value_size(channel)
|
|
371
|
+
return unless channel.target_path == :weights
|
|
372
|
+
|
|
373
|
+
sampler = channel.sampler
|
|
374
|
+
multiplier = sampler.interpolation == :cubicspline ? 3 : 1
|
|
375
|
+
sampler.output.count / (sampler.input.count * multiplier)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def converted_cameras(mapping)
|
|
379
|
+
@document.nodes.filter_map do |node|
|
|
380
|
+
next unless node.camera
|
|
381
|
+
|
|
382
|
+
mapping.fetch(node).children.find { |child| child.is_a?(Cameras::Camera) }
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stagecraft
|
|
4
|
+
module Materials
|
|
5
|
+
class Base
|
|
6
|
+
SIDES = %i[front back double].freeze
|
|
7
|
+
BLENDS = %i[normal additive multiply].freeze
|
|
8
|
+
ALPHA_MODES = %i[opaque mask blend].freeze
|
|
9
|
+
|
|
10
|
+
attr_reader :version
|
|
11
|
+
|
|
12
|
+
def initialize(transparent: false, opacity: 1.0, side: :front, depth_write: true,
|
|
13
|
+
depth_test: true, blend: :normal, alpha_mode: :opaque, alpha_cutoff: 0.5)
|
|
14
|
+
@version = 0
|
|
15
|
+
self.transparent = transparent
|
|
16
|
+
self.opacity = opacity
|
|
17
|
+
self.side = side
|
|
18
|
+
self.depth_write = depth_write
|
|
19
|
+
self.depth_test = depth_test
|
|
20
|
+
self.blend = blend
|
|
21
|
+
self.alpha_mode = alpha_mode
|
|
22
|
+
self.alpha_cutoff = alpha_cutoff
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.versioned_attribute(name, coerce: nil, validate: nil)
|
|
26
|
+
attr_reader name
|
|
27
|
+
define_method(:"#{name}=") do |value|
|
|
28
|
+
next_value = coerce ? coerce.call(value) : value
|
|
29
|
+
if validate && !validate.call(next_value)
|
|
30
|
+
raise ArgumentError, "invalid #{name}: #{value.inspect}"
|
|
31
|
+
end
|
|
32
|
+
return next_value if instance_variable_defined?(:"@#{name}") &&
|
|
33
|
+
instance_variable_get(:"@#{name}") == next_value
|
|
34
|
+
|
|
35
|
+
instance_variable_set(:"@#{name}", next_value)
|
|
36
|
+
@version += 1
|
|
37
|
+
next_value
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
versioned_attribute :transparent, coerce: ->(value) { !!value }
|
|
42
|
+
versioned_attribute :opacity, coerce: ->(value) { Float(value) },
|
|
43
|
+
validate: ->(value) { value.between?(0.0, 1.0) }
|
|
44
|
+
versioned_attribute :side, coerce: :to_sym.to_proc, validate: ->(value) { SIDES.include?(value) }
|
|
45
|
+
versioned_attribute :depth_write, coerce: ->(value) { !!value }
|
|
46
|
+
versioned_attribute :depth_test, coerce: ->(value) { !!value }
|
|
47
|
+
versioned_attribute :blend, validate: ->(value) { value.is_a?(Hash) || BLENDS.include?(value.to_sym) }
|
|
48
|
+
versioned_attribute :alpha_mode, coerce: :to_sym.to_proc,
|
|
49
|
+
validate: ->(value) { ALPHA_MODES.include?(value) }
|
|
50
|
+
versioned_attribute :alpha_cutoff, coerce: ->(value) { Float(value) },
|
|
51
|
+
validate: ->(value) { value.between?(0.0, 1.0) }
|
|
52
|
+
|
|
53
|
+
def transparent?
|
|
54
|
+
transparent || opacity < 1.0 || alpha_mode == :blend
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stagecraft
|
|
4
|
+
module Materials
|
|
5
|
+
class PBR < Base
|
|
6
|
+
TEXTURE_ATTRIBUTES = %i[
|
|
7
|
+
base_color_map metallic_roughness_map normal_map occlusion_map emissive_map
|
|
8
|
+
].freeze
|
|
9
|
+
TRANSFORM_ATTRIBUTES = %i[
|
|
10
|
+
base_color_uv_transform metallic_roughness_uv_transform normal_uv_transform
|
|
11
|
+
occlusion_uv_transform emissive_uv_transform
|
|
12
|
+
].freeze
|
|
13
|
+
UV_SET_ATTRIBUTES = %i[
|
|
14
|
+
base_color_uv_set metallic_roughness_uv_set normal_uv_set
|
|
15
|
+
occlusion_uv_set emissive_uv_set
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
def initialize(base_color: Color.new(1.0), base_color_map: nil, metallic: 1.0,
|
|
19
|
+
roughness: 1.0, metallic_roughness_map: nil, normal_map: nil,
|
|
20
|
+
normal_scale: 1.0, occlusion_map: nil, occlusion_strength: 1.0,
|
|
21
|
+
emissive: Color.new(0.0), emissive_map: nil, emissive_strength: 1.0,
|
|
22
|
+
base_color_uv_transform: Larb::Mat3.new,
|
|
23
|
+
metallic_roughness_uv_transform: Larb::Mat3.new,
|
|
24
|
+
normal_uv_transform: Larb::Mat3.new,
|
|
25
|
+
occlusion_uv_transform: Larb::Mat3.new,
|
|
26
|
+
emissive_uv_transform: Larb::Mat3.new,
|
|
27
|
+
base_color_uv_set: 0, metallic_roughness_uv_set: 0,
|
|
28
|
+
normal_uv_set: 0, occlusion_uv_set: 0, emissive_uv_set: 0, **)
|
|
29
|
+
super(**)
|
|
30
|
+
self.base_color = base_color
|
|
31
|
+
self.base_color_map = base_color_map
|
|
32
|
+
self.metallic = metallic
|
|
33
|
+
self.roughness = roughness
|
|
34
|
+
self.metallic_roughness_map = metallic_roughness_map
|
|
35
|
+
self.normal_map = normal_map
|
|
36
|
+
self.normal_scale = normal_scale
|
|
37
|
+
self.occlusion_map = occlusion_map
|
|
38
|
+
self.occlusion_strength = occlusion_strength
|
|
39
|
+
self.emissive = emissive
|
|
40
|
+
self.emissive_map = emissive_map
|
|
41
|
+
self.emissive_strength = emissive_strength
|
|
42
|
+
TRANSFORM_ATTRIBUTES.each do |name|
|
|
43
|
+
self.public_send(:"#{name}=", binding.local_variable_get(name))
|
|
44
|
+
end
|
|
45
|
+
UV_SET_ATTRIBUTES.each do |name|
|
|
46
|
+
self.public_send(:"#{name}=", binding.local_variable_get(name))
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
versioned_attribute :base_color, coerce: Color.method(:coerce)
|
|
51
|
+
versioned_attribute :metallic, coerce: ->(value) { Float(value) },
|
|
52
|
+
validate: ->(value) { value.between?(0.0, 1.0) }
|
|
53
|
+
versioned_attribute :roughness, coerce: ->(value) { Float(value) },
|
|
54
|
+
validate: ->(value) { value.between?(0.0, 1.0) }
|
|
55
|
+
versioned_attribute :normal_scale, coerce: ->(value) { Float(value) }
|
|
56
|
+
versioned_attribute :occlusion_strength, coerce: ->(value) { Float(value) },
|
|
57
|
+
validate: ->(value) { value.between?(0.0, 1.0) }
|
|
58
|
+
versioned_attribute :emissive, coerce: Color.method(:coerce)
|
|
59
|
+
versioned_attribute :emissive_strength, coerce: ->(value) { Float(value) },
|
|
60
|
+
validate: ->(value) { value >= 0.0 }
|
|
61
|
+
TEXTURE_ATTRIBUTES.each { |name| versioned_attribute(name) }
|
|
62
|
+
TRANSFORM_ATTRIBUTES.each do |name|
|
|
63
|
+
versioned_attribute name, coerce: lambda { |value|
|
|
64
|
+
value.is_a?(Larb::Mat3) ? value : Larb::Mat3.new(value.to_a)
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
UV_SET_ATTRIBUTES.each do |name|
|
|
68
|
+
versioned_attribute name, coerce: ->(value) { Integer(value) },
|
|
69
|
+
validate: ->(value) { value.between?(0, 1) }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
PBRMaterial = PBR
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stagecraft
|
|
4
|
+
module Materials
|
|
5
|
+
class Shader < Base
|
|
6
|
+
def initialize(wgsl:, uniforms: {}, **)
|
|
7
|
+
super(**)
|
|
8
|
+
self.wgsl = wgsl
|
|
9
|
+
self.uniforms = uniforms
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
versioned_attribute :wgsl, coerce: lambda { |value|
|
|
13
|
+
source = value.respond_to?(:to_wgsl) ? value.to_wgsl : value
|
|
14
|
+
String(source).dup.freeze
|
|
15
|
+
}
|
|
16
|
+
versioned_attribute :uniforms, coerce: ->(value) { value.transform_keys(&:to_sym).freeze }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
ShaderMaterial = Shader
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stagecraft
|
|
4
|
+
module Materials
|
|
5
|
+
class Unlit < Base
|
|
6
|
+
def initialize(color: Color.new(1.0), map: nil, uv_transform: Larb::Mat3.new,
|
|
7
|
+
uv_set: 0, **)
|
|
8
|
+
super(**)
|
|
9
|
+
self.color = color
|
|
10
|
+
self.map = map
|
|
11
|
+
self.uv_transform = uv_transform
|
|
12
|
+
self.uv_set = uv_set
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
versioned_attribute :color, coerce: Color.method(:coerce)
|
|
16
|
+
versioned_attribute :map
|
|
17
|
+
versioned_attribute :uv_transform, coerce: lambda { |value|
|
|
18
|
+
value.is_a?(Larb::Mat3) ? value : Larb::Mat3.new(value.to_a)
|
|
19
|
+
}
|
|
20
|
+
versioned_attribute :uv_set, coerce: ->(value) { Integer(value) },
|
|
21
|
+
validate: ->(value) { value.between?(0, 1) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
UnlitMaterial = Unlit
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stagecraft
|
|
4
|
+
class PhysicsBinding
|
|
5
|
+
Entry = Data.define(:node, :body, :previous_position, :previous_rotation)
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@entries = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def bind(node, body)
|
|
12
|
+
raise ArgumentError, "physics binding requires a Stagecraft::Node" unless node.is_a?(Node)
|
|
13
|
+
|
|
14
|
+
validate_body!(body)
|
|
15
|
+
unbind(node)
|
|
16
|
+
@entries << Entry.new(
|
|
17
|
+
node:,
|
|
18
|
+
body:,
|
|
19
|
+
previous_position: vector(body.position),
|
|
20
|
+
previous_rotation: quaternion(body.rotation)
|
|
21
|
+
)
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def unbind(node)
|
|
26
|
+
@entries.reject! { |entry| entry.node.equal?(node) }
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def sync!(alpha = 1.0)
|
|
31
|
+
amount = Float(alpha).clamp(0.0, 1.0)
|
|
32
|
+
@entries.map! do |entry|
|
|
33
|
+
current_position = vector(entry.body.position)
|
|
34
|
+
current_rotation = quaternion(entry.body.rotation)
|
|
35
|
+
entry.node.position.set(entry.previous_position.lerp(current_position, amount))
|
|
36
|
+
entry.node.rotation.set(entry.previous_rotation.slerp(current_rotation, amount))
|
|
37
|
+
Entry.new(
|
|
38
|
+
node: entry.node,
|
|
39
|
+
body: entry.body,
|
|
40
|
+
previous_position: current_position,
|
|
41
|
+
previous_rotation: current_rotation
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def validate_body!(body)
|
|
50
|
+
return if body.respond_to?(:position) && body.respond_to?(:rotation)
|
|
51
|
+
|
|
52
|
+
raise ArgumentError, "physics body must expose position and rotation"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def vector(value)
|
|
56
|
+
Larb::Vec3.new(*value.to_a)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def quaternion(value)
|
|
60
|
+
Larb::Quat.new(*value.to_a)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Stagecraft
|
|
6
|
+
class Renderer
|
|
7
|
+
module Features
|
|
8
|
+
ORDER = %i[
|
|
9
|
+
HAS_UV HAS_NORMAL HAS_TANGENT HAS_VERTEX_COLOR HAS_BASECOLOR_MAP HAS_MR_MAP
|
|
10
|
+
HAS_NORMAL_MAP HAS_OCCLUSION_MAP HAS_EMISSIVE_MAP SKINNED ALPHA_MASK DOUBLE_SIDED
|
|
11
|
+
].freeze
|
|
12
|
+
ATTRIBUTE_FEATURES = {
|
|
13
|
+
uv: :HAS_UV,
|
|
14
|
+
uv1: :HAS_UV1,
|
|
15
|
+
normal: :HAS_NORMAL,
|
|
16
|
+
tangent: :HAS_TANGENT,
|
|
17
|
+
color: :HAS_VERTEX_COLOR
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
def for(mesh)
|
|
23
|
+
values = Set.new
|
|
24
|
+
ATTRIBUTE_FEATURES.each { |attribute, feature| values << feature if mesh.geometry.attribute(attribute) }
|
|
25
|
+
color = mesh.geometry.attribute(:color)
|
|
26
|
+
values << :COLOR_VEC3 if color && color.components == 3
|
|
27
|
+
material_features(mesh.material, values)
|
|
28
|
+
values << :SKINNED if mesh.skin
|
|
29
|
+
values.freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def bits(features)
|
|
33
|
+
ORDER.each_with_index.sum { |feature, index| features.include?(feature) ? (1 << index) : 0 }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def vertex_layout_id(geometry)
|
|
37
|
+
[
|
|
38
|
+
geometry.topology,
|
|
39
|
+
geometry.index&.format,
|
|
40
|
+
geometry.attributes.sort_by(&:first).map { |name, attribute| [name, attribute.format] }
|
|
41
|
+
].hash
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def material_features(material, values)
|
|
45
|
+
values << :ALPHA_MASK if material.alpha_mode == :mask
|
|
46
|
+
values << :DOUBLE_SIDED if material.side == :double
|
|
47
|
+
return unless material.is_a?(Materials::PBR)
|
|
48
|
+
|
|
49
|
+
values << :HAS_BASECOLOR_MAP if material.base_color_map
|
|
50
|
+
values << :HAS_MR_MAP if material.metallic_roughness_map
|
|
51
|
+
values << :HAS_NORMAL_MAP if material.normal_map
|
|
52
|
+
values << :HAS_OCCLUSION_MAP if material.occlusion_map
|
|
53
|
+
values << :HAS_EMISSIVE_MAP if material.emissive_map
|
|
54
|
+
end
|
|
55
|
+
private_class_method :material_features
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|