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,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,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
|