diff --git a/src/document/migrate-scene-document.js b/src/document/migrate-scene-document.js index bfd13186..f7e4f1d0 100644 --- a/src/document/migrate-scene-document.js +++ b/src/document/migrate-scene-document.js @@ -3,8 +3,8 @@ import { createModelInstanceCollisionSettings, createModelInstance, isModelInsta import { isProjectAssetKind } from "../assets/project-assets"; import { createPlayerStartColliderSettings, createInteractableEntity, normalizeEntityName, createPointLightEntity, createPlayerStartEntity, createSoundEmitterEntity, createSpotLightEntity, createTeleportTargetEntity, createTriggerVolumeEntity, isPlayerStartColliderMode } from "../entities/entity-instances"; import { createPlayAnimationInteractionLink, createPlaySoundInteractionLink, createStopAnimationInteractionLink, createStopSoundInteractionLink, createTeleportPlayerInteractionLink, createToggleVisibilityInteractionLink, isInteractionTriggerKind } from "../interactions/interaction-links"; -import { createBoxBrush, createDefaultFaceUvState, DEFAULT_BOX_BRUSH_ROTATION_DEGREES, isBoxFaceId, isFaceUvRotationQuarterTurns, normalizeBrushName } from "./brushes"; -import { BOX_BRUSH_SCENE_DOCUMENT_VERSION, ANIMATION_PLAYBACK_SCENE_DOCUMENT_VERSION, ENTITY_NAMES_SCENE_DOCUMENT_VERSION, ENTITY_SYSTEM_FOUNDATION_SCENE_DOCUMENT_VERSION, FACE_MATERIALS_SCENE_DOCUMENT_VERSION, FIRST_ROOM_POLISH_SCENE_DOCUMENT_VERSION, FOUNDATION_SCENE_DOCUMENT_VERSION, IMPORTED_MODEL_COLLIDERS_SCENE_DOCUMENT_VERSION, LOCAL_LIGHTS_AND_SKYBOX_SCENE_DOCUMENT_VERSION, MODEL_ASSET_PIPELINE_SCENE_DOCUMENT_VERSION, PLAYER_START_COLLIDER_SETTINGS_SCENE_DOCUMENT_VERSION, RUNNER_V1_SCENE_DOCUMENT_VERSION, SPATIAL_AUDIO_SCENE_DOCUMENT_VERSION, SCENE_DOCUMENT_VERSION, TRIGGER_ACTION_TARGET_FOUNDATION_SCENE_DOCUMENT_VERSION, WHITEBOX_FLOAT_TRANSFORM_SCENE_DOCUMENT_VERSION, WORLD_ENVIRONMENT_SCENE_DOCUMENT_VERSION } from "./scene-document"; +import { BOX_VERTEX_IDS, createBoxBrush, createDefaultBoxBrushGeometry, createDefaultFaceUvState, DEFAULT_BOX_BRUSH_ROTATION_DEGREES, isBoxFaceId, isFaceUvRotationQuarterTurns, normalizeBrushName } from "./brushes"; +import { BOX_BRUSH_SCENE_DOCUMENT_VERSION, ANIMATION_PLAYBACK_SCENE_DOCUMENT_VERSION, ENTITY_NAMES_SCENE_DOCUMENT_VERSION, ENTITY_SYSTEM_FOUNDATION_SCENE_DOCUMENT_VERSION, FACE_MATERIALS_SCENE_DOCUMENT_VERSION, FIRST_ROOM_POLISH_SCENE_DOCUMENT_VERSION, FOUNDATION_SCENE_DOCUMENT_VERSION, IMPORTED_MODEL_COLLIDERS_SCENE_DOCUMENT_VERSION, LOCAL_LIGHTS_AND_SKYBOX_SCENE_DOCUMENT_VERSION, MODEL_ASSET_PIPELINE_SCENE_DOCUMENT_VERSION, PLAYER_START_COLLIDER_SETTINGS_SCENE_DOCUMENT_VERSION, RUNNER_V1_SCENE_DOCUMENT_VERSION, SPATIAL_AUDIO_SCENE_DOCUMENT_VERSION, SCENE_DOCUMENT_VERSION, TRIGGER_ACTION_TARGET_FOUNDATION_SCENE_DOCUMENT_VERSION, WHITEBOX_FLOAT_TRANSFORM_SCENE_DOCUMENT_VERSION, WHITEBOX_GEOMETRY_SCENE_DOCUMENT_VERSION, WORLD_ENVIRONMENT_SCENE_DOCUMENT_VERSION } from "./scene-document"; import { createDefaultAdvancedRenderingSettings, isAdvancedRenderingShadowMapSize, isAdvancedRenderingShadowType, isAdvancedRenderingToneMappingMode, isWorldBackgroundMode } from "./world-settings"; function isRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value); @@ -499,6 +499,33 @@ function readBoxBrushFaces(value, label, materials, allowMissingUvState) { negZ: readBrushFace(value.negZ, `${label}.negZ`, materials, allowMissingUvState) }; } +function readBoxBrushGeometry(value, label, size) { + if (value === undefined) { + return createDefaultBoxBrushGeometry(size); + } + if (!isRecord(value)) { + throw new Error(`${label} must be an object.`); + } + if (!isRecord(value.vertices)) { + throw new Error(`${label}.vertices must be an object.`); + } + const extraVertexKeys = Object.keys(value.vertices).filter((vertexId) => !BOX_VERTEX_IDS.includes(vertexId)); + if (extraVertexKeys.length > 0) { + throw new Error(`${label}.vertices contains unsupported vertex ids: ${extraVertexKeys.join(", ")}.`); + } + return { + vertices: { + negX_negY_negZ: readVec3(value.vertices.negX_negY_negZ, `${label}.vertices.negX_negY_negZ`), + posX_negY_negZ: readVec3(value.vertices.posX_negY_negZ, `${label}.vertices.posX_negY_negZ`), + negX_posY_negZ: readVec3(value.vertices.negX_posY_negZ, `${label}.vertices.negX_posY_negZ`), + posX_posY_negZ: readVec3(value.vertices.posX_posY_negZ, `${label}.vertices.posX_posY_negZ`), + negX_negY_posZ: readVec3(value.vertices.negX_negY_posZ, `${label}.vertices.negX_negY_posZ`), + posX_negY_posZ: readVec3(value.vertices.posX_negY_posZ, `${label}.vertices.posX_negY_posZ`), + negX_posY_posZ: readVec3(value.vertices.negX_posY_posZ, `${label}.vertices.negX_posY_posZ`), + posX_posY_posZ: readVec3(value.vertices.posX_posY_posZ, `${label}.vertices.posX_posY_posZ`) + } + }; +} function readBrushes(value, materials, allowMissingUvState) { if (!isRecord(value)) { throw new Error("brushes must be a record."); @@ -522,6 +549,7 @@ function readBrushes(value, materials, allowMissingUvState) { center, rotationDegrees: readOptionalVec3(brushValue.rotationDegrees, `brushes.${brushId}.rotationDegrees`, DEFAULT_BOX_BRUSH_ROTATION_DEGREES), size, + geometry: readBoxBrushGeometry(brushValue.geometry, `brushes.${brushId}.geometry`, size), faces: readBoxBrushFaces(brushValue.faces, `brushes.${brushId}.faces`, materials, allowMissingUvState), layerId: expectOptionalString(brushValue.layerId, `brushes.${brushId}.layerId`), groupId: expectOptionalString(brushValue.groupId, `brushes.${brushId}.groupId`) @@ -1199,7 +1227,7 @@ export function migrateSceneDocument(source) { interactionLinks: readInteractionLinks(source.interactionLinks) }; } - if (source.version !== WHITEBOX_FLOAT_TRANSFORM_SCENE_DOCUMENT_VERSION) { + if (source.version !== WHITEBOX_FLOAT_TRANSFORM_SCENE_DOCUMENT_VERSION && source.version !== WHITEBOX_GEOMETRY_SCENE_DOCUMENT_VERSION) { throw new Error(`Unsupported scene document version: ${String(source.version)}.`); } const materials = readMaterialRegistry(source.materials, "materials"); diff --git a/src/document/scene-document.js b/src/document/scene-document.js index 981ba0bc..5f39b30c 100644 --- a/src/document/scene-document.js +++ b/src/document/scene-document.js @@ -1,6 +1,7 @@ import { cloneMaterialRegistry, createStarterMaterialRegistry } from "../materials/starter-material-library"; import { createDefaultWorldSettings } from "./world-settings"; -export const SCENE_DOCUMENT_VERSION = 18; +export const SCENE_DOCUMENT_VERSION = 19; +export const WHITEBOX_GEOMETRY_SCENE_DOCUMENT_VERSION = 19; export const WHITEBOX_FLOAT_TRANSFORM_SCENE_DOCUMENT_VERSION = 18; export const PLAYER_START_COLLIDER_SETTINGS_SCENE_DOCUMENT_VERSION = 17; export const IMPORTED_MODEL_COLLIDERS_SCENE_DOCUMENT_VERSION = 16; diff --git a/src/runtime-three/runtime-scene-build.js b/src/runtime-three/runtime-scene-build.js index a73c8634..099c53dc 100644 --- a/src/runtime-three/runtime-scene-build.js +++ b/src/runtime-three/runtime-scene-build.js @@ -2,10 +2,11 @@ import { getModelInstances } from "../assets/model-instances"; import { cloneWorldSettings } from "../document/world-settings"; import { getEntityInstances, getPrimaryPlayerStartEntity } from "../entities/entity-instances"; import { getBoxBrushBounds } from "../geometry/box-brush"; +import { buildBoxBrushDerivedMeshData } from "../geometry/box-brush-mesh"; import { buildGeneratedModelCollider } from "../geometry/model-instance-collider-generation"; import { cloneInteractionLink, getInteractionLinks } from "../interactions/interaction-links"; import { cloneMaterialDef } from "../materials/starter-material-library"; -import { cloneFaceUvState } from "../document/brushes"; +import { cloneBoxBrushGeometry, cloneFaceUvState } from "../document/brushes"; import { assertRuntimeSceneBuildable } from "./runtime-scene-validation"; import { FIRST_PERSON_PLAYER_SHAPE } from "./player-collision"; function cloneVec3(vector) { @@ -32,6 +33,7 @@ function buildRuntimeBrush(brush, document) { center: cloneVec3(brush.center), rotationDegrees: cloneVec3(brush.rotationDegrees), size: cloneVec3(brush.size), + geometry: cloneBoxBrushGeometry(brush.geometry), faces: { posX: { materialId: brush.faces.posX.materialId, @@ -68,13 +70,15 @@ function buildRuntimeBrush(brush, document) { } function buildRuntimeCollider(brush) { const bounds = getBoxBrushBounds(brush); + const derivedMesh = buildBoxBrushDerivedMeshData(brush); return { - kind: "box", + kind: "trimesh", source: "brush", brushId: brush.id, center: cloneVec3(brush.center), rotationDegrees: cloneVec3(brush.rotationDegrees), - size: cloneVec3(brush.size), + vertices: derivedMesh.colliderVertices, + indices: derivedMesh.colliderIndices, worldBounds: { min: cloneVec3(bounds.min), max: cloneVec3(bounds.max) diff --git a/src/runtime-three/runtime-scene-validation.js b/src/runtime-three/runtime-scene-validation.js index 68e620c9..6d108c52 100644 --- a/src/runtime-three/runtime-scene-validation.js +++ b/src/runtime-three/runtime-scene-validation.js @@ -1,12 +1,21 @@ import { getModelInstances } from "../assets/model-instances"; import { assertSceneDocumentIsValid, createDiagnostic, formatSceneDiagnosticSummary } from "../document/scene-document-validation"; import { getPrimaryPlayerStartEntity } from "../entities/entity-instances"; +import { validateBoxBrushGeometry } from "../geometry/box-brush-mesh"; import { buildGeneratedModelCollider, ModelColliderGenerationError } from "../geometry/model-instance-collider-generation"; +function validateBrushGeometry(brush, path, diagnostics) { + for (const diagnostic of validateBoxBrushGeometry(brush)) { + diagnostics.push(createDiagnostic("error", diagnostic.code, diagnostic.message, `${path}.geometry`, "build")); + } +} export function validateRuntimeSceneBuild(document, options) { const diagnostics = []; if (options.navigationMode === "firstPerson" && getPrimaryPlayerStartEntity(document.entities) === null) { diagnostics.push(createDiagnostic("error", "missing-player-start", "First-person run requires an authored Player Start. Place one or switch to Orbit Visitor.", "entities", "build")); } + for (const brush of Object.values(document.brushes)) { + validateBrushGeometry(brush, `brushes.${brush.id}`, diagnostics); + } for (const modelInstance of getModelInstances(document.modelInstances)) { const path = `modelInstances.${modelInstance.id}.collision.mode`; const asset = document.assets[modelInstance.assetId]; diff --git a/tests/serialization/scene-document-json.test.ts b/tests/serialization/scene-document-json.test.ts index e9723390..e33ae7b6 100644 --- a/tests/serialization/scene-document-json.test.ts +++ b/tests/serialization/scene-document-json.test.ts @@ -155,11 +155,6 @@ describe("scene document JSON", () => { y: -1.25, z: -1.5 }; - brush.size = { - x: 2.5, - y: 2.25, - z: 2.75 - }; const document = { ...createEmptySceneDocument({ name: "Authored Geometry Scene" }),