[change] src/app/App.tsx [change] src/assets/starter-environment-assets.ts [change] src/document/migrate-scene-document.ts [change] src/document/scene-document-validation.ts [change] src/document/scene-document.ts [change] src/document/world-settings.ts [change] src/rendering/world-background-renderer.ts [change] src/rendering/world-shader-sky.ts [change] src/runtime-three/runtime-host.ts [change] src/runtime-three/runtime-project-time.ts [change] src/shared-ui/world-background-style.ts [change] src/viewport-three/ViewportCanvas.tsx [change] src/viewport-three/viewport-host.ts [change] tests/domain/runtime-project-time.test.ts [change] tests/domain/scene-document-validation.test.ts [change] tests/domain/world-settings.test.ts [change] tests/serialization/project-document-json.test.ts [change] tests/serialization/scene-document-json.test.ts [change] tests/unit/world-shader-sky.test.ts
291 lines
7.9 KiB
TypeScript
291 lines
7.9 KiB
TypeScript
import type { ProjectDocument } from "../document/scene-document";
|
|
import {
|
|
areWorldBackgroundSettingsEqual,
|
|
cloneWorldSettings,
|
|
createDefaultWorldSettings,
|
|
createDefaultWorldTimeOfDaySettings,
|
|
DEFAULT_NIGHT_IMAGE_ENVIRONMENT_INTENSITY,
|
|
type WorldBackgroundSettings
|
|
} from "../document/world-settings";
|
|
|
|
import {
|
|
cloneProjectAssetRecord,
|
|
type ImageAssetRecord,
|
|
type ProjectAssetRecord
|
|
} from "./project-assets";
|
|
|
|
const STARTER_ENVIRONMENT_STORAGE_KEY_PREFIX = "starter-image:";
|
|
const STARTER_ENVIRONMENT_DIRECTORY = "/starter-environments";
|
|
const STARTER_ENVIRONMENT_WIDTH = 1024;
|
|
const STARTER_ENVIRONMENT_HEIGHT = 512;
|
|
const DEFAULT_DAY_ENVIRONMENT_INTENSITY = 0.85;
|
|
|
|
export const STARTER_DAY_ENVIRONMENT_ASSET_ID =
|
|
"starter-image-winter-soccer-field-day" as const;
|
|
export const STARTER_NIGHT_ENVIRONMENT_ASSET_ID =
|
|
"starter-image-snow-mountains-night" as const;
|
|
|
|
interface StarterEnvironmentAssetDefinition {
|
|
asset: ImageAssetRecord;
|
|
folderName: string;
|
|
}
|
|
|
|
const STARTER_ENVIRONMENT_LIBRARY: readonly StarterEnvironmentAssetDefinition[] =
|
|
[
|
|
{
|
|
asset: {
|
|
id: STARTER_DAY_ENVIRONMENT_ASSET_ID,
|
|
kind: "image",
|
|
sourceName: "Starter Winter Soccer Field Day.hdr",
|
|
mimeType: "image/vnd.radiance",
|
|
storageKey: `${STARTER_ENVIRONMENT_STORAGE_KEY_PREFIX}${STARTER_DAY_ENVIRONMENT_ASSET_ID}`,
|
|
byteLength: 1_474_206,
|
|
metadata: {
|
|
kind: "image",
|
|
width: STARTER_ENVIRONMENT_WIDTH,
|
|
height: STARTER_ENVIRONMENT_HEIGHT,
|
|
hasAlpha: false,
|
|
warnings: []
|
|
}
|
|
},
|
|
folderName: "winter_soccer_field_day"
|
|
},
|
|
{
|
|
asset: {
|
|
id: "starter-image-sunset-open-sky",
|
|
kind: "image",
|
|
sourceName: "Starter Sunset Open Sky.hdr",
|
|
mimeType: "image/vnd.radiance",
|
|
storageKey: `${STARTER_ENVIRONMENT_STORAGE_KEY_PREFIX}starter-image-sunset-open-sky`,
|
|
byteLength: 551_007,
|
|
metadata: {
|
|
kind: "image",
|
|
width: STARTER_ENVIRONMENT_WIDTH,
|
|
height: STARTER_ENVIRONMENT_HEIGHT,
|
|
hasAlpha: false,
|
|
warnings: []
|
|
}
|
|
},
|
|
folderName: "sunset_open_sky"
|
|
},
|
|
{
|
|
asset: {
|
|
id: "starter-image-evening-blue-sky",
|
|
kind: "image",
|
|
sourceName: "Starter Evening Blue Sky.hdr",
|
|
mimeType: "image/vnd.radiance",
|
|
storageKey: `${STARTER_ENVIRONMENT_STORAGE_KEY_PREFIX}starter-image-evening-blue-sky`,
|
|
byteLength: 509_472,
|
|
metadata: {
|
|
kind: "image",
|
|
width: STARTER_ENVIRONMENT_WIDTH,
|
|
height: STARTER_ENVIRONMENT_HEIGHT,
|
|
hasAlpha: false,
|
|
warnings: []
|
|
}
|
|
},
|
|
folderName: "evening_blue_sky"
|
|
},
|
|
{
|
|
asset: {
|
|
id: STARTER_NIGHT_ENVIRONMENT_ASSET_ID,
|
|
kind: "image",
|
|
sourceName: "Starter Snow Mountains Night.hdr",
|
|
mimeType: "image/vnd.radiance",
|
|
storageKey: `${STARTER_ENVIRONMENT_STORAGE_KEY_PREFIX}${STARTER_NIGHT_ENVIRONMENT_ASSET_ID}`,
|
|
byteLength: 1_394_352,
|
|
metadata: {
|
|
kind: "image",
|
|
width: STARTER_ENVIRONMENT_WIDTH,
|
|
height: STARTER_ENVIRONMENT_HEIGHT,
|
|
hasAlpha: false,
|
|
warnings: []
|
|
}
|
|
},
|
|
folderName: "snow_mountains_night"
|
|
}
|
|
] as const;
|
|
|
|
function cloneStarterEnvironmentAsset(
|
|
definition: StarterEnvironmentAssetDefinition
|
|
): ImageAssetRecord {
|
|
return cloneProjectAssetRecord(definition.asset) as ImageAssetRecord;
|
|
}
|
|
|
|
function cloneWorldBackground(background: WorldBackgroundSettings) {
|
|
if (background.mode === "image") {
|
|
return {
|
|
mode: "image" as const,
|
|
assetId: background.assetId,
|
|
environmentIntensity: background.environmentIntensity
|
|
};
|
|
}
|
|
|
|
if (background.mode === "shader") {
|
|
return {
|
|
mode: "shader" as const
|
|
};
|
|
}
|
|
|
|
if (background.mode === "solid") {
|
|
return {
|
|
mode: "solid" as const,
|
|
colorHex: background.colorHex
|
|
};
|
|
}
|
|
|
|
return {
|
|
mode: "verticalGradient" as const,
|
|
topColorHex: background.topColorHex,
|
|
bottomColorHex: background.bottomColorHex
|
|
};
|
|
}
|
|
|
|
function createStarterEnvironmentBackground(
|
|
assetId: string,
|
|
environmentIntensity: number
|
|
) {
|
|
return {
|
|
mode: "image" as const,
|
|
assetId,
|
|
environmentIntensity
|
|
};
|
|
}
|
|
|
|
function isDefaultDayBackground(background: WorldBackgroundSettings): boolean {
|
|
return areWorldBackgroundSettingsEqual(
|
|
background,
|
|
createDefaultWorldSettings().background
|
|
);
|
|
}
|
|
|
|
function isDefaultNightBackground(
|
|
background: WorldBackgroundSettings
|
|
): boolean {
|
|
return areWorldBackgroundSettingsEqual(
|
|
background,
|
|
createDefaultWorldTimeOfDaySettings().night.background
|
|
);
|
|
}
|
|
|
|
export function createStarterEnvironmentAssetRegistry(): Record<
|
|
string,
|
|
ImageAssetRecord
|
|
> {
|
|
return Object.fromEntries(
|
|
STARTER_ENVIRONMENT_LIBRARY.map((definition) => [
|
|
definition.asset.id,
|
|
cloneStarterEnvironmentAsset(definition)
|
|
])
|
|
);
|
|
}
|
|
|
|
export function isStarterEnvironmentImageAsset(
|
|
asset: ProjectAssetRecord | ImageAssetRecord
|
|
): boolean {
|
|
return (
|
|
asset.kind === "image" &&
|
|
asset.storageKey.startsWith(STARTER_ENVIRONMENT_STORAGE_KEY_PREFIX)
|
|
);
|
|
}
|
|
|
|
function getStarterEnvironmentDefinitionById(assetId: string) {
|
|
return (
|
|
STARTER_ENVIRONMENT_LIBRARY.find(
|
|
(definition) => definition.asset.id === assetId
|
|
) ?? null
|
|
);
|
|
}
|
|
|
|
function getStarterEnvironmentDefinition(asset: ImageAssetRecord) {
|
|
return getStarterEnvironmentDefinitionById(asset.id);
|
|
}
|
|
|
|
export function getStarterEnvironmentTextureUrl(
|
|
asset: ImageAssetRecord
|
|
): string {
|
|
const definition = getStarterEnvironmentDefinition(asset);
|
|
|
|
if (definition === null) {
|
|
throw new Error(`Unknown starter environment image asset ${asset.id}.`);
|
|
}
|
|
|
|
return `${STARTER_ENVIRONMENT_DIRECTORY}/${definition.folderName}/environment.hdr`;
|
|
}
|
|
|
|
export function getStarterEnvironmentPreviewUrl(
|
|
asset: ImageAssetRecord
|
|
): string {
|
|
const definition = getStarterEnvironmentDefinition(asset);
|
|
|
|
if (definition === null) {
|
|
throw new Error(`Unknown starter environment image asset ${asset.id}.`);
|
|
}
|
|
|
|
return `${STARTER_ENVIRONMENT_DIRECTORY}/${definition.folderName}/preview.jpg`;
|
|
}
|
|
|
|
export function mergeStarterEnvironmentAssets(
|
|
assets: Record<string, ProjectAssetRecord>
|
|
): Record<string, ProjectAssetRecord> {
|
|
const mergedAssets: Record<string, ProjectAssetRecord> = {};
|
|
|
|
for (const definition of STARTER_ENVIRONMENT_LIBRARY) {
|
|
mergedAssets[definition.asset.id] =
|
|
cloneStarterEnvironmentAsset(definition);
|
|
}
|
|
|
|
for (const [assetId, asset] of Object.entries(assets)) {
|
|
mergedAssets[assetId] = cloneProjectAssetRecord(asset);
|
|
}
|
|
|
|
return mergedAssets;
|
|
}
|
|
|
|
export function applyStarterEnvironmentAssetsToProjectDocument(
|
|
document: ProjectDocument
|
|
): ProjectDocument {
|
|
const mergedAssets = mergeStarterEnvironmentAssets(document.assets);
|
|
|
|
const nextScenes = Object.fromEntries(
|
|
Object.entries(document.scenes).map(([sceneId, scene]) => {
|
|
const nextWorld = cloneWorldSettings(scene.world);
|
|
|
|
if (isDefaultDayBackground(nextWorld.background)) {
|
|
nextWorld.background = createStarterEnvironmentBackground(
|
|
STARTER_DAY_ENVIRONMENT_ASSET_ID,
|
|
DEFAULT_DAY_ENVIRONMENT_INTENSITY
|
|
);
|
|
} else {
|
|
nextWorld.background = cloneWorldBackground(nextWorld.background);
|
|
}
|
|
|
|
if (isDefaultNightBackground(nextWorld.timeOfDay.night.background)) {
|
|
nextWorld.timeOfDay.night.background =
|
|
createStarterEnvironmentBackground(
|
|
STARTER_NIGHT_ENVIRONMENT_ASSET_ID,
|
|
DEFAULT_NIGHT_IMAGE_ENVIRONMENT_INTENSITY
|
|
);
|
|
} else {
|
|
nextWorld.timeOfDay.night.background = cloneWorldBackground(
|
|
nextWorld.timeOfDay.night.background
|
|
);
|
|
}
|
|
|
|
return [
|
|
sceneId,
|
|
{
|
|
...scene,
|
|
world: nextWorld
|
|
}
|
|
];
|
|
})
|
|
);
|
|
|
|
return {
|
|
...document,
|
|
assets: mergedAssets,
|
|
scenes: nextScenes
|
|
};
|
|
}
|