diff --git a/src/assets/starter-environment-assets.ts b/src/assets/starter-environment-assets.ts new file mode 100644 index 00000000..c62b74ab --- /dev/null +++ b/src/assets/starter-environment-assets.ts @@ -0,0 +1,279 @@ +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 === "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 +): asset is ImageAssetRecord { + 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 +): Record { + const mergedAssets: Record = {}; + + 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 + }; +}