@call-me-sensei/toonlab 0.4.21 → 0.4.23
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/NPM-LIBRARY.md +87 -27
- package/README.md +11 -10
- package/agents/PROMPTS.md +17 -3
- package/agents/README.md +13 -3
- package/agents/claude/CLAUDE.md +3 -0
- package/agents/codex/AGENTS.md +112 -159
- package/agents/cursor/toonlab.mdc +7 -3
- package/agents/examples/game-foundation.mjs +104 -0
- package/agents/references/game-lifecycle.md +57 -0
- package/agents/references/runtime-entry-points.md +26 -0
- package/agents/references/style-bundles.md +4 -1
- package/agents/skills/claude/asset-sourcing/SKILL.md +7 -4
- package/agents/skills/claude/environment/SKILL.md +4 -0
- package/agents/skills/claude/game-dev/SKILL.md +63 -87
- package/agents/skills/claude/lighting/SKILL.md +36 -0
- package/agents/skills/claude/post-processing/SKILL.md +8 -3
- package/agents/skills/claude/rock-ground-shaders/SKILL.md +10 -15
- package/agents/skills/claude/rockgen/SKILL.md +52 -21
- package/agents/skills/claude/scene-style-application/SKILL.md +4 -0
- package/agents/skills/claude/style-presets/SKILL.md +11 -8
- package/agents/skills/claude/toon-shading/SKILL.md +4 -0
- package/agents/skills/claude/vegetation-sky/SKILL.md +13 -7
- package/agents/skills/claude/visual-verification/SKILL.md +45 -119
- package/agents/skills/claude/water/SKILL.md +6 -2
- package/agents/skills/codex/asset-sourcing/SKILL.md +7 -4
- package/agents/skills/codex/environment/SKILL.md +4 -0
- package/agents/skills/codex/game-dev/SKILL.md +63 -87
- package/agents/skills/codex/lighting/SKILL.md +36 -0
- package/agents/skills/codex/post-processing/SKILL.md +8 -3
- package/agents/skills/codex/rock-ground-shaders/SKILL.md +10 -15
- package/agents/skills/codex/rockgen/SKILL.md +52 -21
- package/agents/skills/codex/scene-style-application/SKILL.md +4 -0
- package/agents/skills/codex/style-presets/SKILL.md +11 -8
- package/agents/skills/codex/toon-shading/SKILL.md +4 -0
- package/agents/skills/codex/vegetation-sky/SKILL.md +13 -7
- package/agents/skills/codex/visual-verification/SKILL.md +45 -119
- package/agents/skills/codex/water/SKILL.md +6 -2
- package/database/apply-one-catalog-seed.mjs +63 -0
- package/database/providers.mjs +1 -0
- package/database/seeds/catalog/0005_2026-08-c8-first12.sql +1101 -0
- package/database/seeds/catalog/0006_2026-09-c8-first100.sql +14245 -0
- package/database/seeds/catalog/0007_2026-09-c8-first100-primary-model.sql +14845 -0
- package/mcp/lab-management.mjs +11 -8
- package/mcp/public-catalog.mjs +0 -16
- package/mcp/tree-lab-contract.mjs +2381 -0
- package/mcp/vite-plugin.mjs +5 -1
- package/package.json +29 -18
- package/src/catalog/officialCatalog.js +1 -0
- package/src/catalog/officialCatalogAssetRuntime.js +98 -1
- package/src/catalog/officialCatalogLod.js +160 -16
- package/src/catalog/officialCatalogPlacement.js +52 -12
- package/src/catalog/officialCatalogProvider.js +79 -8
- package/src/catalog/officialCatalogRockPackage.js +172 -0
- package/src/cloud/cloudReprojection.js +40 -7
- package/src/cloud/cloudVolume.js +44 -7
- package/src/cloud/noise/baseShapeVolume.js +8 -6
- package/src/cloud/noise/cirrusMap.js +2 -1
- package/src/cloud/noise/curlNoise.js +2 -1
- package/src/cloud/noise/erosionVolume.js +2 -1
- package/src/cloud/noise/textureCache.js +77 -0
- package/src/cloud/noise/weatherMap.js +2 -1
- package/src/core/sha256.js +83 -0
- package/src/ground-shader/groundShaderMaterial.js +12 -3
- package/src/rock-shader/rockMaterial.js +205 -28
- package/src/rock-shader/rockShaderRuntime.js +17 -1
- package/src/rock-shader/rockShaderSettings.js +16 -0
- package/src/rockgen/index.js +9 -0
- package/src/rockgen/lod/index.js +1 -0
- package/src/rockgen/lod/rockDenseFieldPolicy.js +143 -0
- package/src/rockgen/rockDocument.js +899 -35
- package/src/rockgen/surface/c7GeologySurface.js +39 -9
- package/src/rockgen/surface/naturalRockSurface.js +1948 -0
- package/src/shaders-tsl/chunks/projected-water-caustics.js +14 -2
- package/src/shaders-tsl/chunks/water-foam.js +6 -6
- package/src/shaders-tsl/chunks/water-lighting.js +28 -5
- package/src/shaders-tsl/chunks/water-shore-state.js +5 -2
- package/src/shaders-tsl/chunks/water-waves.js +10 -5
- package/src/shaders-tsl/flower.js +7 -4
- package/src/shaders-tsl/grass.js +4 -3
- package/src/shaders-tsl/water-shore-state-simulation.js +3 -2
- package/src/shaders-tsl/water.js +209 -41
- package/src/sky/skySystem.js +30 -7
- package/src/toon/settings/perspectiveRemovalSettings.js +1 -1
- package/src/toon/toonSettings.js +74 -0
- package/src/vegetation/stylizedFlower.js +6 -0
- package/src/version.js +1 -1
- package/src/water/waterDetailSpectrum.js +91 -0
- package/src/water/waterDynamics.js +422 -0
- package/src/water/waterFoamParticles.js +196 -0
- package/src/water/waterFoamTexture.js +57 -0
- package/src/water/waterHydrodynamics.js +209 -0
- package/src/water/waterMaterial.js +1 -0
- package/src/water/waterRenderExtension.js +107 -0
- package/src/water/waterScenePasses.js +14 -6
- package/src/water/waterSettings.js +53 -26
- package/src/water/waterShoreMaterial.js +38 -14
- package/src/water/waterSpectralOcean.js +137 -0
- package/src/water/waterStageSettings.js +19 -1
- package/src/water/waterSurface.js +62 -18
- package/src/water/waterUnderwaterAtmosphere.js +30 -3
- package/types/catalog/officialCatalog.d.ts +1 -0
- package/types/catalog/officialCatalogPlacement.d.ts +4 -0
- package/types/catalog/officialCatalogRockPackage.d.ts +68 -0
- package/types/cloud/noise/baseShapeVolume.d.ts +1 -1
- package/types/cloud/noise/textureCache.d.ts +12 -0
- package/types/core/sha256.d.ts +2 -0
- package/types/index.d.ts +30 -0
- package/types/rock-shader/rockMaterial.d.ts +3 -0
- package/types/rock-shader/rockShaderSettings.d.ts +2 -0
- package/types/rockgen/index.d.ts +1 -0
- package/types/rockgen/lod/index.d.ts +1 -0
- package/types/rockgen/lod/rockDenseFieldPolicy.d.ts +181 -0
- package/types/rockgen/rockDocument.d.ts +654 -71
- package/types/rockgen/surface/naturalRockSurface.d.ts +100 -0
- package/types/shaders-tsl/chunks/projected-water-caustics.d.ts +7 -1
- package/types/shaders-tsl/chunks/water-lighting.d.ts +4 -3
- package/types/shaders-tsl/water.d.ts +2 -1
- package/types/version.d.ts +1 -1
- package/types/water/waterDetailSpectrum.d.ts +13 -0
- package/types/water/waterDynamics.d.ts +89 -0
- package/types/water/waterFoamParticles.d.ts +32 -0
- package/types/water/waterFoamTexture.d.ts +1 -0
- package/types/water/waterHydrodynamics.d.ts +49 -0
- package/types/water/waterRenderExtension.d.ts +16 -0
- package/types/water/waterScenePasses.d.ts +1 -1
- package/types/water/waterSettings.d.ts +1 -0
- package/types/water/waterSpectralOcean.d.ts +17 -0
- package/types/water/waterStageSettings.d.ts +1 -1
- package/types/water/waterUnderwaterAtmosphere.d.ts +19 -4
|
@@ -20,7 +20,8 @@ import {
|
|
|
20
20
|
resolveCatalogQualityOptions,
|
|
21
21
|
resolveSceneQualityProfile,
|
|
22
22
|
} from '../styles/sceneQualityProfiles.js';
|
|
23
|
-
import {
|
|
23
|
+
import { createDenseFieldRockLodRuntime } from '../rockgen/lod/rockDenseFieldPolicy.js';
|
|
24
|
+
import { collectCatalogLodBindings, createCatalogLodRuntime } from './officialCatalogLod.js';
|
|
24
25
|
|
|
25
26
|
function vector3(value, fallback = [0, 0, 0]) {
|
|
26
27
|
if (Array.isArray(value)) {
|
|
@@ -39,6 +40,17 @@ function rotation3(value) {
|
|
|
39
40
|
return [0, Number(value) || 0, 0];
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
function mergeSettings(base, patch) {
|
|
44
|
+
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) return base;
|
|
45
|
+
const output = { ...(base ?? {}) };
|
|
46
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
47
|
+
output[key] = value && typeof value === 'object' && !Array.isArray(value)
|
|
48
|
+
? mergeSettings(output[key], value)
|
|
49
|
+
: value;
|
|
50
|
+
}
|
|
51
|
+
return output;
|
|
52
|
+
}
|
|
53
|
+
|
|
42
54
|
/**
|
|
43
55
|
* Validates caller-supplied style textures without reclassifying the asset's
|
|
44
56
|
* own UV-space maps as triplanar detail maps. applyRockShader reads the full
|
|
@@ -54,13 +66,13 @@ function catalogRockTextures(overrides = {}) {
|
|
|
54
66
|
};
|
|
55
67
|
}
|
|
56
68
|
|
|
57
|
-
function styleAdapterFor(asset, root, { textures: overrides, variation }) {
|
|
69
|
+
function styleAdapterFor(asset, root, { settings: assetSettings, textures: overrides, variation }) {
|
|
58
70
|
if (asset.domain !== 'natural.rock') return null;
|
|
59
71
|
const resolved = catalogRockTextures(overrides);
|
|
60
72
|
const report = { rejectedTextures: resolved.rejected };
|
|
61
73
|
return Object.freeze({
|
|
62
74
|
apply(subject, settings) {
|
|
63
|
-
const result = applyRockShader(subject, settings, {
|
|
75
|
+
const result = applyRockShader(subject, mergeSettings(settings, assetSettings), {
|
|
64
76
|
name: `ToonLab · ${asset.label}`,
|
|
65
77
|
textures: resolved.textures,
|
|
66
78
|
variation,
|
|
@@ -124,6 +136,7 @@ export async function loadOfficialCatalogAsset({
|
|
|
124
136
|
styleBundle,
|
|
125
137
|
targetId = null,
|
|
126
138
|
textures = {},
|
|
139
|
+
usePublishedRockSurface = true,
|
|
127
140
|
variation = 0,
|
|
128
141
|
} = {}) {
|
|
129
142
|
if (!assetRuntime?.acquireAsset) {
|
|
@@ -153,6 +166,7 @@ export async function loadOfficialCatalogAsset({
|
|
|
153
166
|
let lod = null;
|
|
154
167
|
let collisionRegistration = null;
|
|
155
168
|
let inspectorRegistration = null;
|
|
169
|
+
let publishedRockSurface = null;
|
|
156
170
|
try {
|
|
157
171
|
metadata = collisionMetadataFor(asset, collision, collisionAdapter);
|
|
158
172
|
labelStyleTarget(root, createStyleTargetLabel(asset.domain, {
|
|
@@ -160,17 +174,33 @@ export async function loadOfficialCatalogAsset({
|
|
|
160
174
|
collision: metadata,
|
|
161
175
|
targetId: id,
|
|
162
176
|
}));
|
|
163
|
-
|
|
177
|
+
if (usePublishedRockSurface
|
|
178
|
+
&& asset.domain === 'natural.rock'
|
|
179
|
+
&& typeof assetRuntime.getRockShaderInput === 'function') {
|
|
180
|
+
publishedRockSurface = await assetRuntime.getRockShaderInput(asset);
|
|
181
|
+
}
|
|
182
|
+
adapter = styleAdapterFor(asset, root, {
|
|
183
|
+
settings: publishedRockSurface?.settings,
|
|
184
|
+
textures: { ...(publishedRockSurface?.textures ?? {}), ...textures },
|
|
185
|
+
variation,
|
|
186
|
+
});
|
|
164
187
|
style = await applyStyleBundle(styleBundle, {
|
|
165
188
|
targets: [createStyleTarget(id, asset.domain, root, { adapter })],
|
|
166
189
|
});
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
|
|
190
|
+
const authoredLodLevels = [...new Set(
|
|
191
|
+
collectCatalogLodBindings(root).map(({ level }) => level),
|
|
192
|
+
)];
|
|
193
|
+
const usesDenseFieldRockLods = asset.domain === 'natural.rock'
|
|
194
|
+
&& authoredLodLevels.includes(4);
|
|
195
|
+
lod = usesDenseFieldRockLods
|
|
196
|
+
? createDenseFieldRockLodRuntime(root, { maxLevel: maxLodLevel })
|
|
197
|
+
: createCatalogLodRuntime(root, {
|
|
198
|
+
distances: resolveCatalogLodDistancesForQuality(
|
|
199
|
+
qualityProfile,
|
|
200
|
+
asset.lod?.distances,
|
|
201
|
+
),
|
|
202
|
+
maxLevel: maxLodLevel,
|
|
203
|
+
});
|
|
174
204
|
if (metadata.kind !== 'none') {
|
|
175
205
|
collisionRegistration = await registerCollisionTarget({
|
|
176
206
|
adapter: collisionAdapter,
|
|
@@ -191,6 +221,8 @@ export async function loadOfficialCatalogAsset({
|
|
|
191
221
|
lod: {
|
|
192
222
|
availableLevels: lod.availableLevels,
|
|
193
223
|
enabled: true,
|
|
224
|
+
cullBelowPixels: lod.cullBelowPixels,
|
|
225
|
+
pixelThresholds: lod.pixelThresholds,
|
|
194
226
|
thresholds: lod.thresholds,
|
|
195
227
|
},
|
|
196
228
|
},
|
|
@@ -221,6 +253,7 @@ export async function loadOfficialCatalogAsset({
|
|
|
221
253
|
lod,
|
|
222
254
|
normalization,
|
|
223
255
|
object: root,
|
|
256
|
+
publishedRockSurface,
|
|
224
257
|
quality: qualityProfile,
|
|
225
258
|
/** Detail maps the artifact shipped that were too small to be usable. */
|
|
226
259
|
get rejectedTextures() { return adapter?.report?.rejectedTextures ?? []; },
|
|
@@ -238,6 +271,13 @@ export async function loadOfficialCatalogAsset({
|
|
|
238
271
|
get released() { return released; },
|
|
239
272
|
style,
|
|
240
273
|
targetId: id,
|
|
241
|
-
updateLod(options
|
|
274
|
+
updateLod(options = {}) {
|
|
275
|
+
if (!lod.pixelThresholds || options.projectedPixels !== undefined) return lod.update(options);
|
|
276
|
+
return lod.update({
|
|
277
|
+
...options,
|
|
278
|
+
viewportHeight: options.viewportHeight
|
|
279
|
+
?? (typeof globalThis.innerHeight === 'number' ? globalThis.innerHeight : 1080),
|
|
280
|
+
});
|
|
281
|
+
},
|
|
242
282
|
});
|
|
243
283
|
}
|
|
@@ -10,8 +10,21 @@ export const OFFICIAL_CATALOG_PROVIDER_TRANSPORTS = Object.freeze([
|
|
|
10
10
|
'public-rock',
|
|
11
11
|
]);
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const LEGACY_ROCK_ID = /^rock-\d{4}$/u;
|
|
14
|
+
const NATURE_REFERENCE_ROCK_ID = /^rock-c8-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
14
15
|
const ROCK_RECIPE_KIND = 'toonlab/rock-recipe';
|
|
16
|
+
const ROCK_GALLERY_RECIPE_SCHEMA = 'toonlab/rock-gallery-recipe';
|
|
17
|
+
|
|
18
|
+
function isRockId(value) {
|
|
19
|
+
return LEGACY_ROCK_ID.test(value) || NATURE_REFERENCE_ROCK_ID.test(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isRockRecipe(value) {
|
|
23
|
+
return isRecord(value) && (
|
|
24
|
+
value.kind === ROCK_RECIPE_KIND
|
|
25
|
+
|| value.schema === ROCK_GALLERY_RECIPE_SCHEMA
|
|
26
|
+
);
|
|
27
|
+
}
|
|
15
28
|
|
|
16
29
|
function isRecord(value) {
|
|
17
30
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -61,6 +74,15 @@ function artifactName(artifact) {
|
|
|
61
74
|
return String(artifact?.name ?? artifact?.relative_path ?? artifact?.relativePath ?? '');
|
|
62
75
|
}
|
|
63
76
|
|
|
77
|
+
function artifactKey(value) {
|
|
78
|
+
return String(value ?? '').trim().replace(/^\.\//u, '').toLowerCase();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function artifactByName(artifacts, ...names) {
|
|
82
|
+
const wanted = new Set(names.map(artifactKey));
|
|
83
|
+
return artifacts.find((artifact) => wanted.has(artifactKey(artifactName(artifact)))) ?? null;
|
|
84
|
+
}
|
|
85
|
+
|
|
64
86
|
function isModelArtifact(artifact) {
|
|
65
87
|
const name = artifactName(artifact).toLowerCase();
|
|
66
88
|
const contentType = String(artifact?.contentType ?? artifact?.content_type ?? '').toLowerCase();
|
|
@@ -107,7 +129,7 @@ export function normalizeOfficialCatalogAsset(rawAsset, {
|
|
|
107
129
|
const source = String(rawAsset.source ?? metadata.source ?? expectedSource ?? '').trim();
|
|
108
130
|
const kind = String(rawAsset.kind ?? 'model').trim();
|
|
109
131
|
const catalog = String(metadata.catalog ?? rawAsset.catalog ?? (
|
|
110
|
-
recipe
|
|
132
|
+
isRockRecipe(recipe) ? 'rocks' : ''
|
|
111
133
|
)).trim().toLowerCase();
|
|
112
134
|
const revision = Number(rawAsset.revision ?? metadata.revision);
|
|
113
135
|
const recipeHash = String(rawAsset.recipeHash ?? metadata.recipeHash ?? '').trim();
|
|
@@ -115,7 +137,7 @@ export function normalizeOfficialCatalogAsset(rawAsset, {
|
|
|
115
137
|
...(Array.isArray(rawAsset.artifacts) ? rawAsset.artifacts : []),
|
|
116
138
|
...(Array.isArray(rawAsset.files) ? rawAsset.files : []),
|
|
117
139
|
].filter(isRecord);
|
|
118
|
-
const modelArtifact = artifacts
|
|
140
|
+
const modelArtifact = artifactByName(artifacts, 'rock.glb')
|
|
119
141
|
?? artifacts.find(isModelArtifact)
|
|
120
142
|
?? null;
|
|
121
143
|
const modelReference = rawAsset.download_url
|
|
@@ -137,9 +159,11 @@ export function normalizeOfficialCatalogAsset(rawAsset, {
|
|
|
137
159
|
if (kind !== 'model') errors.push(`kind must be model, received ${kind || '(empty)'}`);
|
|
138
160
|
if (!modelReference) errors.push('a model download URL is required');
|
|
139
161
|
if (source === 'toonlab-rock') {
|
|
140
|
-
if (!
|
|
162
|
+
if (!isRockId(id)) errors.push('ToonLab rock id must match rock-0001 or rock-c8-geology-subtype');
|
|
141
163
|
if (catalog !== 'rocks') errors.push(`catalog must be rocks, received ${catalog || '(empty)'}`);
|
|
142
|
-
if (recipe
|
|
164
|
+
if (!isRockRecipe(recipe)) {
|
|
165
|
+
errors.push(`recipe must use ${ROCK_RECIPE_KIND} or ${ROCK_GALLERY_RECIPE_SCHEMA}`);
|
|
166
|
+
}
|
|
143
167
|
if (!Number.isInteger(revision) || revision < 1) errors.push('positive integer revision is required');
|
|
144
168
|
if (!recipeHash) errors.push('recipeHash is required');
|
|
145
169
|
}
|
|
@@ -157,15 +181,61 @@ export function normalizeOfficialCatalogAsset(rawAsset, {
|
|
|
157
181
|
sha256: String(artifact.sha256 ?? ''),
|
|
158
182
|
url: resolveOfficialCatalogUrl(artifactUrl(artifact), baseUrl),
|
|
159
183
|
}));
|
|
184
|
+
const artifactMap = Object.freeze(Object.fromEntries(normalizedArtifacts
|
|
185
|
+
.filter((artifact) => artifact.name && artifact.url)
|
|
186
|
+
.map((artifact) => [artifact.name, artifact])));
|
|
187
|
+
const artifactUrlByName = (...names) => {
|
|
188
|
+
const artifact = artifactByName(normalizedArtifacts, ...names);
|
|
189
|
+
return artifact?.url ?? null;
|
|
190
|
+
};
|
|
160
191
|
const release = String(rawAsset.release ?? metadata.release ?? releaseFromUrl(modelUrl) ?? '').trim() || null;
|
|
161
192
|
const domain = catalog === 'rocks' ? 'natural.rock' : String(metadata.domain ?? '').trim() || null;
|
|
162
|
-
const
|
|
193
|
+
const recipeLod = isRecord(recipe?.lod) ? recipe.lod : null;
|
|
194
|
+
const metadataLod = isRecord(rawAsset.lod ?? metadata.lod) ? rawAsset.lod ?? metadata.lod : null;
|
|
195
|
+
const lodLevels = Array.isArray(recipe?.output?.lods)
|
|
196
|
+
? recipe.output.lods.map(String)
|
|
197
|
+
: Array.isArray(metadataLod?.levels)
|
|
198
|
+
? metadataLod.levels.map(String)
|
|
199
|
+
: [];
|
|
200
|
+
const lodUrls = Object.freeze(Array.from({ length: 5 }, (_, level) => (
|
|
201
|
+
artifactUrlByName(`lod${level}.glb`, `lod/LOD${level}.glb`)
|
|
202
|
+
)));
|
|
203
|
+
const hasLodPackage = lodUrls.some(Boolean) || lodLevels.length > 0;
|
|
204
|
+
const lod = recipeLod || metadataLod || hasLodPackage
|
|
205
|
+
? Object.freeze({
|
|
206
|
+
...frozenCopy(recipeLod ?? metadataLod ?? {}),
|
|
207
|
+
cullBelowPixels: Number(
|
|
208
|
+
recipe?.output?.cullBelowPixels
|
|
209
|
+
?? metadataLod?.cullBelowPixels
|
|
210
|
+
?? 0,
|
|
211
|
+
) || 0,
|
|
212
|
+
levels: Object.freeze(lodLevels),
|
|
213
|
+
urls: lodUrls,
|
|
214
|
+
})
|
|
215
|
+
: null;
|
|
216
|
+
const packageFiles = Object.freeze({
|
|
217
|
+
collisionModelUrl: artifactUrlByName('collision.glb'),
|
|
218
|
+
controlModelUrl: artifactUrlByName('control.glb'),
|
|
219
|
+
lodModelUrls: lodUrls,
|
|
220
|
+
manifestUrl: artifactUrlByName('manifest.json'),
|
|
221
|
+
materialConfigUrl: artifactUrlByName('material-config.json'),
|
|
222
|
+
natureProvenanceUrl: artifactUrlByName('nature-provenance.json', 'nature/provenance.json'),
|
|
223
|
+
natureReferenceUrl: artifactUrlByName('nature-reference.jpg', 'nature/reference.jpg'),
|
|
224
|
+
realisticModelUrl: modelUrl,
|
|
225
|
+
realisticPreviewUrl: artifactUrlByName('realistic-preview.png'),
|
|
226
|
+
recipeUrl: artifactUrlByName('recipe.json'),
|
|
227
|
+
retainedHighModelUrl: artifactUrlByName('retained-high.glb', 'high.glb'),
|
|
228
|
+
stylizedModelUrl: artifactUrlByName('rock-stylized.glb'),
|
|
229
|
+
stylizedPreviewUrl: artifactUrlByName('stylized-preview.png'),
|
|
230
|
+
});
|
|
231
|
+
const collisionMetadata = rawAsset.collision ?? metadata.collision;
|
|
163
232
|
|
|
164
233
|
return Object.freeze({
|
|
165
234
|
artifacts: Object.freeze(normalizedArtifacts),
|
|
235
|
+
artifactMap,
|
|
166
236
|
catalog,
|
|
167
|
-
collision: isRecord(
|
|
168
|
-
? frozenCopy(
|
|
237
|
+
collision: isRecord(collisionMetadata) && typeof collisionMetadata.kind === 'string'
|
|
238
|
+
? frozenCopy(collisionMetadata)
|
|
169
239
|
: null,
|
|
170
240
|
domain,
|
|
171
241
|
id,
|
|
@@ -175,6 +245,7 @@ export function normalizeOfficialCatalogAsset(rawAsset, {
|
|
|
175
245
|
lod,
|
|
176
246
|
metadata: frozenCopy(metadata),
|
|
177
247
|
modelUrl,
|
|
248
|
+
packageFiles,
|
|
178
249
|
provider,
|
|
179
250
|
provenance: Object.freeze({
|
|
180
251
|
modelSha256: String(modelArtifact?.sha256 ?? metadata.modelSha256 ?? ''),
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LinearFilter,
|
|
3
|
+
LinearMipmapLinearFilter,
|
|
4
|
+
NoColorSpace,
|
|
5
|
+
RepeatWrapping,
|
|
6
|
+
SRGBColorSpace,
|
|
7
|
+
TextureLoader,
|
|
8
|
+
} from 'three';
|
|
9
|
+
|
|
10
|
+
export const OFFICIAL_ROCK_GALLERY_RECIPE_SCHEMA = 'toonlab/rock-gallery-recipe';
|
|
11
|
+
export const OFFICIAL_ROCK_MATERIAL_SCHEMA = 'toonlab.pro-rock-material';
|
|
12
|
+
|
|
13
|
+
function isRecord(value) {
|
|
14
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function frozenCopy(value) {
|
|
18
|
+
if (Array.isArray(value)) return Object.freeze(value.map(frozenCopy));
|
|
19
|
+
if (!isRecord(value)) return value;
|
|
20
|
+
return Object.freeze(Object.fromEntries(
|
|
21
|
+
Object.entries(value).map(([key, entry]) => [key, frozenCopy(entry)]),
|
|
22
|
+
));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function artifactName(value) {
|
|
26
|
+
return String(value ?? '').trim().replace(/^\.\//u, '').toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function findOfficialCatalogArtifact(asset, ...names) {
|
|
30
|
+
const wanted = new Set(names.map(artifactName));
|
|
31
|
+
return asset?.artifacts?.find((artifact) => wanted.has(artifactName(artifact.name))) ?? null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getOfficialCatalogArtifactUrl(asset, ...names) {
|
|
35
|
+
return findOfficialCatalogArtifact(asset, ...names)?.url ?? null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function fetchJson(fetchImpl, url, label) {
|
|
39
|
+
if (!url) return null;
|
|
40
|
+
if (typeof fetchImpl !== 'function') throw new TypeError(`${label} requires fetch().`);
|
|
41
|
+
const response = await fetchImpl(url, { headers: { accept: 'application/json' } });
|
|
42
|
+
if (!response?.ok) {
|
|
43
|
+
throw new Error(`${label} returned HTTP ${response?.status ?? 'unknown'}: ${url}`);
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return await response.json();
|
|
47
|
+
} catch (cause) {
|
|
48
|
+
throw new Error(`${label} returned invalid JSON: ${url}`, { cause });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function validateRecipe(asset, recipe) {
|
|
53
|
+
if (!recipe) return null;
|
|
54
|
+
if (recipe.schema !== OFFICIAL_ROCK_GALLERY_RECIPE_SCHEMA || recipe.version !== 1) {
|
|
55
|
+
throw new Error(`${asset.id} has an unsupported editable rock recipe.`);
|
|
56
|
+
}
|
|
57
|
+
if (recipe.generator?.unit !== 'metre') {
|
|
58
|
+
throw new Error(`${asset.id} editable recipe must use metres.`);
|
|
59
|
+
}
|
|
60
|
+
return frozenCopy(recipe);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function validateMaterialConfig(asset, materialConfig) {
|
|
64
|
+
if (!materialConfig) return null;
|
|
65
|
+
if (materialConfig.schema !== OFFICIAL_ROCK_MATERIAL_SCHEMA || materialConfig.version !== 2) {
|
|
66
|
+
throw new Error(`${asset.id} has an unsupported Call Me Sensei material package.`);
|
|
67
|
+
}
|
|
68
|
+
if (materialConfig.assetId !== asset.id || materialConfig.shader?.preset !== 'call_me_sensei') {
|
|
69
|
+
throw new Error(`${asset.id} material package failed identity or preset validation.`);
|
|
70
|
+
}
|
|
71
|
+
if (!isRecord(materialConfig.textures)) {
|
|
72
|
+
throw new Error(`${asset.id} material package has no texture bindings.`);
|
|
73
|
+
}
|
|
74
|
+
return frozenCopy(materialConfig);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Load the immutable documents associated with a nature-reference rock.
|
|
79
|
+
* Legacy 480 rocks remain valid and simply return null for documents they do
|
|
80
|
+
* not publish.
|
|
81
|
+
*/
|
|
82
|
+
export async function loadOfficialCatalogRockPackage(asset, {
|
|
83
|
+
fetchImpl = globalThis.fetch?.bind(globalThis),
|
|
84
|
+
includeManifest = false,
|
|
85
|
+
includeNatureProvenance = false,
|
|
86
|
+
} = {}) {
|
|
87
|
+
if (!asset || asset.domain !== 'natural.rock') {
|
|
88
|
+
throw new TypeError('Official rock package loading requires a normalized natural.rock asset.');
|
|
89
|
+
}
|
|
90
|
+
const files = asset.packageFiles ?? {};
|
|
91
|
+
const [recipeDocument, materialDocument, manifest, natureProvenance] = await Promise.all([
|
|
92
|
+
files.recipeUrl ? fetchJson(fetchImpl, files.recipeUrl, `${asset.id} recipe`) : asset.recipe,
|
|
93
|
+
fetchJson(fetchImpl, files.materialConfigUrl, `${asset.id} material package`),
|
|
94
|
+
includeManifest ? fetchJson(fetchImpl, files.manifestUrl, `${asset.id} manifest`) : null,
|
|
95
|
+
includeNatureProvenance
|
|
96
|
+
? fetchJson(fetchImpl, files.natureProvenanceUrl, `${asset.id} nature provenance`)
|
|
97
|
+
: null,
|
|
98
|
+
]);
|
|
99
|
+
const recipe = recipeDocument?.schema === OFFICIAL_ROCK_GALLERY_RECIPE_SCHEMA
|
|
100
|
+
? validateRecipe(asset, recipeDocument)
|
|
101
|
+
: frozenCopy(asset.recipe);
|
|
102
|
+
const materialConfig = validateMaterialConfig(asset, materialDocument);
|
|
103
|
+
return Object.freeze({
|
|
104
|
+
asset,
|
|
105
|
+
files,
|
|
106
|
+
manifest: frozenCopy(manifest),
|
|
107
|
+
materialConfig,
|
|
108
|
+
natureProvenance: frozenCopy(natureProvenance),
|
|
109
|
+
recipe,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function configureRockTexture(texture, descriptor, role) {
|
|
114
|
+
texture.name = `ToonLab ${role} · ${descriptor.url}`;
|
|
115
|
+
texture.colorSpace = descriptor.srgb === true ? SRGBColorSpace : NoColorSpace;
|
|
116
|
+
texture.flipY = false;
|
|
117
|
+
texture.wrapS = RepeatWrapping;
|
|
118
|
+
texture.wrapT = RepeatWrapping;
|
|
119
|
+
texture.magFilter = LinearFilter;
|
|
120
|
+
texture.minFilter = LinearMipmapLinearFilter;
|
|
121
|
+
texture.generateMipmaps = true;
|
|
122
|
+
texture.userData.toonlabOfficialRockRole = role;
|
|
123
|
+
texture.userData.toonlabOfficialRockUrl = descriptor.url;
|
|
124
|
+
texture.needsUpdate = true;
|
|
125
|
+
return texture;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Load the exact texture slots consumed by applyRockShader(). */
|
|
129
|
+
export async function loadOfficialCatalogRockShaderInput(rockPackage, {
|
|
130
|
+
textureLoader = new TextureLoader(),
|
|
131
|
+
} = {}) {
|
|
132
|
+
const config = rockPackage?.materialConfig;
|
|
133
|
+
if (!config) return null;
|
|
134
|
+
const textures = {};
|
|
135
|
+
await Promise.all(Object.entries(config.textures).map(async ([role, descriptor]) => {
|
|
136
|
+
if (!descriptor?.url) throw new Error(`${config.assetId}/${role} has no immutable texture URL.`);
|
|
137
|
+
const texture = await textureLoader.loadAsync(descriptor.url);
|
|
138
|
+
textures[role] = configureRockTexture(texture, descriptor, role);
|
|
139
|
+
}));
|
|
140
|
+
return Object.freeze({
|
|
141
|
+
materialConfig: config,
|
|
142
|
+
settings: frozenCopy(config.shader?.settings ?? {}),
|
|
143
|
+
textures: Object.freeze(textures),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Portable hand-off for Rock Lab or another editor. Geometry derivatives are
|
|
149
|
+
* deliberately separate: edits target control.glb, while retained high, LODs,
|
|
150
|
+
* collision, and baked/runtime outputs can be regenerated or replaced.
|
|
151
|
+
*/
|
|
152
|
+
export function createOfficialCatalogRockEditorDescriptor(asset, rockPackage = null) {
|
|
153
|
+
if (!asset || asset.domain !== 'natural.rock') {
|
|
154
|
+
throw new TypeError('Official rock editor descriptors require a normalized natural.rock asset.');
|
|
155
|
+
}
|
|
156
|
+
const recipe = rockPackage?.recipe ?? asset.recipe;
|
|
157
|
+
const files = asset.packageFiles ?? {};
|
|
158
|
+
if (recipe?.schema !== OFFICIAL_ROCK_GALLERY_RECIPE_SCHEMA) return null;
|
|
159
|
+
return Object.freeze({
|
|
160
|
+
assetId: asset.id,
|
|
161
|
+
controlModelUrl: files.controlModelUrl,
|
|
162
|
+
cullBelowPixels: asset.lod?.cullBelowPixels ?? recipe.output?.cullBelowPixels ?? 0,
|
|
163
|
+
editing: frozenCopy(recipe.editing ?? {}),
|
|
164
|
+
lodModelUrls: asset.lod?.urls ?? files.lodModelUrls ?? Object.freeze([]),
|
|
165
|
+
materialConfigUrl: files.materialConfigUrl,
|
|
166
|
+
recipe: frozenCopy(recipe),
|
|
167
|
+
recipeUrl: files.recipeUrl,
|
|
168
|
+
retainedHighModelUrl: files.retainedHighModelUrl,
|
|
169
|
+
collisionModelUrl: files.collisionModelUrl,
|
|
170
|
+
worldUnitMetres: recipe.generator?.unit === 'metre' ? 1 : null,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
@@ -97,7 +97,7 @@ function createMrtTarget(width, height, name, distanceName) {
|
|
|
97
97
|
|
|
98
98
|
const distance = target.textures[1];
|
|
99
99
|
distance.name = distanceName;
|
|
100
|
-
distance.format = THREE.
|
|
100
|
+
distance.format = THREE.RGFormat;
|
|
101
101
|
distance.type = THREE.HalfFloatType;
|
|
102
102
|
distance.minFilter = THREE.NearestFilter;
|
|
103
103
|
distance.magFilter = THREE.NearestFilter;
|
|
@@ -157,6 +157,7 @@ export function createCloudReprojection({
|
|
|
157
157
|
cloudMaterial = cloudVolume?.material ?? null,
|
|
158
158
|
cloudUniforms = cloudVolume?.uniforms ?? null,
|
|
159
159
|
shape,
|
|
160
|
+
wind = null,
|
|
160
161
|
fade = null,
|
|
161
162
|
historyDiv = 2,
|
|
162
163
|
width = 1,
|
|
@@ -205,6 +206,7 @@ export function createCloudReprojection({
|
|
|
205
206
|
previousCameraPosition: uniform(new THREE.Vector3()),
|
|
206
207
|
historyValid: uniform(0),
|
|
207
208
|
cameraStatic: uniform(1),
|
|
209
|
+
windDelta: uniform(new THREE.Vector3()),
|
|
208
210
|
sourceSize: uniform(new THREE.Vector2(lowWidth, lowHeight)),
|
|
209
211
|
historySize: uniform(new THREE.Vector2(historyWidth, historyHeight)),
|
|
210
212
|
freshSlot: uniform(new THREE.Vector2()),
|
|
@@ -218,6 +220,7 @@ export function createCloudReprojection({
|
|
|
218
220
|
const historyTexture = texture(historyRead.textures[0]);
|
|
219
221
|
const historyDistanceTexture = texture(historyRead.textures[1]);
|
|
220
222
|
const temporalHitDistance = property('float', 'toonlabTemporalHitDistance');
|
|
223
|
+
const temporalSceneDistance = property('float', 'toonlabTemporalSceneDistance');
|
|
221
224
|
|
|
222
225
|
const resolveMaterial = new NodeMaterial();
|
|
223
226
|
resolveMaterial.name = `${name}Resolve`;
|
|
@@ -239,6 +242,11 @@ export function createCloudReprojection({
|
|
|
239
242
|
const freshSample = marchTexture.sample(srcCenter).toVar();
|
|
240
243
|
const fallback = marchTexture.sample(uvNode).toVar();
|
|
241
244
|
const currentHitDistance = marchDistanceTexture.sample(srcCenter).x.toVar();
|
|
245
|
+
const currentSceneDistance = cloudVolume?.sceneDepthNode
|
|
246
|
+
? select(cloudUniforms.sceneDepthEnabled.greaterThan(0.5),
|
|
247
|
+
min(cloudVolume.sceneDepthNode.sample(uvNode).r, 65000), float(65000)).toVar()
|
|
248
|
+
: float(65000);
|
|
249
|
+
temporalSceneDistance.assign(currentSceneDistance);
|
|
242
250
|
|
|
243
251
|
const neighborhoodMin = vec4(freshSample).toVar();
|
|
244
252
|
const neighborhoodMax = vec4(freshSample).toVar();
|
|
@@ -305,6 +313,7 @@ export function createCloudReprojection({
|
|
|
305
313
|
const rayDirection = uniforms.rayBasis.mul(vec3(ndc, 1)).normalize().toVar();
|
|
306
314
|
const worldHit = uniforms.cameraPosition
|
|
307
315
|
.add(rayDirection.mul(reprojectionDistance))
|
|
316
|
+
.sub(uniforms.windDelta)
|
|
308
317
|
.toVar();
|
|
309
318
|
const previousClip = uniforms.previousViewProjection.mul(vec4(worldHit, 1)).toVar();
|
|
310
319
|
const previousNdc = previousClip.xy.div(max(abs(previousClip.w), 1e-6)).toVar();
|
|
@@ -335,11 +344,17 @@ export function createCloudReprojection({
|
|
|
335
344
|
uniforms.historySize,
|
|
336
345
|
).toVar();
|
|
337
346
|
const previousHitDistance = historyDistanceTexture.sample(previousUv).x.toVar();
|
|
347
|
+
const previousSceneDistance = historyDistanceTexture.sample(previousUv).y;
|
|
338
348
|
history.assign(vec4(max(history.rgb, vec3(0)), clamp(history.a, 0, 1)));
|
|
339
349
|
|
|
350
|
+
const depthMatches = abs(expectedPreviousDistance.sub(previousHitDistance))
|
|
351
|
+
.lessThanEqual(float(REJECT_THRESHOLD).mul(max(expectedPreviousDistance, 1)));
|
|
340
352
|
const historyUsable = inBounds
|
|
341
353
|
.and(uniforms.historyValid.greaterThan(0.5))
|
|
342
354
|
.and(carriedSelfDistance.greaterThanEqual(MIN_CARRIED_DISTANCE))
|
|
355
|
+
.and(gateBypass.or(depthMatches))
|
|
356
|
+
.and(abs(previousSceneDistance.sub(currentSceneDistance))
|
|
357
|
+
.lessThanEqual(max(currentSceneDistance.mul(0.02), 10)))
|
|
343
358
|
.toVar();
|
|
344
359
|
const clampedHistory = select(
|
|
345
360
|
uniforms.cameraStatic.greaterThan(0.5),
|
|
@@ -350,6 +365,11 @@ export function createCloudReprojection({
|
|
|
350
365
|
|
|
351
366
|
const currentIsMiss = currentHitDistance
|
|
352
367
|
.greaterThanEqual(CLOUD_HIT_DISTANCE_MISS_THRESHOLD);
|
|
368
|
+
const opacityConfidence = select(
|
|
369
|
+
uniforms.cameraStatic.greaterThan(0.5),
|
|
370
|
+
float(1),
|
|
371
|
+
saturate(abs(history.a.sub(freshSample.a)).mul(2).oneMinus()),
|
|
372
|
+
);
|
|
353
373
|
const freshHistoryWeight = select(
|
|
354
374
|
currentIsMiss,
|
|
355
375
|
float(FRESH_HISTORY_WEIGHT_NEAR),
|
|
@@ -362,7 +382,7 @@ export function createCloudReprojection({
|
|
|
362
382
|
currentHitDistance,
|
|
363
383
|
),
|
|
364
384
|
),
|
|
365
|
-
);
|
|
385
|
+
).mul(opacityConfidence);
|
|
366
386
|
const freshResolve = select(
|
|
367
387
|
historyUsable,
|
|
368
388
|
mix(freshSample, clampedHistory, freshHistoryWeight),
|
|
@@ -370,8 +390,6 @@ export function createCloudReprojection({
|
|
|
370
390
|
).toVar();
|
|
371
391
|
const result = select(isFresh, freshResolve, staleResolve).toVar();
|
|
372
392
|
|
|
373
|
-
const depthMatches = abs(expectedPreviousDistance.sub(previousHitDistance))
|
|
374
|
-
.lessThanEqual(float(REJECT_THRESHOLD).mul(expectedPreviousDistance));
|
|
375
393
|
const distanceTrusted = historyUsable.and(gateBypass.or(depthMatches));
|
|
376
394
|
temporalHitDistance.assign(select(
|
|
377
395
|
isFresh,
|
|
@@ -384,11 +402,12 @@ export function createCloudReprojection({
|
|
|
384
402
|
|
|
385
403
|
resolveMaterial.fragmentNode = mrt({
|
|
386
404
|
output: resolveColor,
|
|
387
|
-
hitDistHistory: vec4(temporalHitDistance,
|
|
405
|
+
hitDistHistory: vec4(temporalHitDistance, temporalSceneDistance, 0, 1),
|
|
388
406
|
});
|
|
389
407
|
|
|
390
408
|
const quad = new QuadMesh(resolveMaterial);
|
|
391
409
|
const previousViewProjection = new THREE.Matrix4();
|
|
410
|
+
const previousProjection = new THREE.Matrix4();
|
|
392
411
|
const previousCameraPosition = new THREE.Vector3();
|
|
393
412
|
const previousQuaternion = new THREE.Quaternion();
|
|
394
413
|
const currentPosition = new THREE.Vector3();
|
|
@@ -396,6 +415,9 @@ export function createCloudReprojection({
|
|
|
396
415
|
const currentForward = new THREE.Vector3();
|
|
397
416
|
const rightScratch = new THREE.Vector3();
|
|
398
417
|
const upScratch = new THREE.Vector3();
|
|
418
|
+
const previousWindOffset = new THREE.Vector3();
|
|
419
|
+
const previousEvolutionOffset = new THREE.Vector3();
|
|
420
|
+
const previousForward = new THREE.Vector3();
|
|
399
421
|
let hasPreviousView = false;
|
|
400
422
|
let frame = 0;
|
|
401
423
|
let framesSinceReset = 0;
|
|
@@ -438,10 +460,18 @@ export function createCloudReprojection({
|
|
|
438
460
|
const cameraMoved = hasPreviousView
|
|
439
461
|
&& (currentPosition.distanceToSquared(previousCameraPosition) >= 1e-8
|
|
440
462
|
|| currentQuaternion.angleTo(previousQuaternion) >= 1e-6);
|
|
463
|
+
const offset = wind?.offset?.value;
|
|
464
|
+
const evolution = wind?.evolutionOffset?.value;
|
|
465
|
+
uniforms.windDelta.value.set(0, 0, 0);
|
|
466
|
+
if (hasPreviousView && offset) uniforms.windDelta.value.subVectors(offset, previousWindOffset);
|
|
467
|
+
const fieldMoved = uniforms.windDelta.value.lengthSq() > 1e-8
|
|
468
|
+
|| (hasPreviousView && evolution && evolution.distanceToSquared(previousEvolutionOffset) > 1e-8);
|
|
441
469
|
const cut = !hasPreviousView
|
|
470
|
+
|| !previousProjection.equals(camera.projectionMatrix)
|
|
442
471
|
|| currentPosition.distanceTo(previousCameraPosition) > CLOUD_HISTORY_CUT_DISTANCE
|
|
472
|
+
|| uniforms.windDelta.value.length() > CLOUD_HISTORY_CUT_DISTANCE
|
|
443
473
|
|| (hasPreviousView
|
|
444
|
-
&& currentForward.dot(
|
|
474
|
+
&& currentForward.dot(previousForward.set(0, 0, -1).applyQuaternion(previousQuaternion))
|
|
445
475
|
< CLOUD_HISTORY_CUT_COSINE);
|
|
446
476
|
if (cut) reset();
|
|
447
477
|
|
|
@@ -461,7 +491,7 @@ export function createCloudReprojection({
|
|
|
461
491
|
|
|
462
492
|
uniforms.cameraPosition.value.copy(currentPosition);
|
|
463
493
|
uniforms.previousCameraPosition.value.copy(previousCameraPosition);
|
|
464
|
-
uniforms.cameraStatic.value = cameraMoved ? 0 : 1;
|
|
494
|
+
uniforms.cameraStatic.value = cameraMoved || fieldMoved ? 0 : 1;
|
|
465
495
|
uniforms.freshSlot.value.set(cellX, cellY);
|
|
466
496
|
uniforms.previousViewProjection.value.copy(previousViewProjection);
|
|
467
497
|
|
|
@@ -501,8 +531,11 @@ export function createCloudReprojection({
|
|
|
501
531
|
previousViewProjection
|
|
502
532
|
.copy(camera.projectionMatrix)
|
|
503
533
|
.multiply(camera.matrixWorldInverse);
|
|
534
|
+
previousProjection.copy(camera.projectionMatrix);
|
|
504
535
|
previousCameraPosition.copy(currentPosition);
|
|
505
536
|
previousQuaternion.copy(currentQuaternion);
|
|
537
|
+
if (offset) previousWindOffset.copy(offset);
|
|
538
|
+
if (evolution) previousEvolutionOffset.copy(evolution);
|
|
506
539
|
hasPreviousView = true;
|
|
507
540
|
uniforms.historyValid.value = 1;
|
|
508
541
|
frame += 1;
|