import { DtsFile , MeshType , DtsParser } from "./parsing/dts_parser" ; import { ResourceManager } from "./resources" ; import { IflParser } from "./parsing/ifl_parser" ; import { Util } from "./util" ; import { TimeState , Level } from "./level" ; import { INTERIOR_DEFAULT_RESTITUTION , INTERIOR_DEFAULT_FRICTION } from "./interior" ; import { AudioManager } from "./audio" ; import { MissionElement } from "./parsing/mis_parser" ; import { Group } from "./rendering/group" ; import { Material } from "./rendering/material" ; import { Geometry } from "./rendering/geometry" ; import { Mesh } from "./rendering/mesh" ; import { Texture } from "./rendering/texture" ; import { RigidBody , RigidBodyType } from "./physics/rigid_body" ; import { CollisionShape , ConvexHullCollisionShape } from "./physics/collision_shape" ; import { Collision } from "./physics/collision" ; import { Vector3 } from "./math/vector3" ; import { Quaternion } from "./math/quaternion" ; import { Matrix4 } from "./math/matrix4" ; import { Box3 } from "./math/box3" ; import { BlendingType } from "./rendering/renderer" ; /** A hardcoded list of shapes that should only use envmaps as textures. */ const DROP_TEXTURE_FOR_ENV_MAP = new Set ( [ 'shapes/items/superjump.dts' , 'shapes/items/antigravity.dts' ] ) ; interface MaterialInfo { keyframes : string [ ] ; } /** Data necessary for updating skinned meshes. */ interface SkinMeshData { meshIndex : number ; vertices : Vector3 [ ] ; normals : Vector3 [ ] ; mesh : Mesh ; } interface ColliderInfo { generateShape : ( scale : Vector3 ) => CollisionShape ; body : RigidBody ; transform : Matrix4 ; } /** Data that is shared with other shapes of the same type. */ export interface SharedShapeData { materials : Material [ ] ; geometries : Geometry [ ] ; geometryMatrixIndices : number [ ] ; collisionGeometries : Set < Geometry > ; nodeTransforms : Matrix4 [ ] ; rootGraphNodes : GraphNode [ ] ; skinnedMeshIndex : number ; } enum MaterialFlags { S_Wrap = 1 << 0 , T_Wrap = 1 << 1 , Translucent = 1 << 2 , Additive = 1 << 3 , Subtractive = 1 << 4 , SelfIlluminating = 1 << 5 , NeverEnvMap = 1 << 6 , NoMipMap = 1 << 7 , MipMap_ZeroBorder = 1 << 8 , IflMaterial = 1 << 27 , IflFrame = 1 << 28 , DetailMapOnly = 1 << 29 , BumpMapOnly = 1 << 30 , ReflectanceMapOnly = 1 << 31 } enum TSDrawPrimitive { Triangles = 0 << 30 , Strip = 1 << 30 , Fan = 2 << 30 , Indexed = 1 << 29 , NoMaterial = 1 << 28 , MaterialMask = ~ ( TSDrawPrimitive . Strip | TSDrawPrimitive . Fan | TSDrawPrimitive . Triangles | TSDrawPrimitive . Indexed | TSDrawPrimitive . NoMaterial ) , TypeMask = TSDrawPrimitive . Strip | TSDrawPrimitive . Fan | TSDrawPrimitive . Triangles } /** Used to model the graph of DTS nodes. */ export interface GraphNode { index : number ; node : DtsFile [ "nodes" ] [ number ] ; children : GraphNode [ ] ; parent ? : GraphNode ; } /** Represents an object created from a DTS file. This is either a static object like the start pad or a sign, or an item like gems or powerups. */ export class Shape { /** The unique id of this shape. */ id : number ; level : Level ; srcElement : MissionElement ; dtsPath : string ; colliderDtsPath : string ; dts : DtsFile ; colliderDts : DtsFile ; directoryPath : string ; /** Whether or not this shape is being used as a TSStatic. TSStatic are static, non-moving shapes that basically can't do anything. */ isTSStatic = false ; group : Group ; meshes : Mesh [ ] = [ ] ; bodies : RigidBody [ ] ; /** Whether the marble can physically collide with this shape. */ collideable = true ; /** Not physical colliders, but a list bodies that overlap is checked with. This is used for things like force fields. */ colliders : ColliderInfo [ ] = [ ] ; /** For each shape, the untransformed vertices of their convex hull geometry. */ shapeVertices = new Map < CollisionShape , Vector3 [ ] > ( ) ; isCurrentlyColliding = false ; worldPosition = new Vector3 ( ) ; worldOrientation = new Quaternion ( ) ; worldScale = new Vector3 ( ) ; worldMatrix = new Matrix4 ( ) ; /** Can be used to override certain material names. */ matNamesOverride : Record < string , string | Texture > = { } ; materials : Material [ ] ; /** Stores information used to animate materials. */ materialInfo : WeakMap < Material , MaterialInfo > ; castShadows = false ; /** Stores all nodes from the node tree. */ graphNodes : GraphNode [ ] ; /** Stores only the roots of the tree (no parent). */ rootGraphNodes : GraphNode [ ] = [ ] ; /** One transformation matrix per DTS node */ nodeTransforms : Matrix4 [ ] = [ ] ; skinMeshInfo : SkinMeshData ; showSequences = true ; /** If the element has non-visual sequences, then these should be updated every simulation tick as well. */ hasNonVisualSequences = false ; /** Can be used to override the current keyframe of a sequence. */ sequenceKeyframeOverride = new WeakMap < DtsFile [ "sequences" ] [ number ] , number > ( ) ; /** Stores the last-used keyframe of a sequence to reduce computational load. */ lastSequenceKeyframes = new WeakMap < DtsFile [ "sequences" ] [ number ] , number > ( ) ; currentOpacity = 1 ; restitution = INTERIOR_DEFAULT_RESTITUTION ; friction = INTERIOR_DEFAULT_FRICTION ; /** Whether or not to continuously spin. */ ambientRotate = false ; ambientSpinFactor = - 1 / 3000 * Math . PI * 2 ; /** Whether or not collision meshes will receive shadows. */ receiveShadows = true ; materialPostprocessor : ( mat : Material ) => void = null ; /** Same shapes with a different shareId cannot share data. */ shareId = 0 ; /** Whether or not to share the same node transforms with other shapes of the same type. */ shareNodeTransforms = true ; /** Whether or not to share the same materials with other shapes of the same type. */ shareMaterials = true ; /** A shape is a master if it was the first shape to run init() amongst those that share data with it. */ isMaster = false ; sounds : string [ ] = [ ] ; /** Shapes with identical share hash can share data. */ getShareHash ( ) { return this . dtsPath + ' ' + this . constructor . name + ' ' + this . shareId ; } async init ( level ? , srcElement = null ) { this . id = srcElement ?. _id ?? 0 ; this . level = level ; this . srcElement = srcElement ; this . colliderDtsPath ??= this . dtsPath ; this . dts = await ( ( this . level ) ? this . level . mission . getDts ( this . dtsPath ) : DtsParser . loadFile ( ResourceManager . mainDataPath + this . dtsPath ) ) ; this . colliderDts = ( this . dtsPath === this . colliderDtsPath ) ? this . dts : await ( ( this . level ) ? this . level . mission . getDts ( this . colliderDtsPath ) : DtsParser . loadFile ( ResourceManager . mainDataPath + this . colliderDtsPath ) ) ; this . directoryPath = this . dtsPath . slice ( 0 , this . dtsPath . lastIndexOf ( '/' ) ) ; this . group = new Group ( ) ; this . bodies = [ ] ; this . materials = [ ] ; this . materialInfo = new WeakMap ( ) ; let sharedDataPromise = this . level ?. sharedShapeData . get ( this . getShareHash ( ) ) ; let sharedData ; if ( sharedDataPromise ) { sharedData = await sharedDataPromise ; } else { let resolveFunc ; if ( this . level ) { sharedDataPromise = new Promise < SharedShapeData > ( ( resolve ) => resolveFunc = resolve ) ; this . level . sharedShapeData . set ( this . getShareHash ( ) , sharedDataPromise ) ; } for ( let i = 0 ; i < this . dts . nodes . length ; i ++ ) this . nodeTransforms . push ( new Matrix4 ( ) ) ; await this . computeMaterials ( ) ; let graphNodes = [ ] ; for ( let i = 0 ; i < this . dts . nodes . length ; i ++ ) { let graphNode = { index : i , node : this . dts . nodes [ i ] , children : [ ] , parent : null } ; graphNodes . push ( graphNode ) ; } for ( let i = 0 ; i < this . dts . nodes . length ; i ++ ) { let node = this . dts . nodes [ i ] ; if ( node . parentIndex !== - 1 ) { graphNodes [ i ] . parent = graphNodes [ node . parentIndex ] ; graphNodes [ node . parentIndex ] . children . push ( graphNodes [ i ] ) ; } } this . graphNodes = graphNodes ; this . rootGraphNodes = graphNodes . filter ( ( node ) => ! node . parent ) ; this . updateNodeTransforms ( ) ; let geometries = [ ] ; let geometryMatrixIndices = [ ] ; let collisionGeometries = new Set < Geometry > ( ) ; for ( let i = 0 ; i < this . dts . nodes . length ; i ++ ) { let objects = this . dts . objects . filter ( ( object ) => object . nodeIndex === i ) ; for ( let object of objects ) { let isCollisionObject = this . dts . names [ object . nameIndex ] . toLowerCase ( ) . startsWith ( "col" ) ; if ( ! isCollisionObject || this . collideable ) { for ( let j = object . startMeshIndex ; j < object . startMeshIndex + object . numMeshes ; j ++ ) { let mesh = this . dts . meshes [ j ] ; if ( ! mesh ) continue ; if ( mesh . parentMesh >= 0 ) continue ; if ( mesh . verts . length === 0 ) continue ; let vertices = mesh . verts . map ( ( v ) => new Vector3 ( v . x , v . y , v . z ) ) ; let vertexNormals = mesh . norms . map ( ( v ) => new Vector3 ( v . x , v . y , v . z ) ) ; let geometry = this . generateGeometryFromMesh ( mesh , vertices , vertexNormals ) ; geometries . push ( geometry ) ; geometryMatrixIndices . push ( i ) ; if ( isCollisionObject ) collisionGeometries . add ( geometry ) ; } } } } let skinnedMeshIndex = null ; for ( let i = 0 ; i < this . dts . meshes . length ; i ++ ) { let dtsMesh = this . dts . meshes [ i ] ; if ( ! dtsMesh || dtsMesh . type !== MeshType . Skin ) continue ; let vertices = new Array ( dtsMesh . verts . length ) . fill ( null ) . map ( ( ) => new Vector3 ( ) ) ; let vertexNormals = new Array ( dtsMesh . norms . length ) . fill ( null ) . map ( ( ) => new Vector3 ( ) ) ; let geometry = this . generateGeometryFromMesh ( dtsMesh , vertices , vertexNormals ) ; geometries . push ( geometry ) ; geometryMatrixIndices . push ( null ) ; skinnedMeshIndex = i ; break ; } sharedData = { materials : this . materials , rootGraphNodes : this . rootGraphNodes , nodeTransforms : this . nodeTransforms , geometries , geometryMatrixIndices , collisionGeometries , skinnedMeshIndex } ; this . isMaster = true ; resolveFunc ?. ( sharedData ) ; } if ( ! this . isMaster ) { this . nodeTransforms = sharedData . nodeTransforms ; if ( ! this . shareNodeTransforms ) this . nodeTransforms = this . nodeTransforms . map ( x => x . clone ( ) ) ; if ( this . shareMaterials ) this . materials = sharedData . materials ; else await this . computeMaterials ( ) ; this . rootGraphNodes = sharedData . rootGraphNodes ; } for ( let [ i , geometry ] of sharedData . geometries . entries ( ) ) { let materials = this . materials ; if ( sharedData . collisionGeometries . has ( geometry ) ) { let shadowMaterial = new Material ( ) ; shadowMaterial . isShadow = true ; shadowMaterial . transparent = true ; shadowMaterial . depthWrite = false ; materials = [ shadowMaterial ] ; geometry . materials . fill ( 0 ) ; } let mesh = new Mesh ( geometry , materials ) ; let transform = this . nodeTransforms [ sharedData . geometryMatrixIndices [ i ] ] ; if ( transform ) mesh . transform = transform ; if ( this . castShadows ) mesh . castShadows = true ; this . group . add ( mesh ) ; this . meshes . push ( mesh ) ; if ( sharedData . skinnedMeshIndex !== null && ! this . skinMeshInfo ) { this . skinMeshInfo = { meshIndex : sharedData . skinnedMeshIndex , vertices : this . dts . meshes [ sharedData . skinnedMeshIndex ] . verts . map ( _ => new Vector3 ( ) ) , normals : this . dts . meshes [ sharedData . skinnedMeshIndex ] . norms . map ( _ => new Vector3 ( ) ) , mesh : mesh } ; } } for ( let i = 0 ; i < this . colliderDts . nodes . length ; i ++ ) { let objects = this . colliderDts . objects . filter ( ( object ) => object . nodeIndex === i ) ; for ( let object of objects ) { let isCollisionObject = this . colliderDts . names [ object . nameIndex ] . toLowerCase ( ) . startsWith ( "col" ) ; if ( isCollisionObject ) { let body = new RigidBody ( ) ; body . type = RigidBodyType . Static ; body . userData = { nodeIndex : i } ; this . bodies . push ( body ) ; } } } if ( this . bodies . length === 0 && ! this . isTSStatic ) { let body = new RigidBody ( ) ; body . type = RigidBodyType . Static ; this . bodies . push ( body ) ; } for ( let body of this . bodies ) { body . onBeforeIntegrate = ( ) => { if ( this . isCurrentlyColliding && body . collisions . length === 0 ) { this . isCurrentlyColliding = false ; this . onMarbleLeave ( ) ; } } ; body . onBeforeCollisionResponse = ( t ) => { if ( ! this . isCurrentlyColliding ) this . onMarbleEnter ( t ) ; this . onMarbleInside ( t ) ; this . isCurrentlyColliding = true ; } ; body . onAfterCollisionResponse = ( ) => { let chosenCollision = body . collisions [ 0 ] ; this . onMarbleContact ( chosenCollision ) ; } ; } await AudioManager . loadBuffers ( this . sounds ) ; if ( this . level ) this . level . loadingState . loaded ++ ; } /** Creates the materials for this shape. */ async computeMaterials ( ) { let environmentMaterial = null ; for ( let i = 0 ; i < this . dts . matNames . length ; i ++ ) { let matName = this . matNamesOverride [ this . dts . matNames [ i ] ] || this . dts . matNames [ i ] ; let flags = this . dts . matFlags [ i ] ; let fullNames = ResourceManager . getFullNamesOf ( this . directoryPath + '/' + matName ) . filter ( ( x ) => ! x . endsWith ( '.dts' ) ) ; let fullName = fullNames . find ( x => x . endsWith ( '.ifl' ) ) || fullNames [ 0 ] ; if ( this . isTSStatic && environmentMaterial && DROP_TEXTURE_FOR_ENV_MAP . has ( this . dtsPath ) ) { this . materials . push ( environmentMaterial ) ; continue ; } let material = new Material ( ) ; if ( ( flags & MaterialFlags . SelfIlluminating ) || environmentMaterial ) material . emissive = true ; this . materials . push ( material ) ; if ( matName instanceof Texture ) { material . diffuseMap = matName ; } else if ( ! fullName || ( this . isTSStatic && ( flags & MaterialFlags . ReflectanceMapOnly ) ) ) { if ( this . isTSStatic ) { material . emissive = true ; if ( flags & MaterialFlags . ReflectanceMapOnly ) environmentMaterial = material ; } } else if ( fullName . endsWith ( '.ifl' ) ) { let keyframes = await IflParser . loadFile ( ResourceManager . mainDataPath + this . directoryPath + '/' + fullName ) ; let fullNameCache = new Map < string , string > ( ) ; keyframes = keyframes . map ( x => { if ( fullNameCache . has ( x ) ) return fullNameCache . get ( x ) ; let fullName = ResourceManager . getFullNamesOf ( this . directoryPath + '/' + x ) . filter ( ( x ) => ! x . endsWith ( '.dts' ) ) [ 0 ] ?? x ; fullNameCache . set ( x , fullName ) ; return fullName ; } ) ; this . materialInfo . set ( material , { keyframes } ) ; let promises = [ ] ; for ( let frame of new Set ( keyframes ) ) { promises . push ( ResourceManager . getTexture ( this . directoryPath + '/' + frame ) ) ; } let textures = await Promise . all ( promises ) ; material . diffuseMap = textures [ 0 ] ; material . differentiator = this . isTSStatic + ResourceManager . mainDataPath + this . directoryPath + '/' + fullName ; } else { let texture = await ResourceManager . getTexture ( this . directoryPath + '/' + fullName ) ; material . diffuseMap = texture ; } if ( flags & MaterialFlags . Translucent ) { material . transparent = true ; material . depthWrite = false ; } if ( flags & MaterialFlags . Additive ) material . blending = BlendingType . Additive ; if ( flags & MaterialFlags . Subtractive ) material . blending = BlendingType . Subtractve ; if ( this . isTSStatic && ! ( flags & MaterialFlags . NeverEnvMap ) ) { material . reflectivity = this . dts . matNames . length === 1 ? 1 : environmentMaterial ? 0.5 : 0.333 ; material . envMap = this . level . envMap ; } this . materialPostprocessor ?. ( material ) ; } if ( this . materials . length === 0 ) { let mat = new Material ( ) ; mat . emissive = true ; mat . envMap = this . level . envMap ; mat . reflectivity = 1 ; this . materials . push ( mat ) ; } } /** Generates geometry info from a given DTS mesh. */ generateGeometryFromMesh ( dtsMesh , vertices , vertexNormals ) { let geometry = new Geometry ( ) ; for ( let i = 0 ; i < vertices . length ; i ++ ) { let vertex = vertices [ i ] ; let uv = dtsMesh . tverts [ i ] ; let normal = vertexNormals [ i ] ; geometry . positions . push ( vertex . x , vertex . y , vertex . z ) ; geometry . normals . push ( normal . x , normal . y , normal . z ) ; geometry . uvs . push ( uv . x , uv . y ) ; } let ab = new Vector3 ( ) ; let ac = new Vector3 ( ) ; const addTriangleFromIndices = ( i1 , i2 , i3 , materialIndex ) => { ab . set ( vertices [ i2 ] . x - vertices [ i1 ] . x , vertices [ i2 ] . y - vertices [ i1 ] . y , vertices [ i2 ] . z - vertices [ i1 ] . z ) ; ac . set ( vertices [ i3 ] . x - vertices [ i1 ] . x , vertices [ i3 ] . y - vertices [ i1 ] . y , vertices [ i3 ] . z - vertices [ i1 ] . z ) ; let normal = ab . cross ( ac ) . normalize ( ) ; let dot1 = normal . dot ( vertexNormals [ i1 ] ) ; let dot2 = normal . dot ( vertexNormals [ i2 ] ) ; let dot3 = normal . dot ( vertexNormals [ i3 ] ) ; if ( ! this . dtsPath . includes ( 'helicopter.dts' ) ) if ( dot1 < 0 && dot2 < 0 && dot3 < 0 ) [ i1 , i3 ] = [ i3 , i1 ] ; geometry . indices . push ( i1 , i2 , i3 ) ; geometry . materials . push ( materialIndex , materialIndex , materialIndex ) ; } ; for ( let primitive of dtsMesh . primitives ) { let materialIndex = primitive . matIndex & TSDrawPrimitive . MaterialMask ; let drawType = primitive . matIndex & TSDrawPrimitive . TypeMask ; if ( drawType === TSDrawPrimitive . Triangles ) { for ( let i = primitive . start ; i < primitive . start + primitive . numElements ; i += 3 ) { let i1 = dtsMesh . indices [ i ] ; let i2 = dtsMesh . indices [ i + 1 ] ; let i3 = dtsMesh . indices [ i + 2 ] ; addTriangleFromIndices ( i1 , i2 , i3 , materialIndex ) ; } } else if ( drawType === TSDrawPrimitive . Strip ) { let k = 0 ; for ( let i = primitive . start ; i < primitive . start + primitive . numElements - 2 ; i ++ ) { let i1 = dtsMesh . indices [ i ] ; let i2 = dtsMesh . indices [ i + 1 ] ; let i3 = dtsMesh . indices [ i + 2 ] ; if ( k % 2 === 0 ) { let temp = i1 ; i1 = i3 ; i3 = temp ; } addTriangleFromIndices ( i1 , i2 , i3 , materialIndex ) ; k ++ ; } } else if ( drawType === TSDrawPrimitive . Fan ) { for ( let i = primitive . start ; i < primitive . start + primitive . numElements - 2 ; i ++ ) { let i1 = dtsMesh . indices [ primitive . start ] ; let i2 = dtsMesh . indices [ i + 1 ] ; let i3 = dtsMesh . indices [ i + 2 ] ; addTriangleFromIndices ( i1 , i2 , i3 , materialIndex ) ; } } } return geometry ; } /** Generates collision objects for this shape. Geometry will be generated later. */ generateCollisionObjects ( ) { let bodyIndex = 0 ; let dts = this . colliderDts ; for ( let i = 0 ; i < dts . nodes . length ; i ++ ) { let objects = dts . objects . filter ( ( object ) => object . nodeIndex === i ) ; for ( let object of objects ) { if ( ! dts . names [ object . nameIndex ] . toLowerCase ( ) . startsWith ( "col" ) ) continue ; let body = this . bodies [ bodyIndex ] ; bodyIndex ++ ; for ( let j = object . startMeshIndex ; j < object . startMeshIndex + object . numMeshes ; j ++ ) { let mesh = dts . meshes [ j ] ; if ( ! mesh ) continue ; for ( let primitive of mesh . primitives ) { let shape = new ConvexHullCollisionShape ( Array ( primitive . numElements ) . fill ( null ) . map ( _ => new Vector3 ( ) ) ) ; shape . restitution = this . restitution ; shape . friction = this . friction ; if ( ! this . collideable ) shape . collisionDetectionMask = 0b10 ; body . addCollisionShape ( shape ) ; let vertices = mesh . indices . slice ( primitive . start , primitive . start + primitive . numElements ) . map ( ( index ) => mesh . verts [ index ] ) . map ( ( vert ) => new Vector3 ( vert . x , vert . y , vert . z ) ) ; this . shapeVertices . set ( shape , vertices ) ; } } } } if ( bodyIndex === 0 && ! this . isTSStatic ) { let body = this . bodies [ 0 ] ; let bounds = new Box3 ( ) ; bounds . min . set ( dts . bounds . min . x , dts . bounds . min . y , dts . bounds . min . z ) ; bounds . max . set ( dts . bounds . max . x , dts . bounds . max . y , dts . bounds . max . z ) ; let shape = new ConvexHullCollisionShape ( Array ( 8 ) . fill ( null ) . map ( _ => new Vector3 ( ) ) ) ; shape . restitution = this . restitution ; shape . friction = this . friction ; if ( ! this . collideable ) shape . collisionDetectionMask = 0b10 ; body . addCollisionShape ( shape ) ; let vertices = Util . getBoxVertices ( bounds ) ; this . shapeVertices . set ( shape , vertices ) ; } } param quaternions param translations param scales param bitfield updateNodeTransforms ( quaternions ? , translations ? , scales ? , bitfield = 0xffffffff ) { if ( ! quaternions ) { quaternions = this . dts . nodes . map ( ( node , index ) => { let rotation = this . dts . defaultRotations [ index ] ; let quaternion = new Quaternion ( rotation . x , rotation . y , rotation . z , rotation . w ) ; quaternion . normalize ( ) ; quaternion . conjugate ( ) ; return quaternion ; } ) ; } if ( ! translations ) { translations = this . dts . nodes . map ( ( node , index ) => { let translation = this . dts . defaultTranslations [ index ] ; return new Vector3 ( translation . x , translation . y , translation . z ) ; } ) ; } if ( ! scales ) { scales = this . dts . nodes . map ( ( ) => { return new Vector3 ( ) . setScalar ( 1 ) ; } ) ; } let utilityMatrix = new Matrix4 ( ) ; const traverse = ( node , needsUpdate ) => { if ( ( ( 1 << node . index ) & bitfield ) !== 0 ) needsUpdate = true ; if ( needsUpdate ) { let mat = this . nodeTransforms [ node . index ] ; if ( ! node . parent ) { mat . identity ( ) ; } else { mat . copy ( this . nodeTransforms [ node . parent . index ] ) ; } utilityMatrix . compose ( translations [ node . index ] , quaternions [ node . index ] , scales [ node . index ] ) ; mat . multiplyMatrices ( mat , utilityMatrix ) ; } for ( let i = 0 ; i < node . children . length ; i ++ ) traverse ( node . children [ i ] , needsUpdate ) ; } ; for ( let i = 0 ; i < this . rootGraphNodes . length ; i ++ ) { let rootNode = this . rootGraphNodes [ i ] ; traverse ( rootNode , false ) ; } } /** Updates the geometries of the bodies matching the bitfield based on node transforms. */ updateCollisionGeometry ( bitfield ) { for ( let i = 0 ; i < this . bodies . length ; i ++ ) { let body = this . bodies [ i ] ; let mat ; if ( body . userData ?. nodeIndex !== undefined ) { if ( ( ( 1 << body . userData . nodeIndex ) & bitfield ) === 0 ) continue ; mat = this . worldMatrix . clone ( ) ; mat . multiplyMatrices ( this . worldMatrix , this . nodeTransforms [ body . userData . nodeIndex ] ) ; } else { mat = this . worldMatrix ; } for ( let shape of ( body . shapes as ConvexHullCollisionShape [ ] ) ) { let vertices = this . shapeVertices . get ( shape ) ; for ( let i = 0 ; i < vertices . length ; i ++ ) { shape . points [ i ] . copy ( vertices [ i ] ) . applyMatrix4 ( mat ) ; } shape . computeLocalBoundingBox ( ) ; } body . syncShapes ( ) ; } } tick ( time , onlyVisual = false ) { let needsSequenceUpdate = true ; if ( ! this . showSequences ) needsSequenceUpdate = false ; if ( ! onlyVisual && ! this . hasNonVisualSequences ) needsSequenceUpdate = false ; if ( needsSequenceUpdate ) { if ( ! this . shareNodeTransforms || this . isMaster ) for ( let sequence of this . dts . sequences ) { let rot = sequence . rotationMatters [ 0 ] ?? 0 ; let trans = sequence . translationMatters [ 0 ] ?? 0 ; let scale = sequence . scaleMatters [ 0 ] ?? 0 ; let affectedCount = 0 ; let completion = time . timeSinceLoad / ( sequence . duration * 1000 ) ; let quaternions ; let translations ; let scales ; let actualKeyframe = this . sequenceKeyframeOverride . get ( sequence ) ?? ( completion * sequence . numKeyframes ) % sequence . numKeyframes ; if ( this . lastSequenceKeyframes . get ( sequence ) === actualKeyframe ) continue ; this . lastSequenceKeyframes . set ( sequence , actualKeyframe ) ; let keyframeLow = Math . floor ( actualKeyframe ) ; let keyframeHigh = Math . ceil ( actualKeyframe ) % sequence . numKeyframes ; let t = ( actualKeyframe - keyframeLow ) % 1 ; if ( rot > 0 ) quaternions = this . dts . nodes . map ( ( node , index ) => { let affected = ( ( 1 << index ) & rot ) !== 0 ; if ( affected ) { let rot1 = this . dts . nodeRotations [ sequence . numKeyframes * affectedCount + keyframeLow ] ; let rot2 = this . dts . nodeRotations [ sequence . numKeyframes * affectedCount + keyframeHigh ] ; let quaternion1 = new Quaternion ( rot1 . x , rot1 . y , rot1 . z , rot1 . w ) ; quaternion1 . normalize ( ) ; quaternion1 . conjugate ( ) ; let quaternion2 = new Quaternion ( rot2 . x , rot2 . y , rot2 . z , rot2 . w ) ; quaternion2 . normalize ( ) ; quaternion2 . conjugate ( ) ; quaternion1 . slerp ( quaternion2 , t ) ; affectedCount ++ ; return quaternion1 ; } else { let rotation = this . dts . defaultRotations [ index ] ; let quaternion = new Quaternion ( rotation . x , rotation . y , rotation . z , rotation . w ) ; quaternion . normalize ( ) ; quaternion . conjugate ( ) ; return quaternion ; } } ) ; affectedCount = 0 ; if ( trans > 0 ) translations = this . dts . nodes . map ( ( node , index ) => { let affected = ( ( 1 << index ) & trans ) !== 0 ; if ( affected ) { let trans1 = this . dts . nodeTranslations [ sequence . numKeyframes * affectedCount + keyframeLow ] ; let trans2 = this . dts . nodeTranslations [ sequence . numKeyframes * affectedCount + keyframeHigh ] ; affectedCount ++ ; return new Vector3 ( Util . lerp ( trans1 . x , trans2 . x , t ) , Util . lerp ( trans1 . y , trans2 . y , t ) , Util . lerp ( trans1 . z , trans2 . z , t ) ) ; } else { let translation = this . dts . defaultTranslations [ index ] ; return new Vector3 ( translation . x , translation . y , translation . z ) ; } } ) ; affectedCount = 0 ; if ( scale > 0 ) scales = this . dts . nodes . map ( ( node , index ) => { let affected = ( ( 1 << index ) & scale ) !== 0 ; if ( affected ) { let scale1 = this . dts . nodeAlignedScales [ sequence . numKeyframes * affectedCount + keyframeLow ] ; let scale2 = this . dts . nodeAlignedScales [ sequence . numKeyframes * affectedCount + keyframeHigh ] ; affectedCount ++ ; return new Vector3 ( Util . lerp ( scale1 . x , scale2 . x , t ) , Util . lerp ( scale1 . y , scale2 . y , t ) , Util . lerp ( scale1 . z , scale2 . z , t ) ) ; } else { return new Vector3 ( ) . setScalar ( 1 ) ; } } ) ; if ( rot | trans | scale ) { this . updateNodeTransforms ( quaternions , translations , scales , rot | trans | scale ) ; if ( ! onlyVisual ) this . updateCollisionGeometry ( rot | trans | scale ) ; } } for ( let mesh of this . meshes ) { mesh . changedTransform ( ) ; } } } render ( time ) { this . tick ( time , true ) ; if ( this . skinMeshInfo && this . isMaster ) { let info = this . skinMeshInfo ; let mesh = this . dts . meshes [ info . meshIndex ] ; for ( let i = 0 ; i < info . vertices . length ; i ++ ) { info . vertices [ i ] . set ( 0 , 0 , 0 ) ; info . normals [ i ] . set ( 0 , 0 , 0 ) ; } let boneTransformations = [ ] ; let boneTransformationsTransposed = [ ] ; for ( let i = 0 ; i < mesh . nodeIndices . length ; i ++ ) { let mat = new Matrix4 ( ) ; mat . elements = mesh . initialTransforms [ i ] . slice ( ) ; mat . transpose ( ) ; mat . multiplyMatrices ( this . nodeTransforms [ mesh . nodeIndices [ i ] ] , mat ) ; boneTransformations . push ( mat ) ; boneTransformationsTransposed . push ( mat . clone ( ) . transpose ( ) ) ; } let vec = new Vector3 ( ) ; let vec2 = new Vector3 ( ) ; for ( let i = 0 ; i < mesh . vertIndices . length ; i ++ ) { let vIndex = mesh . vertIndices [ i ] ; let vertex = mesh . verts [ vIndex ] ; let normal = mesh . norms [ vIndex ] ; vec . set ( vertex . x , vertex . y , vertex . z ) ; vec2 . set ( normal . x , normal . y , normal . z ) ; let mat = boneTransformations [ mesh . boneIndices [ i ] ] ; vec . applyMatrix4 ( mat ) ; vec . multiplyScalar ( mesh . weights [ i ] ) ; Util . m_matF_x_vectorF ( mat , vec2 ) ; vec2 . multiplyScalar ( mesh . weights [ i ] ) ; info . vertices [ vIndex ] . add ( vec ) ; info . normals [ vIndex ] . add ( vec2 ) ; } for ( let i = 0 ; i < info . normals . length ; i ++ ) { let norm = info . normals [ i ] ; let len2 = norm . dot ( norm ) ; if ( len2 > 0.01 ) norm . normalize ( ) ; } let geometry = info . mesh . geometry ; for ( let i = 0 ; i < info . vertices . length ; i ++ ) { let vertex = info . vertices [ i ] ; let normal = info . normals [ i ] ; geometry . positions [ 3 * i + 0 ] = vertex . x ; geometry . positions [ 3 * i + 1 ] = vertex . y ; geometry . positions [ 3 * i + 2 ] = vertex . z ; geometry . normals [ 3 * i + 0 ] = normal . x ; geometry . normals [ 3 * i + 1 ] = normal . y ; geometry . normals [ 3 * i + 2 ] = normal . z ; } } if ( this . skinMeshInfo ) this . skinMeshInfo . mesh . needsVertexBufferUpdate = true ; if ( ! this . shareMaterials || this . isMaster ) for ( let i = 0 ; i < this . materials . length ; i ++ ) { let info = this . materialInfo . get ( this . materials [ i ] ) ; if ( ! info ) continue ; let iflSequence = this . dts . sequences . find ( ( seq ) => seq . iflMatters [ 0 ] > 0 ) ; if ( ! iflSequence || ! this . showSequences ) continue ; let completion = time . timeSinceLoad / ( iflSequence . duration * 1000 ) ; let keyframe = Math . floor ( completion * info . keyframes . length ) % info . keyframes . length ; let currentFile = info . keyframes [ keyframe ] ; let texture = ResourceManager . getTextureFromCache ( this . directoryPath + '/' + currentFile ) ; this . materials [ i ] . diffuseMap = texture ; } if ( this . ambientRotate ) { let spinAnimation = new Quaternion ( ) ; let up = new Vector3 ( 0 , 0 , 1 ) ; spinAnimation . setFromAxisAngle ( up , time . timeSinceLoad * this . ambientSpinFactor ) ; let orientation = this . worldOrientation . clone ( ) ; spinAnimation . multiplyQuaternions ( orientation , spinAnimation ) ; this . group . orientation . copy ( spinAnimation ) ; this . group . recomputeTransform ( ) ; } } /** Updates the transform of the shape's objects and bodies. */ setTransform ( position , orientation , scale ) { let scaleUpdated = scale . clone ( ) . sub ( this . worldScale ) . length ( ) !== 0 ; this . worldPosition = position ; this . worldOrientation = orientation ; this . worldScale = scale ; this . worldMatrix . compose ( position , orientation , scale ) ; this . group . position . copy ( position ) ; this . group . orientation . copy ( orientation ) ; this . group . scale . copy ( scale ) ; this . group . recomputeTransform ( ) ; let colliderMatrix = new Matrix4 ( ) ; colliderMatrix . compose ( this . worldPosition , this . worldOrientation , new Vector3 ( 1 , 1 , 1 ) ) ; for ( let collider of this . colliders ) { let mat = collider . transform . clone ( ) ; mat . multiplyMatrices ( colliderMatrix , mat ) ; let position = new Vector3 ( ) ; let orientation = new Quaternion ( ) ; mat . decompose ( position , orientation , new Vector3 ( ) ) ; collider . body . position . copy ( position ) ; collider . body . orientation . copy ( orientation ) ; while ( collider . body . shapes . length ) collider . body . removeCollisionShape ( collider . body . shapes [ 0 ] ) ; let shape = collider . generateShape ( this . worldScale ) ; shape . collisionDetectionMask = 0b100 ; collider . body . addCollisionShape ( shape ) ; } if ( scaleUpdated ) this . generateCollisionObjects ( ) ; this . updateCollisionGeometry ( 0xffffffff ) ; } /** Sets the opacity of the shape. Since there's no quick and easy way of doing this, this method recursively sets it for all materials. */ setOpacity ( opacity ) { if ( opacity === this . currentOpacity ) return ; this . currentOpacity = opacity ; this . group . setOpacity ( opacity ) ; } /** Adds a collider shape. Whenever the marble overlaps with the shape, a callback is fired. */ addCollider ( generateShape , onInside , localTransform ) { let body = new RigidBody ( ) ; body . type = RigidBodyType . Static ; this . colliders . push ( { generateShape : generateShape , body : body , transform : localTransform } ) ; body . onAfterCollisionResponse = onInside ; } /** Enable or disable collision. */ setCollisionEnabled ( enabled ) { for ( let body of this . bodies ) { body . enabled = enabled ; } } reset ( ) { this . isCurrentlyColliding = false ; } onMarbleContact ( collision ) { } onMarbleInside ( t ) { } onMarbleEnter ( t ) { } onMarbleLeave ( ) { } async onLevelStart ( ) { } }