@zephyr3d/scene 0.9.20 → 0.9.22
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.
- package/dist/camera/camera.js +208 -208
- package/dist/camera/camera.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/posteffect/posteffect.js +16 -0
- package/dist/posteffect/posteffect.js.map +1 -1
- package/dist/posteffect/ssgi.js +644 -0
- package/dist/posteffect/ssgi.js.map +1 -0
- package/dist/posteffect/sss.js +129 -44
- package/dist/posteffect/sss.js.map +1 -1
- package/dist/render/lightpass.js +5 -1
- package/dist/render/lightpass.js.map +1 -1
- package/dist/render/rendergraph/forward_plus_builder.js +21 -6
- package/dist/render/rendergraph/forward_plus_builder.js.map +1 -1
- package/dist/render/rendergraph/frame_graph_context.js +41 -0
- package/dist/render/rendergraph/frame_graph_context.js.map +1 -0
- package/dist/render/shadow_mask_pass.js +42 -39
- package/dist/render/shadow_mask_pass.js.map +1 -1
- package/dist/shaders/ssgi.js +63 -0
- package/dist/shaders/ssgi.js.map +1 -0
- package/dist/shadow/esm.js +3 -1
- package/dist/shadow/esm.js.map +1 -1
- package/dist/shadow/pcf_opt.js +4 -1
- package/dist/shadow/pcf_opt.js.map +1 -1
- package/dist/shadow/pcf_pd.js +4 -1
- package/dist/shadow/pcf_pd.js.map +1 -1
- package/dist/shadow/pcss.js +2 -0
- package/dist/shadow/pcss.js.map +1 -1
- package/dist/shadow/shadow_impl.js.map +1 -1
- package/dist/shadow/shadowmapper.js +5 -1
- package/dist/shadow/shadowmapper.js.map +1 -1
- package/dist/shadow/ssm.js +4 -1
- package/dist/shadow/ssm.js.map +1 -1
- package/dist/shadow/vsm.js +3 -1
- package/dist/shadow/vsm.js.map +1 -1
- package/dist/utility/serialization/scene/camera.js.map +1 -1
- package/package.json +1 -1
- package/dist/animation/joint_dynamics/convex_collider.js +0 -320
- package/dist/animation/joint_dynamics/convex_collider.js.map +0 -1
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
import { AbstractPostEffect, PostEffectLayer } from './posteffect.js';
|
|
2
|
+
import { linearToGamma } from '../shaders/misc.js';
|
|
3
|
+
import { screenSpaceRayTracing_HiZ, screenSpaceRayTracing_Linear2D } from '../shaders/ssr.js';
|
|
4
|
+
import { SSGI_rand2, SSGI_cosineSampleHemisphere } from '../shaders/ssgi.js';
|
|
5
|
+
import { Vector2, Matrix4x4, Vector4 } from '@zephyr3d/base';
|
|
6
|
+
import { copyTexture, fetchSampler } from '../utility/misc.js';
|
|
7
|
+
import { BilateralBlurBlitter } from '../blitter/bilateralblur.js';
|
|
8
|
+
import { ShaderHelper } from '../material/shader/helper.js';
|
|
9
|
+
import '../material/lambert.js';
|
|
10
|
+
import '../material/blinn.js';
|
|
11
|
+
import '../material/unlit.js';
|
|
12
|
+
import '../material/particle.js';
|
|
13
|
+
import '../material/subsurfaceprofile.js';
|
|
14
|
+
import '../material/skin.js';
|
|
15
|
+
import '@zephyr3d/device';
|
|
16
|
+
import '../values.js';
|
|
17
|
+
import '../material/meshmaterial.js';
|
|
18
|
+
import '../material/grassmaterial.js';
|
|
19
|
+
import '../material/terrain-cm.js';
|
|
20
|
+
import '../material/pbrmr.js';
|
|
21
|
+
import '../material/pbrsg.js';
|
|
22
|
+
import '../material/mtoon.js';
|
|
23
|
+
import '../material/pbrblueprint.js';
|
|
24
|
+
import '../material/sprite.js';
|
|
25
|
+
import '../material/sprite_std.js';
|
|
26
|
+
import '../utility/blueprint/material/texture.js';
|
|
27
|
+
import { RGHistoryResources } from '../render/rendergraph/history_resources.js';
|
|
28
|
+
import { FrameResources } from '../render/rendergraph/blackboard.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* SSGI post effect
|
|
32
|
+
*
|
|
33
|
+
* @remarks
|
|
34
|
+
* Screen space global illumination: gathers one-bounce indirect diffuse
|
|
35
|
+
* lighting by cosine-weighted hemisphere ray marching against the scene
|
|
36
|
+
* depth buffer (Hi-Z accelerated when available), then denoises the result
|
|
37
|
+
* with a bilateral blur and optional temporal accumulation before adding
|
|
38
|
+
* it to the scene color. Internal used in light pass.
|
|
39
|
+
*
|
|
40
|
+
* @internal
|
|
41
|
+
*/ class SSGI extends AbstractPostEffect {
|
|
42
|
+
static _tracePrograms = {};
|
|
43
|
+
static _combineProgram;
|
|
44
|
+
static _temporalProgram;
|
|
45
|
+
static _blurBlitterH = null;
|
|
46
|
+
static _blurBlitterV = null;
|
|
47
|
+
_traceBindGroups;
|
|
48
|
+
_combineBindGroup;
|
|
49
|
+
_temporalBindGroup;
|
|
50
|
+
/**
|
|
51
|
+
* Creates an instance of SSGI post effect
|
|
52
|
+
*/ constructor(){
|
|
53
|
+
super();
|
|
54
|
+
this._layer = PostEffectLayer.opaque;
|
|
55
|
+
this._traceBindGroups = {};
|
|
56
|
+
this._combineBindGroup = null;
|
|
57
|
+
this._temporalBindGroup = null;
|
|
58
|
+
}
|
|
59
|
+
/** {@inheritDoc AbstractPostEffect.requireLinearDepthTexture} */ requireLinearDepthTexture() {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
/** {@inheritDoc AbstractPostEffect.requireDepthAttachment} */ requireDepthAttachment() {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
/** {@inheritDoc AbstractPostEffect.requireMotionVectorTexture} */ requireMotionVectorTexture(ctx) {
|
|
66
|
+
return !!ctx.camera.ssgiTemporal;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Declares SSGI's internal steps (trace, optional bilateral blur, optional
|
|
70
|
+
* temporal resolve, combine) as individual render graph passes.
|
|
71
|
+
*/ setup(s) {
|
|
72
|
+
const { graph, ctx, history } = s;
|
|
73
|
+
// SSGI needs the surface normal MRT and dynamic loops; neither is
|
|
74
|
+
// available on WebGL1. Pass the input through untouched in that case.
|
|
75
|
+
if (ctx.device.type === 'webgl' || !s.blackboard.get(FrameResources.SSRNormal)) {
|
|
76
|
+
return s.input;
|
|
77
|
+
}
|
|
78
|
+
const camera = ctx.camera;
|
|
79
|
+
const halfRes = !!camera.ssgiHalfResolution;
|
|
80
|
+
const traceWidth = Math.max(1, halfRes ? s.width >> 1 : s.width);
|
|
81
|
+
const traceHeight = Math.max(1, halfRes ? s.height >> 1 : s.height);
|
|
82
|
+
const linearDepthHandle = s.blackboard.get(FrameResources.LinearDepth);
|
|
83
|
+
const texDesc = {
|
|
84
|
+
format: 'rgba16f',
|
|
85
|
+
sizeMode: 'absolute',
|
|
86
|
+
width: traceWidth,
|
|
87
|
+
height: traceHeight
|
|
88
|
+
};
|
|
89
|
+
const readCommon = (builder)=>{
|
|
90
|
+
if (linearDepthHandle) {
|
|
91
|
+
builder.read(linearDepthHandle);
|
|
92
|
+
}
|
|
93
|
+
// Normal MRT, HiZ and the scene color are reached through DrawContext
|
|
94
|
+
// fields; the dependency list carries their handles.
|
|
95
|
+
for (const dep of s.dependencies){
|
|
96
|
+
builder.read(dep);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const getLinearDepth = (rg)=>linearDepthHandle ? rg.getTexture(linearDepthHandle) : ctx.linearDepthTexture;
|
|
100
|
+
// 1. Trace cosine-weighted hemisphere rays against the depth buffer and
|
|
101
|
+
// gather the radiance at the hit points.
|
|
102
|
+
const traceHandle = graph.addPass('SSGI:Trace', (builder)=>{
|
|
103
|
+
builder.read(s.input);
|
|
104
|
+
readCommon(builder);
|
|
105
|
+
const out = builder.createTexture({
|
|
106
|
+
...texDesc,
|
|
107
|
+
label: 'SSGI:trace'
|
|
108
|
+
});
|
|
109
|
+
const fb = builder.createFramebuffer({
|
|
110
|
+
label: 'SSGI:traceFB',
|
|
111
|
+
width: traceWidth,
|
|
112
|
+
height: traceHeight,
|
|
113
|
+
colorAttachments: out,
|
|
114
|
+
depthAttachment: null,
|
|
115
|
+
ignoreDepthStencil: true
|
|
116
|
+
});
|
|
117
|
+
builder.setExecute((rg)=>{
|
|
118
|
+
const device = ctx.device;
|
|
119
|
+
device.pushDeviceStates();
|
|
120
|
+
try {
|
|
121
|
+
device.setFramebuffer(rg.getFramebuffer(fb));
|
|
122
|
+
this.trace(ctx, rg.getTexture(s.input), getLinearDepth(rg));
|
|
123
|
+
} finally{
|
|
124
|
+
device.popDeviceStates();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
return out;
|
|
128
|
+
});
|
|
129
|
+
// 2. Temporal accumulation BEFORE the spatial blur: accumulating the raw
|
|
130
|
+
// per-pixel estimate raises the effective sample count over time, and the
|
|
131
|
+
// blur then only has to clean the residual noise. (Blurring first would
|
|
132
|
+
// turn per-frame noise into large low-frequency blobs that flicker.)
|
|
133
|
+
// The history stores the pre-blur accumulated signal.
|
|
134
|
+
const wantTemporal = !!camera.ssgiTemporal;
|
|
135
|
+
const motionVectorHandle = wantTemporal ? s.blackboard.get(FrameResources.MotionVector) : null;
|
|
136
|
+
const historySize = {
|
|
137
|
+
width: traceWidth,
|
|
138
|
+
height: traceHeight
|
|
139
|
+
};
|
|
140
|
+
const prevRadianceHandle = wantTemporal && history && motionVectorHandle ? history.importPreviousIfCompatible(graph, RGHistoryResources.SSGI_RADIANCE, texDesc, historySize) : null;
|
|
141
|
+
const prevMotionVectorHandle = wantTemporal && history && motionVectorHandle ? history.importPreviousIfCompatible(graph, RGHistoryResources.SSGI_MOTION_VECTOR, {
|
|
142
|
+
format: 'rgba16f',
|
|
143
|
+
sizeMode: 'absolute',
|
|
144
|
+
width: s.width,
|
|
145
|
+
height: s.height
|
|
146
|
+
}, {
|
|
147
|
+
width: s.width,
|
|
148
|
+
height: s.height
|
|
149
|
+
}) : null;
|
|
150
|
+
let radianceHandle = traceHandle;
|
|
151
|
+
const canTemporal = !!(motionVectorHandle && prevRadianceHandle && prevMotionVectorHandle);
|
|
152
|
+
if (canTemporal) {
|
|
153
|
+
radianceHandle = graph.addPass('SSGI:Temporal', (builder)=>{
|
|
154
|
+
builder.read(traceHandle);
|
|
155
|
+
builder.read(prevRadianceHandle);
|
|
156
|
+
builder.read(prevMotionVectorHandle);
|
|
157
|
+
builder.read(motionVectorHandle);
|
|
158
|
+
readCommon(builder);
|
|
159
|
+
const out = builder.createTexture({
|
|
160
|
+
...texDesc,
|
|
161
|
+
label: 'SSGI:temporal'
|
|
162
|
+
});
|
|
163
|
+
const fb = builder.createFramebuffer({
|
|
164
|
+
label: 'SSGI:temporalFB',
|
|
165
|
+
width: traceWidth,
|
|
166
|
+
height: traceHeight,
|
|
167
|
+
colorAttachments: out,
|
|
168
|
+
depthAttachment: null,
|
|
169
|
+
ignoreDepthStencil: true
|
|
170
|
+
});
|
|
171
|
+
builder.setExecute((rg)=>{
|
|
172
|
+
const device = ctx.device;
|
|
173
|
+
device.pushDeviceStates();
|
|
174
|
+
try {
|
|
175
|
+
this.temporal(ctx, rg.getTexture(traceHandle), rg.getTexture(prevRadianceHandle), rg.getTexture(prevMotionVectorHandle), rg.getFramebuffer(fb));
|
|
176
|
+
} finally{
|
|
177
|
+
device.popDeviceStates();
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
return out;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// The accumulated (pre-blur) signal is what feeds next frame's history.
|
|
184
|
+
const accumulatedHandle = radianceHandle;
|
|
185
|
+
// 3. Bilateral blur (horizontal + vertical) guided by scene depth cleans
|
|
186
|
+
// the residual noise left after accumulation.
|
|
187
|
+
if (camera.ssgiBlurKernelSize > 0) {
|
|
188
|
+
const blurInputHandle = radianceHandle;
|
|
189
|
+
radianceHandle = graph.addPass('SSGI:Blur', (builder)=>{
|
|
190
|
+
builder.read(blurInputHandle);
|
|
191
|
+
readCommon(builder);
|
|
192
|
+
const middle = builder.createTexture({
|
|
193
|
+
...texDesc,
|
|
194
|
+
label: 'SSGI:blurH'
|
|
195
|
+
});
|
|
196
|
+
const out = builder.createTexture({
|
|
197
|
+
...texDesc,
|
|
198
|
+
label: 'SSGI:blur'
|
|
199
|
+
});
|
|
200
|
+
const middleFB = builder.createFramebuffer({
|
|
201
|
+
label: 'SSGI:blurHFB',
|
|
202
|
+
width: traceWidth,
|
|
203
|
+
height: traceHeight,
|
|
204
|
+
colorAttachments: middle,
|
|
205
|
+
depthAttachment: null,
|
|
206
|
+
ignoreDepthStencil: true
|
|
207
|
+
});
|
|
208
|
+
const outFB = builder.createFramebuffer({
|
|
209
|
+
label: 'SSGI:blurFB',
|
|
210
|
+
width: traceWidth,
|
|
211
|
+
height: traceHeight,
|
|
212
|
+
colorAttachments: out,
|
|
213
|
+
depthAttachment: null,
|
|
214
|
+
ignoreDepthStencil: true
|
|
215
|
+
});
|
|
216
|
+
builder.setExecute((rg)=>{
|
|
217
|
+
const device = ctx.device;
|
|
218
|
+
device.pushDeviceStates();
|
|
219
|
+
try {
|
|
220
|
+
const depthTex = getLinearDepth(rg);
|
|
221
|
+
const blitterH = SSGI._blurBlitterH = SSGI._blurBlitterH ?? new BilateralBlurBlitter(false);
|
|
222
|
+
this.blurPass(ctx, blitterH, depthTex, rg.getTexture(blurInputHandle), rg.getFramebuffer(middleFB));
|
|
223
|
+
const blitterV = SSGI._blurBlitterV = SSGI._blurBlitterV ?? new BilateralBlurBlitter(true);
|
|
224
|
+
this.blurPass(ctx, blitterV, depthTex, rg.getTexture(middle), rg.getFramebuffer(outFB));
|
|
225
|
+
} finally{
|
|
226
|
+
device.popDeviceStates();
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
return out;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
// 4. Upsample, modulate by the albedo approximation and add to the scene
|
|
233
|
+
// color; queue history commits.
|
|
234
|
+
const finalRadianceHandle = radianceHandle;
|
|
235
|
+
return graph.addPass('PostEffect:SSGI', (builder)=>{
|
|
236
|
+
builder.read(s.input);
|
|
237
|
+
builder.read(finalRadianceHandle);
|
|
238
|
+
if (accumulatedHandle !== finalRadianceHandle) {
|
|
239
|
+
builder.read(accumulatedHandle);
|
|
240
|
+
}
|
|
241
|
+
if (motionVectorHandle) {
|
|
242
|
+
builder.read(motionVectorHandle);
|
|
243
|
+
}
|
|
244
|
+
readCommon(builder);
|
|
245
|
+
const output = s.createOutput(builder, {
|
|
246
|
+
needDepthAttachment: true
|
|
247
|
+
});
|
|
248
|
+
builder.setExecute((rg)=>{
|
|
249
|
+
const device = ctx.device;
|
|
250
|
+
device.pushDeviceStates();
|
|
251
|
+
try {
|
|
252
|
+
device.setFramebuffer(output.framebuffer ? rg.getFramebuffer(output.framebuffer) : null);
|
|
253
|
+
const inputTex = rg.getTexture(s.input);
|
|
254
|
+
const radianceTex = rg.getTexture(finalRadianceHandle);
|
|
255
|
+
copyTexture(inputTex, device.getFramebuffer(), fetchSampler('clamp_nearest_nomip'), AbstractPostEffect.getDefaultRenderState(ctx, 'eq'));
|
|
256
|
+
this.combine(ctx, inputTex, radianceTex, output.srgbOutput);
|
|
257
|
+
// Commit history whenever temporal is wanted - even on frames where
|
|
258
|
+
// the temporal pass could not run yet (no compatible history). This
|
|
259
|
+
// seeds the very first frame; gating the commit on canTemporal
|
|
260
|
+
// would deadlock: no history -> no temporal -> no commit -> no
|
|
261
|
+
// history, forever.
|
|
262
|
+
if (wantTemporal && history && motionVectorHandle) {
|
|
263
|
+
// Commit the PRE-blur accumulated signal: feeding the blurred
|
|
264
|
+
// result back would compound the blur every frame.
|
|
265
|
+
const accumulatedTex = rg.getTexture(accumulatedHandle);
|
|
266
|
+
history.queueRetainedCommit(RGHistoryResources.SSGI_RADIANCE, {
|
|
267
|
+
format: accumulatedTex.format,
|
|
268
|
+
sizeMode: 'absolute',
|
|
269
|
+
width: accumulatedTex.width,
|
|
270
|
+
height: accumulatedTex.height
|
|
271
|
+
}, {
|
|
272
|
+
width: accumulatedTex.width,
|
|
273
|
+
height: accumulatedTex.height
|
|
274
|
+
}, accumulatedTex);
|
|
275
|
+
if (ctx.motionVectorTexture) {
|
|
276
|
+
history.queueRetainedCommit(RGHistoryResources.SSGI_MOTION_VECTOR, {
|
|
277
|
+
format: ctx.motionVectorTexture.format,
|
|
278
|
+
sizeMode: 'absolute',
|
|
279
|
+
width: ctx.motionVectorTexture.width,
|
|
280
|
+
height: ctx.motionVectorTexture.height
|
|
281
|
+
}, {
|
|
282
|
+
width: ctx.motionVectorTexture.width,
|
|
283
|
+
height: ctx.motionVectorTexture.height
|
|
284
|
+
}, ctx.motionVectorTexture);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} finally{
|
|
288
|
+
device.popDeviceStates();
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
return output.color;
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
/** @internal */ blurPass(ctx, blitter, depthTex, srcTex, fbTo) {
|
|
295
|
+
const camera = ctx.camera;
|
|
296
|
+
blitter.kernelRadius = Math.max(1, camera.ssgiBlurKernelSize >> 0) - 1 >> 1;
|
|
297
|
+
blitter.stdDev = camera.ssgiBlurStdDev;
|
|
298
|
+
blitter.size = new Vector2(srcTex.width, srcTex.height);
|
|
299
|
+
blitter.depthTex = depthTex;
|
|
300
|
+
blitter.depthCutoff = camera.ssgiBlurDepthCutoff;
|
|
301
|
+
blitter.blurSizeTex = null;
|
|
302
|
+
blitter.sampler = fetchSampler('clamp_nearest_nomip');
|
|
303
|
+
blitter.cameraNearFar.setXY(camera.getNearPlane(), camera.getFarPlane());
|
|
304
|
+
blitter.srgbOut = false;
|
|
305
|
+
blitter.blit(srcTex, fbTo, fetchSampler('clamp_linear_nomip'));
|
|
306
|
+
}
|
|
307
|
+
/** @internal */ trace(ctx, inputColorTexture, sceneDepthTexture) {
|
|
308
|
+
const device = ctx.device;
|
|
309
|
+
const hash = `${!!ctx.HiZTexture}:${!!ctx.SSRCalcThickness}`;
|
|
310
|
+
let program = SSGI._tracePrograms[hash];
|
|
311
|
+
if (program === undefined) {
|
|
312
|
+
const created = this._createTraceProgram(ctx);
|
|
313
|
+
if (!created) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
program = created;
|
|
317
|
+
SSGI._tracePrograms[hash] = program;
|
|
318
|
+
}
|
|
319
|
+
let bindGroup = this._traceBindGroups[hash];
|
|
320
|
+
if (!bindGroup) {
|
|
321
|
+
bindGroup = device.createBindGroup(program.bindGroupLayouts[0]);
|
|
322
|
+
this._traceBindGroups[hash] = bindGroup;
|
|
323
|
+
}
|
|
324
|
+
const camera = ctx.camera;
|
|
325
|
+
const nearestSampler = fetchSampler('clamp_nearest');
|
|
326
|
+
const linearSampler = fetchSampler('clamp_linear');
|
|
327
|
+
const fb = device.getFramebuffer();
|
|
328
|
+
const traceWidth = fb.getWidth();
|
|
329
|
+
const traceHeight = fb.getHeight();
|
|
330
|
+
bindGroup.setTexture('colorTex', inputColorTexture, linearSampler);
|
|
331
|
+
bindGroup.setTexture('normalTex', ctx.SSRNormalTexture, nearestSampler);
|
|
332
|
+
bindGroup.setTexture('depthTex', sceneDepthTexture, nearestSampler);
|
|
333
|
+
bindGroup.setValue('cameraNearFar', new Vector2(camera.getNearPlane(), camera.getFarPlane()));
|
|
334
|
+
bindGroup.setValue('projMatrix', camera.getProjectionMatrix());
|
|
335
|
+
bindGroup.setValue('invProjMatrix', Matrix4x4.invert(camera.getProjectionMatrix()));
|
|
336
|
+
bindGroup.setValue('viewMatrix', camera.viewMatrix);
|
|
337
|
+
bindGroup.setValue('ssgiParams', new Vector4(camera.ssgiRadius, camera.ssgiMaxSteps, camera.ssgiThickness, camera.ssgiSamples));
|
|
338
|
+
// Advance the noise sequence only when the temporal resolve can converge
|
|
339
|
+
// it; a static pattern is less objectionable than unaccumulated flicker.
|
|
340
|
+
bindGroup.setValue('frameIndex', camera.ssgiTemporal ? device.frameInfo.frameCounter % 1024 : 0);
|
|
341
|
+
if (ctx.HiZTexture) {
|
|
342
|
+
bindGroup.setTexture('hizTex', ctx.HiZTexture, nearestSampler);
|
|
343
|
+
bindGroup.setValue('depthMipLevels', ctx.HiZTexture.mipLevelCount);
|
|
344
|
+
bindGroup.setValue('targetSize', new Vector4(traceWidth, traceHeight, ctx.HiZTexture.width, ctx.HiZTexture.height));
|
|
345
|
+
} else {
|
|
346
|
+
bindGroup.setValue('ssgiStride', camera.ssgiStride);
|
|
347
|
+
bindGroup.setValue('targetSize', new Vector4(traceWidth, traceHeight, sceneDepthTexture.width, sceneDepthTexture.height));
|
|
348
|
+
}
|
|
349
|
+
bindGroup.setValue('flip', this.needFlip(device) ? 1 : 0);
|
|
350
|
+
device.setProgram(program);
|
|
351
|
+
device.setBindGroup(0, bindGroup);
|
|
352
|
+
this.drawFullscreenQuad(AbstractPostEffect.getDefaultRenderState(ctx, 'always'));
|
|
353
|
+
}
|
|
354
|
+
/** @internal */ temporal(ctx, currentRadianceTex, prevRadianceTex, prevMotionVectorTex, outFramebuffer) {
|
|
355
|
+
const device = ctx.device;
|
|
356
|
+
let program = SSGI._temporalProgram;
|
|
357
|
+
if (!program) {
|
|
358
|
+
program = this._createTemporalProgram(ctx);
|
|
359
|
+
SSGI._temporalProgram = program;
|
|
360
|
+
}
|
|
361
|
+
if (!this._temporalBindGroup) {
|
|
362
|
+
this._temporalBindGroup = device.createBindGroup(program.bindGroupLayouts[0]);
|
|
363
|
+
}
|
|
364
|
+
this._temporalBindGroup.setTexture('historyColorTex', prevRadianceTex, fetchSampler('clamp_linear_nomip'));
|
|
365
|
+
this._temporalBindGroup.setTexture('currentColorTex', currentRadianceTex, fetchSampler('clamp_nearest_nomip'));
|
|
366
|
+
this._temporalBindGroup.setTexture('motionVector', ctx.motionVectorTexture, fetchSampler('clamp_linear_nomip'));
|
|
367
|
+
this._temporalBindGroup.setTexture('prevMotionVector', prevMotionVectorTex, fetchSampler('clamp_linear_nomip'));
|
|
368
|
+
this._temporalBindGroup.setValue('flip', this.needFlip(device) ? 1 : 0);
|
|
369
|
+
this._temporalBindGroup.setValue('texSize', new Vector2(currentRadianceTex.width, currentRadianceTex.height));
|
|
370
|
+
this._temporalBindGroup.setValue('temporalWeight', ctx.camera.ssgiTemporalWeight);
|
|
371
|
+
device.setFramebuffer(outFramebuffer);
|
|
372
|
+
device.setProgram(program);
|
|
373
|
+
device.setBindGroup(0, this._temporalBindGroup);
|
|
374
|
+
this.drawFullscreenQuad(AbstractPostEffect.getDefaultRenderState(ctx, 'always'));
|
|
375
|
+
}
|
|
376
|
+
/** @internal */ combine(ctx, inputColorTexture, radianceTex, srgbOut) {
|
|
377
|
+
const device = ctx.device;
|
|
378
|
+
let program = SSGI._combineProgram;
|
|
379
|
+
if (program === undefined) {
|
|
380
|
+
program = this._createCombineProgram(ctx);
|
|
381
|
+
SSGI._combineProgram = program;
|
|
382
|
+
}
|
|
383
|
+
if (!this._combineBindGroup) {
|
|
384
|
+
this._combineBindGroup = device.createBindGroup(program.bindGroupLayouts[0]);
|
|
385
|
+
}
|
|
386
|
+
this._combineBindGroup.setTexture('colorTex', inputColorTexture, fetchSampler('clamp_nearest'));
|
|
387
|
+
this._combineBindGroup.setTexture('radianceTex', radianceTex, fetchSampler('clamp_linear'));
|
|
388
|
+
this._combineBindGroup.setValue('ssgiIntensity', ctx.camera.ssgiIntensity);
|
|
389
|
+
this._combineBindGroup.setValue('targetSize', new Vector2(inputColorTexture.width, inputColorTexture.height));
|
|
390
|
+
this._combineBindGroup.setValue('flip', this.needFlip(device) ? 1 : 0);
|
|
391
|
+
this._combineBindGroup.setValue('srgbOut', srgbOut ? 1 : 0);
|
|
392
|
+
device.setProgram(program);
|
|
393
|
+
device.setBindGroup(0, this._combineBindGroup);
|
|
394
|
+
this.drawFullscreenQuad(AbstractPostEffect.getDefaultRenderState(ctx, 'gt'));
|
|
395
|
+
}
|
|
396
|
+
/** @internal */ _createTraceProgram(ctx) {
|
|
397
|
+
const program = ctx.device.buildRenderProgram({
|
|
398
|
+
vertex (pb) {
|
|
399
|
+
this.flip = pb.int().uniform(0);
|
|
400
|
+
this.$inputs.pos = pb.vec2().attrib('position');
|
|
401
|
+
this.$outputs.uv = pb.vec2();
|
|
402
|
+
pb.main(function() {
|
|
403
|
+
this.$builtins.position = pb.vec4(this.$inputs.pos, 1, 1);
|
|
404
|
+
this.$outputs.uv = pb.add(pb.mul(this.$inputs.pos.xy, 0.5), pb.vec2(0.5));
|
|
405
|
+
this.$if(pb.notEqual(this.flip, 0), function() {
|
|
406
|
+
this.$builtins.position.y = pb.neg(this.$builtins.position.y);
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
},
|
|
410
|
+
fragment (pb) {
|
|
411
|
+
this.colorTex = pb.tex2D().uniform(0);
|
|
412
|
+
this.normalTex = pb.tex2D().uniform(0);
|
|
413
|
+
this.depthTex = pb.tex2D().uniform(0);
|
|
414
|
+
this.cameraNearFar = pb.vec2().uniform(0);
|
|
415
|
+
this.projMatrix = pb.mat4().uniform(0);
|
|
416
|
+
this.invProjMatrix = pb.mat4().uniform(0);
|
|
417
|
+
this.viewMatrix = pb.mat4().uniform(0);
|
|
418
|
+
// (radius, maxIterations, thickness, numRays)
|
|
419
|
+
this.ssgiParams = pb.vec4().uniform(0);
|
|
420
|
+
this.frameIndex = pb.float().uniform(0);
|
|
421
|
+
this.targetSize = pb.vec4().uniform(0);
|
|
422
|
+
if (ctx.HiZTexture) {
|
|
423
|
+
this.hizTex = pb.tex2D().uniform(0);
|
|
424
|
+
this.depthMipLevels = pb.int().uniform(0);
|
|
425
|
+
} else {
|
|
426
|
+
this.ssgiStride = pb.float().uniform(0);
|
|
427
|
+
}
|
|
428
|
+
this.$outputs.outColor = pb.vec4();
|
|
429
|
+
pb.func('getPosition', [
|
|
430
|
+
pb.vec2('uv'),
|
|
431
|
+
pb.mat4('mat')
|
|
432
|
+
], function() {
|
|
433
|
+
this.$l.linearDepth = ShaderHelper.sampleLinearDepth(this, this.depthTex, this.uv, 0);
|
|
434
|
+
this.$l.nonLinearDepth = pb.div(pb.sub(pb.div(this.cameraNearFar.x, this.linearDepth), this.cameraNearFar.y), pb.sub(this.cameraNearFar.x, this.cameraNearFar.y));
|
|
435
|
+
this.$l.clipSpacePos = pb.vec4(pb.sub(pb.mul(this.uv, 2), pb.vec2(1)), pb.sub(pb.mul(pb.clamp(this.nonLinearDepth, 0, 1), 2), 1), 1);
|
|
436
|
+
this.$l.wPos = pb.mul(this.mat, this.clipSpacePos);
|
|
437
|
+
this.$return(pb.vec4(pb.div(this.wPos.xyz, this.wPos.w), this.linearDepth));
|
|
438
|
+
});
|
|
439
|
+
pb.main(function() {
|
|
440
|
+
this.$l.screenUV = pb.div(pb.vec2(this.$builtins.fragCoord.xy), this.targetSize.xy);
|
|
441
|
+
this.$l.color = pb.vec3(0);
|
|
442
|
+
this.$l.pos = this.getPosition(this.screenUV, this.invProjMatrix);
|
|
443
|
+
this.$if(pb.lessThan(this.pos.w, 1), function() {
|
|
444
|
+
this.$l.viewPos = this.pos.xyz;
|
|
445
|
+
this.$l.worldNormal = pb.sub(pb.mul(pb.textureSampleLevel(this.normalTex, this.screenUV, 0).rgb, 2), pb.vec3(1));
|
|
446
|
+
this.$l.viewNormal = pb.normalize(pb.mul(this.viewMatrix, pb.vec4(this.worldNormal, 0)).xyz);
|
|
447
|
+
this.$l.numRays = pb.max(this.ssgiParams.w, 1);
|
|
448
|
+
this.$l.sum = pb.vec3(0);
|
|
449
|
+
this.$for(pb.float('ray'), 0, this.numRays, function() {
|
|
450
|
+
this.$l.rand = SSGI_rand2(this, pb.vec2(this.$builtins.fragCoord.xy), pb.add(pb.mul(this.frameIndex, this.numRays), this.ray));
|
|
451
|
+
this.$l.rayDir = SSGI_cosineSampleHemisphere(this, this.viewNormal, this.rand);
|
|
452
|
+
this.$l.hitInfo = pb.vec4(0);
|
|
453
|
+
if (ctx.HiZTexture) {
|
|
454
|
+
this.hitInfo = screenSpaceRayTracing_HiZ(this, this.viewPos, this.rayDir, this.viewMatrix, this.projMatrix, this.invProjMatrix, this.cameraNearFar, this.depthMipLevels, this.ssgiParams.y, this.ssgiParams.x, this.ssgiParams.z, this.targetSize, this.hizTex, this.normalTex);
|
|
455
|
+
} else {
|
|
456
|
+
this.hitInfo = screenSpaceRayTracing_Linear2D(this, this.viewPos, this.rayDir, this.viewMatrix, this.projMatrix, this.invProjMatrix, this.cameraNearFar, this.ssgiParams.x, this.ssgiParams.y, this.ssgiParams.z, this.ssgiStride, this.targetSize, this.depthTex, this.normalTex, // Backface depth is only present when the depth prepass was
|
|
457
|
+
// built with SSR thickness computation enabled.
|
|
458
|
+
!!ctx.SSRCalcThickness);
|
|
459
|
+
}
|
|
460
|
+
this.$l.hitAlpha = pb.clamp(this.hitInfo.w, 0, 1);
|
|
461
|
+
// Hemisphere rays frequently graze the screen plane; the
|
|
462
|
+
// projective ray setup can then produce Inf/NaN which would be
|
|
463
|
+
// smeared into large blocks by the bilateral blur. NaN fails
|
|
464
|
+
// any comparison, so this test also rejects it.
|
|
465
|
+
this.$l.hitValid = pb.and(pb.all(pb.lessThan(pb.abs(this.hitInfo), pb.vec4(1e30))), pb.greaterThan(this.hitAlpha, 0));
|
|
466
|
+
this.$if(this.hitValid, function() {
|
|
467
|
+
this.$l.hitUV = pb.clamp(this.hitInfo.xy, pb.vec2(0), pb.vec2(1));
|
|
468
|
+
// Firefly clamp: an HDR sun/sky sample can be orders of
|
|
469
|
+
// magnitude brighter than the surroundings; unclamped it
|
|
470
|
+
// dominates the low-spp estimate as isolated bright dots no
|
|
471
|
+
// spatial filter can remove.
|
|
472
|
+
this.$l.radiance = pb.min(pb.textureSampleLevel(this.colorTex, this.hitUV, 0).rgb, pb.vec3(4));
|
|
473
|
+
this.sum = pb.add(this.sum, pb.mul(this.radiance, this.hitAlpha));
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
// Cosine-weighted sampling: the irradiance estimate over the
|
|
477
|
+
// hemisphere reduces to the plain average of the hit radiances.
|
|
478
|
+
this.color = pb.div(this.sum, this.numRays);
|
|
479
|
+
});
|
|
480
|
+
// Reinhard-compress into [0, 1) (mirrors the SSR intersect pass).
|
|
481
|
+
// The bilateral blur weights neighbors by color distance; in linear
|
|
482
|
+
// HDR a firefly is "far" from every neighbor and survives the blur
|
|
483
|
+
// untouched. Compressed, bright outliers stay within blurring range
|
|
484
|
+
// and the denoiser can actually converge them. combine() inverts
|
|
485
|
+
// this mapping.
|
|
486
|
+
this.color = pb.div(this.color, pb.add(this.color, pb.vec3(1)));
|
|
487
|
+
this.$outputs.outColor = pb.vec4(this.color, 1);
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
if (!program) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
program.name = '@SSGI_Trace';
|
|
495
|
+
return program;
|
|
496
|
+
}
|
|
497
|
+
/** @internal */ _createTemporalProgram(ctx) {
|
|
498
|
+
const program = ctx.device.buildRenderProgram({
|
|
499
|
+
vertex (pb) {
|
|
500
|
+
this.flip = pb.int().uniform(0);
|
|
501
|
+
this.$inputs.pos = pb.vec2().attrib('position');
|
|
502
|
+
this.$outputs.uv = pb.vec2();
|
|
503
|
+
pb.main(function() {
|
|
504
|
+
this.$builtins.position = pb.vec4(this.$inputs.pos, 1, 1);
|
|
505
|
+
this.$outputs.uv = pb.add(pb.mul(this.$inputs.pos.xy, 0.5), pb.vec2(0.5));
|
|
506
|
+
this.$if(pb.notEqual(this.flip, 0), function() {
|
|
507
|
+
this.$builtins.position.y = pb.neg(this.$builtins.position.y);
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
},
|
|
511
|
+
fragment (pb) {
|
|
512
|
+
this.historyColorTex = pb.tex2D().uniform(0);
|
|
513
|
+
this.currentColorTex = pb.tex2D().uniform(0);
|
|
514
|
+
this.motionVector = pb.tex2D().uniform(0);
|
|
515
|
+
this.prevMotionVector = pb.tex2D().uniform(0);
|
|
516
|
+
this.texSize = pb.vec2().uniform(0);
|
|
517
|
+
this.temporalWeight = pb.float().uniform(0);
|
|
518
|
+
this.$outputs.outColor = pb.vec4();
|
|
519
|
+
pb.main(function() {
|
|
520
|
+
// Sample-count adaptive accumulation with fixed-gamma variance
|
|
521
|
+
// clipping. The accumulated frame count N rides in the alpha
|
|
522
|
+
// channel of the radiance history: early frames blend with
|
|
523
|
+
// 1/(N+1) (fast convergence), converged pixels blend at the
|
|
524
|
+
// temporalWeight cap. A FIXED-weight EMA never settles - its
|
|
525
|
+
// stationary state is a random walk inside the clip box, visible
|
|
526
|
+
// as slowly rippling low-frequency noise after the blur. With
|
|
527
|
+
// count-based weighting the estimator is a true running average,
|
|
528
|
+
// so the converged value stops moving.
|
|
529
|
+
// TAA-style box collapsing with velocity is deliberately avoided:
|
|
530
|
+
// camera jitter alone would collapse the box and reject all
|
|
531
|
+
// history, preventing convergence of the stochastic signal.
|
|
532
|
+
this.$l.screenUV = pb.div(pb.vec2(this.$builtins.fragCoord.xy), this.texSize);
|
|
533
|
+
this.$l.currentColor = pb.textureSampleLevel(this.currentColorTex, this.screenUV, 0).rgb;
|
|
534
|
+
this.$l.velocitySample = pb.textureSampleLevel(this.motionVector, this.screenUV, 0);
|
|
535
|
+
this.$l.velocity = this.velocitySample.xy;
|
|
536
|
+
// Sky/unwritten motion vector sentinel: no history
|
|
537
|
+
this.$if(pb.and(pb.greaterThanEqual(this.velocity.x, 5e4), pb.greaterThanEqual(this.velocity.y, 5e4)), function() {
|
|
538
|
+
this.$outputs.outColor = pb.vec4(this.currentColor, 1);
|
|
539
|
+
}).$else(function() {
|
|
540
|
+
this.$l.reprojectedUV = pb.sub(this.screenUV, this.velocity);
|
|
541
|
+
this.$l.offscreen = pb.or(pb.any(pb.lessThan(this.reprojectedUV, pb.vec2(0))), pb.any(pb.greaterThan(this.reprojectedUV, pb.vec2(1))));
|
|
542
|
+
this.$if(this.offscreen, function() {
|
|
543
|
+
this.$outputs.outColor = pb.vec4(this.currentColor, 1);
|
|
544
|
+
}).$else(function() {
|
|
545
|
+
this.$l.historySample = pb.textureSampleLevel(this.historyColorTex, this.reprojectedUV, 0);
|
|
546
|
+
this.$l.historyColor = this.historySample.rgb;
|
|
547
|
+
this.$l.historyCount = pb.max(this.historySample.a, 1);
|
|
548
|
+
// 3x3 neighborhood statistics of the current stochastic estimate
|
|
549
|
+
this.$l.m1 = pb.vec3(0);
|
|
550
|
+
this.$l.m2 = pb.vec3(0);
|
|
551
|
+
for(let i = -1; i <= 1; i++){
|
|
552
|
+
for(let j = -1; j <= 1; j++){
|
|
553
|
+
const c = `c${(i + 1) * 3 + j + 1}`;
|
|
554
|
+
this.$l[c] = pb.textureSampleLevel(this.currentColorTex, pb.add(this.screenUV, pb.div(pb.vec2(i, j), this.texSize)), 0).rgb;
|
|
555
|
+
this.m1 = pb.add(this.m1, this[c]);
|
|
556
|
+
this.m2 = pb.add(this.m2, pb.mul(this[c], this[c]));
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
this.m1 = pb.div(this.m1, 9);
|
|
560
|
+
this.m2 = pb.div(this.m2, 9);
|
|
561
|
+
this.$l.sigma = pb.sqrt(pb.max(pb.sub(this.m2, pb.mul(this.m1, this.m1)), pb.vec3(0)));
|
|
562
|
+
// gamma = 2 with a small absolute slack: tolerant to the noise a
|
|
563
|
+
// converged history legitimately sits in (compressed-space values).
|
|
564
|
+
this.$l.halfExtent = pb.add(pb.mul(this.sigma, 2), pb.vec3(0.03));
|
|
565
|
+
this.$l.clampedHistory = pb.clamp(this.historyColor, pb.sub(this.m1, this.halfExtent), pb.add(this.m1, this.halfExtent));
|
|
566
|
+
// How much the clip moved the history signals stale data;
|
|
567
|
+
// knock the effective count down so recovery is fast.
|
|
568
|
+
this.$l.clipAmount = pb.length(pb.sub(this.clampedHistory, this.historyColor));
|
|
569
|
+
this.$l.clipReset = pb.clamp(pb.mul(this.clipAmount, 8), 0, 1);
|
|
570
|
+
// Disocclusion: previous frame's motion at the reprojected point
|
|
571
|
+
// should agree with the current motion; a large mismatch means the
|
|
572
|
+
// surface was not visible last frame.
|
|
573
|
+
this.$l.prevVelocity = pb.textureSampleLevel(this.prevMotionVector, this.reprojectedUV, 0).xy;
|
|
574
|
+
this.$l.velocityDiff = pb.length(pb.mul(pb.sub(this.velocity, this.prevVelocity), this.texSize));
|
|
575
|
+
this.$l.disocclusion = pb.clamp(pb.mul(pb.sub(this.velocityDiff, 2.5), 0.1), 0, 1);
|
|
576
|
+
this.$l.confidence = pb.mul(pb.sub(1, this.disocclusion), pb.sub(1, this.clipReset));
|
|
577
|
+
this.$l.effectiveCount = pb.max(pb.mul(this.historyCount, this.confidence), 0);
|
|
578
|
+
// Count-based blend weight, capped by temporalWeight:
|
|
579
|
+
// maxCount = w/(1-w) so the cap and the weight limit agree.
|
|
580
|
+
this.$l.wMax = pb.clamp(this.temporalWeight, 0, 0.99);
|
|
581
|
+
this.$l.maxCount = pb.div(this.wMax, pb.sub(1, this.wMax));
|
|
582
|
+
this.$l.w = pb.min(this.wMax, pb.div(this.effectiveCount, pb.add(this.effectiveCount, 1)));
|
|
583
|
+
this.$l.newCount = pb.min(pb.add(this.effectiveCount, 1), this.maxCount);
|
|
584
|
+
this.$outputs.outColor = pb.vec4(pb.mix(this.currentColor, this.clampedHistory, this.w), this.newCount);
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
program.name = '@SSGI_Temporal';
|
|
591
|
+
return program;
|
|
592
|
+
}
|
|
593
|
+
/** @internal */ _createCombineProgram(ctx) {
|
|
594
|
+
const program = ctx.device.buildRenderProgram({
|
|
595
|
+
vertex (pb) {
|
|
596
|
+
this.flip = pb.int().uniform(0);
|
|
597
|
+
this.$inputs.pos = pb.vec2().attrib('position');
|
|
598
|
+
this.$outputs.uv = pb.vec2();
|
|
599
|
+
pb.main(function() {
|
|
600
|
+
this.$builtins.position = pb.vec4(this.$inputs.pos, 1, 1);
|
|
601
|
+
this.$outputs.uv = pb.add(pb.mul(this.$inputs.pos.xy, 0.5), pb.vec2(0.5));
|
|
602
|
+
this.$if(pb.notEqual(this.flip, 0), function() {
|
|
603
|
+
this.$builtins.position.y = pb.neg(this.$builtins.position.y);
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
},
|
|
607
|
+
fragment (pb) {
|
|
608
|
+
this.colorTex = pb.tex2D().uniform(0);
|
|
609
|
+
this.radianceTex = pb.tex2D().uniform(0);
|
|
610
|
+
this.targetSize = pb.vec2().uniform(0);
|
|
611
|
+
this.ssgiIntensity = pb.float().uniform(0);
|
|
612
|
+
this.srgbOut = pb.int().uniform(0);
|
|
613
|
+
this.$outputs.outColor = pb.vec4();
|
|
614
|
+
pb.main(function() {
|
|
615
|
+
this.$l.screenUV = pb.div(pb.vec2(this.$builtins.fragCoord.xy), this.targetSize);
|
|
616
|
+
this.$l.sceneColor = pb.textureSampleLevel(this.colorTex, this.screenUV, 0).rgb;
|
|
617
|
+
// Invert the Reinhard compression applied by the trace pass
|
|
618
|
+
// (x/(1+x) -> y/(1-y)); the clamp bounds the expansion so a fully
|
|
619
|
+
// saturated texel cannot blow up to infinity.
|
|
620
|
+
this.$l.compressed = pb.clamp(pb.textureSampleLevel(this.radianceTex, this.screenUV, 0).rgb, pb.vec3(0), pb.vec3(0.98));
|
|
621
|
+
this.$l.radiance = pb.div(this.compressed, pb.sub(pb.vec3(1), this.compressed));
|
|
622
|
+
// The forward pipeline keeps no albedo GBuffer; approximate the
|
|
623
|
+
// surface reflectance by the chromaticity of the lit scene color
|
|
624
|
+
// (hue preserved, luminance normalized). Unlike scaling by the
|
|
625
|
+
// scene color itself this keeps indirect light visible in shadowed
|
|
626
|
+
// areas - exactly where GI matters.
|
|
627
|
+
this.$l.luma = pb.dot(this.sceneColor, pb.vec3(0.2126, 0.7152, 0.0722));
|
|
628
|
+
this.$l.albedoApprox = pb.clamp(pb.div(this.sceneColor, pb.vec3(pb.add(this.luma, 1e-3))), pb.vec3(0), pb.vec3(1));
|
|
629
|
+
this.$l.combined = pb.add(this.sceneColor, pb.mul(this.radiance, this.albedoApprox, this.ssgiIntensity));
|
|
630
|
+
this.$if(pb.equal(this.srgbOut, 0), function() {
|
|
631
|
+
this.$outputs.outColor = pb.vec4(this.combined, 1);
|
|
632
|
+
}).$else(function() {
|
|
633
|
+
this.$outputs.outColor = pb.vec4(linearToGamma(this, this.combined), 1);
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
program.name = '@SSGI_Combine';
|
|
639
|
+
return program;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export { SSGI };
|
|
644
|
+
//# sourceMappingURL=ssgi.js.map
|