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,336 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stagecraft
|
|
4
|
+
class Renderer
|
|
5
|
+
attr_reader :device, :queue, :surface_format, :stats, :width, :height, :sample_count
|
|
6
|
+
|
|
7
|
+
def self.offscreen(width:, height:, msaa: 1, **options)
|
|
8
|
+
new(width:, height:, msaa:, **options)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(window: nil, width: 800, height: 600, msaa: 4, device: nil, queue: nil,
|
|
12
|
+
surface: nil, backend: nil)
|
|
13
|
+
@backend = backend
|
|
14
|
+
@window = window
|
|
15
|
+
@width = Integer(width)
|
|
16
|
+
@height = Integer(height)
|
|
17
|
+
@sample_count = Integer(msaa)
|
|
18
|
+
@stats = Stats.new
|
|
19
|
+
@after_render_callbacks = []
|
|
20
|
+
@disposed = false
|
|
21
|
+
@start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
22
|
+
return if backend
|
|
23
|
+
|
|
24
|
+
@context = GPUContext.new(window:, width:, height:, device:, queue:, surface:)
|
|
25
|
+
@device = @context.device
|
|
26
|
+
@queue = @context.queue
|
|
27
|
+
@surface_format = @context.surface_format
|
|
28
|
+
@resources = FrameResources.new(context: @context, stats:, sample_count:)
|
|
29
|
+
@sample_count = @resources.sample_count
|
|
30
|
+
@resource_cache = ResourceCache.new(device: @device, queue: @queue, stats:)
|
|
31
|
+
@pipelines = PipelineFactory.new(
|
|
32
|
+
device: @device, queue: @queue, resources: @resources, resource_cache: @resource_cache,
|
|
33
|
+
stats:, sample_count: @sample_count
|
|
34
|
+
)
|
|
35
|
+
create_post_resources
|
|
36
|
+
rescue StandardError
|
|
37
|
+
dispose_after_failed_initialization
|
|
38
|
+
raise
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def render(scene, camera)
|
|
42
|
+
ensure_alive!
|
|
43
|
+
resize_from_window
|
|
44
|
+
list = RenderList.new(scene, camera)
|
|
45
|
+
update_stats(list)
|
|
46
|
+
if @backend
|
|
47
|
+
frame = record_frame(list, camera)
|
|
48
|
+
report_debug_stats
|
|
49
|
+
return frame
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
light, light_vp = shadow_light_and_matrix(list)
|
|
53
|
+
@resources.resize_shadow(light.shadow.map_size) if light
|
|
54
|
+
ordered_items = list.items
|
|
55
|
+
@resources.begin_frame(
|
|
56
|
+
ordered_items,
|
|
57
|
+
camera:,
|
|
58
|
+
ambient: list.ambient,
|
|
59
|
+
lights: prioritized_lights(list.lights, light),
|
|
60
|
+
light_vp:,
|
|
61
|
+
time: elapsed_time
|
|
62
|
+
)
|
|
63
|
+
encoder = device.create_command_encoder(label: "stagecraft frame")
|
|
64
|
+
encode_shadow_pass(encoder, light ? ordered_items : [])
|
|
65
|
+
encode_main_pass(encoder, ordered_items, scene)
|
|
66
|
+
@after_render_callbacks.each { |callback| callback.call(encoder, @resources.hdr_view) }
|
|
67
|
+
target_texture, target_view = @resources.frame_target
|
|
68
|
+
encode_post_pass(encoder, target_view)
|
|
69
|
+
queue.submit([encoder.finish])
|
|
70
|
+
@context.present
|
|
71
|
+
@resource_cache.advance_frame
|
|
72
|
+
frame = Frame.new(
|
|
73
|
+
texture: target_texture,
|
|
74
|
+
width:,
|
|
75
|
+
height:,
|
|
76
|
+
device:,
|
|
77
|
+
queue:,
|
|
78
|
+
readable: @context.surface.nil?
|
|
79
|
+
)
|
|
80
|
+
report_debug_stats
|
|
81
|
+
frame
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def resize(width, height)
|
|
85
|
+
ensure_alive!
|
|
86
|
+
@width = [Integer(width), 1].max
|
|
87
|
+
@height = [Integer(height), 1].max
|
|
88
|
+
return self if @backend
|
|
89
|
+
return self unless @context.resize(@width, @height)
|
|
90
|
+
|
|
91
|
+
@resources.resize(@width, @height)
|
|
92
|
+
create_post_resources
|
|
93
|
+
self
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def on_after_render(&block)
|
|
97
|
+
@after_render_callbacks << block
|
|
98
|
+
self
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def dispose
|
|
102
|
+
return self if @disposed
|
|
103
|
+
|
|
104
|
+
@disposed = true
|
|
105
|
+
first_error = nil
|
|
106
|
+
[
|
|
107
|
+
-> { warn_live_resources },
|
|
108
|
+
-> { release_post_resources },
|
|
109
|
+
-> { @pipelines&.dispose },
|
|
110
|
+
-> { @resource_cache&.dispose_all },
|
|
111
|
+
-> { @resources&.dispose },
|
|
112
|
+
-> { @context&.dispose }
|
|
113
|
+
].each do |cleanup|
|
|
114
|
+
cleanup.call
|
|
115
|
+
rescue StandardError => error
|
|
116
|
+
first_error ||= error
|
|
117
|
+
end
|
|
118
|
+
raise first_error if first_error
|
|
119
|
+
|
|
120
|
+
self
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def disposed?
|
|
124
|
+
@disposed
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def dispose_after_failed_initialization
|
|
130
|
+
dispose
|
|
131
|
+
rescue StandardError
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def warn_live_resources
|
|
136
|
+
return unless ENV["STAGECRAFT_DEBUG"] == "1"
|
|
137
|
+
return unless @resource_cache&.live_count&.positive?
|
|
138
|
+
|
|
139
|
+
warn "Stagecraft renderer disposed with #{@resource_cache.live_count} live CPU resources"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def resize_from_window
|
|
143
|
+
return unless @window&.respond_to?(:drawable_size)
|
|
144
|
+
|
|
145
|
+
next_width, next_height = @window.drawable_size
|
|
146
|
+
resize(next_width, next_height) if next_width.positive? && next_height.positive?
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def update_stats(list)
|
|
150
|
+
stats.reset_frame!
|
|
151
|
+
stats.increment(:visible_objects, list.items.length)
|
|
152
|
+
stats.increment(:culled_objects, list.culled_count)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def record_frame(list, camera)
|
|
156
|
+
@backend.begin_frame(camera:) if @backend.respond_to?(:begin_frame)
|
|
157
|
+
list.items.each do |item|
|
|
158
|
+
@backend.draw(
|
|
159
|
+
mesh: item.mesh,
|
|
160
|
+
features: item.features,
|
|
161
|
+
depth: item.depth,
|
|
162
|
+
pipeline_key: [
|
|
163
|
+
item.mesh.material.class,
|
|
164
|
+
Features.bits(item.features),
|
|
165
|
+
Features.vertex_layout_id(item.mesh.geometry)
|
|
166
|
+
]
|
|
167
|
+
)
|
|
168
|
+
track_draw(item.mesh.geometry)
|
|
169
|
+
end
|
|
170
|
+
@backend.end_frame if @backend.respond_to?(:end_frame)
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def shadow_light_and_matrix(list)
|
|
175
|
+
light = list.lights.find { |candidate| candidate.is_a?(Lights::Directional) && candidate.cast_shadow }
|
|
176
|
+
return [nil, Larb::Mat4.identity] unless light
|
|
177
|
+
|
|
178
|
+
config = light.shadow
|
|
179
|
+
eye = light.world_position
|
|
180
|
+
target = eye + light.direction
|
|
181
|
+
view = Larb::Mat4.look_at(eye, target, Larb::Vec3.up)
|
|
182
|
+
extent = config.extent
|
|
183
|
+
projection = Cameras::Orthographic.new(
|
|
184
|
+
left: -extent, right: extent, top: extent, bottom: -extent,
|
|
185
|
+
near: config.near, far: config.far
|
|
186
|
+
).projection_matrix
|
|
187
|
+
[light, projection * view]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def prioritized_lights(lights, shadow_light)
|
|
191
|
+
return lights unless shadow_light
|
|
192
|
+
|
|
193
|
+
[shadow_light, *lights.reject { |light| light.equal?(shadow_light) }]
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def encode_shadow_pass(encoder, items)
|
|
197
|
+
pass = encoder.begin_render_pass(
|
|
198
|
+
label: "stagecraft shadow",
|
|
199
|
+
color_attachments: [],
|
|
200
|
+
depth_stencil_attachment: {
|
|
201
|
+
view: @resources.shadow_view,
|
|
202
|
+
depth_load_op: :clear,
|
|
203
|
+
depth_store_op: :store,
|
|
204
|
+
depth_clear_value: 1.0
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
items.each_with_index do |item, index|
|
|
208
|
+
next unless item.mesh.cast_shadow
|
|
209
|
+
|
|
210
|
+
draw_item(pass, item, index, shadow: true)
|
|
211
|
+
end
|
|
212
|
+
pass.end_pass
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def encode_main_pass(encoder, items, scene)
|
|
216
|
+
clear = scene.background || Color.new(0.0)
|
|
217
|
+
attachment = @resources.main_color_attachment.merge(
|
|
218
|
+
clear_value: { r: clear.r, g: clear.g, b: clear.b, a: clear.a }
|
|
219
|
+
)
|
|
220
|
+
pass = encoder.begin_render_pass(
|
|
221
|
+
label: "stagecraft main",
|
|
222
|
+
color_attachments: [attachment],
|
|
223
|
+
depth_stencil_attachment: {
|
|
224
|
+
view: @resources.depth_view,
|
|
225
|
+
depth_load_op: :clear,
|
|
226
|
+
depth_store_op: :store,
|
|
227
|
+
depth_clear_value: 1.0
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
items.each_with_index { |item, index| draw_item(pass, item, index) }
|
|
231
|
+
pass.end_pass
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def draw_item(pass, item, index, shadow: false)
|
|
235
|
+
geometry = @resource_cache.gpu_geometry(item.mesh.geometry)
|
|
236
|
+
pass.set_pipeline(@pipelines.pipeline_for(item, shadow:))
|
|
237
|
+
pass.set_bind_group(0, shadow ? @resources.shadow_frame_group : @resources.frame_group)
|
|
238
|
+
if shadow
|
|
239
|
+
pass.set_bind_group(1, @resources.empty_group)
|
|
240
|
+
else
|
|
241
|
+
pass.set_bind_group(1, @pipelines.material_binding(item.mesh.material, item.features))
|
|
242
|
+
end
|
|
243
|
+
pass.set_bind_group(
|
|
244
|
+
2,
|
|
245
|
+
@resources.object_group_for(item.mesh),
|
|
246
|
+
dynamic_offsets: [@resources.object_offset(index)]
|
|
247
|
+
)
|
|
248
|
+
bind_vertex_buffers(pass, item.mesh.geometry, geometry, item.mesh.material, shadow:)
|
|
249
|
+
if geometry.index_buffer
|
|
250
|
+
pass.set_index_buffer(geometry.index_buffer, geometry.index_format)
|
|
251
|
+
pass.draw_indexed(geometry.index_count)
|
|
252
|
+
else
|
|
253
|
+
pass.draw(geometry.vertex_count)
|
|
254
|
+
end
|
|
255
|
+
track_draw(item.mesh.geometry) unless shadow
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def bind_vertex_buffers(pass, geometry, gpu_geometry, material, shadow:)
|
|
259
|
+
names = if shadow
|
|
260
|
+
%i[position joints weights]
|
|
261
|
+
elsif material.is_a?(Materials::Unlit)
|
|
262
|
+
%i[position uv uv1 color joints weights]
|
|
263
|
+
else
|
|
264
|
+
PipelineFactory::ATTRIBUTE_LOCATIONS.keys
|
|
265
|
+
end
|
|
266
|
+
slot = 0
|
|
267
|
+
names.each do |name|
|
|
268
|
+
next unless geometry.attribute(name)
|
|
269
|
+
|
|
270
|
+
pass.set_vertex_buffer(slot, gpu_geometry.vertex_buffers.fetch(name))
|
|
271
|
+
slot += 1
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def track_draw(geometry)
|
|
276
|
+
stats.increment(:draw_calls)
|
|
277
|
+
count = geometry.index&.count || geometry.attribute(:position).count
|
|
278
|
+
triangles = case geometry.topology
|
|
279
|
+
when :triangle_list then count / 3
|
|
280
|
+
when :triangle_strip then [count - 2, 0].max
|
|
281
|
+
else 0
|
|
282
|
+
end
|
|
283
|
+
stats.increment(:triangles, triangles)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def create_post_resources
|
|
287
|
+
return unless @resources
|
|
288
|
+
|
|
289
|
+
release_post_resources
|
|
290
|
+
@post_sampler = device.create_sampler(mag_filter: :linear, min_filter: :linear)
|
|
291
|
+
@post_group = device.create_bind_group(
|
|
292
|
+
layout: @pipelines.post_layout,
|
|
293
|
+
entries: [
|
|
294
|
+
{ binding: 0, texture_view: @resources.hdr_view },
|
|
295
|
+
{ binding: 1, sampler: @post_sampler }
|
|
296
|
+
]
|
|
297
|
+
)
|
|
298
|
+
@post_pipeline = @pipelines.post_pipeline(surface_format)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def encode_post_pass(encoder, target_view)
|
|
302
|
+
pass = encoder.begin_render_pass(
|
|
303
|
+
label: "stagecraft post",
|
|
304
|
+
color_attachments: [{ view: target_view, load_op: :clear, store_op: :store }]
|
|
305
|
+
)
|
|
306
|
+
pass.set_pipeline(@post_pipeline)
|
|
307
|
+
pass.set_bind_group(0, @post_group)
|
|
308
|
+
pass.draw(3)
|
|
309
|
+
pass.end_pass
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def release_post_resources
|
|
313
|
+
@post_group&.release
|
|
314
|
+
@post_sampler&.release
|
|
315
|
+
@post_group = @post_sampler = nil
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def elapsed_time
|
|
319
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start_time
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def report_debug_stats
|
|
323
|
+
return unless ENV["STAGECRAFT_DEBUG"] == "1"
|
|
324
|
+
|
|
325
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
326
|
+
return if @last_debug_report && now - @last_debug_report < 1.0
|
|
327
|
+
|
|
328
|
+
warn "Stagecraft #{stats}"
|
|
329
|
+
@last_debug_report = now
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def ensure_alive!
|
|
333
|
+
raise DisposedError, "renderer has been disposed" if disposed?
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stagecraft
|
|
4
|
+
class Renderer
|
|
5
|
+
class ResourceCache
|
|
6
|
+
GPUGeometry = Data.define(:vertex_buffers, :index_buffer, :index_format, :index_count, :vertex_count, :layout_id)
|
|
7
|
+
GPUTexture = Data.define(:texture, :view, :sampler)
|
|
8
|
+
Entry = Struct.new(:owner, :version, :resource)
|
|
9
|
+
|
|
10
|
+
attr_reader :frame
|
|
11
|
+
|
|
12
|
+
def initialize(device:, queue:, stats:, in_flight_frames: 3)
|
|
13
|
+
@device = device
|
|
14
|
+
@queue = queue
|
|
15
|
+
@stats = stats
|
|
16
|
+
@in_flight_frames = in_flight_frames
|
|
17
|
+
@geometries = {}
|
|
18
|
+
@textures = {}
|
|
19
|
+
@dispose_queue = []
|
|
20
|
+
@registered = {}
|
|
21
|
+
@frame = 0
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def gpu_geometry(geometry)
|
|
25
|
+
raise DisposedError, "cannot upload disposed geometry" if geometry.disposed?
|
|
26
|
+
|
|
27
|
+
existing = @geometries[geometry.object_id]
|
|
28
|
+
if existing&.owner&.equal?(geometry) && existing.version == geometry.version
|
|
29
|
+
return existing.resource
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
schedule_resource(existing.resource) if existing
|
|
33
|
+
resource = upload_geometry(geometry)
|
|
34
|
+
@geometries[geometry.object_id] = Entry.new(geometry, geometry.version, resource)
|
|
35
|
+
register_disposal(geometry)
|
|
36
|
+
resource
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def gpu_texture(texture)
|
|
40
|
+
raise DisposedError, "cannot upload disposed texture" if texture.disposed?
|
|
41
|
+
|
|
42
|
+
existing = @textures[texture.object_id]
|
|
43
|
+
if existing&.owner&.equal?(texture) && existing.version == texture.version
|
|
44
|
+
return existing.resource
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
schedule_resource(existing.resource) if existing
|
|
48
|
+
resource = upload_texture(texture)
|
|
49
|
+
@textures[texture.object_id] = Entry.new(texture, texture.version, resource)
|
|
50
|
+
register_disposal(texture)
|
|
51
|
+
resource
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def schedule_dispose(cpu_object)
|
|
55
|
+
entry = delete_owned_entry(@geometries, cpu_object) ||
|
|
56
|
+
delete_owned_entry(@textures, cpu_object)
|
|
57
|
+
schedule_resource(entry.resource) if entry
|
|
58
|
+
@registered.delete(cpu_object.object_id)
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def advance_frame
|
|
63
|
+
@frame += 1
|
|
64
|
+
ready, @dispose_queue = @dispose_queue.partition { |due, _resource| due <= frame }
|
|
65
|
+
ready.each { |_due, resource| destroy_resource(resource) }
|
|
66
|
+
self
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def dispose_all
|
|
70
|
+
resources = @geometries.values.concat(@textures.values).map(&:resource)
|
|
71
|
+
resources.concat(@dispose_queue.map(&:last))
|
|
72
|
+
resources.each { |resource| destroy_resource(resource) }
|
|
73
|
+
@geometries.clear
|
|
74
|
+
@textures.clear
|
|
75
|
+
@dispose_queue.clear
|
|
76
|
+
@registered.clear
|
|
77
|
+
self
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def live_count
|
|
81
|
+
@geometries.length + @textures.length
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def upload_geometry(geometry)
|
|
87
|
+
buffers = geometry.attributes.to_h do |name, attribute|
|
|
88
|
+
buffer = @device.create_buffer_with_data(
|
|
89
|
+
label: "stagecraft #{name}",
|
|
90
|
+
data: attribute.data,
|
|
91
|
+
usage: %i[vertex copy_dst]
|
|
92
|
+
)
|
|
93
|
+
@stats.increment(:buffers)
|
|
94
|
+
[name, buffer]
|
|
95
|
+
end
|
|
96
|
+
index_buffer = if geometry.index
|
|
97
|
+
@stats.increment(:buffers)
|
|
98
|
+
@device.create_buffer_with_data(
|
|
99
|
+
label: "stagecraft index",
|
|
100
|
+
data: geometry.index.data,
|
|
101
|
+
usage: %i[index copy_dst]
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
GPUGeometry.new(
|
|
105
|
+
vertex_buffers: buffers.freeze,
|
|
106
|
+
index_buffer:,
|
|
107
|
+
index_format: geometry.index&.format,
|
|
108
|
+
index_count: geometry.index&.count,
|
|
109
|
+
vertex_count: geometry.attribute(:position).count,
|
|
110
|
+
layout_id: Features.vertex_layout_id(geometry)
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def upload_texture(texture)
|
|
115
|
+
require "texel/wgpu"
|
|
116
|
+
raise Error, "texture has no image" unless texture.image
|
|
117
|
+
|
|
118
|
+
image = texture.image
|
|
119
|
+
if texture.color_space && image.color_space != texture.color_space
|
|
120
|
+
image = image.dup_with(color_space: texture.color_space)
|
|
121
|
+
end
|
|
122
|
+
gpu_texture = Texel::WGPU.upload(image, device: @device, queue: @queue)
|
|
123
|
+
sampler_state = texture.sampler
|
|
124
|
+
sampler = @device.create_sampler(
|
|
125
|
+
address_mode_u: sampler_state.wrap_u,
|
|
126
|
+
address_mode_v: sampler_state.wrap_v,
|
|
127
|
+
address_mode_w: sampler_state.wrap_w,
|
|
128
|
+
mag_filter: sampler_state.mag_filter,
|
|
129
|
+
min_filter: sampler_state.min_filter,
|
|
130
|
+
mipmap_filter: sampler_state.mipmap_filter,
|
|
131
|
+
lod_min_clamp: sampler_state.lod_min,
|
|
132
|
+
lod_max_clamp: sampler_state.lod_max,
|
|
133
|
+
compare: sampler_state.compare,
|
|
134
|
+
max_anisotropy: sampler_state.max_anisotropy
|
|
135
|
+
)
|
|
136
|
+
@stats.increment(:textures)
|
|
137
|
+
GPUTexture.new(texture: gpu_texture, view: gpu_texture.create_view, sampler:)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def register_disposal(object)
|
|
141
|
+
return if @registered[object.object_id]
|
|
142
|
+
|
|
143
|
+
@registered[object.object_id] = true
|
|
144
|
+
object.on_dispose { |disposed| schedule_dispose(disposed) }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def delete_owned_entry(collection, owner)
|
|
148
|
+
entry = collection[owner.object_id]
|
|
149
|
+
return unless entry&.owner&.equal?(owner)
|
|
150
|
+
|
|
151
|
+
collection.delete(owner.object_id)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def schedule_resource(resource)
|
|
155
|
+
@dispose_queue << [frame + @in_flight_frames, resource]
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def destroy_resource(resource)
|
|
159
|
+
case resource
|
|
160
|
+
when GPUGeometry
|
|
161
|
+
[*resource.vertex_buffers.values, resource.index_buffer].compact.each do |buffer|
|
|
162
|
+
buffer.destroy if buffer.respond_to?(:destroy)
|
|
163
|
+
buffer.release if buffer.respond_to?(:release)
|
|
164
|
+
@stats.decrement(:buffers)
|
|
165
|
+
end
|
|
166
|
+
when GPUTexture
|
|
167
|
+
resource.sampler.release if resource.sampler.respond_to?(:release)
|
|
168
|
+
resource.view.release if resource.view.respond_to?(:release)
|
|
169
|
+
resource.texture.destroy if resource.texture.respond_to?(:destroy)
|
|
170
|
+
resource.texture.release if resource.texture.respond_to?(:release)
|
|
171
|
+
@stats.decrement(:textures)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
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 Light {
|
|
11
|
+
kind: u32,
|
|
12
|
+
color: vec3f,
|
|
13
|
+
position: vec3f,
|
|
14
|
+
direction: vec3f,
|
|
15
|
+
range: f32,
|
|
16
|
+
cone: vec2f,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
struct ObjectUniforms {
|
|
20
|
+
model: mat4x4f,
|
|
21
|
+
normal_matrix: mat4x4f,
|
|
22
|
+
receive_shadow: u32,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
@group(0) @binding(0) var<uniform> frame: FrameUniforms;
|
|
26
|
+
@group(0) @binding(1) var<storage, read> lights: array<Light>;
|
|
27
|
+
@group(0) @binding(2) var shadow_map: texture_depth_2d;
|
|
28
|
+
@group(0) @binding(3) var shadow_sampler: sampler_comparison;
|
|
29
|
+
@group(2) @binding(0) var<uniform> object: ObjectUniforms;
|
|
30
|
+
@group(2) @binding(1) var<storage, read> joints: array<mat4x4f>;
|
|
31
|
+
|
|
32
|
+
fn shadow_visibility(world_position: vec3f) -> f32 {
|
|
33
|
+
let projected = frame.light_vp * vec4f(world_position, 1.0);
|
|
34
|
+
let uvz = projected.xyz / projected.w;
|
|
35
|
+
let uv = vec2f(uvz.x * 0.5 + 0.5, 0.5 - uvz.y * 0.5);
|
|
36
|
+
if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0 || uvz.z > 1.0) {
|
|
37
|
+
return 1.0;
|
|
38
|
+
}
|
|
39
|
+
var visibility = 0.0;
|
|
40
|
+
let dimensions = vec2f(textureDimensions(shadow_map));
|
|
41
|
+
for (var y = -1; y <= 1; y = y + 1) {
|
|
42
|
+
for (var x = -1; x <= 1; x = x + 1) {
|
|
43
|
+
let offset = vec2f(f32(x), f32(y)) / dimensions;
|
|
44
|
+
visibility += textureSampleCompare(shadow_map, shadow_sampler, uv + offset, uvz.z - 0.001);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return visibility / 9.0;
|
|
48
|
+
}
|