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,251 @@
1
+ //#include "common.wgsl"
2
+
3
+ const PI = 3.14159265359;
4
+
5
+ struct MaterialUniforms {
6
+ base_color: vec4f,
7
+ factors: vec4f,
8
+ emissive: vec4f,
9
+ texture_flags: vec4u,
10
+ emissive_map_flag: u32,
11
+ alpha_cutoff: f32,
12
+ _padding: vec2f,
13
+ uv_sets: vec4u,
14
+ emissive_uv_set: u32,
15
+ _uv_padding0: u32,
16
+ _uv_padding1: u32,
17
+ _uv_padding2: u32,
18
+ base_color_uv_transform: mat3x3f,
19
+ mr_uv_transform: mat3x3f,
20
+ normal_uv_transform: mat3x3f,
21
+ occlusion_uv_transform: mat3x3f,
22
+ emissive_uv_transform: mat3x3f,
23
+ };
24
+
25
+ @group(1) @binding(0) var<uniform> material: MaterialUniforms;
26
+ @group(1) @binding(1) var base_color_sampler: sampler;
27
+ @group(1) @binding(2) var base_color_map: texture_2d<f32>;
28
+ @group(1) @binding(3) var mr_sampler: sampler;
29
+ @group(1) @binding(4) var mr_map: texture_2d<f32>;
30
+ @group(1) @binding(5) var normal_sampler: sampler;
31
+ @group(1) @binding(6) var normal_map: texture_2d<f32>;
32
+ @group(1) @binding(7) var occlusion_sampler: sampler;
33
+ @group(1) @binding(8) var occlusion_map: texture_2d<f32>;
34
+ @group(1) @binding(9) var emissive_sampler: sampler;
35
+ @group(1) @binding(10) var emissive_map: texture_2d<f32>;
36
+
37
+ struct VertexInput {
38
+ @location(0) position: vec3f,
39
+ //#if HAS_NORMAL
40
+ @location(1) normal: vec3f,
41
+ //#endif
42
+ //#if HAS_UV
43
+ @location(2) uv: vec2f,
44
+ //#endif
45
+ //#if HAS_UV1
46
+ @location(7) uv1: vec2f,
47
+ //#endif
48
+ //#if HAS_TANGENT
49
+ @location(3) tangent: vec4f,
50
+ //#endif
51
+ //#if HAS_VERTEX_COLOR
52
+ //#if COLOR_VEC3
53
+ @location(4) color: vec3f,
54
+ //#else
55
+ @location(4) color: vec4f,
56
+ //#endif
57
+ //#endif
58
+ //#if SKINNED
59
+ @location(5) joint: vec4u,
60
+ @location(6) weight: vec4f,
61
+ //#endif
62
+ };
63
+
64
+ struct VertexOutput {
65
+ @builtin(position) position: vec4f,
66
+ @location(0) world_position: vec3f,
67
+ @location(1) normal: vec3f,
68
+ @location(2) uv: vec2f,
69
+ @location(3) color: vec4f,
70
+ //#if HAS_TANGENT
71
+ @location(4) tangent: vec4f,
72
+ //#endif
73
+ @location(5) uv1: vec2f,
74
+ };
75
+
76
+ @vertex fn vs_main(input: VertexInput) -> VertexOutput {
77
+ var output: VertexOutput;
78
+ var local_position = vec4f(input.position, 1.0);
79
+ //#if SKINNED
80
+ let skin = input.weight.x * joints[input.joint.x] +
81
+ input.weight.y * joints[input.joint.y] +
82
+ input.weight.z * joints[input.joint.z] +
83
+ input.weight.w * joints[input.joint.w];
84
+ local_position = skin * local_position;
85
+ //#endif
86
+ let world = object.model * local_position;
87
+ output.position = frame.view_proj * world;
88
+ output.world_position = world.xyz;
89
+ //#if HAS_NORMAL
90
+ var local_normal = input.normal;
91
+ //#if SKINNED
92
+ local_normal = (skin * vec4f(local_normal, 0.0)).xyz;
93
+ //#endif
94
+ output.normal = normalize((object.normal_matrix * vec4f(local_normal, 0.0)).xyz);
95
+ //#else
96
+ output.normal = vec3f(0.0, 1.0, 0.0);
97
+ //#endif
98
+ //#if HAS_UV
99
+ output.uv = input.uv;
100
+ //#else
101
+ output.uv = vec2f(0.0);
102
+ //#endif
103
+ //#if HAS_UV1
104
+ output.uv1 = input.uv1;
105
+ //#else
106
+ output.uv1 = output.uv;
107
+ //#endif
108
+ //#if HAS_VERTEX_COLOR
109
+ //#if COLOR_VEC3
110
+ output.color = vec4f(input.color, 1.0);
111
+ //#else
112
+ output.color = input.color;
113
+ //#endif
114
+ //#else
115
+ output.color = vec4f(1.0);
116
+ //#endif
117
+ //#if HAS_TANGENT
118
+ var local_tangent = input.tangent.xyz;
119
+ //#if SKINNED
120
+ local_tangent = (skin * vec4f(local_tangent, 0.0)).xyz;
121
+ //#endif
122
+ let world_tangent = normalize((object.model * vec4f(local_tangent, 0.0)).xyz);
123
+ output.tangent = vec4f(world_tangent, input.tangent.w);
124
+ //#endif
125
+ return output;
126
+ }
127
+
128
+ fn distribution_ggx(n_dot_h: f32, roughness: f32) -> f32 {
129
+ let a = roughness * roughness;
130
+ let a2 = a * a;
131
+ let denominator = n_dot_h * n_dot_h * (a2 - 1.0) + 1.0;
132
+ return a2 / max(PI * denominator * denominator, 0.0001);
133
+ }
134
+
135
+ fn geometry_schlick(n_dot_v: f32, roughness: f32) -> f32 {
136
+ let r = roughness + 1.0;
137
+ let k = (r * r) / 8.0;
138
+ return n_dot_v / max(n_dot_v * (1.0 - k) + k, 0.0001);
139
+ }
140
+
141
+ fn fresnel_schlick(cos_theta: f32, f0: vec3f) -> vec3f {
142
+ return f0 + (vec3f(1.0) - f0) * pow(1.0 - cos_theta, 5.0);
143
+ }
144
+
145
+ fn cotangent_frame(surface_normal: vec3f, position: vec3f, uv: vec2f) -> mat3x3f {
146
+ let position_dx = dpdx(position);
147
+ let position_dy = dpdy(position);
148
+ let uv_dx = dpdx(uv);
149
+ let uv_dy = dpdy(uv);
150
+ let position_dy_perp = cross(position_dy, surface_normal);
151
+ let position_dx_perp = cross(surface_normal, position_dx);
152
+ let tangent = position_dy_perp * uv_dx.x + position_dx_perp * uv_dy.x;
153
+ let bitangent = position_dy_perp * uv_dx.y + position_dx_perp * uv_dy.y;
154
+ let squared_scale = max(dot(tangent, tangent), dot(bitangent, bitangent));
155
+ let inverse_scale = inverseSqrt(max(squared_scale, 1.0e-8));
156
+ return mat3x3f(
157
+ tangent * inverse_scale,
158
+ bitangent * inverse_scale,
159
+ surface_normal
160
+ );
161
+ }
162
+
163
+ @fragment fn fs_main(
164
+ input: VertexOutput,
165
+ @builtin(front_facing) front_facing: bool
166
+ ) -> @location(0) vec4f {
167
+ var base = material.base_color * input.color;
168
+ if (material.texture_flags.x != 0u) {
169
+ let source_uv = select(input.uv, input.uv1, material.uv_sets.x != 0u);
170
+ let uv = (material.base_color_uv_transform * vec3f(source_uv, 1.0)).xy;
171
+ base *= textureSample(base_color_map, base_color_sampler, uv);
172
+ }
173
+ //#if ALPHA_MASK
174
+ if (base.a < material.alpha_cutoff) { discard; }
175
+ //#endif
176
+ var metallic = material.factors.x;
177
+ var roughness = max(material.factors.y, 0.04);
178
+ if (material.texture_flags.y != 0u) {
179
+ let source_uv = select(input.uv, input.uv1, material.uv_sets.y != 0u);
180
+ let uv = (material.mr_uv_transform * vec3f(source_uv, 1.0)).xy;
181
+ let mr = textureSample(mr_map, mr_sampler, uv);
182
+ metallic *= mr.b;
183
+ roughness *= mr.g;
184
+ }
185
+ var normal = normalize(input.normal);
186
+ //#if DOUBLE_SIDED
187
+ normal *= select(-1.0, 1.0, front_facing);
188
+ //#endif
189
+ if (material.texture_flags.z != 0u) {
190
+ let source_uv = select(input.uv, input.uv1, material.uv_sets.z != 0u);
191
+ let uv = (material.normal_uv_transform * vec3f(source_uv, 1.0)).xy;
192
+ var mapped = textureSample(normal_map, normal_sampler, uv).xyz * 2.0 - 1.0;
193
+ mapped = vec3f(mapped.xy * material.factors.z, mapped.z);
194
+ //#if HAS_TANGENT
195
+ let bitangent = normalize(cross(normal, input.tangent.xyz)) * input.tangent.w;
196
+ normal = normalize(mat3x3f(input.tangent.xyz, bitangent, normal) * mapped);
197
+ //#else
198
+ normal = normalize(cotangent_frame(normal, input.world_position, uv) * mapped);
199
+ //#endif
200
+ }
201
+ let view_direction = normalize(frame.camera_pos - input.world_position);
202
+ let f0 = mix(vec3f(0.04), base.rgb, metallic);
203
+ var result = frame.ambient * base.rgb * (1.0 - metallic);
204
+
205
+ for (var index = 0u; index < frame.light_count; index = index + 1u) {
206
+ let light = lights[index];
207
+ var light_direction = -light.direction;
208
+ var attenuation = 1.0;
209
+ if (light.kind != 0u) {
210
+ let to_light = light.position - input.world_position;
211
+ let distance = length(to_light);
212
+ light_direction = to_light / max(distance, 0.0001);
213
+ let ratio = distance / max(light.range, 0.0001);
214
+ attenuation = pow(clamp(1.0 - pow(ratio, 4.0), 0.0, 1.0), 2.0) /
215
+ max(distance * distance, 0.0001);
216
+ if (light.kind == 2u) {
217
+ let angle = dot(-light_direction, light.direction);
218
+ attenuation *= smoothstep(light.cone.y, light.cone.x, angle);
219
+ }
220
+ }
221
+ let halfway = normalize(view_direction + light_direction);
222
+ let n_dot_l = max(dot(normal, light_direction), 0.0);
223
+ let n_dot_v = max(dot(normal, view_direction), 0.0);
224
+ let n_dot_h = max(dot(normal, halfway), 0.0);
225
+ let h_dot_v = max(dot(halfway, view_direction), 0.0);
226
+ let fresnel = fresnel_schlick(h_dot_v, f0);
227
+ let specular = distribution_ggx(n_dot_h, roughness) *
228
+ geometry_schlick(n_dot_v, roughness) *
229
+ geometry_schlick(n_dot_l, roughness) * fresnel /
230
+ max(4.0 * n_dot_v * n_dot_l, 0.0001);
231
+ let diffuse = (vec3f(1.0) - fresnel) * (1.0 - metallic) * base.rgb / PI;
232
+ var visibility = 1.0;
233
+ if (light.kind == 0u && index == 0u && object.receive_shadow != 0u) {
234
+ visibility = shadow_visibility(input.world_position);
235
+ }
236
+ result += (diffuse + specular) * light.color * n_dot_l * attenuation * visibility;
237
+ }
238
+ var emissive = material.emissive.rgb * material.emissive.a;
239
+ if (material.emissive_map_flag != 0u) {
240
+ let source_uv = select(input.uv, input.uv1, material.emissive_uv_set != 0u);
241
+ let uv = (material.emissive_uv_transform * vec3f(source_uv, 1.0)).xy;
242
+ emissive *= textureSample(emissive_map, emissive_sampler, uv).rgb;
243
+ }
244
+ if (material.texture_flags.w != 0u) {
245
+ let source_uv = select(input.uv, input.uv1, material.uv_sets.w != 0u);
246
+ let uv = (material.occlusion_uv_transform * vec3f(source_uv, 1.0)).xy;
247
+ result *= mix(1.0, textureSample(occlusion_map, occlusion_sampler, uv).r,
248
+ material.factors.w);
249
+ }
250
+ return vec4f(result + emissive, base.a);
251
+ }
@@ -0,0 +1,37 @@
1
+ struct FrameUniforms {
2
+ view_proj: mat4x4f,
3
+ camera_pos: vec3f,
4
+ time: f32,
5
+ ambient: vec3f,
6
+ light_count: u32,
7
+ light_vp: mat4x4f,
8
+ };
9
+
10
+ struct ObjectUniforms {
11
+ model: mat4x4f,
12
+ normal_matrix: mat4x4f,
13
+ };
14
+
15
+ @group(0) @binding(0) var<uniform> frame: FrameUniforms;
16
+ @group(2) @binding(0) var<uniform> object: ObjectUniforms;
17
+ @group(2) @binding(1) var<storage, read> joints: array<mat4x4f>;
18
+
19
+ struct VertexInput {
20
+ @location(0) position: vec3f,
21
+ //#if SKINNED
22
+ @location(5) joint: vec4u,
23
+ @location(6) weight: vec4f,
24
+ //#endif
25
+ };
26
+
27
+ @vertex fn vs_main(input: VertexInput) -> @builtin(position) vec4f {
28
+ var local_position = vec4f(input.position, 1.0);
29
+ //#if SKINNED
30
+ let skin = input.weight.x * joints[input.joint.x] +
31
+ input.weight.y * joints[input.joint.y] +
32
+ input.weight.z * joints[input.joint.z] +
33
+ input.weight.w * joints[input.joint.w];
34
+ local_position = skin * local_position;
35
+ //#endif
36
+ return frame.light_vp * object.model * local_position;
37
+ }
@@ -0,0 +1,6 @@
1
+ fn skin_matrix(joint: vec4u, weight: vec4f) -> mat4x4f {
2
+ return weight.x * joints[joint.x] +
3
+ weight.y * joints[joint.y] +
4
+ weight.z * joints[joint.z] +
5
+ weight.w * joints[joint.w];
6
+ }
@@ -0,0 +1,37 @@
1
+ @group(0) @binding(0) var hdr_texture: texture_2d<f32>;
2
+ @group(0) @binding(1) var hdr_sampler: sampler;
3
+
4
+ struct VertexOutput {
5
+ @builtin(position) position: vec4f,
6
+ @location(0) uv: vec2f,
7
+ };
8
+
9
+ @vertex fn vs_main(@builtin(vertex_index) index: u32) -> VertexOutput {
10
+ let positions = array<vec2f, 3>(
11
+ vec2f(-1.0, -1.0),
12
+ vec2f(3.0, -1.0),
13
+ vec2f(-1.0, 3.0)
14
+ );
15
+ var output: VertexOutput;
16
+ output.position = vec4f(positions[index], 0.0, 1.0);
17
+ output.uv = output.position.xy * vec2f(0.5, -0.5) + vec2f(0.5);
18
+ return output;
19
+ }
20
+
21
+ fn aces(color: vec3f) -> vec3f {
22
+ let a = 2.51;
23
+ let b = 0.03;
24
+ let c = 2.43;
25
+ let d = 0.59;
26
+ let e = 0.14;
27
+ return clamp((color * (a * color + b)) / (color * (c * color + d) + e), vec3f(0.0), vec3f(1.0));
28
+ }
29
+
30
+ @fragment fn fs_main(input: VertexOutput) -> @location(0) vec4f {
31
+ var color = aces(textureSample(hdr_texture, hdr_sampler, input.uv).rgb);
32
+ //#if ENCODE_SRGB
33
+ color = select(12.92 * color, 1.055 * pow(color, vec3f(1.0 / 2.4)) - 0.055,
34
+ color > vec3f(0.0031308));
35
+ //#endif
36
+ return vec4f(color, 1.0);
37
+ }
@@ -0,0 +1,89 @@
1
+ //#include "common.wgsl"
2
+
3
+ struct MaterialUniforms {
4
+ color: vec4f,
5
+ use_map: u32,
6
+ alpha_cutoff: f32,
7
+ uv_set: u32,
8
+ _padding: f32,
9
+ uv_transform: mat3x3f,
10
+ };
11
+
12
+ @group(1) @binding(0) var<uniform> material: MaterialUniforms;
13
+ @group(1) @binding(1) var material_sampler: sampler;
14
+ @group(1) @binding(2) var color_map: texture_2d<f32>;
15
+
16
+ struct VertexInput {
17
+ @location(0) position: vec3f,
18
+ //#if HAS_UV
19
+ @location(2) uv: vec2f,
20
+ //#endif
21
+ //#if HAS_UV1
22
+ @location(7) uv1: vec2f,
23
+ //#endif
24
+ //#if HAS_VERTEX_COLOR
25
+ //#if COLOR_VEC3
26
+ @location(4) color: vec3f,
27
+ //#else
28
+ @location(4) color: vec4f,
29
+ //#endif
30
+ //#endif
31
+ //#if SKINNED
32
+ @location(5) joint: vec4u,
33
+ @location(6) weight: vec4f,
34
+ //#endif
35
+ };
36
+
37
+ struct VertexOutput {
38
+ @builtin(position) position: vec4f,
39
+ @location(0) uv: vec2f,
40
+ @location(1) uv1: vec2f,
41
+ @location(2) color: vec4f,
42
+ };
43
+
44
+ @vertex fn vs_main(input: VertexInput) -> VertexOutput {
45
+ var output: VertexOutput;
46
+ var local_position = vec4f(input.position, 1.0);
47
+ //#if SKINNED
48
+ let skin = input.weight.x * joints[input.joint.x] +
49
+ input.weight.y * joints[input.joint.y] +
50
+ input.weight.z * joints[input.joint.z] +
51
+ input.weight.w * joints[input.joint.w];
52
+ local_position = skin * local_position;
53
+ //#endif
54
+ let world = object.model * local_position;
55
+ output.position = frame.view_proj * world;
56
+ //#if HAS_UV
57
+ output.uv = input.uv;
58
+ //#else
59
+ output.uv = vec2f(0.0);
60
+ //#endif
61
+ //#if HAS_UV1
62
+ output.uv1 = input.uv1;
63
+ //#else
64
+ output.uv1 = output.uv;
65
+ //#endif
66
+ //#if HAS_VERTEX_COLOR
67
+ //#if COLOR_VEC3
68
+ output.color = vec4f(input.color, 1.0);
69
+ //#else
70
+ output.color = input.color;
71
+ //#endif
72
+ //#else
73
+ output.color = vec4f(1.0);
74
+ //#endif
75
+ return output;
76
+ }
77
+
78
+ @fragment fn fs_main(input: VertexOutput) -> @location(0) vec4f {
79
+ var color = material.color * input.color;
80
+ if (material.use_map != 0u) {
81
+ let source_uv = select(input.uv, input.uv1, material.uv_set != 0u);
82
+ let uv = (material.uv_transform * vec3f(source_uv, 1.0)).xy;
83
+ color *= textureSample(color_map, material_sampler, uv);
84
+ }
85
+ //#if ALPHA_MASK
86
+ if (color.a < material.alpha_cutoff) { discard; }
87
+ //#endif
88
+ return color;
89
+ }
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stagecraft
4
+ class Renderer
5
+ module Shaders
6
+ ROOT = File.expand_path("shaders", __dir__)
7
+ DIRECTIVE = %r{\A\s*//#(if|else|endif|include)\b(?:\s+["']?([^"'\s]+)["']?)?\s*\z}
8
+
9
+ module_function
10
+
11
+ def compose(name, defines: Set.new)
12
+ @cache ||= {}
13
+ key = [name.to_s, defines.to_a.map(&:to_s).sort].freeze
14
+ @cache[key] ||= process(read(name), defines.map(&:to_s).to_set, [name.to_s]).freeze
15
+ end
16
+
17
+ def clear_cache
18
+ @cache = {}
19
+ end
20
+
21
+ def read(name)
22
+ path = File.expand_path(name.to_s, ROOT)
23
+ raise ArgumentError, "shader path escapes shader root" unless path.start_with?("#{ROOT}/")
24
+ raise ArgumentError, "shader not found: #{name}" unless File.file?(path)
25
+
26
+ File.read(path)
27
+ end
28
+
29
+ def process(source, defines, stack)
30
+ enabled = [true]
31
+ output = []
32
+ source.each_line do |line|
33
+ match = DIRECTIVE.match(line)
34
+ unless match
35
+ output << line if enabled.last
36
+ next
37
+ end
38
+
39
+ operation, argument = match.captures
40
+ case operation
41
+ when "if"
42
+ enabled << (enabled.last && defines.include?(argument))
43
+ when "else"
44
+ parent_enabled = enabled[-2]
45
+ enabled[-1] = parent_enabled && !enabled[-1]
46
+ when "endif"
47
+ raise Error, "unmatched shader endif in #{stack.last}" if enabled.length == 1
48
+
49
+ enabled.pop
50
+ when "include"
51
+ next unless enabled.last
52
+ raise Error, "shader include cycle: #{[*stack, argument].join(" -> ")}" if stack.include?(argument)
53
+
54
+ output << process(read(argument), defines, [*stack, argument])
55
+ end
56
+ end
57
+ raise Error, "unterminated shader condition in #{stack.last}" unless enabled.length == 1
58
+
59
+ output.join
60
+ end
61
+ private_class_method :process
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stagecraft
4
+ class Renderer
5
+ class Stats
6
+ COUNTERS = %i[draw_calls triangles buffers textures pipelines visible_objects culled_objects].freeze
7
+
8
+ attr_reader(*COUNTERS)
9
+
10
+ def initialize
11
+ COUNTERS.each { |counter| instance_variable_set(:"@#{counter}", 0) }
12
+ end
13
+
14
+ def reset_frame!
15
+ @draw_calls = 0
16
+ @triangles = 0
17
+ @visible_objects = 0
18
+ @culled_objects = 0
19
+ self
20
+ end
21
+
22
+ def increment(counter, amount = 1)
23
+ raise ArgumentError, "unknown stats counter #{counter.inspect}" unless COUNTERS.include?(counter)
24
+
25
+ instance_variable_set(:"@#{counter}", public_send(counter) + amount)
26
+ end
27
+
28
+ def decrement(counter, amount = 1)
29
+ increment(counter, -amount)
30
+ end
31
+
32
+ def to_h
33
+ COUNTERS.to_h { |counter| [counter, public_send(counter)] }
34
+ end
35
+
36
+ def to_s
37
+ to_h.map { |name, value| "#{name}=#{value}" }.join(" ")
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stagecraft
4
+ class Renderer
5
+ class UniformPacker
6
+ Field = Data.define(:name, :type, :offset, :size, :alignment, :value)
7
+ Result = Data.define(:bytes, :fields, :struct_source, :textures)
8
+
9
+ TYPE_INFO = {
10
+ f32: [4, 4], u32: [4, 4],
11
+ vec2f: [8, 8], vec3f: [16, 12], vec4f: [16, 16],
12
+ mat4x4f: [16, 64]
13
+ }.freeze
14
+
15
+ def pack(uniforms = nil, struct_name: "MaterialUniforms", **keyword_uniforms)
16
+ uniforms = (uniforms || {}).merge(keyword_uniforms)
17
+ offset = 0
18
+ fields = []
19
+ textures = []
20
+ uniforms.each do |name, value|
21
+ if texture?(value)
22
+ textures << [name.to_sym, value]
23
+ next
24
+ end
25
+
26
+ type, normalized = infer(value)
27
+ alignment, size = TYPE_INFO.fetch(type)
28
+ offset = align(offset, alignment)
29
+ fields << Field.new(name: name.to_sym, type:, offset:, size:, alignment:, value: normalized)
30
+ offset += size
31
+ end
32
+ total_size = align(offset, [fields.map(&:alignment).max || 1, 16].max)
33
+ bytes = "\0".b * total_size
34
+ fields.each { |field| write(bytes, field) }
35
+ Result.new(
36
+ bytes: bytes.freeze,
37
+ fields: fields.freeze,
38
+ struct_source: declaration(struct_name, fields),
39
+ textures: textures.freeze
40
+ )
41
+ end
42
+
43
+ private
44
+
45
+ def infer(value)
46
+ case value
47
+ when Integer then [:u32, value]
48
+ when Numeric then [:f32, value.to_f]
49
+ when Color then [:vec4f, value.to_a]
50
+ when Larb::Vec2 then [:vec2f, value.to_a]
51
+ when Larb::Vec3 then [:vec3f, value.to_a]
52
+ when Larb::Vec4, Larb::Quat then [:vec4f, value.to_a]
53
+ when Larb::Mat4 then [:mat4x4f, value.to_a]
54
+ when Array then infer_array(value)
55
+ else
56
+ if value.respond_to?(:to_larb)
57
+ infer(value.to_larb)
58
+ else
59
+ raise ArgumentError, "unsupported uniform #{value.class}"
60
+ end
61
+ end
62
+ end
63
+
64
+ def infer_array(value)
65
+ type = { 2 => :vec2f, 3 => :vec3f, 4 => :vec4f, 16 => :mat4x4f }[value.length]
66
+ raise ArgumentError, "uniform arrays must contain 2, 3, 4, or 16 numbers" unless type
67
+ raise ArgumentError, "uniform arrays must be numeric" unless value.all? { |item| item.is_a?(Numeric) }
68
+
69
+ [type, value.map(&:to_f)]
70
+ end
71
+
72
+ def texture?(value)
73
+ value.is_a?(Textures::Texture)
74
+ end
75
+
76
+ def align(value, alignment)
77
+ ((value + alignment - 1) / alignment) * alignment
78
+ end
79
+
80
+ def write(bytes, field)
81
+ packed = field.type == :u32 ? [field.value].pack("L<") : Array(field.value).pack("e*")
82
+ bytes[field.offset, packed.bytesize] = packed
83
+ end
84
+
85
+ def declaration(name, fields)
86
+ body = fields.map { |field| " #{field.name}: #{field.type}," }.join("\n")
87
+ "struct #{name} {\n#{body}\n};"
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stagecraft
4
+ class Renderer
5
+ module WGPUCompatibility
6
+ module_function
7
+
8
+ def install!
9
+ return unless defined?(::WGPU::Native::TextureViewDescriptor)
10
+
11
+ native = ::WGPU::Native
12
+ descriptor = native::TextureViewDescriptor
13
+ return if descriptor.members.include?(:usage)
14
+
15
+ native.send(:remove_const, :TextureViewDescriptor)
16
+ native.const_set(:TextureViewDescriptor, texture_view_descriptor_class(native))
17
+ end
18
+
19
+ def texture_view_descriptor_class(native)
20
+ Class.new(FFI::Struct) do
21
+ layout :next_in_chain, :pointer,
22
+ :label, native::StringView,
23
+ :format, native::TextureFormat,
24
+ :dimension, native::TextureViewDimension,
25
+ :base_mip_level, :uint32,
26
+ :mip_level_count, :uint32,
27
+ :base_array_layer, :uint32,
28
+ :array_layer_count, :uint32,
29
+ :aspect, native::TextureAspect,
30
+ :usage, :uint64
31
+
32
+ def initialize(*arguments)
33
+ super
34
+ self[:usage] = 0
35
+ end
36
+ end
37
+ end
38
+ private_class_method :texture_view_descriptor_class
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stagecraft
4
+ module Textures
5
+ class CubeTexture < Texture
6
+ attr_reader :faces
7
+
8
+ def initialize(faces, **)
9
+ raise ArgumentError, "cube texture requires six faces" unless faces.length == 6
10
+
11
+ @faces = faces.freeze
12
+ super(nil, **)
13
+ end
14
+ end
15
+ end
16
+ end