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,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support/demo"
|
|
4
|
+
|
|
5
|
+
demo = StagecraftExamples::Demo.new(title: "03 — PBR material grid")
|
|
6
|
+
demo.light!
|
|
7
|
+
5.times do |row|
|
|
8
|
+
5.times do |column|
|
|
9
|
+
material = Stagecraft::Materials::PBR.new(
|
|
10
|
+
base_color: "#d6d9df",
|
|
11
|
+
metallic: column / 4.0,
|
|
12
|
+
roughness: [row / 4.0, 0.04].max
|
|
13
|
+
)
|
|
14
|
+
sphere = Stagecraft::Mesh.new(Stagecraft::Geometries.sphere(0.38), material)
|
|
15
|
+
sphere.position.set((column - 2) * 0.9, (row - 2) * 0.9, 0)
|
|
16
|
+
demo.scene.add(sphere)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
demo.camera.position.set(0, 0, 7)
|
|
20
|
+
demo.run
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support/demo"
|
|
4
|
+
|
|
5
|
+
demo = StagecraftExamples::Demo.new(title: "04 — Light types")
|
|
6
|
+
demo.scene.add(Stagecraft::Lights::Ambient.new(intensity: 0.08))
|
|
7
|
+
point = Stagecraft::Lights::Point.new(color: "#ff5252", intensity: 25, range: 8)
|
|
8
|
+
point.position.set(-2, 2, 2)
|
|
9
|
+
spot = Stagecraft::Lights::Spot.new(
|
|
10
|
+
color: "#40c4ff", intensity: 35, range: 10,
|
|
11
|
+
inner_angle: 0.2, outer_angle: 0.55
|
|
12
|
+
)
|
|
13
|
+
spot.position.set(2, 3, 3)
|
|
14
|
+
spot.look_at(Larb::Vec3.new)
|
|
15
|
+
demo.scene.add(point, spot)
|
|
16
|
+
demo.scene.add(
|
|
17
|
+
Stagecraft::Mesh.new(
|
|
18
|
+
Stagecraft::Geometries.sphere(1.2),
|
|
19
|
+
Stagecraft::Materials::PBR.new(base_color: "#eeeeee", roughness: 0.45)
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
demo.run
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support/demo"
|
|
4
|
+
|
|
5
|
+
demo = StagecraftExamples::Demo.new(title: "05 — Directional shadow")
|
|
6
|
+
demo.scene.add(Stagecraft::Lights::Ambient.new(intensity: 0.12))
|
|
7
|
+
sun = Stagecraft::Lights::Directional.new(intensity: 4.0, cast_shadow: true)
|
|
8
|
+
sun.position.set(4, 8, 5)
|
|
9
|
+
sun.look_at(Larb::Vec3.new)
|
|
10
|
+
cube = Stagecraft::Mesh.new(
|
|
11
|
+
Stagecraft::Geometries.box,
|
|
12
|
+
Stagecraft::Materials::PBR.new(base_color: "#ffab40", roughness: 0.5)
|
|
13
|
+
)
|
|
14
|
+
cube.position.y = 0.7
|
|
15
|
+
cube.cast_shadow = true
|
|
16
|
+
floor = Stagecraft::Mesh.new(
|
|
17
|
+
Stagecraft::Geometries.plane(12, 12),
|
|
18
|
+
Stagecraft::Materials::PBR.new(base_color: "#78909c", roughness: 0.9)
|
|
19
|
+
)
|
|
20
|
+
floor.rotation.rotate_x!(-Math::PI / 2)
|
|
21
|
+
floor.receive_shadow = true
|
|
22
|
+
demo.scene.add(sun, cube, floor)
|
|
23
|
+
demo.run
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support/demo"
|
|
4
|
+
|
|
5
|
+
demo = StagecraftExamples::Demo.new(title: "06 — Transparent sorting")
|
|
6
|
+
colors = %w[#ff174480 #00e5ff80 #76ff0380]
|
|
7
|
+
colors.each_with_index do |color, index|
|
|
8
|
+
material = Stagecraft::Materials::Unlit.new(
|
|
9
|
+
color:,
|
|
10
|
+
transparent: true,
|
|
11
|
+
alpha_mode: :blend,
|
|
12
|
+
depth_write: false
|
|
13
|
+
)
|
|
14
|
+
plane = Stagecraft::Mesh.new(Stagecraft::Geometries.plane(2.4, 2.4), material)
|
|
15
|
+
plane.position.set((index - 1) * 0.6, 0, -index * 0.5)
|
|
16
|
+
demo.scene.add(plane)
|
|
17
|
+
end
|
|
18
|
+
demo.camera.position.set(0, 0, 5)
|
|
19
|
+
demo.run
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support/demo"
|
|
4
|
+
require "texel"
|
|
5
|
+
|
|
6
|
+
pixels = [
|
|
7
|
+
255, 255, 255, 255, 20, 20, 20, 255,
|
|
8
|
+
20, 20, 20, 255, 255, 255, 255, 255
|
|
9
|
+
].pack("C*")
|
|
10
|
+
image = Texel::Image.new(
|
|
11
|
+
width: 2, height: 2, channels: 4, dtype: :u8, color_space: :srgb, data: pixels
|
|
12
|
+
)
|
|
13
|
+
texture = Stagecraft::Texture.new(image, color_space: :srgb)
|
|
14
|
+
demo = StagecraftExamples::Demo.new(title: "07 — Texture")
|
|
15
|
+
demo.light!
|
|
16
|
+
demo.scene.add(
|
|
17
|
+
Stagecraft::Mesh.new(
|
|
18
|
+
Stagecraft::Geometries.box(2, 2, 2),
|
|
19
|
+
Stagecraft::Materials::PBR.new(base_color_map: texture, roughness: 0.7)
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
demo.run
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support/demo"
|
|
4
|
+
|
|
5
|
+
wgsl = <<~WGSL
|
|
6
|
+
//#include "common.wgsl"
|
|
7
|
+
struct VertexInput { @location(0) position: vec3f };
|
|
8
|
+
struct VertexOutput {
|
|
9
|
+
@builtin(position) position: vec4f,
|
|
10
|
+
@location(0) local: vec3f,
|
|
11
|
+
};
|
|
12
|
+
@vertex fn vs_main(input: VertexInput) -> VertexOutput {
|
|
13
|
+
var output: VertexOutput;
|
|
14
|
+
output.position = frame.view_proj * object.model * vec4f(input.position, 1.0);
|
|
15
|
+
output.local = input.position;
|
|
16
|
+
return output;
|
|
17
|
+
}
|
|
18
|
+
@fragment fn fs_main(input: VertexOutput) -> @location(0) vec4f {
|
|
19
|
+
let pulse = 0.55 + 0.45 * sin(frame.time * material.speed);
|
|
20
|
+
return vec4f(material.tint.rgb * pulse, material.tint.a);
|
|
21
|
+
}
|
|
22
|
+
WGSL
|
|
23
|
+
|
|
24
|
+
demo = StagecraftExamples::Demo.new(title: "08 — Shader material")
|
|
25
|
+
material = Stagecraft::Materials::Shader.new(
|
|
26
|
+
wgsl:,
|
|
27
|
+
uniforms: { tint: Stagecraft::Color.new("#7c4dff"), speed: 3.0 }
|
|
28
|
+
)
|
|
29
|
+
demo.scene.add(Stagecraft::Mesh.new(Stagecraft::Geometries.torus, material))
|
|
30
|
+
demo.run
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support/demo"
|
|
4
|
+
require "rgltf"
|
|
5
|
+
|
|
6
|
+
positions = [-1, -1, 0, 1, -1, 0, 0, 1, 0].pack("e*")
|
|
7
|
+
json = {
|
|
8
|
+
"asset" => { "version" => "2.0" },
|
|
9
|
+
"buffers" => [{ "byteLength" => positions.bytesize }],
|
|
10
|
+
"bufferViews" => [{ "buffer" => 0, "byteLength" => positions.bytesize }],
|
|
11
|
+
"accessors" => [
|
|
12
|
+
{ "bufferView" => 0, "componentType" => 5_126, "count" => 3, "type" => "VEC3" }
|
|
13
|
+
],
|
|
14
|
+
"meshes" => [{ "primitives" => [{ "attributes" => { "POSITION" => 0 } }] }],
|
|
15
|
+
"nodes" => [{ "name" => "triangle", "mesh" => 0 }],
|
|
16
|
+
"scenes" => [{ "nodes" => [0] }],
|
|
17
|
+
"scene" => 0
|
|
18
|
+
}
|
|
19
|
+
document = Rgltf.load_json(json, buffers: positions)
|
|
20
|
+
model = Stagecraft::Loaders::GLTF.load(document)
|
|
21
|
+
demo = StagecraftExamples::Demo.new(title: "09 — glTF loader")
|
|
22
|
+
demo.scene.add(model.scene)
|
|
23
|
+
demo.camera.position.set(0, 0, 4)
|
|
24
|
+
demo.run
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support/demo"
|
|
4
|
+
|
|
5
|
+
demo = StagecraftExamples::Demo.new(title: "10 — Animation mixer")
|
|
6
|
+
demo.light!
|
|
7
|
+
cube = Stagecraft::Mesh.new(
|
|
8
|
+
Stagecraft::Geometries.box,
|
|
9
|
+
Stagecraft::Materials::PBR.new(base_color: "#00b0ff")
|
|
10
|
+
)
|
|
11
|
+
demo.scene.add(cube)
|
|
12
|
+
track = Stagecraft::Animation::Track.new(
|
|
13
|
+
times: [0.0, 1.0, 2.0],
|
|
14
|
+
values: [-2, 0, 0, 2, 0, 0, -2, 0, 0],
|
|
15
|
+
target: cube,
|
|
16
|
+
target_path: :translation
|
|
17
|
+
)
|
|
18
|
+
mixer = Stagecraft::Animation::Mixer.new(demo.scene)
|
|
19
|
+
mixer.play(Stagecraft::Animation::Clip.new(name: "slide", tracks: [track]))
|
|
20
|
+
demo.update { |dt| mixer.update(dt) }
|
|
21
|
+
demo.run
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support/demo"
|
|
4
|
+
|
|
5
|
+
demo = StagecraftExamples::Demo.new(title: "11 — Scene graph")
|
|
6
|
+
demo.light!
|
|
7
|
+
pivot = Stagecraft::Node.new(name: "pivot")
|
|
8
|
+
sun = Stagecraft::Mesh.new(
|
|
9
|
+
Stagecraft::Geometries.sphere(0.7),
|
|
10
|
+
Stagecraft::Materials::PBR.new(base_color: "#ffd740", emissive: "#ffab00", emissive_strength: 0.5)
|
|
11
|
+
)
|
|
12
|
+
moon = Stagecraft::Mesh.new(
|
|
13
|
+
Stagecraft::Geometries.sphere(0.25),
|
|
14
|
+
Stagecraft::Materials::PBR.new(base_color: "#b0bec5")
|
|
15
|
+
)
|
|
16
|
+
moon.position.x = 2.0
|
|
17
|
+
pivot.add(sun, moon)
|
|
18
|
+
demo.scene.add(pivot)
|
|
19
|
+
demo.update { |dt| pivot.rotation.rotate_y!(dt) }
|
|
20
|
+
demo.run
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support/demo"
|
|
4
|
+
|
|
5
|
+
demo = StagecraftExamples::Demo.new(title: "12 — Orbit controls")
|
|
6
|
+
demo.light!
|
|
7
|
+
demo.scene.add(
|
|
8
|
+
Stagecraft::Mesh.new(
|
|
9
|
+
Stagecraft::Geometries.torus(1.2, 0.35),
|
|
10
|
+
Stagecraft::Materials::PBR.new(base_color: "#18ffff", metallic: 0.65, roughness: 0.22)
|
|
11
|
+
)
|
|
12
|
+
)
|
|
13
|
+
demo.run
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support/demo"
|
|
4
|
+
|
|
5
|
+
demo = StagecraftExamples::Demo.new(title: "13 — Orthographic camera", controls: false)
|
|
6
|
+
demo.camera = Stagecraft::Cameras::Orthographic.new(
|
|
7
|
+
left: -4, right: 4, top: 2.25, bottom: -2.25, near: 0.1, far: 20
|
|
8
|
+
)
|
|
9
|
+
demo.camera.position.z = 6
|
|
10
|
+
7.times do |index|
|
|
11
|
+
square = Stagecraft::Mesh.new(
|
|
12
|
+
Stagecraft::Geometries.plane(0.8, 0.8),
|
|
13
|
+
Stagecraft::Materials::Unlit.new(color: Stagecraft::Color.new(index / 7.0, 0.5, 1.0))
|
|
14
|
+
)
|
|
15
|
+
square.position.x = index - 3
|
|
16
|
+
demo.scene.add(square)
|
|
17
|
+
end
|
|
18
|
+
demo.run
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support/demo"
|
|
4
|
+
|
|
5
|
+
demo = StagecraftExamples::Demo.new(title: "14 — Raycaster")
|
|
6
|
+
mesh = Stagecraft::Mesh.new(
|
|
7
|
+
Stagecraft::Geometries.sphere,
|
|
8
|
+
Stagecraft::Materials::Unlit.new(color: "#69f0ae")
|
|
9
|
+
)
|
|
10
|
+
demo.scene.add(mesh)
|
|
11
|
+
raycaster = Stagecraft::Raycaster.new
|
|
12
|
+
raycaster.set_from_camera(0.0, 0.0, demo.camera)
|
|
13
|
+
warn "center ray hits: #{raycaster.intersect(demo.scene).map { |hit| hit.object.name || "mesh" }.join(", ")}"
|
|
14
|
+
demo.run
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support/demo"
|
|
4
|
+
require "texel"
|
|
5
|
+
|
|
6
|
+
scene = Stagecraft::Scene.new
|
|
7
|
+
scene.background = "#263238"
|
|
8
|
+
scene.add(
|
|
9
|
+
Stagecraft::Mesh.new(
|
|
10
|
+
Stagecraft::Geometries.box,
|
|
11
|
+
Stagecraft::Materials::Unlit.new(color: "#ff4081")
|
|
12
|
+
)
|
|
13
|
+
)
|
|
14
|
+
camera = Stagecraft::Cameras::Perspective.new(aspect: 1.0)
|
|
15
|
+
camera.position.z = 3
|
|
16
|
+
renderer = Stagecraft::Renderer.offscreen(width: 512, height: 512, msaa: 4)
|
|
17
|
+
image = renderer.render(scene, camera).read_pixels
|
|
18
|
+
destination = ARGV.first || File.expand_path("../tmp/stagecraft-offscreen.png", __dir__)
|
|
19
|
+
Dir.mkdir(File.dirname(destination)) unless Dir.exist?(File.dirname(destination))
|
|
20
|
+
Texel.save(image, destination)
|
|
21
|
+
renderer.dispose
|
|
22
|
+
puts destination
|
data/examples/16_msaa.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support/demo"
|
|
4
|
+
|
|
5
|
+
demo = StagecraftExamples::Demo.new(title: "16 — 4x MSAA", msaa: 4)
|
|
6
|
+
demo.light!
|
|
7
|
+
demo.scene.add(
|
|
8
|
+
Stagecraft::Mesh.new(
|
|
9
|
+
Stagecraft::Geometries.torus(1.2, 0.18, radial_segments: 8, tubular_segments: 96),
|
|
10
|
+
Stagecraft::Materials::PBR.new(base_color: "#ffffff", roughness: 0.25)
|
|
11
|
+
)
|
|
12
|
+
)
|
|
13
|
+
demo.run
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support/demo"
|
|
4
|
+
|
|
5
|
+
Body = Struct.new(:position, :rotation)
|
|
6
|
+
demo = StagecraftExamples::Demo.new(title: "17 — Physics binding")
|
|
7
|
+
demo.light!
|
|
8
|
+
mesh = Stagecraft::Mesh.new(
|
|
9
|
+
Stagecraft::Geometries.box,
|
|
10
|
+
Stagecraft::Materials::PBR.new(base_color: "#ff6d00")
|
|
11
|
+
)
|
|
12
|
+
body = Body.new(Larb::Vec3.new, Larb::Quat.new)
|
|
13
|
+
binding = Stagecraft::PhysicsBinding.new.bind(mesh, body)
|
|
14
|
+
elapsed = 0.0
|
|
15
|
+
demo.scene.add(mesh)
|
|
16
|
+
demo.update do |dt|
|
|
17
|
+
elapsed += dt
|
|
18
|
+
body.position = Larb::Vec3.new(Math.sin(elapsed) * 2.0, 0, 0)
|
|
19
|
+
body.rotation = Larb::Quat.from_axis_angle(Larb::Vec3.up, elapsed)
|
|
20
|
+
binding.sync!(1.0)
|
|
21
|
+
end
|
|
22
|
+
demo.run
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support/demo"
|
|
4
|
+
|
|
5
|
+
geometry = Stagecraft::Geometry.new
|
|
6
|
+
geometry.set_attribute(
|
|
7
|
+
:position,
|
|
8
|
+
data: [0, 1.5, 0, -1.5, -1, 0, 1.5, -1, 0].pack("e*"),
|
|
9
|
+
format: :float32x3,
|
|
10
|
+
count: 3
|
|
11
|
+
)
|
|
12
|
+
geometry.compute_normals!
|
|
13
|
+
demo = StagecraftExamples::Demo.new(title: "18 — Custom geometry")
|
|
14
|
+
demo.light!
|
|
15
|
+
demo.scene.add(
|
|
16
|
+
Stagecraft::Mesh.new(
|
|
17
|
+
geometry,
|
|
18
|
+
Stagecraft::Materials::PBR.new(base_color: "#d500f9", roughness: 0.55)
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
demo.camera.position.set(0, 0, 5)
|
|
22
|
+
demo.run
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support/demo"
|
|
4
|
+
|
|
5
|
+
demo = StagecraftExamples::Demo.new(title: "19 — Stats / ImGui hook")
|
|
6
|
+
demo.light!
|
|
7
|
+
demo.scene.add(
|
|
8
|
+
Stagecraft::Mesh.new(
|
|
9
|
+
Stagecraft::Geometries.sphere,
|
|
10
|
+
Stagecraft::Materials::PBR.new(base_color: "#64ffda")
|
|
11
|
+
)
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# An imgui-ruby WGPU backend can encode its draw data in this callback. The same
|
|
15
|
+
# device and queue are available as demo.app.renderer.device / .queue.
|
|
16
|
+
last_report = 0.0
|
|
17
|
+
demo.app.renderer.on_after_render do |_encoder, _target_view|
|
|
18
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
19
|
+
next unless now - last_report >= 1.0
|
|
20
|
+
|
|
21
|
+
warn demo.app.renderer.stats.to_s
|
|
22
|
+
last_report = now
|
|
23
|
+
end
|
|
24
|
+
demo.run
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "support/demo"
|
|
4
|
+
|
|
5
|
+
# SDL3 is the portable default. Set STAGECRAFT_WINDOW=glfw only when supplying
|
|
6
|
+
# the platform-specific WebGPU surface bridge described in the README.
|
|
7
|
+
window = ENV.fetch("STAGECRAFT_WINDOW", "sdl3").to_sym
|
|
8
|
+
demo = StagecraftExamples::Demo.new(title: "20 — #{window} adapter", window:)
|
|
9
|
+
demo.scene.add(
|
|
10
|
+
Stagecraft::Mesh.new(
|
|
11
|
+
Stagecraft::Geometries.box,
|
|
12
|
+
Stagecraft::Materials::Unlit.new(color: "#448aff")
|
|
13
|
+
)
|
|
14
|
+
)
|
|
15
|
+
demo.run
|
data/examples/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Stagecraft gallery
|
|
2
|
+
|
|
3
|
+
Run any example from the repository root:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
bundle exec ruby examples/01_spinning_cube.rb
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The window examples use SDL3 by default. Set `STAGECRAFT_SMOKE=1` to stop after
|
|
10
|
+
one rendered frame. Example 15 is headless and writes a PNG; it accepts the
|
|
11
|
+
destination path as its first argument.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
|
|
4
|
+
|
|
5
|
+
require "stagecraft"
|
|
6
|
+
|
|
7
|
+
module StagecraftExamples
|
|
8
|
+
class Demo
|
|
9
|
+
attr_reader :app, :scene
|
|
10
|
+
attr_accessor :camera
|
|
11
|
+
|
|
12
|
+
def initialize(title:, width: 960, height: 540, msaa: 4, window: :sdl3, controls: true)
|
|
13
|
+
@app = Stagecraft::App.new(title:, width:, height:, msaa:, window:)
|
|
14
|
+
@scene = Stagecraft::Scene.new
|
|
15
|
+
@scene.background = "#10141d"
|
|
16
|
+
@camera = Stagecraft::Cameras::Perspective.new(
|
|
17
|
+
fov: 55, aspect: app.aspect, near: 0.1, far: 200
|
|
18
|
+
)
|
|
19
|
+
@camera.position.set(0, 2, 6)
|
|
20
|
+
@controls_enabled = controls
|
|
21
|
+
@updates = []
|
|
22
|
+
app.window.on_resize do |next_width, next_height|
|
|
23
|
+
camera.aspect = next_width.to_f / [next_height, 1].max if camera.respond_to?(:aspect=)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def light!
|
|
28
|
+
scene.add(Stagecraft::Lights::Ambient.new(intensity: 0.3))
|
|
29
|
+
sun = Stagecraft::Lights::Directional.new(intensity: 3.0)
|
|
30
|
+
sun.position.set(4, 7, 5)
|
|
31
|
+
scene.add(sun)
|
|
32
|
+
sun
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def update(&block)
|
|
36
|
+
@updates << block
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def run
|
|
41
|
+
controls = Stagecraft::Controls::Orbit.new(camera, app.window) if @controls_enabled
|
|
42
|
+
app.run do |dt|
|
|
43
|
+
controls&.update(dt)
|
|
44
|
+
@updates.each { |callback| callback.call(dt) }
|
|
45
|
+
app.renderer.render(scene, camera)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stagecraft
|
|
4
|
+
module Animation
|
|
5
|
+
class Action
|
|
6
|
+
LOOPS = %i[repeat once ping_pong].freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :clip, :time, :loop
|
|
9
|
+
attr_accessor :weight, :time_scale, :enabled, :clamp_when_finished
|
|
10
|
+
|
|
11
|
+
def initialize(clip, loop: :repeat, fade_in: 0.0)
|
|
12
|
+
@clip = clip
|
|
13
|
+
@loop = loop.to_sym
|
|
14
|
+
raise ArgumentError, "invalid animation loop #{loop.inspect}" unless LOOPS.include?(@loop)
|
|
15
|
+
|
|
16
|
+
@time = 0.0
|
|
17
|
+
@weight = fade_in.to_f.positive? ? 0.0 : 1.0
|
|
18
|
+
@fade_duration = [Float(fade_in), 0.0].max
|
|
19
|
+
@fade_elapsed = 0.0
|
|
20
|
+
@time_scale = 1.0
|
|
21
|
+
@enabled = true
|
|
22
|
+
@clamp_when_finished = true
|
|
23
|
+
@ping_pong_phase = 0.0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def advance(dt)
|
|
27
|
+
return time unless enabled
|
|
28
|
+
|
|
29
|
+
elapsed = Float(dt)
|
|
30
|
+
update_fade(elapsed)
|
|
31
|
+
duration = clip.duration
|
|
32
|
+
return @time = 0.0 if duration.zero?
|
|
33
|
+
|
|
34
|
+
delta = elapsed * time_scale
|
|
35
|
+
return advance_ping_pong(delta, duration) if loop == :ping_pong
|
|
36
|
+
|
|
37
|
+
@time += delta
|
|
38
|
+
apply_loop(duration)
|
|
39
|
+
time
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def stop
|
|
43
|
+
@enabled = false
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def update_fade(dt)
|
|
50
|
+
return unless @fade_duration.positive? && weight < 1.0
|
|
51
|
+
|
|
52
|
+
@fade_elapsed += [dt, 0.0].max
|
|
53
|
+
@weight = [@fade_elapsed / @fade_duration, 1.0].min
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def apply_loop(duration)
|
|
57
|
+
case loop
|
|
58
|
+
when :repeat
|
|
59
|
+
@time %= duration
|
|
60
|
+
when :once
|
|
61
|
+
return unless @time > duration || @time.negative?
|
|
62
|
+
|
|
63
|
+
@time = @time.clamp(0.0, duration)
|
|
64
|
+
@enabled = false unless clamp_when_finished
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def advance_ping_pong(delta, duration)
|
|
69
|
+
period = duration * 2.0
|
|
70
|
+
@ping_pong_phase = (@ping_pong_phase + delta) % period
|
|
71
|
+
@time = if @ping_pong_phase <= duration
|
|
72
|
+
@ping_pong_phase
|
|
73
|
+
else
|
|
74
|
+
period - @ping_pong_phase
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stagecraft
|
|
4
|
+
module Animation
|
|
5
|
+
class Clip
|
|
6
|
+
attr_reader :name, :tracks, :duration
|
|
7
|
+
|
|
8
|
+
def initialize(name: nil, tracks:, duration: nil)
|
|
9
|
+
@name = name
|
|
10
|
+
@tracks = tracks.freeze
|
|
11
|
+
@duration = duration ? Float(duration) : (@tracks.map(&:duration).max || 0.0)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stagecraft
|
|
4
|
+
module Animation
|
|
5
|
+
class Mixer
|
|
6
|
+
attr_reader :root, :actions
|
|
7
|
+
|
|
8
|
+
def initialize(root)
|
|
9
|
+
@root = root
|
|
10
|
+
@actions = []
|
|
11
|
+
@resolved_targets = {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def play(clip, loop: :repeat, fade_in: 0.0)
|
|
15
|
+
action = Action.new(clip, loop:, fade_in:)
|
|
16
|
+
actions << action
|
|
17
|
+
clip.tracks.each { |track| resolve_target(track) }
|
|
18
|
+
action
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def stop_all
|
|
22
|
+
actions.each(&:stop)
|
|
23
|
+
actions.clear
|
|
24
|
+
self
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def update(dt)
|
|
28
|
+
samples = Hash.new { |hash, key| hash[key] = [] }
|
|
29
|
+
actions.each do |action|
|
|
30
|
+
next unless action.enabled
|
|
31
|
+
|
|
32
|
+
action.advance(dt)
|
|
33
|
+
action.clip.tracks.each do |track|
|
|
34
|
+
target = resolve_target(track)
|
|
35
|
+
next unless target
|
|
36
|
+
|
|
37
|
+
samples[[target, track.target_path]] << [track.sample(action.time), action.weight]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
samples.each { |(target, path), values| apply_blend(target, path, values) }
|
|
41
|
+
self
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def resolve_target(track)
|
|
47
|
+
@resolved_targets[track.object_id] ||= case track.target
|
|
48
|
+
when Node then track.target
|
|
49
|
+
when String, Symbol then root.find(track.target.to_s)
|
|
50
|
+
else track.target
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def apply_blend(target, path, samples)
|
|
55
|
+
total = samples.sum(&:last)
|
|
56
|
+
return if total.zero?
|
|
57
|
+
|
|
58
|
+
value = if path == :rotation
|
|
59
|
+
blend_quaternions(samples, total)
|
|
60
|
+
else
|
|
61
|
+
blend_vectors(samples, total)
|
|
62
|
+
end
|
|
63
|
+
case path
|
|
64
|
+
when :translation then target.position.set(value)
|
|
65
|
+
when :rotation then target.rotation.set(value)
|
|
66
|
+
when :scale then target.scale.set(value)
|
|
67
|
+
when :weights
|
|
68
|
+
if target.respond_to?(:morph_weights=)
|
|
69
|
+
target.morph_weights = value
|
|
70
|
+
elsif target.respond_to?(:traverse)
|
|
71
|
+
target.traverse do |node|
|
|
72
|
+
node.morph_weights = value.dup if node.respond_to?(:morph_weights=)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def blend_vectors(samples, total)
|
|
79
|
+
size = samples.first.first.length
|
|
80
|
+
Array.new(size) do |index|
|
|
81
|
+
samples.sum { |value, weight| value[index] * weight } / total
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def blend_quaternions(samples, total)
|
|
86
|
+
reference = samples.first.first
|
|
87
|
+
components = Array.new(4, 0.0)
|
|
88
|
+
samples.each do |value, weight|
|
|
89
|
+
dot = 4.times.sum { |index| reference[index] * value[index] }
|
|
90
|
+
sign = dot.negative? ? -1.0 : 1.0
|
|
91
|
+
4.times { |index| components[index] += value[index] * weight * sign }
|
|
92
|
+
end
|
|
93
|
+
Larb::Quat.new(*components.map { |component| component / total }).normalize.to_a
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|