@hraness/slopcamera 3.2.7 → 3.3.1
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/NOTICE.md +1 -7
- package/PRIVACY.md +16 -7
- package/README.md +65 -20
- package/apps/desktop/README.md +14 -130
- package/apps/desktop/application/context.ts +6 -9
- package/apps/desktop/application/default-registry.ts +26 -10
- package/apps/desktop/application/operation.ts +14 -5
- package/apps/desktop/application/operations/index.ts +4 -1
- package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
- package/apps/desktop/application/operations/spatial-direction.ts +222 -0
- package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
- package/apps/desktop/application/operations/spatial-review.ts +194 -0
- package/apps/desktop/application/operations/spatial-scene.ts +25 -1
- package/apps/desktop/application/registry.ts +2 -2
- package/apps/desktop/application/spatial-asset-admission.ts +201 -0
- package/apps/desktop/application/spatial-assets.ts +160 -31
- package/apps/desktop/application/spatial-render.ts +217 -20
- package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
- package/apps/desktop/application/spatial-review.ts +361 -0
- package/apps/desktop/application/spatial-spz.ts +48 -3
- package/apps/desktop/application/spatial-world-import.ts +26 -5
- package/apps/desktop/application/spatial-world-metadata.ts +105 -0
- package/apps/desktop/cli/args.ts +528 -97
- package/apps/desktop/cli/capability-manifest.ts +374 -0
- package/apps/desktop/cli/cinema-renderer.ts +608 -0
- package/apps/desktop/cli/command-host-resources.ts +31 -2
- package/apps/desktop/cli/commands.ts +1215 -70
- package/apps/desktop/cli/gateway-review-provider.ts +334 -0
- package/apps/desktop/cli/help.ts +183 -23
- package/apps/desktop/cli/html-scene.ts +139 -28
- package/apps/desktop/cli/io.ts +2 -1
- package/apps/desktop/cli/main.ts +26 -50
- package/apps/desktop/cli/menubar.ts +244 -0
- package/apps/desktop/cli/music-analysis-service.ts +61 -0
- package/apps/desktop/cli/portable-surface.ts +18 -5
- package/apps/desktop/cli/spatial-asset-service.ts +124 -0
- package/apps/desktop/cli/spatial-design-service.ts +237 -0
- package/apps/desktop/cli/spatial-generate-service.ts +370 -0
- package/apps/desktop/cli/spatial-scene-service.ts +268 -4
- package/apps/desktop/cli/workflow-code.ts +1 -1
- package/apps/desktop/code/application-node-planner.ts +0 -47
- package/apps/desktop/code/host-source-layout.ts +46 -0
- package/apps/desktop/code/public.ts +0 -1
- package/apps/desktop/code/runtime-identity.ts +50 -8
- package/apps/desktop/code/semantic-builder.ts +24 -43
- package/apps/desktop/code/source-bundle.ts +23 -6
- package/apps/desktop/code/source-typecheck.ts +131 -43
- package/apps/desktop/code/worker-client.ts +14 -2
- package/apps/desktop/contracts/cinema.ts +736 -0
- package/apps/desktop/contracts/index.ts +1 -0
- package/apps/desktop/contracts/spatial-asset.ts +16 -0
- package/apps/desktop/contracts/spatial-world.ts +26 -3
- package/apps/desktop/core/cinema-plan.ts +1320 -0
- package/apps/desktop/core/index.ts +1 -0
- package/apps/desktop/core/music-analysis.ts +124 -1
- package/apps/desktop/core/storage.ts +75 -0
- package/apps/desktop/dist/cli/main.js +737 -291
- package/apps/desktop/html-overlay/audio-reactivity.ts +127 -0
- package/apps/desktop/html-overlay/index.ts +9 -0
- package/apps/desktop/html-overlay/runtime.ts +2 -0
- package/apps/desktop/html-overlay/scene.ts +29 -0
- package/apps/desktop/html-overlay/spatial.ts +619 -57
- package/apps/desktop/workflows/cinematic-world.ts +127 -0
- package/apps/desktop/workflows/index.ts +6 -0
- package/dist/cli.js +32 -434
- package/dist/code/advanced.js +1 -269
- package/dist/code/index.js +16 -2239
- package/dist/generate.js +1 -23
- package/dist/host-resources.js +1 -25
- package/dist/index-0bbesn4m.js +35 -0
- package/dist/index-296qhpf2.js +3 -0
- package/dist/index-3cxf2c58.js +3 -0
- package/dist/index-77fjfg9f.js +3 -0
- package/dist/index-80a36bc0.js +3 -0
- package/dist/index-a3vvs3rf.js +3 -0
- package/dist/index-b3geqczn.js +5 -0
- package/dist/index-cgpkyjqa.js +3 -0
- package/dist/index-f0ptv8dm.js +7 -0
- package/dist/index-grk47tgs.js +3 -0
- package/dist/index-qry58nj2.js +10 -0
- package/dist/index.js +1 -240
- package/dist/operations.js +1 -28
- package/dist/skill-install-gx7afxsd.js +2 -0
- package/dist/vectorize/worker.js +1 -141
- package/dist/workflow.js +1 -15
- package/docs/README.md +5 -1
- package/docs/studio.md +1 -1
- package/package.json +11 -24
- package/skills/slopcamera/SKILL.md +14 -1
- package/skills/slopcamera/references/diagrams.md +5 -14
- package/skills/slopcamera/references/directed-scenes.md +22 -2
- package/skills/slopcamera/references/gateway-media.md +15 -1
- package/skills/slopcamera/references/image-galleries.md +166 -0
- package/skills/slopcamera/references/install.md +2 -2
- package/skills/slopcamera/references/music-video.md +23 -3
- package/skills/slopcamera/references/parametric-design.md +14 -0
- package/skills/slopcamera/references/reference-led-3d.md +1 -1
- package/skills/slopcamera/references/scene-building.md +32 -0
- package/skills/slopcamera/references/social-collage-banners.md +204 -0
- package/skills/slopcamera/references/support.md +25 -0
- package/skills/slopcamera/references/web-media-excerpts.md +120 -0
- package/skills/slopcamera/references/workflows-sdk.md +3 -3
- package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
- package/src/capability-manifest.ts +361 -0
- package/src/cli.ts +250 -102
- package/src/code/canonical-json.ts +9 -0
- package/src/generate.ts +2 -2
- package/src/icon.ts +1111 -0
- package/src/image-gallery.ts +1049 -0
- package/src/index.ts +11 -23
- package/src/mcp/boundary.ts +11 -11
- package/src/mcp/index.ts +3 -0
- package/src/mcp/server.ts +2 -1
- package/src/mcp/tools.ts +1642 -130
- package/src/mcp/types.ts +19 -0
- package/src/operations.ts +412 -0
- package/src/portable-capability-manifest.ts +67 -0
- package/src/scene-gallery.ts +184 -0
- package/src/spatial-scene/asset-admission.ts +183 -0
- package/src/spatial-scene/audit-rendered.ts +692 -0
- package/src/spatial-scene/audit.ts +617 -0
- package/src/spatial-scene/behavior-audit.ts +252 -0
- package/src/spatial-scene/behavior-bake.ts +233 -0
- package/src/spatial-scene/behavior-fns.ts +585 -0
- package/src/spatial-scene/behavior-gallery.ts +109 -0
- package/src/spatial-scene/behavior-stdlib.ts +323 -0
- package/src/spatial-scene/behavior-trace.ts +325 -0
- package/src/spatial-scene/behavior.ts +618 -0
- package/src/spatial-scene/build.ts +632 -0
- package/src/spatial-scene/camera-rig.ts +125 -0
- package/src/spatial-scene/camera-track.ts +4 -2
- package/src/spatial-scene/character.ts +124 -0
- package/src/spatial-scene/contracts.ts +75 -7
- package/src/spatial-scene/design-templates.ts +351 -0
- package/src/spatial-scene/design.ts +327 -0
- package/src/spatial-scene/direction-compile.ts +584 -0
- package/src/spatial-scene/direction.ts +122 -0
- package/src/spatial-scene/effects.ts +149 -0
- package/src/spatial-scene/evaluate.ts +109 -29
- package/src/spatial-scene/gallery.ts +277 -0
- package/src/spatial-scene/generate.ts +239 -0
- package/src/spatial-scene/geometry-native.ts +165 -0
- package/src/spatial-scene/geometry.ts +1216 -0
- package/src/spatial-scene/gltf.ts +600 -77
- package/src/spatial-scene/identity.ts +55 -2
- package/src/spatial-scene/index.ts +36 -0
- package/src/spatial-scene/inspect.ts +35 -6
- package/src/spatial-scene/material-lighting.ts +609 -0
- package/src/spatial-scene/math.ts +21 -4
- package/src/spatial-scene/motion-evidence.ts +88 -0
- package/src/spatial-scene/parametric.ts +765 -0
- package/src/spatial-scene/particle-preparation.ts +183 -0
- package/src/spatial-scene/particle.ts +195 -0
- package/src/spatial-scene/patch.ts +109 -15
- package/src/spatial-scene/performance.ts +1613 -0
- package/src/spatial-scene/probe.ts +137 -0
- package/src/spatial-scene/recipe-pack.ts +86 -0
- package/src/spatial-scene/render-effects.ts +240 -0
- package/src/spatial-scene/review.ts +639 -0
- package/src/spatial-scene/simulation-bake.ts +206 -0
- package/src/spatial-scene/simulation.ts +187 -0
- package/src/spatial-scene/solve.ts +366 -0
- package/src/spatial-scene/temporal-audit.ts +354 -0
- package/src/support-completion.ts +16 -0
- package/src/support.ts +42 -0
- package/src/version.ts +1 -1
- package/apps/desktop/application/operations/recording/index.ts +0 -5
- package/apps/desktop/application/operations/recording/pause.ts +0 -34
- package/apps/desktop/application/operations/recording/resume.ts +0 -34
- package/apps/desktop/application/operations/recording/shared.ts +0 -199
- package/apps/desktop/application/operations/recording/start.ts +0 -169
- package/apps/desktop/application/operations/recording/stop.ts +0 -34
- package/apps/desktop/capture/protocol.ts +0 -689
- package/apps/desktop/cli/capture-bundle.ts +0 -1117
- package/apps/desktop/cli/recording-controller.ts +0 -1567
- package/apps/desktop/cli/recording-daemon.ts +0 -931
- package/dist/index-42zsesc1.js +0 -2257
- package/dist/index-7308egqr.js +0 -48
- package/dist/index-8txs6fkn.js +0 -712
- package/dist/index-mcy8z0br.js +0 -192
- package/dist/index-p63wavx0.js +0 -2029
- package/dist/index-r7gdhmsp.js +0 -424
- package/dist/index-rvghzt7b.js +0 -2013
- package/dist/index-sh6xbav6.js +0 -1219
- package/dist/index-z1w83f81.js +0 -4
- package/dist/index-z7239b4h.js +0 -2363
- package/dist/skill-install-dh0ntqba.js +0 -10
- package/src/desktop.ts +0 -253
|
@@ -15,9 +15,14 @@ import {
|
|
|
15
15
|
type SpatialCamera,
|
|
16
16
|
type SpatialEntity,
|
|
17
17
|
type SpatialProjection,
|
|
18
|
+
type SpatialSpotLight,
|
|
18
19
|
} from "../../../src/spatial-scene/contracts";
|
|
19
20
|
import { cameraMathView, composeTransform, invertTransform, multiplyTransforms, type Mat4 } from "../../../src/spatial-scene/math";
|
|
20
21
|
import { spatialAssetClosureDigests } from "../../../src/spatial-scene/identity";
|
|
22
|
+
import { pbrMaterialMapAssetIds } from "../../../src/spatial-scene/material-lighting";
|
|
23
|
+
import { SpatialRenderEffectsBindingSchema } from "../../../src/spatial-scene/render-effects";
|
|
24
|
+
import { SpatialAuditBoundsSchema } from "../../../src/spatial-scene/audit";
|
|
25
|
+
import { SPATIAL_SPLAT_PROXY_REPRESENTATION } from "../../../src/spatial-scene/audit-rendered";
|
|
21
26
|
import { canonicalJson, canonicalJsonSha256 } from "../core/canonical-json";
|
|
22
27
|
import {
|
|
23
28
|
HtmlOverlayAuthoringInputSchema,
|
|
@@ -30,6 +35,75 @@ import { HtmlOverlayExecutionProfileSchema } from "./execution-profile";
|
|
|
30
35
|
import { SpatialSpzFactsSchema, SPATIAL_SPLAT_LIMITS, spatialSpzAllocationBounds } from "../contracts/spatial-world";
|
|
31
36
|
import { addSpatialSplatRuntime } from "./spatial-splat-runtime";
|
|
32
37
|
|
|
38
|
+
/** Fixed renderer policy; source is host-owned and runs only against admitted scene meshes. */
|
|
39
|
+
export const SPATIAL_SHADOW_POLICY = Object.freeze({
|
|
40
|
+
profile: "slopcamera.caster-fit-shadows-v1",
|
|
41
|
+
maximumMapSize: 2048,
|
|
42
|
+
maximumAtlasTexels: 16_777_216,
|
|
43
|
+
pointMapFaces: 6,
|
|
44
|
+
depthBias: -0.00005,
|
|
45
|
+
maximumNormalBiasMeters: 0.05,
|
|
46
|
+
normalBiasTexels: 0.35,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** Kept as one injected source seam so CPU tests execute the exact browser algorithm. */
|
|
50
|
+
export function spatialShadowRuntimeSource(): string {
|
|
51
|
+
return `const spatialShadowPolicy=${JSON.stringify(SPATIAL_SHADOW_POLICY)};
|
|
52
|
+
const configureSpatialShadows=(world,maximumTextureSize,track)=>{
|
|
53
|
+
world.updateMatrixWorld(true);
|
|
54
|
+
const lights=[],casters=new THREE.Box3();
|
|
55
|
+
for(const object of world.children){
|
|
56
|
+
if(object.isLight&&object.castShadow&&object.shadow)lights.push(object);
|
|
57
|
+
if(object.isMesh&&object.castShadow){
|
|
58
|
+
if(!object.geometry.boundingBox)object.geometry.computeBoundingBox();
|
|
59
|
+
if(object.geometry.boundingBox)casters.union(object.geometry.boundingBox.clone().applyMatrix4(object.matrixWorld));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if(lights.length===0)return;
|
|
63
|
+
const atlasFaces=lights.reduce((sum,light)=>sum+(light.isPointLight?spatialShadowPolicy.pointMapFaces:1),0);
|
|
64
|
+
const fairSize=2**Math.floor(Math.log2(Math.sqrt(spatialShadowPolicy.maximumAtlasTexels/atlasFaces)));
|
|
65
|
+
const center=new THREE.Vector3(),size=new THREE.Vector3();
|
|
66
|
+
const hasCasters=!casters.isEmpty();
|
|
67
|
+
if(hasCasters){casters.getCenter(center);casters.getSize(size);}
|
|
68
|
+
const diameter=hasCasters?Math.max(size.length(),0.001):1;
|
|
69
|
+
for(const light of lights){
|
|
70
|
+
const shadow=light.shadow;
|
|
71
|
+
const hardwareSize=2**Math.floor(Math.log2(maximumTextureSize));
|
|
72
|
+
const mapSize=Math.max(1,Math.min(spatialShadowPolicy.maximumMapSize,fairSize,hardwareSize));
|
|
73
|
+
shadow.mapSize.set(mapSize,mapSize);
|
|
74
|
+
shadow.bias=spatialShadowPolicy.depthBias;
|
|
75
|
+
let texelSpan=diameter/mapSize;
|
|
76
|
+
if(light.isDirectionalLight&&hasCasters){
|
|
77
|
+
const lightPosition=new THREE.Vector3().setFromMatrixPosition(light.matrixWorld);
|
|
78
|
+
const targetPosition=new THREE.Vector3().setFromMatrixPosition(light.target.matrixWorld);
|
|
79
|
+
const direction=targetPosition.sub(lightPosition).normalize();
|
|
80
|
+
// Directional translation has no photometric effect; move the shadow camera
|
|
81
|
+
// in front of all casters so translated/tall models never fall behind near.
|
|
82
|
+
if(direction.lengthSq()>0){
|
|
83
|
+
light.matrix.setPosition(center.clone().addScaledVector(direction,-diameter));
|
|
84
|
+
light.matrix.decompose(light.position,light.quaternion,light.scale);
|
|
85
|
+
light.target.position.copy(center);light.target.updateMatrixWorld(true);light.updateMatrixWorld(true);
|
|
86
|
+
}
|
|
87
|
+
shadow.updateMatrices(light);
|
|
88
|
+
const inLight=casters.clone().applyMatrix4(shadow.camera.matrixWorldInverse);
|
|
89
|
+
const extent=new THREE.Vector3();inLight.getSize(extent);
|
|
90
|
+
const margin=Math.max(0.001,Math.max(extent.x,extent.y)*0.05);
|
|
91
|
+
const camera=shadow.camera;
|
|
92
|
+
camera.left=inLight.min.x-margin;camera.right=inLight.max.x+margin;
|
|
93
|
+
camera.bottom=inLight.min.y-margin;camera.top=inLight.max.y+margin;
|
|
94
|
+
camera.near=Math.max(0.0001,-inLight.max.z-margin);
|
|
95
|
+
// Receiver-only grounds do not dilute XY resolution. Extra depth retains
|
|
96
|
+
// contact shadows beyond caster bounds without fitting a huge ground plane.
|
|
97
|
+
camera.far=Math.max(camera.near+0.001,-inLight.min.z+diameter*2);
|
|
98
|
+
camera.updateProjectionMatrix();shadow.updateMatrices(light);
|
|
99
|
+
texelSpan=Math.max(camera.right-camera.left,camera.top-camera.bottom)/mapSize;
|
|
100
|
+
}
|
|
101
|
+
shadow.normalBias=Math.min(spatialShadowPolicy.maximumNormalBiasMeters,Math.max(0.000001,texelSpan*spatialShadowPolicy.normalBiasTexels));
|
|
102
|
+
track(shadow);
|
|
103
|
+
}
|
|
104
|
+
};`;
|
|
105
|
+
}
|
|
106
|
+
|
|
33
107
|
export const SPATIAL_OVERLAY_LIMITS = Object.freeze({
|
|
34
108
|
frames: 32,
|
|
35
109
|
requestBytes: 16 * 1024 * 1024,
|
|
@@ -48,6 +122,7 @@ const textureShape = {
|
|
|
48
122
|
width: z.number().int().min(1).max(8_192),
|
|
49
123
|
height: z.number().int().min(1).max(8_192),
|
|
50
124
|
alpha: alphaSchema,
|
|
125
|
+
uvTransform: z.strictObject({ offset: z.tuple([z.number().finite(), z.number().finite()]), rotation: z.number().finite().min(-Math.PI).max(Math.PI), scale: z.tuple([z.number().finite(), z.number().finite()]) }).optional(),
|
|
51
126
|
};
|
|
52
127
|
const samplerSchema = z.strictObject({
|
|
53
128
|
wrapS: z.union([z.literal(33071), z.literal(33648), z.literal(10497)]),
|
|
@@ -72,14 +147,28 @@ const primitiveSchema = z.strictObject({
|
|
|
72
147
|
/** Host-resolved final material; omitted means the entity's material. */
|
|
73
148
|
material: SpatialMaterialSchema.optional(),
|
|
74
149
|
texture: z.strictObject({ ...textureShape, flipY: z.boolean().optional(), sampler: samplerSchema.optional() }).optional(),
|
|
150
|
+
normalTexture: z.strictObject({ ...textureShape, scale: z.number().finite().optional(), flipY: z.boolean().optional(), sampler: samplerSchema.optional() }).optional(),
|
|
151
|
+
metallicRoughnessTexture: z.strictObject({ ...textureShape, flipY: z.boolean().optional(), sampler: samplerSchema.optional() }).optional(),
|
|
152
|
+
occlusionTexture: z.strictObject({ ...textureShape, strength: z.number().finite().min(0).max(1).optional(), flipY: z.boolean().optional(), sampler: samplerSchema.optional() }).optional(),
|
|
153
|
+
emissiveTexture: z.strictObject({ ...textureShape, flipY: z.boolean().optional(), sampler: samplerSchema.optional() }).optional(),
|
|
154
|
+
clearcoatTexture: z.strictObject({ ...textureShape, flipY: z.boolean().optional(), sampler: samplerSchema.optional() }).optional(),
|
|
155
|
+
clearcoatRoughnessTexture: z.strictObject({ ...textureShape, flipY: z.boolean().optional(), sampler: samplerSchema.optional() }).optional(),
|
|
156
|
+
clearcoatNormalTexture: z.strictObject({ ...textureShape, scale: z.number().finite().optional(), flipY: z.boolean().optional(), sampler: samplerSchema.optional() }).optional(),
|
|
157
|
+
transmissionTexture: z.strictObject({ ...textureShape, flipY: z.boolean().optional(), sampler: samplerSchema.optional() }).optional(),
|
|
158
|
+
sheenColorTexture: z.strictObject({ ...textureShape, flipY: z.boolean().optional(), sampler: samplerSchema.optional() }).optional(),
|
|
159
|
+
sheenRoughnessTexture: z.strictObject({ ...textureShape, flipY: z.boolean().optional(), sampler: samplerSchema.optional() }).optional(),
|
|
160
|
+
anisotropyTexture: z.strictObject({ ...textureShape, flipY: z.boolean().optional(), sampler: samplerSchema.optional() }).optional(),
|
|
161
|
+
physical: z.strictObject({ emissiveLinear: z.tuple([z.number(), z.number(), z.number()]).optional(), emissiveStrength: z.number().min(0).max(100_000).optional(), clearcoat: z.strictObject({ factor: z.number().min(0).max(1), roughness: z.number().min(0).max(1) }).optional(), transmission: z.number().min(0).max(1).optional(), sheen: z.strictObject({ colorLinear: z.tuple([z.number(), z.number(), z.number()]), roughness: z.number().min(0).max(1) }).optional(), anisotropy: z.strictObject({ strength: z.number().min(0).max(1), rotation: z.number().min(0).max(2 * Math.PI) }).optional(), ior: z.number().min(1).max(5).optional() }).optional(),
|
|
75
162
|
}).superRefine((primitive, context) => {
|
|
76
163
|
const vertices = primitive.positions.length / 3;
|
|
164
|
+
const hasTexture = primitive.texture !== undefined || primitive.normalTexture !== undefined || primitive.metallicRoughnessTexture !== undefined
|
|
165
|
+
|| primitive.occlusionTexture !== undefined || primitive.emissiveTexture !== undefined || primitive.clearcoatTexture !== undefined || primitive.clearcoatRoughnessTexture !== undefined || primitive.clearcoatNormalTexture !== undefined || primitive.transmissionTexture !== undefined || primitive.sheenColorTexture !== undefined || primitive.sheenRoughnessTexture !== undefined || primitive.anisotropyTexture !== undefined;
|
|
77
166
|
if (!Number.isInteger(vertices)
|
|
78
167
|
|| (primitive.indices === undefined ? vertices % 3 !== 0 : primitive.indices.length % 3 !== 0)
|
|
79
168
|
|| primitive.indices?.some(index => index >= vertices)
|
|
80
169
|
|| (primitive.normals !== undefined && primitive.normals.length !== primitive.positions.length)
|
|
81
170
|
|| (primitive.uvs !== undefined && primitive.uvs.length !== vertices * 2)
|
|
82
|
-
|| (
|
|
171
|
+
|| (hasTexture && primitive.uvs === undefined)) {
|
|
83
172
|
context.addIssue({ code: "custom", message: "Prepared geometry must contain complete triangles, matching attributes, and in-range indices; textured geometry requires UVs." });
|
|
84
173
|
}
|
|
85
174
|
try { invertTransform(primitive.matrix); } catch {
|
|
@@ -89,7 +178,9 @@ const primitiveSchema = z.strictObject({
|
|
|
89
178
|
|
|
90
179
|
export const PreparedSpatialAssetSchema = z.discriminatedUnion("kind", [
|
|
91
180
|
z.strictObject({ kind: z.literal("splat"), assetId: SpatialAssetIdSchema, entityId: SpatialEntityIdSchema, assetManifestSha256: SpatialDigestSchema,
|
|
92
|
-
resource: HtmlOverlayDeclaredResourceSchema, facts: SpatialSpzFactsSchema
|
|
181
|
+
resource: HtmlOverlayDeclaredResourceSchema, facts: SpatialSpzFactsSchema,
|
|
182
|
+
/** Entity-local axis-aligned enclosure decoded from the SPZ position section; the object-ID proxy derives from it. */
|
|
183
|
+
bounds: SpatialAuditBoundsSchema }),
|
|
93
184
|
z.strictObject({
|
|
94
185
|
kind: z.literal("raster"),
|
|
95
186
|
assetId: SpatialAssetIdSchema,
|
|
@@ -107,6 +198,21 @@ export const PreparedSpatialAssetSchema = z.discriminatedUnion("kind", [
|
|
|
107
198
|
entityContentSha256: SpatialDigestSchema.optional(),
|
|
108
199
|
...textureShape,
|
|
109
200
|
}),
|
|
201
|
+
z.strictObject({
|
|
202
|
+
kind: z.literal("lut"),
|
|
203
|
+
assetId: SpatialAssetIdSchema,
|
|
204
|
+
assetManifestSha256: SpatialDigestSchema,
|
|
205
|
+
...textureShape,
|
|
206
|
+
}),
|
|
207
|
+
z.strictObject({
|
|
208
|
+
kind: z.literal("particle-instances"),
|
|
209
|
+
entityId: SpatialEntityIdSchema,
|
|
210
|
+
instanceCount: z.number().int().min(0).max(1_000_000),
|
|
211
|
+
resource: HtmlOverlayDeclaredResourceSchema,
|
|
212
|
+
strideBytes: z.literal(64),
|
|
213
|
+
systemSha256: SpatialDigestSchema,
|
|
214
|
+
timeUs: SpatialTimeUsSchema,
|
|
215
|
+
}),
|
|
110
216
|
z.strictObject({
|
|
111
217
|
kind: z.literal("geometry"),
|
|
112
218
|
assetId: SpatialAssetIdSchema,
|
|
@@ -129,11 +235,21 @@ export const SpatialRenderModeSchema = z.discriminatedUnion("kind", [
|
|
|
129
235
|
z.strictObject({ kind: z.literal("beauty") }),
|
|
130
236
|
z.strictObject({ kind: z.literal("object-id"), coverage: SpatialCoverageSchema }),
|
|
131
237
|
z.strictObject({ kind: z.literal("axial-depth"), coverage: SpatialCoverageSchema }),
|
|
238
|
+
z.strictObject({ kind: z.literal("motion"), coverage: SpatialCoverageSchema, entityId: SpatialEntityIdSchema,
|
|
239
|
+
/** Pixels per frame interval mapped to the packed evidence midpoint range. */
|
|
240
|
+
motionScale: z.number().finite().positive().max(1_000_000) }),
|
|
132
241
|
]);
|
|
133
242
|
|
|
134
243
|
export const SpatialOverlayBatchInputSchema = z.strictObject({
|
|
135
244
|
snapshots: z.array(EvaluatedSpatialSceneSchema).min(1).max(SPATIAL_OVERLAY_LIMITS.frames),
|
|
245
|
+
/**
|
|
246
|
+
* The immediately preceding evaluated sample, retained only to derive exact
|
|
247
|
+
* screen-space velocity for the first frame of this batch. It is never
|
|
248
|
+
* rendered, and its scene digest must match the rendered snapshots.
|
|
249
|
+
*/
|
|
250
|
+
previousSnapshot: EvaluatedSpatialSceneSchema.optional(),
|
|
136
251
|
frameRate: SpatialFrameRateSchema,
|
|
252
|
+
effects: SpatialRenderEffectsBindingSchema.optional(),
|
|
137
253
|
mode: SpatialRenderModeSchema,
|
|
138
254
|
executionProfile: HtmlOverlayExecutionProfileSchema.optional(),
|
|
139
255
|
preparedAssets: z.array(PreparedSpatialAssetSchema).max(SPATIAL_OVERLAY_LIMITS.preparedAssets).default([]),
|
|
@@ -144,6 +260,7 @@ export type SpatialRenderMode = DeepReadonly<z.infer<typeof SpatialRenderModeSch
|
|
|
144
260
|
export type SpatialOverlayBatchInput = DeepReadonly<z.input<typeof SpatialOverlayBatchInputSchema>>;
|
|
145
261
|
type PreparedRaster = Extract<PreparedSpatialAsset, { kind: "raster" }>;
|
|
146
262
|
type PreparedGeometry = Extract<PreparedSpatialAsset, { kind: "geometry" }>;
|
|
263
|
+
type PreparedParticleInstances = Extract<PreparedSpatialAsset, { kind: "particle-instances" }>;
|
|
147
264
|
type PreparedSplat = Extract<PreparedSpatialAsset, { kind: "splat" }>;
|
|
148
265
|
type LoweredSplat = Readonly<{ kind: "splat"; entityId: string; key: string; matrix: Mat4 }>;
|
|
149
266
|
type Material = DeepReadonly<z.infer<typeof SpatialMaterialSchema>>;
|
|
@@ -161,6 +278,18 @@ export function spatialVideoRasterContentSha256(entity: Extract<SpatialEntity, {
|
|
|
161
278
|
return canonicalJsonSha256({ domain: "slopcamera.spatial-video-binding.v1", assetId: entity.assetId, sourceOffsetUs: entity.sourceOffsetUs, playback: entity.playback });
|
|
162
279
|
}
|
|
163
280
|
|
|
281
|
+
/**
|
|
282
|
+
* The content normalization every splat representation applies: the declared
|
|
283
|
+
* `sourceUp`→Y rotation plus the uniform `metersPerUnit` scale, ahead of the
|
|
284
|
+
* entity's own transform. Beauty rendering and proxy bounds share this map.
|
|
285
|
+
*/
|
|
286
|
+
export function spatialSplatContentTransform(interpretation: { readonly metersPerUnit: number; readonly sourceUp: "x" | "y" | "z" }): Mat4 {
|
|
287
|
+
const rotation = interpretation.sourceUp === "x" ? [0, 0, Math.SQRT1_2, Math.SQRT1_2] as const
|
|
288
|
+
: interpretation.sourceUp === "z" ? [-Math.SQRT1_2, 0, 0, Math.SQRT1_2] as const : [0, 0, 0, 1] as const;
|
|
289
|
+
const unit = interpretation.metersPerUnit;
|
|
290
|
+
return composeTransform({ position: [0, 0, 0], rotation, scale: [unit, unit, unit] });
|
|
291
|
+
}
|
|
292
|
+
|
|
164
293
|
export class SpatialOverlayCapabilityError extends Error {
|
|
165
294
|
readonly code = "unsupported-capability";
|
|
166
295
|
constructor(readonly capability: string, message: string) {
|
|
@@ -215,11 +344,15 @@ type LoweredMesh = Readonly<{
|
|
|
215
344
|
entityId: string;
|
|
216
345
|
selectionId: number;
|
|
217
346
|
matrix: Mat4;
|
|
347
|
+
/** Exact previous-sample model matrix for screen-space velocity; present only when the batch derives motion. */
|
|
348
|
+
previousMatrix?: Mat4;
|
|
218
349
|
placement: SpatialEntity["placement"];
|
|
219
350
|
geometry: Exclude<Extract<SpatialEntity, { kind: "mesh" }>["geometry"], { kind: "asset" }> | { readonly kind: "prepared"; readonly key: string; readonly primitive: number };
|
|
220
351
|
material: Material;
|
|
221
352
|
textureName?: string;
|
|
222
353
|
textureAlpha?: "straight" | "opaque";
|
|
354
|
+
/** Exact prepared resource name for every PBR map asset used by this mesh. */
|
|
355
|
+
pbrTextures?: Readonly<Record<string, string>>;
|
|
223
356
|
uvScale: readonly [number, number];
|
|
224
357
|
uvOffset: readonly [number, number];
|
|
225
358
|
surfaceScale: readonly [number, number];
|
|
@@ -228,15 +361,31 @@ type LoweredMesh = Readonly<{
|
|
|
228
361
|
alphaCutoff: number;
|
|
229
362
|
alphaMode?: "OPAQUE" | "MASK" | "BLEND";
|
|
230
363
|
linearColor?: readonly [number, number, number];
|
|
231
|
-
|
|
364
|
+
castShadow?: boolean;
|
|
365
|
+
receiveShadow?: boolean;
|
|
366
|
+
textureOptions?: Readonly<{ readonly flipY?: boolean; readonly sampler?: DeepReadonly<z.infer<typeof samplerSchema>>; readonly uvTransform?: { readonly offset: readonly [number, number]; readonly rotation: number; readonly scale: readonly [number, number] } }>;
|
|
367
|
+
sourcePhysical?: DeepReadonly<NonNullable<z.infer<typeof primitiveSchema>["physical"]>>;
|
|
368
|
+
sourceTextures?: Readonly<Record<string, { readonly name: string; readonly uvTransform?: { readonly offset: readonly [number, number]; readonly rotation: number; readonly scale: readonly [number, number] } }>>;
|
|
232
369
|
}>;
|
|
233
370
|
type LoweredLight = Readonly<{
|
|
234
371
|
kind: "light";
|
|
235
372
|
entityId: string;
|
|
236
373
|
matrix: Mat4;
|
|
237
|
-
light: "ambient" | "point" | "directional";
|
|
374
|
+
light: "ambient" | "point" | "directional" | "spot";
|
|
238
375
|
color: string;
|
|
239
376
|
intensity: number;
|
|
377
|
+
spot?: SpatialSpotLight;
|
|
378
|
+
shadow?: boolean;
|
|
379
|
+
}>;
|
|
380
|
+
/** Equirect sky surface: camera-following background and optional standard-material environment light. */
|
|
381
|
+
type LoweredEnvironment = Readonly<{
|
|
382
|
+
kind: "environment";
|
|
383
|
+
entityId: string;
|
|
384
|
+
textureName: string;
|
|
385
|
+
/** Effective world rotation in XYZW; environment entities are unparented so the authored rotation is world-space. */
|
|
386
|
+
rotation: readonly [number, number, number, number];
|
|
387
|
+
intensity: number;
|
|
388
|
+
role: "background" | "environment" | "both";
|
|
240
389
|
}>;
|
|
241
390
|
|
|
242
391
|
function unsupported(capability: string, message: string): never {
|
|
@@ -245,6 +394,8 @@ function unsupported(capability: string, message: string): never {
|
|
|
245
394
|
|
|
246
395
|
function preparedKey(asset: PreparedSpatialAsset): string {
|
|
247
396
|
if (asset.kind === "splat") return `${asset.assetId}:${asset.entityId}:splat`;
|
|
397
|
+
if (asset.kind === "lut") return `lut:${asset.assetId}`;
|
|
398
|
+
if (asset.kind === "particle-instances") return `${asset.systemSha256}:${asset.entityId}:${asset.timeUs}`;
|
|
248
399
|
return asset.kind === "raster"
|
|
249
400
|
? `${asset.assetId}:${asset.entityId}:${asset.timeUs === null ? "static" : String(asset.timeUs)}`
|
|
250
401
|
: `${asset.assetId}:${asset.entityId}:${asset.nodeIndex === undefined ? "all" : String(asset.nodeIndex)}:${asset.timeUs === null ? "static" : String(asset.timeUs)}`;
|
|
@@ -300,6 +451,9 @@ function calibratedCamera(camera: SpatialCamera) {
|
|
|
300
451
|
near: camera.projection.near,
|
|
301
452
|
far: camera.projection.far,
|
|
302
453
|
kind: camera.projection.kind,
|
|
454
|
+
// Physical values are carried to the renderer payload for deterministic
|
|
455
|
+
// cinematic consumers; calibrated projection remains authoritative.
|
|
456
|
+
...(camera.lens === undefined ? {} : { lens: camera.lens }),
|
|
303
457
|
};
|
|
304
458
|
}
|
|
305
459
|
|
|
@@ -316,13 +470,36 @@ export function createSpatialOverlayBatch(input: unknown) {
|
|
|
316
470
|
const first = request.snapshots[0]!;
|
|
317
471
|
const width = first.camera.projection.width;
|
|
318
472
|
const height = first.camera.projection.height;
|
|
473
|
+
if (request.effects !== undefined && request.snapshots.some(snapshot => snapshot.sceneSha256 !== request.effects!.document.sceneSha256)) {
|
|
474
|
+
throw new RangeError("Render effects belong to a different evaluated scene source.");
|
|
475
|
+
}
|
|
476
|
+
const activeEffects = request.mode.kind === "beauty" ? request.effects : undefined;
|
|
477
|
+
const effectSteps = activeEffects?.document.renderPlan.postProcess?.steps ?? [];
|
|
478
|
+
const needsVelocity = request.mode.kind === "motion" || effectSteps.some(step => step.kind === "motion-blur");
|
|
479
|
+
const needsDepth = effectSteps.some(step => step.kind === "depth-of-field");
|
|
480
|
+
if (request.previousSnapshot !== undefined) {
|
|
481
|
+
if (!needsVelocity) throw new RangeError("A previous snapshot is only meaningful when the batch derives motion.");
|
|
482
|
+
if (request.previousSnapshot.sceneSha256 !== first.sceneSha256 || request.previousSnapshot.stateSha256 === first.stateSha256
|
|
483
|
+
|| request.previousSnapshot.timeUs >= first.timeUs) {
|
|
484
|
+
throw new RangeError("The previous snapshot must precede this batch on the same scene source.");
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (request.mode.kind === "motion" && request.snapshots.length < 2 && request.previousSnapshot === undefined) {
|
|
488
|
+
throw new RangeError("Motion evidence requires a leading sample or at least two rendered samples.");
|
|
489
|
+
}
|
|
319
490
|
const prepared = new Map<string, PreparedSpatialAsset>();
|
|
320
491
|
const geometries = new Map<string, PreparedGeometry>();
|
|
492
|
+
const particles = new Map<string, PreparedParticleInstances>();
|
|
321
493
|
const splats = new Map<string, PreparedSplat>();
|
|
494
|
+
const splatResources = new Map<string, HtmlOverlayDeclaredResource>();
|
|
322
495
|
const resources = new Map<string, HtmlOverlayDeclaredResource>();
|
|
323
496
|
const textures = new Map<string, { readonly width: number; readonly height: number; readonly alpha: "straight" | "opaque" }>();
|
|
497
|
+
const lutBindings = new Map<string, { readonly resourceName: string; readonly size: number }>();
|
|
324
498
|
const usedPrepared = new Set<string>();
|
|
325
499
|
const bindTexture = (texture: NonNullable<Texture>): void => {
|
|
500
|
+
// Motion evidence draws velocity only; prepared raster bindings still count
|
|
501
|
+
// as used through bindAsset, but their pixels are never declared or served.
|
|
502
|
+
if (request.mode.kind === "motion") return;
|
|
326
503
|
assertRasterResource(texture);
|
|
327
504
|
const existing = resources.get(texture.resource.name);
|
|
328
505
|
if (existing !== undefined && canonicalJson(existing) !== canonicalJson(texture.resource)) {
|
|
@@ -353,20 +530,78 @@ export function createSpatialOverlayBatch(input: unknown) {
|
|
|
353
530
|
resources.set(asset.resource.name, asset.resource);
|
|
354
531
|
}
|
|
355
532
|
}
|
|
533
|
+
if (asset.kind === "lut") {
|
|
534
|
+
if (activeEffects === undefined || !activeEffects.document.renderPlan.postProcess?.steps.some(step => step.kind === "lut-grade" && step.assetId === asset.assetId)) {
|
|
535
|
+
throw new RangeError(`Prepared LUT ${asset.assetId} does not bind an active lut-grade step.`);
|
|
536
|
+
}
|
|
537
|
+
const size = asset.height;
|
|
538
|
+
if (asset.width !== size * size || size < 2 || size > 256) {
|
|
539
|
+
throw new RangeError(`Prepared LUT ${asset.assetId} must be an s²×s strip image encoding an s³ grid, with 2 ≤ s ≤ 256.`);
|
|
540
|
+
}
|
|
541
|
+
bindTexture(asset);
|
|
542
|
+
lutBindings.set(asset.assetId, { resourceName: asset.resource.name, size });
|
|
543
|
+
usedPrepared.add(key);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (asset.kind === "particle-instances") {
|
|
547
|
+
if (activeEffects === undefined || asset.resource.transport !== "fetch" || asset.resource.mediaType !== "application/octet-stream"
|
|
548
|
+
|| asset.resource.bytes !== asset.instanceCount * asset.strideBytes
|
|
549
|
+
|| !request.snapshots.some(snapshot => snapshot.timeUs === asset.timeUs)
|
|
550
|
+
|| !activeEffects.document.particleSystems.some(binding => binding.systemSha256 === asset.systemSha256 && binding.system.entityId === asset.entityId)) {
|
|
551
|
+
throw new RangeError("Prepared particle instances must bind an active system, exact sample, fixed layout, and immutable binary resource.");
|
|
552
|
+
}
|
|
553
|
+
const previous = resources.get(asset.resource.name);
|
|
554
|
+
if (previous !== undefined && canonicalJson(previous) !== canonicalJson(asset.resource)) throw new RangeError("Particle resource names must identify exact immutable bytes.");
|
|
555
|
+
particles.set(key, asset);
|
|
556
|
+
resources.set(asset.resource.name, asset.resource);
|
|
557
|
+
usedPrepared.add(key);
|
|
558
|
+
}
|
|
356
559
|
if (asset.kind === "splat") {
|
|
357
560
|
const expected = spatialSpzAllocationBounds(asset.facts.splats, asset.resource.bytes, asset.facts.decompressedBytes);
|
|
358
561
|
if (asset.resource.mediaType !== "application/octet-stream" || asset.resource.bytes < 18 || expected.gpuBytesBound !== asset.facts.gpuBytesBound || expected.hostBytesBound !== asset.facts.hostBytesBound) throw new RangeError("Prepared SPZ allocation or binary resource evidence is invalid.");
|
|
359
|
-
const previous =
|
|
360
|
-
|
|
361
|
-
|
|
562
|
+
const previous = splatResources.get(asset.resource.name);
|
|
563
|
+
const declared = resources.get(asset.resource.name);
|
|
564
|
+
if ((previous !== undefined && canonicalJson(previous) !== canonicalJson(asset.resource))
|
|
565
|
+
|| (declared !== undefined && canonicalJson(declared) !== canonicalJson(asset.resource))) throw new RangeError("Splat resource names must identify exact immutable bytes.");
|
|
566
|
+
splatResources.set(asset.resource.name, asset.resource);
|
|
567
|
+
splats.set(key, asset);
|
|
568
|
+
// Object-ID lowers only the bounding-box proxy: the SPZ payload is never
|
|
569
|
+
// declared, served, decoded, or allocated for that mode.
|
|
570
|
+
if (request.mode.kind === "beauty") resources.set(asset.resource.name, asset.resource);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (activeEffects !== undefined) {
|
|
574
|
+
for (const binding of activeEffects.document.particleSystems) {
|
|
575
|
+
for (const snapshot of request.snapshots) {
|
|
576
|
+
const key = `${binding.systemSha256}:${binding.system.entityId}:${snapshot.timeUs}`;
|
|
577
|
+
if (!particles.has(key)) throw new RangeError(`Particle system ${binding.system.entityId} has no exact prepared buffer at ${snapshot.timeUs}.`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
for (const step of effectSteps) {
|
|
581
|
+
if (step.kind === "lut-grade" && !lutBindings.has(step.assetId)) {
|
|
582
|
+
throw new RangeError(`lut-grade step has no exact prepared LUT for ${step.assetId}.`);
|
|
583
|
+
}
|
|
362
584
|
}
|
|
363
585
|
}
|
|
586
|
+
/** Screen-space velocity derives from exact per-sample transforms; the first frame of a batch falls back to its leading snapshot, then to zero motion. */
|
|
587
|
+
const previousModelMatrix = (entityId: string, currentWorld: Mat4, previousSnapshot: EvaluatedSpatialScene | undefined, local?: Mat4, previousLocal?: Mat4): Mat4 => {
|
|
588
|
+
const world = previousSnapshot === undefined ? currentWorld
|
|
589
|
+
: previousSnapshot.entities.find(entry => entry.entity.entityId === entityId)?.worldMatrix ?? currentWorld;
|
|
590
|
+
return local === undefined ? world : multiplyTransforms(world, previousLocal ?? local);
|
|
591
|
+
};
|
|
592
|
+
const previousPrimitiveMatrix = (baseKey: string, primitive: number, previousTimeUs: number | undefined, fallback: Mat4): Mat4 => {
|
|
593
|
+
if (previousTimeUs === undefined) return fallback;
|
|
594
|
+
const asset = prepared.get(`${baseKey}:${previousTimeUs}`);
|
|
595
|
+
return asset?.kind === "geometry" && asset.primitives[primitive] !== undefined ? asset.primitives[primitive].matrix : fallback;
|
|
596
|
+
};
|
|
364
597
|
const frameEvidence: Array<{
|
|
365
598
|
readonly sceneSha256: string; readonly stateSha256: string; readonly viewSha256: string; readonly timeUs: number;
|
|
366
599
|
readonly camera: SpatialCamera;
|
|
367
600
|
readonly objects: ReadonlyArray<{ readonly entityId: string; readonly selectionId: number; readonly representation: string; readonly placement: "world" | "view"; readonly assetManifestSha256?: string }>;
|
|
368
601
|
}> = [];
|
|
369
|
-
const frames = request.snapshots.map((snapshot: EvaluatedSpatialScene) => {
|
|
602
|
+
const frames = request.snapshots.map((snapshot: EvaluatedSpatialScene, snapshotIndex: number) => {
|
|
603
|
+
const previousSnapshot = snapshotIndex > 0 ? request.snapshots[snapshotIndex - 1]! : request.previousSnapshot;
|
|
604
|
+
if (snapshot.fog?.kind === "height") unsupported("height-fog", "Height-dependent fog is not representable by Three FogExp2; use linear fog until a qualified height-fog lowering exists.");
|
|
370
605
|
if (snapshot.camera.projection.width !== width || snapshot.camera.projection.height !== height) {
|
|
371
606
|
throw new RangeError("A spatial overlay batch requires identical calibrated output dimensions.");
|
|
372
607
|
}
|
|
@@ -375,13 +610,14 @@ export function createSpatialOverlayBatch(input: unknown) {
|
|
|
375
610
|
if (manifests.size !== snapshot.assets.length) throw new RangeError("Snapshot asset IDs must be unique.");
|
|
376
611
|
const ids = new Set<string>();
|
|
377
612
|
const selections = new Set<number>();
|
|
378
|
-
const objects: (LoweredMesh | LoweredLight | LoweredSplat)[] = [];
|
|
613
|
+
const objects: (LoweredMesh | LoweredLight | LoweredSplat | LoweredEnvironment)[] = [];
|
|
379
614
|
const evidence: Array<{ entityId: string; selectionId: number; representation: string; placement: "world" | "view"; assetManifestSha256?: string }> = [];
|
|
380
615
|
const bindAsset = (assetId: string, key: string): PreparedSpatialAsset => {
|
|
381
616
|
const manifest = manifests.get(assetId);
|
|
382
617
|
if (manifest === undefined) throw new RangeError(`Snapshot has no manifest for ${assetId}.`);
|
|
383
618
|
const asset = prepared.get(key);
|
|
384
619
|
if (asset === undefined) unsupported(`prepare-${manifest.interpretation.kind}`, `No exact prepared representation for ${key}.`);
|
|
620
|
+
if (asset.kind === "particle-instances") throw new RangeError(`Prepared particle binding cannot substitute scene asset ${assetId}.`);
|
|
385
621
|
if (asset.assetManifestSha256 !== manifestDigests[assetId]) {
|
|
386
622
|
throw new RangeError(`Prepared asset manifest identity is stale for ${assetId}.`);
|
|
387
623
|
}
|
|
@@ -394,29 +630,71 @@ export function createSpatialOverlayBatch(input: unknown) {
|
|
|
394
630
|
ids.add(entity.entityId); selections.add(entry.selectionId);
|
|
395
631
|
invertTransform(entry.worldMatrix);
|
|
396
632
|
if (entity.kind === "splat") {
|
|
397
|
-
|
|
398
|
-
if (request.mode.kind
|
|
399
|
-
|
|
400
|
-
|
|
633
|
+
const key = `${entity.assetId}:${entity.entityId}:splat`;
|
|
634
|
+
if (request.mode.kind === "beauty") {
|
|
635
|
+
if (request.executionProfile !== "three-spark-webgl2-hardware-v1") unsupported("splat-profile", "Splat rendering requires the explicit Three/Spark hardware profile.");
|
|
636
|
+
if (entity.placement.kind !== "world" || snapshot.camera.projection.kind !== "perspective") unsupported("splat-camera", "The initial Spark profile requires world placement and a perspective camera.");
|
|
637
|
+
const preparedSplat = bindAsset(entity.assetId, key), manifest = manifests.get(entity.assetId)!;
|
|
638
|
+
if (preparedSplat.kind !== "splat" || manifest.interpretation.kind !== "splat" || manifest.interpretation.format !== "spz" || preparedSplat.resource.sha256 !== manifest.payload.sha256 || preparedSplat.resource.bytes !== manifest.payload.bytes) throw new RangeError("Splat resource does not match the exact source payload.");
|
|
639
|
+
const m = entry.worldMatrix, lengths = [Math.hypot(m[0]!, m[1]!, m[2]!), Math.hypot(m[4]!, m[5]!, m[6]!), Math.hypot(m[8]!, m[9]!, m[10]!)];
|
|
640
|
+
if (Math.max(...lengths) - Math.min(...lengths) > Math.max(...lengths) * 1e-6) unsupported("splat-transform", "The initial Spark profile requires uniform world scale.");
|
|
641
|
+
for (const [left, right] of [[0, 4], [0, 8], [4, 8]] as const) if (Math.abs(m[left]! * m[right]! + m[left + 1]! * m[right + 1]! + m[left + 2]! * m[right + 2]!) > lengths[0]! ** 2 * 1e-6) unsupported("splat-transform", "The initial Spark profile does not accept sheared world transforms.");
|
|
642
|
+
if (entry.visible) objects.push({ kind: "splat", entityId: entity.entityId, key, matrix: multiplyTransforms(entry.worldMatrix, spatialSplatContentTransform(manifest.interpretation)) });
|
|
643
|
+
evidence.push({ entityId: entity.entityId, selectionId: entry.selectionId, representation: "spz-static-radiance; authored-wrapper-id; no-depth-or-object-id", placement: "world", assetManifestSha256: preparedSplat.assetManifestSha256 });
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
if (request.mode.kind !== "object-id" && request.mode.kind !== "motion") unsupported("splat-aov", "Splat axial-depth is unsupported; retained colliders are approximate evidence, not pixel truth.");
|
|
647
|
+
// Object-ID and motion lower the splat's bounding-box proxy under its
|
|
648
|
+
// normal selection code — approximate coverage, never splat pixel
|
|
649
|
+
// truth. The box derives from the prepared decoded-position bounds, so
|
|
650
|
+
// a splat without trusted bounds has no representation to bind.
|
|
651
|
+
const preparedSplat = bindAsset(entity.assetId, key), manifest = manifests.get(entity.assetId)!;
|
|
401
652
|
if (preparedSplat.kind !== "splat" || manifest.interpretation.kind !== "splat" || manifest.interpretation.format !== "spz" || preparedSplat.resource.sha256 !== manifest.payload.sha256 || preparedSplat.resource.bytes !== manifest.payload.bytes) throw new RangeError("Splat resource does not match the exact source payload.");
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
653
|
+
if (entry.visible && entity.placement.kind === "world") {
|
|
654
|
+
const bounds = preparedSplat.bounds;
|
|
655
|
+
const center: [number, number, number] = [(bounds.min[0] + bounds.max[0]) / 2, (bounds.min[1] + bounds.max[1]) / 2, (bounds.min[2] + bounds.max[2]) / 2];
|
|
656
|
+
const size: [number, number, number] = [bounds.max[0] - bounds.min[0], bounds.max[1] - bounds.min[1], bounds.max[2] - bounds.min[2]];
|
|
657
|
+
const material: Material = { kind: "unlit", color: "#ffffff", opacity: 1 };
|
|
658
|
+
assertCoverage(request.mode, material, undefined);
|
|
659
|
+
const proxyLocal = composeTransform({ position: center, rotation: [0, 0, 0, 1], scale: [1, 1, 1] });
|
|
660
|
+
objects.push({ kind: "mesh", entityId: entity.entityId, selectionId: entry.selectionId,
|
|
661
|
+
matrix: multiplyTransforms(entry.worldMatrix, proxyLocal),
|
|
662
|
+
...(needsVelocity ? { previousMatrix: previousModelMatrix(entity.entityId, entry.worldMatrix, previousSnapshot, proxyLocal) } : {}),
|
|
663
|
+
placement: entity.placement, geometry: { kind: "box", size }, material,
|
|
664
|
+
uvScale: [1, 1], uvOffset: [0, 0], surfaceScale: [1, 1], flipViewUv: false, doubleSided: true, alphaCutoff: 0 });
|
|
665
|
+
evidence.push({ entityId: entity.entityId, selectionId: entry.selectionId, representation: SPATIAL_SPLAT_PROXY_REPRESENTATION, placement: "world", assetManifestSha256: preparedSplat.assetManifestSha256 });
|
|
666
|
+
}
|
|
409
667
|
continue;
|
|
410
668
|
}
|
|
411
669
|
if (entity.kind === "group") continue;
|
|
412
670
|
if (entity.kind === "light") {
|
|
413
671
|
if (entity.placement.kind !== "world") unsupported("view-light", "Lights require world placement.");
|
|
414
|
-
if (entry.visible) objects.push({
|
|
672
|
+
if (entry.visible) objects.push({
|
|
673
|
+
kind: "light", entityId: entity.entityId, matrix: entry.worldMatrix, light: entity.light, color: entity.color, intensity: entity.intensity,
|
|
674
|
+
...(entity.spot === undefined ? {} : { spot: entity.spot }),
|
|
675
|
+
...(entity.shadow === undefined ? {} : { shadow: entity.shadow }),
|
|
676
|
+
});
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
if (entity.kind === "environment") {
|
|
680
|
+
// The equirect raster is bound for every frame even when hidden: the
|
|
681
|
+
// prepared batch and auxiliary passes must see identical bindings.
|
|
682
|
+
const key = `${entity.assetId}:${entity.entityId}:static`;
|
|
683
|
+
const asset = bindAsset(entity.assetId, key);
|
|
684
|
+
if (asset.kind !== "raster" || manifests.get(entity.assetId)!.interpretation.kind !== "image") {
|
|
685
|
+
unsupported("prepared-environment-kind", "Environment entities require a prepared image raster.");
|
|
686
|
+
}
|
|
687
|
+
bindTexture(asset);
|
|
688
|
+
if (entry.visible) objects.push({ kind: "environment", entityId: entity.entityId, textureName: asset.resource.name,
|
|
689
|
+
rotation: entity.transform.rotation, intensity: entity.intensity, role: entity.role });
|
|
415
690
|
continue;
|
|
416
691
|
}
|
|
417
692
|
// Capability checks include invisible/off-camera entities, so changing
|
|
418
693
|
// visibility or cameras cannot discover an unplanned loader during render.
|
|
419
|
-
const base = { kind: "mesh" as const, entityId: entity.entityId, selectionId: entry.selectionId, matrix: entry.worldMatrix, placement: entity.placement
|
|
694
|
+
const base = { kind: "mesh" as const, entityId: entity.entityId, selectionId: entry.selectionId, matrix: entry.worldMatrix, placement: entity.placement,
|
|
695
|
+
...(needsVelocity ? { previousMatrix: previousModelMatrix(entity.entityId, entry.worldMatrix, previousSnapshot) } : {}),
|
|
696
|
+
...(entity.kind === "mesh" && entity.castShadow !== undefined ? { castShadow: entity.castShadow } : {}),
|
|
697
|
+
...(entity.kind === "mesh" && entity.receiveShadow !== undefined ? { receiveShadow: entity.receiveShadow } : {}) };
|
|
420
698
|
const unitFit = { uvScale: [1, 1] as const, uvOffset: [0, 0] as const, surfaceScale: [1, 1] as const, flipViewUv: false, doubleSided: true, alphaCutoff: 0 };
|
|
421
699
|
let meshes: LoweredMesh[];
|
|
422
700
|
let representation: string;
|
|
@@ -437,7 +715,9 @@ export function createSpatialOverlayBatch(input: unknown) {
|
|
|
437
715
|
representation = asset.timeUs === null ? "prepared-static-triangle-mesh" : "prepared-evaluated-triangle-mesh";
|
|
438
716
|
const materialMode = entity.geometry.materialMode;
|
|
439
717
|
meshes = asset.primitives.map((primitive, index) => {
|
|
718
|
+
const sourceTextureSlots = ["normalTexture", "metallicRoughnessTexture", "occlusionTexture", "emissiveTexture", "clearcoatTexture", "clearcoatRoughnessTexture", "clearcoatNormalTexture", "transmissionTexture", "sheenColorTexture", "sheenRoughnessTexture", "anisotropyTexture"] as const;
|
|
440
719
|
if (primitive.texture !== undefined) bindTexture(primitive.texture);
|
|
720
|
+
for (const slot of sourceTextureSlots) if (primitive[slot] !== undefined) bindTexture(primitive[slot]);
|
|
441
721
|
const material = primitive.material ?? entity.material;
|
|
442
722
|
if (materialMode === "source" && primitive.material === undefined) throw new RangeError("Source-material geometry requires the exact resolved source material for every primitive.");
|
|
443
723
|
if (materialMode !== "source" && (primitive.material !== undefined || primitive.texture !== undefined || primitive.linearColor !== undefined)) {
|
|
@@ -445,17 +725,42 @@ export function createSpatialOverlayBatch(input: unknown) {
|
|
|
445
725
|
}
|
|
446
726
|
assertCoverage(request.mode, material, primitive.texture, primitive.alphaMode);
|
|
447
727
|
return { ...base, ...unitFit, geometry: { kind: "prepared", key, primitive: index }, material,
|
|
728
|
+
...(needsVelocity ? { previousMatrix: previousModelMatrix(entity.entityId, entry.worldMatrix, previousSnapshot, primitive.matrix,
|
|
729
|
+
previousPrimitiveMatrix(baseKey, index, previousSnapshot?.timeUs, primitive.matrix)) } : {}),
|
|
448
730
|
doubleSided: primitive.doubleSided ?? true, alphaCutoff: primitive.alphaCutoff ?? 0,
|
|
449
731
|
...(primitive.alphaMode === undefined ? {} : { alphaMode: primitive.alphaMode }),
|
|
450
732
|
...(primitive.linearColor === undefined ? {} : { linearColor: primitive.linearColor }),
|
|
733
|
+
...(primitive.physical === undefined ? {} : { sourcePhysical: primitive.physical }),
|
|
734
|
+
...(sourceTextureSlots.every(slot => primitive[slot] === undefined) ? {} : { sourceTextures: Object.fromEntries(sourceTextureSlots.flatMap(slot => primitive[slot] === undefined ? [] : [[slot, { name: primitive[slot]!.resource.name, ...(primitive[slot]!.uvTransform === undefined ? {} : { uvTransform: primitive[slot]!.uvTransform }) }]])) }),
|
|
451
735
|
...(primitive.texture === undefined ? {} : { textureName: primitive.texture.resource.name, textureAlpha: primitive.texture.alpha,
|
|
452
736
|
textureOptions: { ...(primitive.texture.flipY === undefined ? {} : { flipY: primitive.texture.flipY }),
|
|
453
|
-
...(primitive.texture.sampler === undefined ? {} : { sampler: primitive.texture.sampler }) } }) };
|
|
737
|
+
...(primitive.texture.sampler === undefined ? {} : { sampler: primitive.texture.sampler }), ...(primitive.texture.uvTransform === undefined ? {} : { uvTransform: primitive.texture.uvTransform }) } }) };
|
|
454
738
|
});
|
|
455
739
|
} else {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
740
|
+
let mapRaster: PreparedRaster | undefined;
|
|
741
|
+
const materialMap = entity.material.kind !== "pbr" ? entity.material.map : undefined;
|
|
742
|
+
const pbrTextures: Record<string, string> = {};
|
|
743
|
+
const mapIds = entity.material.kind === "pbr" ? pbrMaterialMapAssetIds(entity.material) : materialMap === undefined ? [] : [materialMap];
|
|
744
|
+
let mapDimensions: readonly [number, number] | undefined;
|
|
745
|
+
for (const mapId of mapIds) {
|
|
746
|
+
const key = `${mapId}:${entity.entityId}:static`;
|
|
747
|
+
const asset = bindAsset(mapId, key);
|
|
748
|
+
if (asset.kind !== "raster" || manifests.get(mapId)!.interpretation.kind !== "image") unsupported("prepared-material-map", "Material maps require a prepared image raster.");
|
|
749
|
+
bindTexture(asset);
|
|
750
|
+
const dimensions = [asset.width, asset.height] as const;
|
|
751
|
+
if (mapDimensions !== undefined && (dimensions[0] !== mapDimensions[0] || dimensions[1] !== mapDimensions[1])) throw new RangeError("PBR maps must have identical decoded dimensions.");
|
|
752
|
+
mapDimensions = dimensions;
|
|
753
|
+
if (entity.material.kind === "pbr") {
|
|
754
|
+
pbrTextures[mapId] = asset.resource.name;
|
|
755
|
+
if (entity.material.baseColorMap?.assetId === mapId) mapRaster = asset;
|
|
756
|
+
} else mapRaster = asset;
|
|
757
|
+
assetManifestSha256 = asset.assetManifestSha256;
|
|
758
|
+
}
|
|
759
|
+
assertCoverage(request.mode, entity.material, mapRaster);
|
|
760
|
+
representation = `primitive-${entity.geometry.kind}${mapIds.length === 0 ? "" : "-textured"}`;
|
|
761
|
+
meshes = [{ ...base, ...unitFit, geometry: entity.geometry, material: entity.material,
|
|
762
|
+
...(Object.keys(pbrTextures).length === 0 ? {} : { pbrTextures }),
|
|
763
|
+
...(mapRaster === undefined ? {} : { textureName: mapRaster.resource.name, textureAlpha: mapRaster.alpha }) }];
|
|
459
764
|
}
|
|
460
765
|
} else {
|
|
461
766
|
const assetId = entity.kind === "text" ? entity.fontAssetId : entity.assetId;
|
|
@@ -487,15 +792,37 @@ export function createSpatialOverlayBatch(input: unknown) {
|
|
|
487
792
|
evidence.push({ entityId: entity.entityId, selectionId: entry.selectionId, representation, placement: entity.placement.kind,
|
|
488
793
|
...(assetManifestSha256 === undefined ? {} : { assetManifestSha256 }) });
|
|
489
794
|
}
|
|
795
|
+
const frameParticles = activeEffects?.document.particleSystems.flatMap(binding => {
|
|
796
|
+
const target = snapshot.entities.find(entry => entry.entity.entityId === binding.system.entityId);
|
|
797
|
+
if (target === undefined) throw new RangeError(`Particle system target ${binding.system.entityId} is absent from the evaluated scene.`);
|
|
798
|
+
if (target.entity.placement.kind !== "world") unsupported("particle-placement", "Particle systems require world-placed target entities.");
|
|
799
|
+
const key = `${binding.systemSha256}:${binding.system.entityId}:${snapshot.timeUs}`;
|
|
800
|
+
const preparedInstances = particles.get(key)!;
|
|
801
|
+
return target.visible && preparedInstances.instanceCount > 0 ? [{ key, entityId: binding.system.entityId, matrix: target.worldMatrix,
|
|
802
|
+
...(needsVelocity ? { previousMatrix: previousModelMatrix(binding.system.entityId, target.worldMatrix, previousSnapshot) } : {}) }] : [];
|
|
803
|
+
}) ?? [];
|
|
804
|
+
if (objects.reduce((count, object) => count + (object.kind === "environment" ? 1 : 0), 0) > 1) {
|
|
805
|
+
throw new RangeError("A frame may show at most one visible environment entity; gate alternates through authored visibility.");
|
|
806
|
+
}
|
|
490
807
|
const meshes = objects.filter((object): object is LoweredMesh => object.kind === "mesh");
|
|
491
|
-
|
|
808
|
+
// Transparency sorting is a Spark beauty-pipeline constraint; object-ID
|
|
809
|
+
// proxies draw as ordinary opaque-coded meshes and never invoke it.
|
|
810
|
+
if (request.mode.kind === "beauty" && splats.size > 0 && meshes.some(mesh => mesh.placement.kind === "world" && (mesh.material.opacity < 1 || mesh.alphaMode === "BLEND" || mesh.textureAlpha === "straight" && mesh.alphaCutoff === 0))) unsupported("splat-transparency", "The initial world profile supports opaque 3D meshes and view overlays; interleaved transparent mesh/splat sorting is not qualified.");
|
|
492
811
|
const triangles = meshes.reduce((sum, mesh) => sum + geometryTriangleCount(mesh, geometries), 0);
|
|
493
812
|
if (meshes.length > SPATIAL_OVERLAY_LIMITS.drawCallsPerFrame || triangles > SPATIAL_OVERLAY_LIMITS.trianglesPerFrame) {
|
|
494
813
|
throw new RangeError("Spatial overlay exceeds its per-frame draw-call or triangle budget.");
|
|
495
814
|
}
|
|
815
|
+
if (request.mode.kind === "motion") {
|
|
816
|
+
const entityId = request.mode.entityId;
|
|
817
|
+
if (!meshes.some(mesh => mesh.entityId === entityId) && !frameParticles.some(item => item.entityId === entityId)) {
|
|
818
|
+
throw new RangeError(`Motion target ${entityId} has no renderable velocity representation at ${snapshot.timeUs}.`);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
496
821
|
frameEvidence.push({ sceneSha256: snapshot.sceneSha256, stateSha256: snapshot.stateSha256, viewSha256: snapshot.viewSha256,
|
|
497
822
|
timeUs: snapshot.timeUs, camera: snapshot.camera, objects: evidence });
|
|
498
|
-
return { camera: calibratedCamera(snapshot.camera),
|
|
823
|
+
return { camera: calibratedCamera(snapshot.camera), timeUs: snapshot.timeUs,
|
|
824
|
+
...(needsVelocity ? { previousCamera: previousSnapshot === undefined ? calibratedCamera(snapshot.camera) : calibratedCamera(previousSnapshot.camera) } : {}),
|
|
825
|
+
objects, particles: frameParticles, ...(snapshot.fog === undefined ? {} : { fog: snapshot.fog }) };
|
|
499
826
|
});
|
|
500
827
|
if (usedPrepared.size !== prepared.size) throw new RangeError("Prepared assets must be referenced by this exact batch; unused bindings are rejected.");
|
|
501
828
|
const declaredResources = HtmlOverlayDeclaredResourcesSchema.parse([...resources.values()]);
|
|
@@ -510,8 +837,14 @@ export function createSpatialOverlayBatch(input: unknown) {
|
|
|
510
837
|
const payload = {
|
|
511
838
|
mode: request.mode,
|
|
512
839
|
frames,
|
|
840
|
+
...(activeEffects === undefined ? {} : { effects: {
|
|
841
|
+
documentSha256: activeEffects.documentSha256,
|
|
842
|
+
renderPlanSha256: activeEffects.document.renderPlanSha256,
|
|
843
|
+
steps: effectSteps.map(step => step.kind === "lut-grade" ? { ...step, lut: lutBindings.get(step.assetId)! } : step),
|
|
844
|
+
} }),
|
|
513
845
|
geometry: Object.fromEntries([...geometries].filter(([, asset]) => asset.resource === undefined).map(([key, asset]) => [key, asset.primitives])),
|
|
514
846
|
geometryResources: [...geometries].filter(([, asset]) => asset.resource !== undefined).map(([key, asset]) => ({ key, resource: asset.resource!, url: htmlOverlayAssetLocalUrl(asset.resource!) })),
|
|
847
|
+
particleBuffers: [...particles].map(([key, asset]) => ({ key, instanceCount: asset.instanceCount, resource: asset.resource, strideBytes: asset.strideBytes, url: htmlOverlayAssetLocalUrl(asset.resource) })),
|
|
515
848
|
textures: declaredResources.filter(resource => textures.has(resource.name)).map(resource => ({ ...textures.get(resource.name)!, name: resource.name, url: htmlOverlayAssetLocalUrl(resource) })),
|
|
516
849
|
...(request.executionProfile === "three-spark-webgl2-hardware-v1" ? { splatKernel, splats: [...splats].map(([key, asset]) => ({ key, ...asset, url: htmlOverlayAssetLocalUrl(asset.resource) })) } : {}),
|
|
517
850
|
};
|
|
@@ -519,7 +852,9 @@ export function createSpatialOverlayBatch(input: unknown) {
|
|
|
519
852
|
// 32 B/pixel conservatively covers the RGBA16F/depth beauty target and the
|
|
520
853
|
// default RGBA8/depth canvas plus renderer framebuffer slack. Driver/process
|
|
521
854
|
// and platform compositor overhead remain outside this allocation accounting.
|
|
522
|
-
|
|
855
|
+
// The bound guards Spark's beauty allocations only — object-ID proxies never
|
|
856
|
+
// load or allocate splat payloads.
|
|
857
|
+
if (request.mode.kind === "beauty" && splats.size > 0 && (totalSplats > SPATIAL_SPLAT_LIMITS.splats || [...splats.values()].reduce((sum, asset) => sum + asset.facts.gpuBytesBound, width * height * 32) > SPATIAL_SPLAT_LIMITS.gpuBytes || [...splats.values()].reduce((sum, asset) => sum + asset.facts.hostBytesBound, 0) > SPATIAL_SPLAT_LIMITS.hostBytes)) throw new RangeError("Prepared world exceeds its aggregate splat/GPU/host allocation bounds.");
|
|
523
858
|
const sparkProfile = request.executionProfile === "three-spark-webgl2-hardware-v1";
|
|
524
859
|
if (sparkProfile && request.mode.kind !== "beauty") unsupported("splat-aov", "The initial Spark profile renders beauty only.");
|
|
525
860
|
const libraries = sparkProfile ? ["@sparkjsdev/spark", "three", "three/addons/postprocessing/Pass.js"] as const : ["three"] as const;
|
|
@@ -530,9 +865,44 @@ export function createSpatialOverlayBatch(input: unknown) {
|
|
|
530
865
|
html, libraries, parameters: {}, resources: declaredResources, seed: 0,
|
|
531
866
|
timing: { fps: 1, durationUs: frames.length * 1_000_000 },
|
|
532
867
|
});
|
|
868
|
+
const preparation: Array<{
|
|
869
|
+
key: string; assetManifestSha256?: string; instanceCount?: number; resourceSha256?: string; systemSha256?: string; timeUs?: number;
|
|
870
|
+
sourceTimeUs?: number; sourceFrameIndex?: number; sourcePresentationTimeUs?: number; sourcePts?: number;
|
|
871
|
+
sourceTimeBase?: { numerator: string; denominator: string }; sourceTimestamp?: string; sourceExactTimeUs?: { numerator: string; denominator: string };
|
|
872
|
+
}> = request.preparedAssets.map(asset => asset.kind === "particle-instances" ? {
|
|
873
|
+
key: preparedKey(asset), instanceCount: asset.instanceCount, resourceSha256: asset.resource.sha256,
|
|
874
|
+
systemSha256: asset.systemSha256, timeUs: asset.timeUs,
|
|
875
|
+
} : { key: preparedKey(asset), assetManifestSha256: asset.assetManifestSha256,
|
|
876
|
+
...(asset.kind === "raster" && asset.sourceTimeUs !== undefined ? { sourceTimeUs: asset.sourceTimeUs } : {}),
|
|
877
|
+
...(asset.kind === "raster" && asset.sourceFrameIndex !== undefined ? { sourceFrameIndex: asset.sourceFrameIndex } : {}),
|
|
878
|
+
...(asset.kind === "raster" && asset.sourcePresentationTimeUs !== undefined ? { sourcePresentationTimeUs: asset.sourcePresentationTimeUs } : {}),
|
|
879
|
+
...(asset.kind === "raster" && asset.sourcePts !== undefined ? { sourcePts: asset.sourcePts } : {}),
|
|
880
|
+
...(asset.kind === "raster" && asset.sourceTimeBase !== undefined ? { sourceTimeBase: asset.sourceTimeBase } : {}),
|
|
881
|
+
...(asset.kind === "raster" && asset.sourceTimestamp !== undefined ? { sourceTimestamp: asset.sourceTimestamp } : {}),
|
|
882
|
+
...(asset.kind === "raster" && asset.sourceExactTimeUs !== undefined ? { sourceExactTimeUs: asset.sourceExactTimeUs } : {}),
|
|
883
|
+
});
|
|
533
884
|
const metadataValue = {
|
|
534
885
|
kind: "slopcamera.spatial-overlay-batch", schemaVersion: 1,
|
|
535
886
|
renderer: "three-webgl2-snapshot-v1", mode: request.mode,
|
|
887
|
+
...(request.effects === undefined ? {} : { effects: {
|
|
888
|
+
applied: activeEffects !== undefined,
|
|
889
|
+
documentSha256: request.effects.documentSha256,
|
|
890
|
+
renderPlanSha256: request.effects.document.renderPlanSha256,
|
|
891
|
+
stepKinds: request.effects.document.renderPlan.postProcess?.steps.map(step => step.kind) ?? [],
|
|
892
|
+
...(activeEffects === undefined ? {} : { postProcess: {
|
|
893
|
+
pipeline: "ordered-fullscreen-passes; premultiplied-linear-half-float-source; straight-alpha-working-domain; srgb-premultiplied-output",
|
|
894
|
+
...(lutBindings.size === 0 ? {} : { lutDomain: "linear-clamped-0..1; strip-s2-by-s-3d-lut; two-slice-linear",
|
|
895
|
+
luts: Object.fromEntries([...lutBindings].map(([assetId, binding]) => [assetId, binding])) }),
|
|
896
|
+
...(needsDepth ? { depthSource: "beauty-depth-buffer; world-surfaces-only; view-overlays-unchanged" } : {}),
|
|
897
|
+
...(needsVelocity ? { velocitySource: "per-sample-rigid-transform-delta; previous-sample-or-leading-snapshot; first-sample-zero" } : {}),
|
|
898
|
+
} }),
|
|
899
|
+
} }),
|
|
900
|
+
...(request.mode.kind === "motion" ? { motion: {
|
|
901
|
+
entityId: request.mode.entityId, motionScale: request.mode.motionScale,
|
|
902
|
+
encoding: "rg16un; R,G=x hi/lo; B,A=y hi/lo; alpha=hit",
|
|
903
|
+
source: "per-sample-rigid-transform-delta; previous-sample-or-leading-snapshot; first-sample-zero",
|
|
904
|
+
packing: "velocity_px/motionScale*0.5+0.5 clamped to [0,1]; 16-bit unsigned per axis",
|
|
905
|
+
} } : {}),
|
|
536
906
|
...(request.executionProfile === undefined ? {} : { executionProfile: request.executionProfile }),
|
|
537
907
|
...(sparkProfile ? { splatProfile: { adapter: "spark-2.1.0-spz-v2-v3-ext-v1", kernel: splatKernel, lod: false, sorting: "await-explicit-camera-update", readback: "synchronous-exact-MRT-buffer-before-worker-sort", color: "srgb-radiance-to-linear", depth: "unsupported", objectId: "unsupported", collider: "approximate-retained-only", splats: totalSplats } } : {}),
|
|
538
908
|
frameRate: request.frameRate,
|
|
@@ -540,6 +910,7 @@ export function createSpatialOverlayBatch(input: unknown) {
|
|
|
540
910
|
color: request.mode.kind === "beauty"
|
|
541
911
|
? { source: "srgb", working: "linear-srgb", compositing: "linear-half-float-premultiplied", output: "srgb", alpha: "straight-png", toneMapping: "none" }
|
|
542
912
|
: { source: "rgba8-data", working: "rgba8-data", output: "rgba8-data", alpha: "binary-validity", toneMapping: "none" },
|
|
913
|
+
...(request.mode.kind === "beauty" ? { shadows: SPATIAL_SHADOW_POLICY } : {}),
|
|
543
914
|
rasterization: { antialias: false, sampling: "single-pixel-center", surfaces: "authored-primitives-double-sided; prepared-mesh-explicit-sidedness", transparentOrdering: "three-object-sort" },
|
|
544
915
|
selectionEncoding: { channels: "RGB-big-endian-uint24", noHit: [0, 0, 0, 0] },
|
|
545
916
|
depthEncoding: {
|
|
@@ -550,15 +921,7 @@ export function createSpatialOverlayBatch(input: unknown) {
|
|
|
550
921
|
},
|
|
551
922
|
cameraConvention: "camera-to-world; column-major; XYZW; local-minus-Z; top-left-image-boundary; pixel-centers-i+0.5,j+0.5",
|
|
552
923
|
viewConvention: "top-left-image-boundary; X-right,Y-down; normalized-XY-scaled-by-output-dimensions; order-after-world",
|
|
553
|
-
preparation
|
|
554
|
-
...(asset.kind === "raster" && asset.sourceTimeUs !== undefined ? { sourceTimeUs: asset.sourceTimeUs } : {}),
|
|
555
|
-
...(asset.kind === "raster" && asset.sourceFrameIndex !== undefined ? { sourceFrameIndex: asset.sourceFrameIndex } : {}),
|
|
556
|
-
...(asset.kind === "raster" && asset.sourcePresentationTimeUs !== undefined ? { sourcePresentationTimeUs: asset.sourcePresentationTimeUs } : {}),
|
|
557
|
-
...(asset.kind === "raster" && asset.sourcePts !== undefined ? { sourcePts: asset.sourcePts } : {}),
|
|
558
|
-
...(asset.kind === "raster" && asset.sourceTimeBase !== undefined ? { sourceTimeBase: asset.sourceTimeBase } : {}),
|
|
559
|
-
...(asset.kind === "raster" && asset.sourceTimestamp !== undefined ? { sourceTimestamp: asset.sourceTimestamp } : {}),
|
|
560
|
-
...(asset.kind === "raster" && asset.sourceExactTimeUs !== undefined ? { sourceExactTimeUs: asset.sourceExactTimeUs } : {}),
|
|
561
|
-
})),
|
|
924
|
+
preparation,
|
|
562
925
|
costs: { requestBytes: captured.bytes, htmlBytes: new TextEncoder().encode(html).byteLength,
|
|
563
926
|
decodedTexturePixels, resourceBytes: declaredResources.reduce((sum, resource) => sum + resource.bytes, 0),
|
|
564
927
|
note: sparkProfile ? "One isolated browser per batch; retained SPZ sources load once, each exact camera sample awaits full-resolution sort. Admission bounds are not measured GPU performance." : "Each batch uses one isolated browser render; scenes and frame-local GPU objects are rebuilt for every selected snapshot. These are admission counts, not measured GPU performance." },
|
|
@@ -568,6 +931,14 @@ export function createSpatialOverlayBatch(input: unknown) {
|
|
|
568
931
|
return Object.freeze({ authoring: Object.freeze(authoring), metadata: metadata.value as unknown as DeepReadonly<typeof metadataValue>, metadataSha256: metadata.sha256 });
|
|
569
932
|
}
|
|
570
933
|
|
|
934
|
+
/** Fixed golden-spiral gather taps; computed on the host so generated shaders contain no transcendentals. */
|
|
935
|
+
const DOF_TAPS = Object.freeze(Array.from({ length: 16 }, (_, index) => {
|
|
936
|
+
const angle = index * 2.399_963_229_728_653;
|
|
937
|
+
const radius = Math.sqrt((index + 0.5) / 16);
|
|
938
|
+
return `vec2(${(Math.cos(angle) * radius).toFixed(7)},${(Math.sin(angle) * radius).toFixed(7)})`;
|
|
939
|
+
}).join(","));
|
|
940
|
+
const BLOOM_WEIGHTS = Object.freeze([0.2270270270, 0.1945945946, 0.1216216216, 0.0540540541, 0.0162162162].map(weight => weight.toFixed(10)).join(","));
|
|
941
|
+
|
|
571
942
|
function spatialDocument(payload: string): string {
|
|
572
943
|
return `<!doctype html>
|
|
573
944
|
<html><head><meta charset="utf-8"><style>html,body{margin:0;width:100%;height:100%;overflow:hidden;background:transparent}canvas{display:block;width:100%;height:100%}</style>
|
|
@@ -579,20 +950,43 @@ const canvas=document.querySelector("canvas");
|
|
|
579
950
|
const renderer=new THREE.WebGLRenderer({canvas,alpha:true,antialias:false,premultipliedAlpha:true,preserveDrawingBuffer:true});
|
|
580
951
|
renderer.setPixelRatio(1);renderer.setSize(SlopcameraOverlay.width,SlopcameraOverlay.height,false);
|
|
581
952
|
renderer.setClearColor(0x000000,0);renderer.outputColorSpace=THREE.SRGBColorSpace;
|
|
582
|
-
renderer.toneMapping=THREE.NoToneMapping;renderer.autoClear=false;
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
953
|
+
renderer.toneMapping=THREE.NoToneMapping;renderer.autoClear=false;renderer.shadowMap.enabled=input.mode.kind==="beauty";renderer.shadowMap.type=THREE.PCFSoftShadowMap;
|
|
954
|
+
${spatialShadowRuntimeSource()}
|
|
955
|
+
const mode=input.mode;const postSteps=input.effects?input.effects.steps:[];
|
|
956
|
+
const isMotion=mode.kind==="motion";
|
|
957
|
+
const needsVelocity=isMotion||postSteps.some(step=>step.kind==="motion-blur");
|
|
958
|
+
const needsDepth=postSteps.some(step=>step.kind==="depth-of-field");
|
|
959
|
+
const needsBright=postSteps.some(step=>step.kind==="bloom"||step.kind==="flare");
|
|
960
|
+
const hasPost=postSteps.length>0;
|
|
961
|
+
let beautyTarget=null,velocityTarget=null,postA=null,postB=null,brightA=null,brightB=null,outputScene=null,outputCamera=null,outputGeometry=null,outputMaterial=null,postScene=null,postMesh=null,postOutputMaterial=null;
|
|
962
|
+
const resolution=new THREE.Vector2(SlopcameraOverlay.width,SlopcameraOverlay.height);
|
|
963
|
+
const postVertex="precision highp float;attribute vec3 position;attribute vec2 uv;varying vec2 sampleUv;void main(){sampleUv=uv;gl_Position=vec4(position.xy,0.0,1.0);}";
|
|
964
|
+
const toSrgbFn="vec3 toSrgb(vec3 v){return mix(12.92*v,1.055*pow(max(v,vec3(0.0)),vec3(1.0/2.4))-0.055,step(vec3(0.0031308),v));}";
|
|
965
|
+
const postHeader="precision highp float;uniform sampler2D source;uniform vec2 resolution;varying vec2 sampleUv;";
|
|
966
|
+
const makePostTarget=(w,h,depth)=>new THREE.WebGLRenderTarget(w,h,{type:THREE.HalfFloatType,format:THREE.RGBAFormat,colorSpace:THREE.LinearSRGBColorSpace,minFilter:THREE.NearestFilter,magFilter:THREE.NearestFilter,depthBuffer:depth,stencilBuffer:false});
|
|
967
|
+
if(mode.kind==="beauty"||isMotion){
|
|
968
|
+
if(!renderer.extensions.has("EXT_color_buffer_float"))throw new Error("Spatial floating-point passes require renderable half-float color.");
|
|
969
|
+
if(mode.kind==="beauty"){
|
|
970
|
+
beautyTarget=makePostTarget(SlopcameraOverlay.width,SlopcameraOverlay.height,true);
|
|
971
|
+
if(needsDepth){beautyTarget.depthTexture=new THREE.DepthTexture(SlopcameraOverlay.width,SlopcameraOverlay.height);beautyTarget.depthTexture.type=THREE.UnsignedIntType;}
|
|
972
|
+
}
|
|
973
|
+
if(needsVelocity)velocityTarget=makePostTarget(SlopcameraOverlay.width,SlopcameraOverlay.height,true);
|
|
974
|
+
if(hasPost){postA=makePostTarget(SlopcameraOverlay.width,SlopcameraOverlay.height,false);postB=makePostTarget(SlopcameraOverlay.width,SlopcameraOverlay.height,false);}
|
|
975
|
+
if(needsBright){const hw=Math.max(1,SlopcameraOverlay.width>>1),hh=Math.max(1,SlopcameraOverlay.height>>1);brightA=makePostTarget(hw,hh,false);brightB=makePostTarget(hw,hh,false);}
|
|
587
976
|
outputGeometry=new THREE.PlaneGeometry(2,2);
|
|
588
977
|
outputMaterial=new THREE.RawShaderMaterial({depthTest:false,depthWrite:false,blending:THREE.NoBlending,transparent:false,toneMapped:false,dithering:false,
|
|
589
|
-
uniforms:{linearPremultiplied:{value:beautyTarget.texture}},
|
|
590
|
-
vertexShader:
|
|
591
|
-
fragmentShader:"precision highp float;uniform sampler2D linearPremultiplied;varying vec2 sampleUv;
|
|
978
|
+
uniforms:{linearPremultiplied:{value:beautyTarget?beautyTarget.texture:null}},
|
|
979
|
+
vertexShader:postVertex,
|
|
980
|
+
fragmentShader:"precision highp float;uniform sampler2D linearPremultiplied;varying vec2 sampleUv;"+toSrgbFn+"void main(){vec4 c=texture2D(linearPremultiplied,sampleUv);if(c.a<=0.0){gl_FragColor=vec4(0.0);return;}gl_FragColor=vec4(toSrgb(c.rgb/c.a)*c.a,c.a);}",
|
|
981
|
+
});
|
|
982
|
+
postOutputMaterial=new THREE.RawShaderMaterial({depthTest:false,depthWrite:false,blending:THREE.NoBlending,transparent:false,toneMapped:false,dithering:false,
|
|
983
|
+
uniforms:{source:{value:null},resolution:{value:resolution}},vertexShader:postVertex,
|
|
984
|
+
fragmentShader:postHeader+toSrgbFn+"void main(){vec4 c=texture2D(source,sampleUv);gl_FragColor=vec4(toSrgb(max(c.rgb,vec3(0.0)))*c.a,c.a);}",
|
|
592
985
|
});
|
|
593
986
|
outputScene=new THREE.Scene();const outputMesh=new THREE.Mesh(outputGeometry,outputMaterial);outputMesh.frustumCulled=false;outputScene.add(outputMesh);outputCamera=new THREE.Camera();
|
|
987
|
+
postScene=new THREE.Scene();postMesh=new THREE.Mesh(outputGeometry);postMesh.frustumCulled=false;postScene.add(postMesh);
|
|
594
988
|
}
|
|
595
|
-
const textures=new Map();let disposed=false;let contextFailure=null;
|
|
989
|
+
const textures=new Map();const particleBuffers=new Map();let disposed=false;let contextFailure=null;
|
|
596
990
|
const loseContext=event=>{event.preventDefault();contextFailure=new Error("Spatial WebGL context was lost.");};
|
|
597
991
|
canvas.addEventListener("webglcontextlost",loseContext);
|
|
598
992
|
const initialization=(async()=>{
|
|
@@ -612,6 +1006,17 @@ const initialization=(async()=>{
|
|
|
612
1006
|
if(disposed)throw new Error("Spatial renderer was disposed during geometry preparation.");
|
|
613
1007
|
input.geometry[item.key]=primitives;
|
|
614
1008
|
}
|
|
1009
|
+
for(const item of input.particleBuffers){
|
|
1010
|
+
const response=await fetch(item.url);if(!response.ok)throw new Error("Prepared particle buffer is unavailable.");
|
|
1011
|
+
const bytes=await response.arrayBuffer();
|
|
1012
|
+
if(bytes.byteLength!==item.resource.bytes||bytes.byteLength!==item.instanceCount*item.strideBytes)throw new Error("Prepared particle buffer length changed.");
|
|
1013
|
+
const hash=new Uint8Array(await crypto.subtle.digest("SHA-256",bytes));
|
|
1014
|
+
const sha256=Array.from(hash,value=>value.toString(16).padStart(2,"0")).join("");
|
|
1015
|
+
if(sha256!==item.resource.sha256)throw new Error("Prepared particle buffer digest changed.");
|
|
1016
|
+
const view=new DataView(bytes);const positions=new Float32Array(item.instanceCount*3);const colors=new Float32Array(item.instanceCount*3);const sizes=new Float32Array(item.instanceCount);const opacity=new Float32Array(item.instanceCount);
|
|
1017
|
+
for(let index=0;index<item.instanceCount;index++){const offset=index*item.strideBytes;positions.set([view.getFloat32(offset,true),view.getFloat32(offset+4,true),view.getFloat32(offset+8,true)],index*3);sizes[index]=view.getFloat32(offset+32,true);opacity[index]=view.getFloat32(offset+36,true);colors.set([view.getFloat32(offset+40,true),view.getFloat32(offset+44,true),view.getFloat32(offset+48,true)],index*3);}
|
|
1018
|
+
particleBuffers.set(item.key,{colors,opacity,positions,sizes});
|
|
1019
|
+
}
|
|
615
1020
|
for(const item of input.textures){
|
|
616
1021
|
const texture=await new THREE.TextureLoader().loadAsync(item.url);
|
|
617
1022
|
if(disposed){texture.dispose();throw new Error("Spatial renderer was disposed during texture preparation.");}
|
|
@@ -640,15 +1045,17 @@ float high=floor(code/65536.0);float middle=floor((code-high*65536.0)/256.0);flo
|
|
|
640
1045
|
gl_FragColor=vec4(vec3(high,middle,low)/255.0,1.0);}\`;
|
|
641
1046
|
function makeGeometry(object,track){
|
|
642
1047
|
const g=object.geometry;let geometry;
|
|
643
|
-
|
|
1048
|
+
const displaced=object.material.kind==="pbr"&&object.material.heightMap!==undefined;
|
|
1049
|
+
if(g.kind==="box")geometry=new THREE.BoxGeometry(...g.size,...(displaced?[16,16,16]:[1,1,1]));
|
|
644
1050
|
else if(g.kind==="sphere")geometry=new THREE.SphereGeometry(g.radius,32,16);
|
|
645
|
-
else if(g.kind==="plane")geometry=new THREE.PlaneGeometry(g.width*object.surfaceScale[0],g.height*object.surfaceScale[1]);
|
|
646
|
-
else if(g.kind==="cylinder")geometry=new THREE.CylinderGeometry(g.radius,g.radius,g.height,32);
|
|
1051
|
+
else if(g.kind==="plane")geometry=new THREE.PlaneGeometry(g.width*object.surfaceScale[0],g.height*object.surfaceScale[1],displaced?32:1,displaced?32:1);
|
|
1052
|
+
else if(g.kind==="cylinder")geometry=new THREE.CylinderGeometry(g.radius,g.radius,g.height,32,displaced?16:1);
|
|
647
1053
|
else{const p=input.geometry[g.key][g.primitive];geometry=new THREE.BufferGeometry();
|
|
648
1054
|
geometry.setAttribute("position",new THREE.Float32BufferAttribute(p.positions,3));
|
|
649
1055
|
if(p.normals)geometry.setAttribute("normal",new THREE.Float32BufferAttribute(p.normals,3));
|
|
650
1056
|
if(p.uvs)geometry.setAttribute("uv",new THREE.Float32BufferAttribute(p.uvs,2));
|
|
651
1057
|
if(p.indices)geometry.setIndex(p.indices);if(!p.normals)geometry.computeVertexNormals();}
|
|
1058
|
+
if(geometry.attributes.uv&&!geometry.attributes.uv1)geometry.setAttribute("uv1",geometry.attributes.uv.clone());
|
|
652
1059
|
return track(geometry);
|
|
653
1060
|
}
|
|
654
1061
|
function makeMaterial(object,frame,track){
|
|
@@ -661,7 +1068,7 @@ function makeMaterial(object,frame,track){
|
|
|
661
1068
|
const wraps={33071:THREE.ClampToEdgeWrapping,33648:THREE.MirroredRepeatWrapping,10497:THREE.RepeatWrapping};
|
|
662
1069
|
const filters={9728:THREE.NearestFilter,9729:THREE.LinearFilter,9984:THREE.NearestMipmapNearestFilter,9985:THREE.LinearMipmapNearestFilter,9986:THREE.NearestMipmapLinearFilter,9987:THREE.LinearMipmapLinearFilter};
|
|
663
1070
|
map.wrapS=wraps[s.wrapS];map.wrapT=wraps[s.wrapT];map.magFilter=filters[s.magFilter??9729];map.minFilter=filters[s.minFilter??9987];map.generateMipmaps=(s.minFilter??9987)>=9984;
|
|
664
|
-
}map.needsUpdate=true;
|
|
1071
|
+
}if(options?.uvTransform){map.offset.fromArray(options.uvTransform.offset);map.rotation=options.uvTransform.rotation;map.repeat.fromArray(options.uvTransform.scale);map.wrapS=map.wrapT=THREE.RepeatWrapping;}map.needsUpdate=true;
|
|
665
1072
|
}
|
|
666
1073
|
const view=object.placement.kind==="view";
|
|
667
1074
|
if(input.mode.kind!=="beauty"){
|
|
@@ -674,11 +1081,36 @@ function makeMaterial(object,frame,track){
|
|
|
674
1081
|
selection:{value:new THREE.Vector3(Math.floor(id/65536)/255,(Math.floor(id/256)%256)/255,(id%256)/255)},
|
|
675
1082
|
nearClip:{value:frame.camera.near},farClip:{value:frame.camera.far},mode:{value:input.mode.kind==="object-id"?0:view?2:1}}}));
|
|
676
1083
|
}
|
|
677
|
-
if(map){map.repeat.fromArray(object.uvScale);map.offset.fromArray(object.uvOffset);
|
|
1084
|
+
if(map&&object.textureOptions?.uvTransform===undefined){map.repeat.fromArray(object.uvScale);map.offset.fromArray(object.uvOffset);
|
|
678
1085
|
if(object.flipViewUv){map.repeat.y*=-1;map.offset.y+=object.uvScale[1];}map.needsUpdate=true;}
|
|
679
1086
|
const transparent=object.alphaMode===undefined?object.alphaCutoff===0&&(m.opacity<1||object.textureAlpha==="straight"):object.alphaMode==="BLEND";
|
|
680
1087
|
const options={color:m.color,opacity:m.opacity,transparent,map,
|
|
681
1088
|
side:object.doubleSided?THREE.DoubleSide:THREE.FrontSide,alphaTest:object.alphaCutoff,depthTest:!view,depthWrite:!view&&!transparent,toneMapped:false};
|
|
1089
|
+
const sourceMap=slot=>{const binding=object.sourceTextures?.[slot];if(!binding)return null;const texture=track(textures.get(binding.name).clone());if(binding.uvTransform){texture.offset.fromArray(binding.uvTransform.offset);texture.rotation=binding.uvTransform.rotation;texture.repeat.fromArray(binding.uvTransform.scale);texture.wrapS=texture.wrapT=THREE.RepeatWrapping;}texture.colorSpace=(slot==="emissiveTexture"||slot==="sheenColorTexture")?THREE.SRGBColorSpace:THREE.NoColorSpace;texture.needsUpdate=true;return texture;};
|
|
1090
|
+
if(object.sourcePhysical){const p=object.sourcePhysical;const physical=track(new THREE.MeshPhysicalMaterial({...options,roughness:m.roughness,metalness:m.metalness}));if(object.linearColor)physical.color.setRGB(...object.linearColor,THREE.LinearSRGBColorSpace);
|
|
1091
|
+
physical.normalMap=sourceMap("normalTexture");physical.roughnessMap=physical.metalnessMap=sourceMap("metallicRoughnessTexture");physical.aoMap=sourceMap("occlusionTexture");physical.emissiveMap=sourceMap("emissiveTexture");if(p.emissiveLinear)physical.emissive.setRGB(...p.emissiveLinear,THREE.LinearSRGBColorSpace);physical.emissiveIntensity=p.emissiveStrength??1;
|
|
1092
|
+
if(p.clearcoat){physical.clearcoat=p.clearcoat.factor;physical.clearcoatRoughness=p.clearcoat.roughness;}physical.clearcoatMap=sourceMap("clearcoatTexture");physical.clearcoatRoughnessMap=sourceMap("clearcoatRoughnessTexture");physical.clearcoatNormalMap=sourceMap("clearcoatNormalTexture");
|
|
1093
|
+
physical.transmission=p.transmission??0;physical.transmissionMap=sourceMap("transmissionTexture");if(p.sheen){physical.sheen=1;physical.sheenColor.setRGB(...p.sheen.colorLinear,THREE.LinearSRGBColorSpace);physical.sheenRoughness=p.sheen.roughness;}physical.sheenColorMap=sourceMap("sheenColorTexture");physical.sheenRoughnessMap=sourceMap("sheenRoughnessTexture");if(p.anisotropy){physical.anisotropy=p.anisotropy.strength;physical.anisotropyRotation=p.anisotropy.rotation;}physical.anisotropyMap=sourceMap("anisotropyTexture");if(p.ior!==undefined)physical.ior=p.ior;physical.needsUpdate=true;return physical;}
|
|
1094
|
+
if(m.kind==="pbr"){
|
|
1095
|
+
const pbrMap=ref=>{if(!ref)return null;const name=object.pbrTextures?.[ref.assetId];if(!name)throw new Error("PBR map has no exact prepared texture binding.");
|
|
1096
|
+
const texture=track(textures.get(name).clone());texture.colorSpace=ref.colorSpace==="srgb"?THREE.SRGBColorSpace:THREE.NoColorSpace;
|
|
1097
|
+
if(ref.uvTransform){texture.offset.fromArray(ref.uvTransform.offset);texture.rotation=ref.uvTransform.rotation;texture.repeat.fromArray(ref.uvTransform.scale);texture.center.set(0,0);texture.wrapS=texture.wrapT=THREE.RepeatWrapping;}
|
|
1098
|
+
texture.needsUpdate=true;return texture;};
|
|
1099
|
+
const baseMap=pbrMap(m.baseColorMap);const physical=track(new THREE.MeshPhysicalMaterial({...options,map:baseMap,roughness:m.roughness,metalness:m.metalness}));
|
|
1100
|
+
if(object.linearColor)physical.color.setRGB(...object.linearColor,THREE.LinearSRGBColorSpace);
|
|
1101
|
+
physical.normalMap=pbrMap(m.normalMap);if(physical.normalMap)physical.normalScale.setScalar(m.normalScale??1);
|
|
1102
|
+
if(m.ormMap){const orm=pbrMap(m.ormMap);physical.roughnessMap=orm;physical.metalnessMap=orm;physical.aoMap=orm;}
|
|
1103
|
+
else{physical.roughnessMap=pbrMap(m.roughnessMap);physical.metalnessMap=pbrMap(m.metalnessMap);physical.aoMap=pbrMap(m.aoMap);}
|
|
1104
|
+
physical.aoMapIntensity=m.aoMapIntensity??1;
|
|
1105
|
+
if(m.emissive){physical.emissive=new THREE.Color(m.emissive.color);physical.emissiveIntensity=m.emissive.intensity;physical.emissiveMap=pbrMap(m.emissive.map);}
|
|
1106
|
+
if(m.heightMap){physical.displacementMap=pbrMap(m.heightMap);physical.displacementScale=m.heightScale;}
|
|
1107
|
+
if(m.clearcoat){physical.clearcoat=m.clearcoat.factor;physical.clearcoatRoughness=m.clearcoat.roughness;physical.clearcoatMap=pbrMap(m.clearcoat.map);physical.clearcoatRoughnessMap=pbrMap(m.clearcoat.roughnessMap);physical.clearcoatNormalMap=pbrMap(m.clearcoat.normalMap);if(physical.clearcoatNormalMap)physical.clearcoatNormalScale.setScalar(m.clearcoat.normalScale??1);}
|
|
1108
|
+
if(m.transmission){physical.transmission=m.transmission.factor;physical.transmissionMap=pbrMap(m.transmission.map);}
|
|
1109
|
+
if(m.sheen){physical.sheen=1;physical.sheenColor=new THREE.Color(m.sheen.color);physical.sheenRoughness=m.sheen.roughness;physical.sheenColorMap=pbrMap(m.sheen.colorMap);physical.sheenRoughnessMap=pbrMap(m.sheen.roughnessMap);}
|
|
1110
|
+
if(m.anisotropy){physical.anisotropy=m.anisotropy.strength;physical.anisotropyRotation=m.anisotropy.rotation;physical.anisotropyMap=pbrMap(m.anisotropy.map);}
|
|
1111
|
+
if(m.ior!==undefined)physical.ior=m.ior;physical.needsUpdate=true;
|
|
1112
|
+
return physical;
|
|
1113
|
+
}
|
|
682
1114
|
const material=track(m.kind==="unlit"?new THREE.MeshBasicMaterial(options):new THREE.MeshStandardMaterial({...options,roughness:m.roughness,metalness:m.metalness}));
|
|
683
1115
|
if(object.linearColor)material.color.setRGB(...object.linearColor,THREE.LinearSRGBColorSpace);
|
|
684
1116
|
return material;
|
|
@@ -686,23 +1118,145 @@ function makeMaterial(object,frame,track){
|
|
|
686
1118
|
function makeCamera(data){
|
|
687
1119
|
const camera=data.kind==="perspective"?new THREE.PerspectiveCamera():new THREE.OrthographicCamera();camera.matrixAutoUpdate=false;camera.matrix.fromArray(data.cameraToWorld);
|
|
688
1120
|
camera.projectionMatrix.fromArray(data.projection);camera.projectionMatrixInverse.copy(camera.projectionMatrix).invert();
|
|
1121
|
+
// Preserve physical metadata for renderer effects without recomputing the authoritative calibrated projection.
|
|
1122
|
+
if(data.lens){camera.filmGauge=data.lens.sensorWidthMm;camera.focus=data.lens.focusDistanceM??10;camera.userData.slopcameraLens=Object.freeze({...data.lens});}
|
|
689
1123
|
camera.updateMatrixWorld(true);return camera;
|
|
690
1124
|
}
|
|
1125
|
+
const postMaterial=(fragment,uniforms)=>new THREE.RawShaderMaterial({depthTest:false,depthWrite:false,blending:THREE.NoBlending,transparent:false,toneMapped:false,dithering:false,vertexShader:postVertex,fragmentShader:fragment,uniforms});
|
|
1126
|
+
const velocityVertex="precision highp float;uniform mat4 currentMvp;uniform mat4 previousMvp;uniform vec2 resolution;attribute vec3 position;varying vec2 vPx;void main(){vec4 c=currentMvp*vec4(position,1.0);vec4 p=previousMvp*vec4(position,1.0);vPx=(c.xy/max(c.w,0.000001)-p.xy/max(p.w,0.000001))*resolution*0.5;gl_Position=c;}";
|
|
1127
|
+
const velocityPointVertex="precision highp float;uniform mat4 currentMvp;uniform mat4 previousMvp;uniform mat4 currentMv;uniform mat4 projection;uniform vec2 resolution;uniform float viewportHeight;uniform bool perspective;attribute vec3 position;attribute float particleSize;varying vec2 vPx;void main(){vec4 c=currentMvp*vec4(position,1.0);vec4 p=previousMvp*vec4(position,1.0);vPx=(c.xy/max(c.w,0.000001)-p.xy/max(p.w,0.000001))*resolution*0.5;gl_Position=c;vec4 mv=currentMv*vec4(position,1.0);float scale=0.5*viewportHeight*projection[1][1];gl_PointSize=max(1.0,particleSize*scale/(perspective?max(0.000001,-mv.z):1.0));}";
|
|
1128
|
+
const velocityFragment="precision highp float;varying vec2 vPx;void main(){gl_FragColor=vec4(vPx,0.0,1.0);}";
|
|
1129
|
+
const velocityPointFragment="precision highp float;varying vec2 vPx;void main(){vec2 p=gl_PointCoord*2.0-1.0;float coverage=clamp((1.0-dot(p,p))*8.0,0.0,1.0);if(coverage<=0.0)discard;gl_FragColor=vec4(vPx,0.0,coverage);}";
|
|
1130
|
+
const viewProjection=cam=>new THREE.Matrix4().fromArray(cam.projection).multiply(new THREE.Matrix4().fromArray(cam.cameraToWorld).invert());
|
|
1131
|
+
const buildVelocityScene=(frame,track)=>{
|
|
1132
|
+
const scene=new THREE.Scene();
|
|
1133
|
+
const curVP=viewProjection(frame.camera),prevVP=viewProjection(frame.previousCamera);
|
|
1134
|
+
const curView=new THREE.Matrix4().fromArray(frame.camera.cameraToWorld).invert();
|
|
1135
|
+
const projection=new THREE.Matrix4().fromArray(frame.camera.projection);
|
|
1136
|
+
const add=(geometry,matrix,previous,points)=>{
|
|
1137
|
+
const model=new THREE.Matrix4().fromArray(matrix),previousModel=new THREE.Matrix4().fromArray(previous??matrix);
|
|
1138
|
+
const uniforms={resolution:{value:resolution},currentMvp:{value:curVP.clone().multiply(model)},previousMvp:{value:prevVP.clone().multiply(previousModel)}};
|
|
1139
|
+
const material=points?track(new THREE.RawShaderMaterial({vertexShader:velocityPointVertex,fragmentShader:velocityPointFragment,depthTest:true,depthWrite:true,transparent:false,blending:THREE.NoBlending,toneMapped:false,dithering:false,
|
|
1140
|
+
uniforms:{...uniforms,currentMv:{value:curView.clone().multiply(model)},projection:{value:projection},viewportHeight:{value:SlopcameraOverlay.height},perspective:{value:frame.camera.kind==="perspective"}}}))
|
|
1141
|
+
:track(new THREE.RawShaderMaterial({vertexShader:velocityVertex,fragmentShader:velocityFragment,depthTest:true,depthWrite:true,transparent:false,blending:THREE.NoBlending,toneMapped:false,dithering:false,side:THREE.DoubleSide,uniforms}));
|
|
1142
|
+
const drawable=points?new THREE.Points(geometry,material):new THREE.Mesh(geometry,material);
|
|
1143
|
+
drawable.frustumCulled=false;scene.add(drawable);
|
|
1144
|
+
};
|
|
1145
|
+
for(const object of frame.objects){
|
|
1146
|
+
if(object.kind!=="mesh"||object.placement.kind!=="world")continue;
|
|
1147
|
+
if(isMotion&&object.entityId!==mode.entityId)continue;
|
|
1148
|
+
// Prepared primitives apply their local matrix after the world transform in
|
|
1149
|
+
// the beauty path, while previousMatrix already carries prevWorld*prevPrimitive.
|
|
1150
|
+
const primitive=object.geometry.kind==="prepared"?new THREE.Matrix4().fromArray(input.geometry[object.geometry.key][object.geometry.primitive].matrix):null;
|
|
1151
|
+
const current=primitive===null?object.matrix:new THREE.Matrix4().fromArray(object.matrix).multiply(primitive).toArray();
|
|
1152
|
+
add(makeGeometry(object,track),current,object.previousMatrix??current,false);
|
|
1153
|
+
}
|
|
1154
|
+
for(const item of frame.particles){
|
|
1155
|
+
if(isMotion&&item.entityId!==mode.entityId)continue;
|
|
1156
|
+
const data=particleBuffers.get(item.key);if(!data)continue;
|
|
1157
|
+
const geometry=track(new THREE.BufferGeometry());
|
|
1158
|
+
geometry.setAttribute("position",new THREE.BufferAttribute(data.positions,3));
|
|
1159
|
+
geometry.setAttribute("particleSize",new THREE.BufferAttribute(data.sizes,1));
|
|
1160
|
+
add(geometry,item.matrix,item.previousMatrix,true);
|
|
1161
|
+
}
|
|
1162
|
+
return scene;
|
|
1163
|
+
};
|
|
1164
|
+
const unpremultiplyFragment=postHeader+"void main(){vec4 c=texture2D(source,sampleUv);if(c.a<=0.0){gl_FragColor=vec4(0.0);return;}gl_FragColor=vec4(c.rgb/c.a,c.a);}";
|
|
1165
|
+
const buildStepPasses=(step,frame,index,track,read)=>{
|
|
1166
|
+
const base={source:{value:read},resolution:{value:resolution}};
|
|
1167
|
+
const u=extra=>Object.assign(base,extra);
|
|
1168
|
+
switch(step.kind){
|
|
1169
|
+
case "tone-map":return[{target:"write",material:track(postMaterial(postHeader+"uniform float exposure;uniform float whitePoint;void main(){vec4 c=texture2D(source,sampleUv);vec3 x=max(c.rgb,vec3(0.0))*exp2(exposure);float w2=whitePoint*whitePoint;gl_FragColor=vec4(x*(1.0+x/w2)/(1.0+x),c.a);}",u({exposure:{value:step.exposure},whitePoint:{value:step.whitePoint}})))}];
|
|
1170
|
+
case "vignette":return[{target:"write",material:track(postMaterial(postHeader+"uniform float intensity;uniform float radius;void main(){vec4 c=texture2D(source,sampleUv);float d=length((sampleUv-0.5)*1.41421356);gl_FragColor=vec4(c.rgb*(1.0-intensity*smoothstep(radius,1.0,d)),c.a);}",u({intensity:{value:step.intensity},radius:{value:step.radius}})))}];
|
|
1171
|
+
case "chromatic-aberration":return[{target:"write",material:track(postMaterial(postHeader+"uniform float offsetPixels;uniform float radialFalloff;void main(){vec2 dir=sampleUv-0.5;float len=max(length(dir),0.000001);vec2 off=dir/len*offsetPixels*pow(len*1.41421356,radialFalloff)/resolution;vec4 c=texture2D(source,sampleUv);gl_FragColor=vec4(texture2D(source,sampleUv+off).r,c.g,texture2D(source,sampleUv-off).b,c.a);}",u({offsetPixels:{value:step.offsetPixels},radialFalloff:{value:step.radialFalloff}})))}];
|
|
1172
|
+
case "grain":return[{target:"write",material:track(postMaterial(postHeader+"uniform float intensity;uniform float seed;float grainHash(vec2 p){vec3 q=fract(vec3(p.xyx)*0.1031);q+=dot(q,q.yzx+33.33);return fract((q.x+q.y)*q.z);}void main(){vec4 c=texture2D(source,sampleUv);float n=grainHash(floor(sampleUv*resolution)+vec2(seed,seed*1.618034));gl_FragColor=vec4(max(c.rgb+vec3((n-0.5)*intensity),vec3(0.0)),c.a);}",u({intensity:{value:step.intensity},seed:{value:step.seed+frame.timeUs%7919}})))}];
|
|
1173
|
+
case "depth-of-field":return[{target:"write",material:track(postMaterial(postHeader+"uniform sampler2D depthTex;uniform float nearClip;uniform float farClip;uniform float focusDistance;uniform float aperture;uniform float focalLength;uniform float sensorWidth;uniform bool isPerspective;const vec2 dofTaps[16]=vec2[16](${DOF_TAPS});float viewZ(float d){return isPerspective?nearClip*farClip/max(farClip-d*(farClip-nearClip),0.000001):nearClip+d*(farClip-nearClip);}void main(){vec4 c=texture2D(source,sampleUv);float z=viewZ(texture2D(depthTex,sampleUv).x);float coc=clamp(aperture*focalLength*abs(focusDistance-z)/(focusDistance*max(z,0.0001))*resolution.x/sensorWidth,0.0,32.0);vec3 acc=c.rgb;for(int i=0;i<16;i++){acc+=texture2D(source,sampleUv+dofTaps[i]*coc/resolution).rgb;}gl_FragColor=vec4(acc/17.0,c.a);}",u({depthTex:{value:beautyTarget.depthTexture},nearClip:{value:frame.camera.near},farClip:{value:frame.camera.far},focusDistance:{value:step.focusDistance},aperture:{value:step.aperture},focalLength:{value:step.focalLength},sensorWidth:{value:frame.camera.lens?frame.camera.lens.sensorWidthMm:36},isPerspective:{value:frame.camera.kind==="perspective"}})))}];
|
|
1174
|
+
case "motion-blur":return[{target:"write",material:track(postMaterial(postHeader+"uniform sampler2D velocityTex;uniform int samples;uniform float shutter;void main(){vec4 c=texture2D(source,sampleUv);vec2 v=texture2D(velocityTex,sampleUv).xy*shutter/resolution;vec3 acc=c.rgb;float n=1.0;for(int i=1;i<64;i++){if(i>=samples)break;float t=float(i)/float(samples-1)-0.5;acc+=texture2D(source,sampleUv+v*t).rgb;n+=1.0;}gl_FragColor=vec4(acc/n,c.a);}",u({velocityTex:{value:velocityTarget.texture},samples:{value:step.samples},shutter:{value:step.shutterAngle/360}})))}];
|
|
1175
|
+
case "bloom":{
|
|
1176
|
+
const bright=postHeader+"uniform float threshold;void main(){vec4 c=texture2D(source,sampleUv);gl_FragColor=vec4(max(c.rgb-vec3(threshold),vec3(0.0)),1.0);}";
|
|
1177
|
+
const blur=postHeader+"uniform vec2 texel;uniform float spread;uniform vec2 direction;const float bloomW[5]=float[5](${BLOOM_WEIGHTS});void main(){vec3 acc=texture2D(source,sampleUv).rgb*bloomW[0];for(int i=1;i<5;i++){vec2 o=direction*float(i)*spread*0.125*texel;acc+=(texture2D(source,sampleUv+o).rgb+texture2D(source,sampleUv-o).rgb)*bloomW[i];}gl_FragColor=vec4(acc,1.0);}";
|
|
1178
|
+
const composite=postHeader+"uniform sampler2D bright;uniform float intensity;void main(){vec4 c=texture2D(source,sampleUv);gl_FragColor=vec4(c.rgb+texture2D(bright,sampleUv).rgb*intensity,c.a);}";
|
|
1179
|
+
const halfTexel=new THREE.Vector2(1/brightA.width,1/brightA.height);
|
|
1180
|
+
return[{target:brightA,material:track(postMaterial(bright,u({threshold:{value:step.threshold}})))},
|
|
1181
|
+
{target:brightB,material:track(postMaterial(blur,{source:{value:brightA.texture},resolution:{value:resolution},texel:{value:halfTexel},spread:{value:step.radius},direction:{value:new THREE.Vector2(1,0)}}))},
|
|
1182
|
+
{target:brightA,material:track(postMaterial(blur,{source:{value:brightB.texture},resolution:{value:resolution},texel:{value:halfTexel},spread:{value:step.radius},direction:{value:new THREE.Vector2(0,1)}}))},
|
|
1183
|
+
{target:"write",material:track(postMaterial(composite,u({bright:{value:brightA.texture},intensity:{value:step.intensity}})))}];
|
|
1184
|
+
}
|
|
1185
|
+
case "flare":{
|
|
1186
|
+
const bright=postHeader+"uniform float threshold;void main(){vec4 c=texture2D(source,sampleUv);gl_FragColor=vec4(max(c.rgb-vec3(threshold),vec3(0.0)),1.0);}";
|
|
1187
|
+
const ghosts=postHeader+"uniform sampler2D bright;uniform int ghosts;uniform float haloWidth;uniform float intensity;void main(){vec4 c=texture2D(source,sampleUv);vec3 g=vec3(0.0);float n=0.0;for(int i=0;i<16;i++){if(i>=ghosts)break;float t=float(i)/max(float(ghosts-1),1.0);vec2 guv=vec2(0.5)-(sampleUv-0.5)*mix(0.25,1.5,t);g+=texture2D(bright,guv).rgb*(1.0-t)*(1.0-t);n+=1.0;}vec2 dir=sampleUv-0.5;float len=max(length(dir),0.000001);vec3 h=texture2D(bright,sampleUv-dir/len*haloWidth*0.5).rgb;gl_FragColor=vec4(c.rgb+(g/max(n,1.0)+h)*intensity,c.a);}";
|
|
1188
|
+
return[{target:brightA,material:track(postMaterial(bright,u({threshold:{value:step.threshold}})))},
|
|
1189
|
+
{target:"write",material:track(postMaterial(ghosts,u({bright:{value:brightA.texture},ghosts:{value:step.ghosts},haloWidth:{value:step.haloWidth},intensity:{value:step.intensity}})))}];
|
|
1190
|
+
}
|
|
1191
|
+
case "lut-grade":{
|
|
1192
|
+
const lutTexture=track(textures.get(step.lut.resourceName).clone());
|
|
1193
|
+
lutTexture.flipY=false;lutTexture.colorSpace=THREE.NoColorSpace;lutTexture.needsUpdate=true;
|
|
1194
|
+
const fragment=postHeader+"uniform sampler2D lutTex;uniform float lutSize;uniform float intensity;vec3 lutLookup(vec3 c){float s=lutSize;vec3 cl=clamp(c,0.0,1.0);float b=cl.b*(s-1.0);float b0=floor(b);float b1=min(b0+1.0,s-1.0);vec2 rg=cl.rg*(s-1.0)+0.5;vec3 c0=texture2D(lutTex,vec2((b0*s+rg.x)/(s*s),rg.y/s)).rgb;vec3 c1=texture2D(lutTex,vec2((b1*s+rg.x)/(s*s),rg.y/s)).rgb;return mix(c0,c1,b-b0);}void main(){vec4 c=texture2D(source,sampleUv);gl_FragColor=vec4(mix(c.rgb,lutLookup(c.rgb),intensity),c.a);}";
|
|
1195
|
+
return[{target:"write",material:track(postMaterial(fragment,u({lutTex:{value:lutTexture},lutSize:{value:step.lut.size},intensity:{value:step.intensity}})))}];
|
|
1196
|
+
}
|
|
1197
|
+
default:throw new Error("Unsupported post-process step "+String(step&&step.kind)+".");
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
const runPostChain=(frame,index,track)=>{
|
|
1201
|
+
let read=beautyTarget.texture;let written=0;
|
|
1202
|
+
const targets=[postA,postB];
|
|
1203
|
+
const run=(material,target)=>{postMesh.material=material;renderer.setRenderTarget(target);renderer.clear(true,true,true);renderer.render(postScene,outputCamera);};
|
|
1204
|
+
run(track(postMaterial(unpremultiplyFragment,{source:{value:read},resolution:{value:resolution}})),postA);
|
|
1205
|
+
read=postA.texture;
|
|
1206
|
+
for(const step of postSteps)for(const item of buildStepPasses(step,frame,index,track,read)){
|
|
1207
|
+
const target=item.target==="write"?targets[1-written]:item.target;
|
|
1208
|
+
run(item.material,target);
|
|
1209
|
+
if(item.target==="write"){read=targets[1-written].texture;written=1-written;}
|
|
1210
|
+
}
|
|
1211
|
+
return read;
|
|
1212
|
+
};
|
|
691
1213
|
SlopcameraOverlay.onFrame(({frame:index})=>{
|
|
692
1214
|
if(disposed)throw new Error("Spatial renderer is disposed.");if(contextFailure)throw contextFailure;
|
|
693
1215
|
const frame=input.frames[index];if(!frame)throw new Error("Spatial frame index is outside the immutable batch.");
|
|
694
1216
|
const disposable=[];const track=value=>(disposable.push(value),value);
|
|
695
1217
|
try{
|
|
1218
|
+
if(isMotion){
|
|
1219
|
+
renderer.setRenderTarget(velocityTarget);renderer.clear(true,true,true);
|
|
1220
|
+
renderer.render(buildVelocityScene(frame,track),outputCamera);
|
|
1221
|
+
postMesh.material=track(postMaterial(postHeader+"uniform float motionScale;uniform float threshold;void main(){vec4 v=texture2D(source,sampleUv);if(v.a<threshold){gl_FragColor=vec4(0.0);return;}vec2 e=clamp(v.xy/motionScale*0.5+0.5,0.0,1.0)*65535.0;vec2 hi=floor(e/256.0);vec2 lo=e-hi*256.0;gl_FragColor=vec4(hi.x/255.0,lo.x/255.0,hi.y/255.0,lo.y/255.0,1.0);}",
|
|
1222
|
+
{source:{value:velocityTarget.texture},resolution:{value:resolution},motionScale:{value:mode.motionScale},threshold:{value:mode.coverage.kind==="opaque"?1:mode.coverage.threshold}}));
|
|
1223
|
+
renderer.setRenderTarget(null);renderer.clear(true,true,true);renderer.render(postScene,outputCamera);
|
|
1224
|
+
if(contextFailure)throw contextFailure;
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
696
1227
|
const world=new THREE.Scene();const view=new THREE.Scene();const camera=makeCamera(frame.camera);
|
|
697
1228
|
const viewCamera=new THREE.OrthographicCamera(0,SlopcameraOverlay.width,0,SlopcameraOverlay.height,0.01,2000002);viewCamera.position.z=1000001;viewCamera.updateMatrixWorld(true);
|
|
1229
|
+
if(frame.fog&&input.mode.kind==="beauty"){
|
|
1230
|
+
if(frame.fog.kind==="linear")world.fog=new THREE.Fog(frame.fog.color,frame.fog.near,frame.fog.far);
|
|
1231
|
+
else world.fog=new THREE.FogExp2(frame.fog.color,frame.fog.density);
|
|
1232
|
+
}
|
|
1233
|
+
const environment=frame.objects.find(object=>object.kind==="environment");
|
|
1234
|
+
if(environment!==undefined&&input.mode.kind==="beauty"){
|
|
1235
|
+
const equirect=track(textures.get(environment.textureName).clone());
|
|
1236
|
+
equirect.mapping=THREE.EquirectangularReflectionMapping;equirect.needsUpdate=true;
|
|
1237
|
+
const skyRotation=new THREE.Quaternion(...environment.rotation);
|
|
1238
|
+
if(environment.role!=="environment"){world.background=equirect;world.backgroundRotation.setFromQuaternion(skyRotation);world.backgroundIntensity=environment.intensity;}
|
|
1239
|
+
if(environment.role!=="background"){world.environment=equirect;world.environmentRotation.setFromQuaternion(skyRotation);world.environmentIntensity=environment.intensity;}
|
|
1240
|
+
}
|
|
1241
|
+
for(const item of frame.particles){
|
|
1242
|
+
const data=particleBuffers.get(item.key);if(!data)throw new Error("Prepared particle frame buffer is absent.");
|
|
1243
|
+
const geometry=track(new THREE.BufferGeometry());geometry.setAttribute("position",new THREE.BufferAttribute(data.positions,3));geometry.setAttribute("particleColor",new THREE.BufferAttribute(data.colors,3));geometry.setAttribute("particleSize",new THREE.BufferAttribute(data.sizes,1));geometry.setAttribute("particleOpacity",new THREE.BufferAttribute(data.opacity,1));
|
|
1244
|
+
const material=track(new THREE.RawShaderMaterial({transparent:true,depthTest:true,depthWrite:false,blending:THREE.NormalBlending,toneMapped:false,dithering:false,
|
|
1245
|
+
uniforms:{viewportHeight:{value:SlopcameraOverlay.height},perspective:{value:frame.camera.kind==="perspective"}},
|
|
1246
|
+
vertexShader:"precision highp float;uniform mat4 projectionMatrix;uniform mat4 modelViewMatrix;uniform float viewportHeight;uniform bool perspective;attribute vec3 position;attribute vec3 particleColor;attribute float particleSize;attribute float particleOpacity;varying vec3 color;varying float opacity;void main(){vec4 p=modelViewMatrix*vec4(position,1.0);gl_Position=projectionMatrix*p;float scale=0.5*viewportHeight*projectionMatrix[1][1];gl_PointSize=max(1.0,particleSize*scale/(perspective?max(0.000001,-p.z):1.0));color=particleColor;opacity=particleOpacity;}",
|
|
1247
|
+
fragmentShader:"precision highp float;varying vec3 color;varying float opacity;void main(){vec2 p=gl_PointCoord*2.0-1.0;float coverage=clamp((1.0-dot(p,p))*8.0,0.0,1.0);float alpha=opacity*coverage;if(alpha<=0.0)discard;gl_FragColor=vec4(color*alpha,alpha);}"}));
|
|
1248
|
+
const points=new THREE.Points(geometry,material);points.matrixAutoUpdate=false;points.matrix.fromArray(item.matrix);world.add(points);
|
|
1249
|
+
}
|
|
698
1250
|
for(const object of frame.objects){
|
|
699
1251
|
if(object.kind==="light"){
|
|
700
|
-
const light=object.light==="ambient"?new THREE.AmbientLight(object.color,object.intensity):object.light==="point"?new THREE.PointLight(object.color,object.intensity):new THREE.DirectionalLight(object.color,object.intensity);
|
|
701
|
-
light.matrixAutoUpdate=false;light.matrix.fromArray(object.matrix);world.add(light);
|
|
702
|
-
if(object.light==="directional"){const target=new THREE.Object3D();target.position.set(0,0,-1).applyMatrix4(light.matrix);world.add(target);light.target=target;}
|
|
1252
|
+
const light=object.light==="ambient"?new THREE.AmbientLight(object.color,object.intensity):object.light==="point"?new THREE.PointLight(object.color,object.intensity):object.light==="spot"?new THREE.SpotLight(object.color,object.intensity,object.spot.distance??0,object.spot.angle,object.spot.penumbra,object.spot.decay??2):new THREE.DirectionalLight(object.color,object.intensity);
|
|
1253
|
+
light.matrixAutoUpdate=false;light.matrix.fromArray(object.matrix);if("castShadow" in light)light.castShadow=object.shadow===true;world.add(light);
|
|
1254
|
+
if(object.light==="directional"||object.light==="spot"){const target=new THREE.Object3D();target.position.set(0,0,-1).applyMatrix4(light.matrix);world.add(target);light.target=target;}
|
|
703
1255
|
continue;
|
|
704
1256
|
}
|
|
1257
|
+
if(object.kind==="environment")continue;
|
|
705
1258
|
const mesh=new THREE.Mesh(makeGeometry(object,track),makeMaterial(object,frame,track));mesh.name=object.entityId;
|
|
1259
|
+
mesh.castShadow=object.castShadow===true;mesh.receiveShadow=object.receiveShadow===true;
|
|
706
1260
|
mesh.matrixAutoUpdate=false;mesh.matrix.fromArray(object.matrix);
|
|
707
1261
|
if(object.geometry.kind==="prepared")mesh.matrix.multiply(new THREE.Matrix4().fromArray(input.geometry[object.geometry.key][object.geometry.primitive].matrix));
|
|
708
1262
|
if(object.placement.kind==="view"){
|
|
@@ -710,11 +1264,19 @@ SlopcameraOverlay.onFrame(({frame:index})=>{
|
|
|
710
1264
|
mesh.renderOrder=object.placement.order;mesh.frustumCulled=false;view.add(mesh);
|
|
711
1265
|
}else world.add(mesh);
|
|
712
1266
|
}
|
|
1267
|
+
if(input.mode.kind==="beauty")configureSpatialShadows(world,renderer.capabilities.maxTextureSize,track);
|
|
713
1268
|
renderer.setRenderTarget(beautyTarget);renderer.clear(true,true,true);renderer.render(world,camera);renderer.clearDepth();renderer.render(view,viewCamera);
|
|
714
|
-
if(beautyTarget){
|
|
1269
|
+
if(beautyTarget){
|
|
1270
|
+
if(velocityTarget){renderer.setRenderTarget(velocityTarget);renderer.clear(true,true,true);renderer.render(buildVelocityScene(frame,track),outputCamera);}
|
|
1271
|
+
renderer.setRenderTarget(null);renderer.clear(true,true,true);
|
|
1272
|
+
if(hasPost){
|
|
1273
|
+
postOutputMaterial.uniforms.source.value=runPostChain(frame,index,track);
|
|
1274
|
+
postMesh.material=postOutputMaterial;renderer.render(postScene,outputCamera);
|
|
1275
|
+
}else renderer.render(outputScene,outputCamera);
|
|
1276
|
+
}
|
|
715
1277
|
if(contextFailure)throw contextFailure;
|
|
716
1278
|
}finally{for(const resource of disposable)resource.dispose();}
|
|
717
1279
|
});
|
|
718
|
-
addEventListener("pagehide",()=>{disposed=true;canvas.removeEventListener("webglcontextlost",loseContext);for(const texture of textures.values())texture.dispose();textures.clear();beautyTarget?.dispose();outputGeometry?.dispose();outputMaterial?.dispose();renderer.dispose();renderer.forceContextLoss();});
|
|
1280
|
+
addEventListener("pagehide",()=>{disposed=true;canvas.removeEventListener("webglcontextlost",loseContext);for(const texture of textures.values())texture.dispose();textures.clear();beautyTarget?.dispose();beautyTarget?.depthTexture?.dispose();velocityTarget?.dispose();postA?.dispose();postB?.dispose();brightA?.dispose();brightB?.dispose();outputGeometry?.dispose();outputMaterial?.dispose();postOutputMaterial?.dispose();renderer.dispose();renderer.forceContextLoss();});
|
|
719
1281
|
</script></body></html>`;
|
|
720
1282
|
}
|