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,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stagecraft
|
|
4
|
+
class Renderer
|
|
5
|
+
class Frame
|
|
6
|
+
attr_reader :texture, :width, :height
|
|
7
|
+
|
|
8
|
+
def initialize(texture:, width:, height:, device:, queue:, readable:)
|
|
9
|
+
@texture = texture
|
|
10
|
+
@width = width
|
|
11
|
+
@height = height
|
|
12
|
+
@device = device
|
|
13
|
+
@queue = queue
|
|
14
|
+
@readable = readable
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def read_pixels
|
|
18
|
+
raise Error, "surface frames cannot be read after presentation; use Renderer.offscreen" unless @readable
|
|
19
|
+
|
|
20
|
+
require "texel"
|
|
21
|
+
bytes_per_row = align(width * 4, 256)
|
|
22
|
+
padded = @queue.read_texture(
|
|
23
|
+
source: { texture: },
|
|
24
|
+
data_layout: { bytes_per_row:, rows_per_image: height },
|
|
25
|
+
size: { width:, height:, depth_or_array_layers: 1 },
|
|
26
|
+
device: @device
|
|
27
|
+
)
|
|
28
|
+
pixels = if bytes_per_row == width * 4
|
|
29
|
+
padded
|
|
30
|
+
else
|
|
31
|
+
height.times.map { |row| padded.byteslice(row * bytes_per_row, width * 4) }.join
|
|
32
|
+
end
|
|
33
|
+
Texel::Image.new(
|
|
34
|
+
width:, height:, channels: 4, dtype: :u8, color_space: :srgb, data: pixels
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def align(value, alignment)
|
|
41
|
+
((value + alignment - 1) / alignment) * alignment
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stagecraft
|
|
4
|
+
class Renderer
|
|
5
|
+
class FrameResources
|
|
6
|
+
SkinBinding = Struct.new(:mesh, :skin, :buffer, :bind_group)
|
|
7
|
+
OBJECT_SLOT_SIZE = 256
|
|
8
|
+
OBJECT_DATA_SIZE = 144
|
|
9
|
+
FRAMES_IN_FLIGHT = 3
|
|
10
|
+
LIGHT_SIZE = 80
|
|
11
|
+
GUARANTEED_SAMPLE_COUNTS = [1, 4].freeze
|
|
12
|
+
|
|
13
|
+
attr_reader :device, :queue, :stats, :frame_layout, :object_layout, :empty_layout, :empty_group,
|
|
14
|
+
:shadow_frame_layout, :frame_group, :shadow_frame_group, :object_group,
|
|
15
|
+
:hdr_texture, :hdr_view, :depth_texture, :depth_view, :shadow_texture,
|
|
16
|
+
:shadow_view, :fallback_white, :fallback_black, :fallback_normal,
|
|
17
|
+
:output_texture, :output_view, :width, :height, :sample_count
|
|
18
|
+
|
|
19
|
+
def self.guaranteed_sample_count(requested)
|
|
20
|
+
value = Integer(requested)
|
|
21
|
+
GUARANTEED_SAMPLE_COUNTS.reverse.find { |count| count <= value } ||
|
|
22
|
+
GUARANTEED_SAMPLE_COUNTS.first
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(context:, stats:, sample_count:)
|
|
26
|
+
@context = context
|
|
27
|
+
@device = context.device
|
|
28
|
+
@queue = context.queue
|
|
29
|
+
@stats = stats
|
|
30
|
+
@sample_count = self.class.guaranteed_sample_count(sample_count)
|
|
31
|
+
@frame_number = 0
|
|
32
|
+
@object_capacity = 1
|
|
33
|
+
@light_capacity = 1
|
|
34
|
+
@skin_bindings = {}
|
|
35
|
+
create_layouts
|
|
36
|
+
create_buffers
|
|
37
|
+
create_shadow_resources
|
|
38
|
+
create_fallback_textures
|
|
39
|
+
resize(context.width, context.height)
|
|
40
|
+
rescue StandardError
|
|
41
|
+
dispose_after_failed_initialization
|
|
42
|
+
raise
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def resize(width, height)
|
|
46
|
+
@width = Integer(width)
|
|
47
|
+
@height = Integer(height)
|
|
48
|
+
release_attachments
|
|
49
|
+
create_hdr_attachments
|
|
50
|
+
create_output_attachment unless @context.surface
|
|
51
|
+
self
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def begin_frame(items, camera:, ambient:, lights:, light_vp:, time:)
|
|
55
|
+
ensure_object_capacity(items.length)
|
|
56
|
+
ensure_light_capacity(lights.length)
|
|
57
|
+
write_frame(camera:, ambient:, lights:, light_vp:, time:)
|
|
58
|
+
write_objects(items)
|
|
59
|
+
write_skins(items)
|
|
60
|
+
@frame_number += 1
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def object_offset(index)
|
|
65
|
+
frame_slot = (@frame_number - 1) % FRAMES_IN_FLIGHT
|
|
66
|
+
(frame_slot * @object_capacity * OBJECT_SLOT_SIZE) + (index * OBJECT_SLOT_SIZE)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def object_group_for(mesh)
|
|
70
|
+
mesh.skin ? @skin_bindings.fetch(mesh.object_id).bind_group : object_group
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def resize_shadow(map_size)
|
|
74
|
+
next_size = Integer(map_size)
|
|
75
|
+
return self if next_size == @shadow_size
|
|
76
|
+
|
|
77
|
+
release_resource(@shadow_view)
|
|
78
|
+
destroy_texture(@shadow_texture)
|
|
79
|
+
create_shadow_texture(next_size)
|
|
80
|
+
recreate_frame_groups
|
|
81
|
+
self
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def main_color_attachment
|
|
85
|
+
if sample_count > 1
|
|
86
|
+
{
|
|
87
|
+
view: @hdr_msaa_view,
|
|
88
|
+
resolve_target: hdr_view,
|
|
89
|
+
load_op: :clear,
|
|
90
|
+
store_op: :discard
|
|
91
|
+
}
|
|
92
|
+
else
|
|
93
|
+
{ view: hdr_view, load_op: :clear, store_op: :store }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def frame_target
|
|
98
|
+
return [output_texture, output_view] unless @context.surface
|
|
99
|
+
|
|
100
|
+
@context.current_target
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def dispose
|
|
104
|
+
release_attachments
|
|
105
|
+
release_resource(@shadow_sampler, @shadow_view)
|
|
106
|
+
destroy_texture(@shadow_texture)
|
|
107
|
+
[fallback_white, fallback_black, fallback_normal].compact.each do |resource|
|
|
108
|
+
release_gpu_texture(resource)
|
|
109
|
+
end
|
|
110
|
+
release_resource(@frame_group, @shadow_frame_group, @object_group, @empty_group)
|
|
111
|
+
@skin_bindings.each_value do |binding|
|
|
112
|
+
release_resource(binding.bind_group)
|
|
113
|
+
destroy_buffer(binding.buffer)
|
|
114
|
+
end
|
|
115
|
+
@skin_bindings.clear
|
|
116
|
+
[@frame_buffer, @lights_buffer, @object_buffer, @joint_buffer].compact.each do |buffer|
|
|
117
|
+
destroy_buffer(buffer)
|
|
118
|
+
end
|
|
119
|
+
release_resource(frame_layout, object_layout, empty_layout, shadow_frame_layout)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def dispose_after_failed_initialization
|
|
125
|
+
dispose
|
|
126
|
+
rescue StandardError
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def create_layouts
|
|
131
|
+
@frame_layout = device.create_bind_group_layout(
|
|
132
|
+
label: "stagecraft frame layout",
|
|
133
|
+
entries: [
|
|
134
|
+
{ binding: 0, visibility: %i[vertex fragment], buffer: { type: :uniform } },
|
|
135
|
+
{ binding: 1, visibility: :fragment, buffer: { type: :read_only_storage } },
|
|
136
|
+
{ binding: 2, visibility: :fragment, texture: { sample_type: :depth } },
|
|
137
|
+
{ binding: 3, visibility: :fragment, sampler: { type: :comparison } }
|
|
138
|
+
]
|
|
139
|
+
)
|
|
140
|
+
@shadow_frame_layout = device.create_bind_group_layout(
|
|
141
|
+
entries: [{ binding: 0, visibility: :vertex, buffer: { type: :uniform } }]
|
|
142
|
+
)
|
|
143
|
+
@object_layout = device.create_bind_group_layout(
|
|
144
|
+
label: "stagecraft object layout",
|
|
145
|
+
entries: [
|
|
146
|
+
{
|
|
147
|
+
binding: 0,
|
|
148
|
+
visibility: %i[vertex fragment],
|
|
149
|
+
buffer: { type: :uniform, has_dynamic_offset: true, min_binding_size: OBJECT_DATA_SIZE }
|
|
150
|
+
},
|
|
151
|
+
{ binding: 1, visibility: :vertex, buffer: { type: :read_only_storage } }
|
|
152
|
+
]
|
|
153
|
+
)
|
|
154
|
+
@empty_layout = device.create_bind_group_layout(entries: [])
|
|
155
|
+
@empty_group = device.create_bind_group(layout: empty_layout, entries: [])
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def create_buffers
|
|
159
|
+
@frame_buffer = device.create_buffer(
|
|
160
|
+
label: "stagecraft frame uniforms", size: 256, usage: %i[uniform copy_dst]
|
|
161
|
+
)
|
|
162
|
+
stats.increment(:buffers)
|
|
163
|
+
@lights_buffer = device.create_buffer(
|
|
164
|
+
label: "stagecraft lights", size: LIGHT_SIZE, usage: %i[storage copy_dst]
|
|
165
|
+
)
|
|
166
|
+
stats.increment(:buffers)
|
|
167
|
+
@joint_buffer = device.create_buffer(
|
|
168
|
+
label: "stagecraft empty joints", size: 64, usage: %i[storage copy_dst]
|
|
169
|
+
)
|
|
170
|
+
stats.increment(:buffers)
|
|
171
|
+
@object_buffer = create_object_buffer
|
|
172
|
+
stats.increment(:buffers)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def create_shadow_resources
|
|
176
|
+
create_shadow_texture(2_048)
|
|
177
|
+
@shadow_sampler = device.create_sampler(
|
|
178
|
+
compare: :less_equal,
|
|
179
|
+
mag_filter: :linear,
|
|
180
|
+
min_filter: :linear
|
|
181
|
+
)
|
|
182
|
+
recreate_frame_groups
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def create_shadow_texture(size)
|
|
186
|
+
@shadow_size = Integer(size)
|
|
187
|
+
@shadow_texture = device.create_texture(
|
|
188
|
+
label: "stagecraft shadow map",
|
|
189
|
+
size: { width: @shadow_size, height: @shadow_size, depth_or_array_layers: 1 },
|
|
190
|
+
format: :depth32_float,
|
|
191
|
+
usage: %i[render_attachment texture_binding]
|
|
192
|
+
)
|
|
193
|
+
@shadow_view = shadow_texture.create_view
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def create_fallback_textures
|
|
197
|
+
@fallback_white = solid_texture([255, 255, 255, 255], srgb: true)
|
|
198
|
+
@fallback_black = solid_texture([0, 0, 0, 255], srgb: true)
|
|
199
|
+
@fallback_normal = solid_texture([128, 128, 255, 255], srgb: false)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def solid_texture(rgba, srgb:)
|
|
203
|
+
texture = device.create_texture(
|
|
204
|
+
size: { width: 1, height: 1, depth_or_array_layers: 1 },
|
|
205
|
+
format: srgb ? :rgba8_unorm_srgb : :rgba8_unorm,
|
|
206
|
+
usage: %i[texture_binding copy_dst]
|
|
207
|
+
)
|
|
208
|
+
queue.write_texture(
|
|
209
|
+
destination: { texture: },
|
|
210
|
+
data: rgba.pack("C*"),
|
|
211
|
+
data_layout: { bytes_per_row: 4, rows_per_image: 1 },
|
|
212
|
+
size: { width: 1, height: 1, depth_or_array_layers: 1 }
|
|
213
|
+
)
|
|
214
|
+
sampler = device.create_sampler(mag_filter: :linear, min_filter: :linear, mipmap_filter: :linear)
|
|
215
|
+
stats.increment(:textures)
|
|
216
|
+
ResourceCache::GPUTexture.new(texture:, view: texture.create_view, sampler:)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def create_hdr_attachments
|
|
220
|
+
@hdr_texture = device.create_texture(
|
|
221
|
+
label: "stagecraft hdr",
|
|
222
|
+
size: extent,
|
|
223
|
+
format: :rgba16_float,
|
|
224
|
+
usage: %i[render_attachment texture_binding copy_src]
|
|
225
|
+
)
|
|
226
|
+
@hdr_view = hdr_texture.create_view
|
|
227
|
+
if sample_count > 1
|
|
228
|
+
@hdr_msaa_texture = device.create_texture(
|
|
229
|
+
label: "stagecraft hdr msaa",
|
|
230
|
+
size: extent,
|
|
231
|
+
format: :rgba16_float,
|
|
232
|
+
usage: :render_attachment,
|
|
233
|
+
sample_count:
|
|
234
|
+
)
|
|
235
|
+
@hdr_msaa_view = @hdr_msaa_texture.create_view
|
|
236
|
+
end
|
|
237
|
+
@depth_texture = device.create_texture(
|
|
238
|
+
label: "stagecraft depth",
|
|
239
|
+
size: extent,
|
|
240
|
+
format: :depth24_plus,
|
|
241
|
+
usage: :render_attachment,
|
|
242
|
+
sample_count:
|
|
243
|
+
)
|
|
244
|
+
@depth_view = depth_texture.create_view
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def create_output_attachment
|
|
248
|
+
@output_texture = device.create_texture(
|
|
249
|
+
label: "stagecraft output",
|
|
250
|
+
size: extent,
|
|
251
|
+
format: @context.surface_format,
|
|
252
|
+
usage: %i[render_attachment texture_binding copy_src]
|
|
253
|
+
)
|
|
254
|
+
@output_view = output_texture.create_view
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def ensure_object_capacity(count)
|
|
258
|
+
return if count <= @object_capacity
|
|
259
|
+
|
|
260
|
+
release_resource(@object_group)
|
|
261
|
+
destroy_buffer(@object_buffer)
|
|
262
|
+
@object_capacity *= 2 while @object_capacity < count
|
|
263
|
+
@object_buffer = create_object_buffer
|
|
264
|
+
stats.increment(:buffers)
|
|
265
|
+
recreate_object_group
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def ensure_light_capacity(count)
|
|
269
|
+
return if count <= @light_capacity
|
|
270
|
+
|
|
271
|
+
destroy_buffer(@lights_buffer)
|
|
272
|
+
@light_capacity *= 2 while @light_capacity < count
|
|
273
|
+
@lights_buffer = device.create_buffer(
|
|
274
|
+
label: "stagecraft lights",
|
|
275
|
+
size: @light_capacity * LIGHT_SIZE,
|
|
276
|
+
usage: %i[storage copy_dst]
|
|
277
|
+
)
|
|
278
|
+
stats.increment(:buffers)
|
|
279
|
+
recreate_frame_groups
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def create_object_buffer
|
|
283
|
+
device.create_buffer(
|
|
284
|
+
label: "stagecraft object uniforms",
|
|
285
|
+
size: @object_capacity * OBJECT_SLOT_SIZE * FRAMES_IN_FLIGHT,
|
|
286
|
+
usage: %i[uniform copy_dst]
|
|
287
|
+
)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def recreate_frame_groups
|
|
291
|
+
release_resource(@frame_group, @shadow_frame_group)
|
|
292
|
+
@frame_group = device.create_bind_group(
|
|
293
|
+
label: "stagecraft frame bind group",
|
|
294
|
+
layout: frame_layout,
|
|
295
|
+
entries: [
|
|
296
|
+
{ binding: 0, buffer: @frame_buffer, size: 160 },
|
|
297
|
+
{ binding: 1, buffer: @lights_buffer },
|
|
298
|
+
{ binding: 2, texture_view: shadow_view },
|
|
299
|
+
{ binding: 3, sampler: @shadow_sampler }
|
|
300
|
+
]
|
|
301
|
+
)
|
|
302
|
+
@shadow_frame_group = device.create_bind_group(
|
|
303
|
+
layout: shadow_frame_layout,
|
|
304
|
+
entries: [{ binding: 0, buffer: @frame_buffer, size: 160 }]
|
|
305
|
+
)
|
|
306
|
+
recreate_object_group
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def recreate_object_group
|
|
310
|
+
release_resource(@object_group)
|
|
311
|
+
@object_group = device.create_bind_group(
|
|
312
|
+
label: "stagecraft object bind group",
|
|
313
|
+
layout: object_layout,
|
|
314
|
+
entries: [
|
|
315
|
+
{ binding: 0, buffer: @object_buffer, size: OBJECT_DATA_SIZE },
|
|
316
|
+
{ binding: 1, buffer: @joint_buffer }
|
|
317
|
+
]
|
|
318
|
+
)
|
|
319
|
+
@skin_bindings.each_value do |binding|
|
|
320
|
+
release_resource(binding.bind_group)
|
|
321
|
+
binding.bind_group = create_object_group(binding.buffer)
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def create_object_group(joint_buffer)
|
|
326
|
+
device.create_bind_group(
|
|
327
|
+
label: "stagecraft skinned object bind group",
|
|
328
|
+
layout: object_layout,
|
|
329
|
+
entries: [
|
|
330
|
+
{ binding: 0, buffer: @object_buffer, size: OBJECT_DATA_SIZE },
|
|
331
|
+
{ binding: 1, buffer: joint_buffer }
|
|
332
|
+
]
|
|
333
|
+
)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def write_frame(camera:, ambient:, lights:, light_vp:, time:)
|
|
337
|
+
bytes = String.new(capacity: 160, encoding: Encoding::BINARY)
|
|
338
|
+
bytes << camera.view_projection_matrix.to_a.pack("e*")
|
|
339
|
+
bytes << [*camera.world_position.to_a, time.to_f].pack("e*")
|
|
340
|
+
bytes << ambient.to_a.pack("e*")
|
|
341
|
+
bytes << [lights.length].pack("L<")
|
|
342
|
+
bytes << light_vp.to_a.pack("e*")
|
|
343
|
+
queue.write_buffer(@frame_buffer, 0, bytes)
|
|
344
|
+
return if lights.empty?
|
|
345
|
+
|
|
346
|
+
queue.write_buffer(@lights_buffer, 0, pack_lights(lights))
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def pack_lights(lights)
|
|
350
|
+
lights.map do |light|
|
|
351
|
+
bytes = "\0".b * LIGHT_SIZE
|
|
352
|
+
kind = { Lights::Directional => 0, Lights::Point => 1, Lights::Spot => 2 }.fetch(light.class, 0)
|
|
353
|
+
bytes[0, 4] = [kind].pack("L<")
|
|
354
|
+
color = light.color.to_a.first(3).map { |component| component * light.intensity }
|
|
355
|
+
bytes[16, 12] = color.pack("e*")
|
|
356
|
+
bytes[32, 12] = light.world_position.to_a.pack("e*")
|
|
357
|
+
bytes[48, 12] = light.direction.to_a.pack("e*")
|
|
358
|
+
range = light.respond_to?(:range) ? light.range : 0.0
|
|
359
|
+
bytes[60, 4] = [range.finite? ? range : 3.4e38].pack("e")
|
|
360
|
+
cone = light.is_a?(Lights::Spot) ? [Math.cos(light.inner_angle), Math.cos(light.outer_angle)] : [1.0, -1.0]
|
|
361
|
+
bytes[64, 8] = cone.pack("e*")
|
|
362
|
+
bytes
|
|
363
|
+
end.join
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def write_objects(items)
|
|
367
|
+
return if items.empty?
|
|
368
|
+
|
|
369
|
+
frame_slot = @frame_number % FRAMES_IN_FLIGHT
|
|
370
|
+
bytes = "\0".b * (@object_capacity * OBJECT_SLOT_SIZE)
|
|
371
|
+
items.each_with_index do |item, index|
|
|
372
|
+
offset = index * OBJECT_SLOT_SIZE
|
|
373
|
+
model = item.mesh.world_matrix
|
|
374
|
+
bytes[offset, 64] = model.to_a.pack("e*")
|
|
375
|
+
normal = model.inverse.transpose
|
|
376
|
+
bytes[offset + 64, 64] = normal.to_a.pack("e*")
|
|
377
|
+
rescue StandardError
|
|
378
|
+
bytes[offset + 64, 64] = Larb::Mat4.identity.to_a.pack("e*")
|
|
379
|
+
ensure
|
|
380
|
+
bytes[offset + 128, 4] = [item.mesh.receive_shadow ? 1 : 0].pack("L<")
|
|
381
|
+
end
|
|
382
|
+
queue.write_buffer(
|
|
383
|
+
@object_buffer,
|
|
384
|
+
frame_slot * @object_capacity * OBJECT_SLOT_SIZE,
|
|
385
|
+
bytes
|
|
386
|
+
)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def write_skins(items)
|
|
390
|
+
prune_skin_bindings(items)
|
|
391
|
+
items.each do |item|
|
|
392
|
+
mesh = item.mesh
|
|
393
|
+
next unless mesh.skin
|
|
394
|
+
|
|
395
|
+
binding = @skin_bindings[mesh.object_id]
|
|
396
|
+
if !binding || !binding.mesh.equal?(mesh) || !binding.skin.equal?(mesh.skin)
|
|
397
|
+
if binding
|
|
398
|
+
release_resource(binding.bind_group)
|
|
399
|
+
destroy_buffer(binding.buffer)
|
|
400
|
+
end
|
|
401
|
+
size = [mesh.skin.joints.length, 1].max * 64
|
|
402
|
+
buffer = device.create_buffer(
|
|
403
|
+
label: "stagecraft joint matrices",
|
|
404
|
+
size:,
|
|
405
|
+
usage: %i[storage copy_dst]
|
|
406
|
+
)
|
|
407
|
+
stats.increment(:buffers)
|
|
408
|
+
binding = SkinBinding.new(mesh, mesh.skin, buffer, create_object_group(buffer))
|
|
409
|
+
@skin_bindings[mesh.object_id] = binding
|
|
410
|
+
end
|
|
411
|
+
matrices = mesh.skin.joint_matrices(mesh)
|
|
412
|
+
bytes = matrices.empty? ? Larb::Mat4.identity.to_a.pack("e*") :
|
|
413
|
+
matrices.flat_map(&:to_a).pack("e*")
|
|
414
|
+
queue.write_buffer(binding.buffer, 0, bytes)
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def prune_skin_bindings(items)
|
|
419
|
+
active = items.filter_map do |item|
|
|
420
|
+
[item.mesh.object_id, item.mesh] if item.mesh.skin
|
|
421
|
+
end.to_h
|
|
422
|
+
@skin_bindings.delete_if do |object_id, binding|
|
|
423
|
+
next false if active[object_id]&.equal?(binding.mesh)
|
|
424
|
+
|
|
425
|
+
release_resource(binding.bind_group)
|
|
426
|
+
destroy_buffer(binding.buffer)
|
|
427
|
+
true
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def extent
|
|
432
|
+
{ width:, height:, depth_or_array_layers: 1 }
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def release_attachments
|
|
436
|
+
release_resource(@hdr_view, @hdr_msaa_view, @depth_view, @output_view)
|
|
437
|
+
[@hdr_texture, @hdr_msaa_texture, @depth_texture, @output_texture].compact.each do |texture|
|
|
438
|
+
destroy_texture(texture)
|
|
439
|
+
end
|
|
440
|
+
@hdr_texture = @hdr_view = @hdr_msaa_texture = @hdr_msaa_view = nil
|
|
441
|
+
@depth_texture = @depth_view = @output_texture = @output_view = nil
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def release_gpu_texture(resource)
|
|
445
|
+
resource.view.release if resource.view.respond_to?(:release)
|
|
446
|
+
resource.sampler.release if resource.sampler.respond_to?(:release)
|
|
447
|
+
destroy_texture(resource.texture)
|
|
448
|
+
stats.decrement(:textures)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def destroy_texture(texture)
|
|
452
|
+
texture.destroy if texture.respond_to?(:destroy)
|
|
453
|
+
texture.release if texture.respond_to?(:release)
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def destroy_buffer(buffer)
|
|
457
|
+
buffer.destroy if buffer.respond_to?(:destroy)
|
|
458
|
+
buffer.release if buffer.respond_to?(:release)
|
|
459
|
+
stats.decrement(:buffers)
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def release_resource(*resources)
|
|
463
|
+
resources.compact.each { |resource| resource.release if resource.respond_to?(:release) }
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stagecraft
|
|
4
|
+
class Renderer
|
|
5
|
+
class GPUContext
|
|
6
|
+
attr_reader :instance, :adapter, :device, :queue, :surface, :surface_format, :width, :height
|
|
7
|
+
|
|
8
|
+
def initialize(window: nil, width:, height:, device: nil, queue: nil, surface: nil)
|
|
9
|
+
WGPUCompatibility.install!
|
|
10
|
+
@window = window
|
|
11
|
+
@width = Integer(width)
|
|
12
|
+
@height = Integer(height)
|
|
13
|
+
@owns_device = device.nil?
|
|
14
|
+
@disposed = false
|
|
15
|
+
if device
|
|
16
|
+
@device = device
|
|
17
|
+
@queue = queue || device.queue
|
|
18
|
+
@surface = surface
|
|
19
|
+
@adapter = device.respond_to?(:adapter) ? device.adapter : nil
|
|
20
|
+
else
|
|
21
|
+
initialize_wgpu
|
|
22
|
+
end
|
|
23
|
+
@surface_format = choose_surface_format
|
|
24
|
+
configure_surface if @surface
|
|
25
|
+
rescue StandardError
|
|
26
|
+
dispose_after_failed_initialization
|
|
27
|
+
raise
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def resize(width, height)
|
|
31
|
+
next_width = [Integer(width), 1].max
|
|
32
|
+
next_height = [Integer(height), 1].max
|
|
33
|
+
return false if next_width == @width && next_height == @height
|
|
34
|
+
|
|
35
|
+
@width = next_width
|
|
36
|
+
@height = next_height
|
|
37
|
+
configure_surface if surface
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def current_target
|
|
42
|
+
raise Error, "renderer has no surface" unless surface
|
|
43
|
+
|
|
44
|
+
texture = surface.current_texture
|
|
45
|
+
[texture, texture.create_view]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def present
|
|
49
|
+
surface&.present
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def dispose
|
|
53
|
+
return self if @disposed
|
|
54
|
+
|
|
55
|
+
@disposed = true
|
|
56
|
+
cleanups = [
|
|
57
|
+
-> { surface&.unconfigure },
|
|
58
|
+
-> { surface&.release }
|
|
59
|
+
]
|
|
60
|
+
if @owns_device
|
|
61
|
+
cleanups.concat(
|
|
62
|
+
[
|
|
63
|
+
-> { device.destroy if device.respond_to?(:destroy) },
|
|
64
|
+
-> { device.release if device.respond_to?(:release) },
|
|
65
|
+
-> { adapter.release if adapter&.respond_to?(:release) },
|
|
66
|
+
-> { instance.release if instance&.respond_to?(:release) }
|
|
67
|
+
]
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
first_error = nil
|
|
71
|
+
cleanups.each do |cleanup|
|
|
72
|
+
cleanup.call
|
|
73
|
+
rescue StandardError => error
|
|
74
|
+
first_error ||= error
|
|
75
|
+
end
|
|
76
|
+
raise first_error if first_error
|
|
77
|
+
|
|
78
|
+
self
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def dispose_after_failed_initialization
|
|
84
|
+
dispose
|
|
85
|
+
rescue StandardError
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def initialize_wgpu
|
|
90
|
+
require "wgpu"
|
|
91
|
+
WGPUCompatibility.install!
|
|
92
|
+
@instance = WGPU::Instance.new
|
|
93
|
+
@surface = @window&.create_surface(instance)
|
|
94
|
+
@adapter = instance.request_adapter(compatible_surface: surface)
|
|
95
|
+
@device = adapter.request_device(label: "stagecraft device")
|
|
96
|
+
@queue = device.queue
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def choose_surface_format
|
|
100
|
+
return :rgba8_unorm_srgb unless surface
|
|
101
|
+
|
|
102
|
+
formats = surface.capabilities(adapter).fetch(:formats)
|
|
103
|
+
formats.find { |format| format.to_s.end_with?("_srgb") } ||
|
|
104
|
+
formats.find { |format| %i[bgra8_unorm rgba8_unorm].include?(format) } ||
|
|
105
|
+
formats.first ||
|
|
106
|
+
:bgra8_unorm
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def configure_surface
|
|
110
|
+
surface.configure(
|
|
111
|
+
device:,
|
|
112
|
+
format: surface_format,
|
|
113
|
+
width:,
|
|
114
|
+
height:,
|
|
115
|
+
present_mode: :fifo
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stagecraft
|
|
4
|
+
class Renderer
|
|
5
|
+
class PipelineCache
|
|
6
|
+
Key = Data.define(
|
|
7
|
+
:material_class, :feature_bits, :vertex_layout_id, :blend_state, :depth_state,
|
|
8
|
+
:cull_mode, :sample_count, :color_format, :shadow_pass
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
attr_reader :limit
|
|
12
|
+
|
|
13
|
+
def initialize(limit: 256, on_change: nil)
|
|
14
|
+
@limit = Integer(limit)
|
|
15
|
+
raise ArgumentError, "pipeline cache limit must be positive" unless @limit.positive?
|
|
16
|
+
|
|
17
|
+
@entries = {}
|
|
18
|
+
@on_change = on_change
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def fetch(key)
|
|
22
|
+
if @entries.key?(key)
|
|
23
|
+
value = @entries.delete(key)
|
|
24
|
+
@entries[key] = value
|
|
25
|
+
return value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
value = yield
|
|
29
|
+
@entries[key] = value
|
|
30
|
+
@on_change&.call(1)
|
|
31
|
+
evict! while @entries.length > limit
|
|
32
|
+
value
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def size
|
|
36
|
+
@entries.size
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def clear
|
|
40
|
+
@entries.each_value { |value| release(value) }
|
|
41
|
+
@on_change&.call(-@entries.length)
|
|
42
|
+
@entries.clear
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def evict!
|
|
49
|
+
_key, value = @entries.shift
|
|
50
|
+
release(value)
|
|
51
|
+
@on_change&.call(-1)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def release(value)
|
|
55
|
+
value.release if value.respond_to?(:release)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|