@hology/core 0.0.241 → 0.0.242
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/dist/gameplay/services/physics/abstract-physics-system.d.ts +4 -0
- package/dist/gameplay/services/physics/physics-system.d.ts +4 -8
- package/dist/gameplay/services/physics/physics-system.js +1 -1
- package/dist/scene/asset-resource-loader.js +1 -1
- package/dist/scene/materializer.js +1 -1
- package/dist/secondary-motion/runtime.js +1 -1
- package/dist/test/asset-resource-loader-converted-fbx.test.d.ts +2 -0
- package/dist/test/asset-resource-loader-converted-fbx.test.js +4 -0
- package/dist/test/materializer-dynamic-transform.test.d.ts +2 -0
- package/dist/test/materializer-dynamic-transform.test.js +4 -0
- package/package.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{ComputeKernel as e}from"@hology/webgpu-renderer";const t=64;export class WebGpuSecondaryMotionRuntime{constructor(t,n={}){this.device=t,this.collisionColliderData=new Float32Array(96),this.collisionCollidersInitialized=!1,this.collisionBindGroups=new Map,this.integrationParamsData=new ArrayBuffer(80),this.integrationParamsView=new DataView(this.integrationParamsData),this.regions=new Map,this.pendingSteps=0,this.disposed=!1,this.lastExecuteSeconds=null,this.fixedStep=new SecondaryMotionFixedStep(n),this.constraintIterations=Math.max(0,Math.trunc(n.constraintIterations??4)),this.constraintStiffness=Math.max(0,Math.min(1,n.constraintStiffness??.9)),this.autoAdvance=n.autoAdvance??!0,this.integrationKernel=new e(t,{label:"secondary-motion-integrate",code:s,bindGroupLayoutEntries:[i(0,!0),i(1,!1),i(2,!1),i(3,!0),i(4,!0),{binding:5,visibility:GPUShaderStage.COMPUTE,buffer:{type:"uniform"}}]}),this.constraintKernel=new e(t,{label:"secondary-motion-distance-constraints",code:o,bindGroupLayoutEntries:[i(0,!1),i(1,!0),i(2,!0),i(3,!0),{binding:4,visibility:GPUShaderStage.COMPUTE,buffer:{type:"uniform"}}]}),this.packKernel=new e(t,{label:"secondary-motion-pack-render-positions",code:a,bindGroupLayoutEntries:[i(0,!0),i(1,!0),i(2,!1)]}),this.finalizeKernel=new e(t,{label:"secondary-motion-finalize-velocity",code:l,bindGroupLayoutEntries:[i(0,!0),i(1,!0),i(2,!1),{binding:3,visibility:GPUShaderStage.COMPUTE,buffer:{type:"uniform"}}]}),this.collisionKernel=new e(t,{label:"secondary-motion-sphere-collision",code:c,bindGroupLayoutEntries:[i(0,!1),i(1,!0),i(2,!0),i(3,!0),i(4,!0),{binding:5,visibility:GPUShaderStage.COMPUTE,buffer:{type:"uniform"}}]}),this.integrationParams=t.createBuffer({label:"secondary-motion-integration-params",size:80,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),this.collisionSpheres=t.createBuffer({label:"secondary-motion-collision-spheres",size:384,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),this.previousCollisionSpheres=t.createBuffer({label:"secondary-motion-previous-collision-spheres",size:384,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),this.collisionParams=t.createBuffer({label:"secondary-motion-collision-params",size:16,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),t.queue.writeBuffer(this.collisionParams,0,new Uint32Array(4));const r=this.integrationParamsView,d=n.gravity??[0,-9.81,0];r.setFloat32(0,this.fixedStep.step,!0),r.setFloat32(4,Math.max(0,Math.min(1,n.damping??.985)),!0),r.setFloat32(8,Math.max(0,n.attachment??12),!0),r.setFloat32(16,d[0],!0),r.setFloat32(20,d[1],!0),r.setFloat32(24,d[2],!0),r.setFloat32(76,Math.max(1,4*Math.hypot(d[0],d[1],d[2])),!0),t.queue.writeBuffer(this.integrationParams,0,this.integrationParamsData)}setFrameMotion(e){this.writeFrameVector(32,e.linearAcceleration),this.writeFrameVector(48,e.angularVelocity),this.writeFrameVector(64,e.angularAcceleration),this.device.queue.writeBuffer(this.integrationParams,32,this.integrationParamsData,32,48)}setSphereColliders(e){this.setEllipsoidColliders(e.map(e=>({center:e.center,radii:[e.radius,e.radius,e.radius]})))}setEllipsoidColliders(e){const t=Math.min(e.length,8),i=new Float32Array(96);for(let n=0;n<t;n++){const t=e[n],r=12*n;i[r]=t.center[0],i[r+1]=t.center[1],i[r+2]=t.center[2],i[r+4]=Math.max(0,t.radii[0]),i[r+5]=Math.max(0,t.radii[1]),i[r+6]=Math.max(0,t.radii[2]),i[r+8]=t.rotation?.[0]??0,i[r+9]=t.rotation?.[1]??0,i[r+10]=t.rotation?.[2]??0,i[r+11]=t.rotation?.[3]??1}this.collisionCollidersInitialized?this.device.queue.writeBuffer(this.previousCollisionSpheres,0,this.collisionColliderData):(this.device.queue.writeBuffer(this.previousCollisionSpheres,0,i),this.collisionCollidersInitialized=!0),this.collisionColliderData.set(i),this.device.queue.writeBuffer(this.collisionSpheres,0,this.collisionColliderData),this.device.queue.writeBuffer(this.collisionParams,0,new Uint32Array([t,0,0,0]))}advance(e){const t=this.fixedStep.consume(e);return this.pendingSteps=Math.min(this.pendingSteps+t,this.fixedStep.maxSubsteps),t}addRegion(e){if(this.assertActive(),this.regions.has(e.id))throw new Error(`Secondary-motion region "${e.id}" is already registered`);const t=e.particleInverseMass.length,i=e.renderVertexIndices.length;if(e.particlePositions.length!==3*t)throw new Error("Secondary-motion particle positions must contain three values per particle");const s=r(e.particlePositions),o=[this.createStorageBuffer(`${e.id}-positions-a`,s),this.createStorageBuffer(`${e.id}-positions-b`,s)],a=this.createStorageBuffer(`${e.id}-velocities`,new Float32Array(4*t)),c=this.createStorageBuffer(`${e.id}-targets`,e.particlePositions),l=this.createStorageBuffer(`${e.id}-inverse-mass`,e.particleInverseMass),d=this.createStorageBuffer(`${e.id}-render-positions`,new Float32Array(3*i)),u=this.createStorageBuffer(`${e.id}-render-vertex-indices`,e.renderVertexIndices),p=[0,1].map(t=>this.integrationKernel.createBindGroup(this.device,[n(0,o[t]),n(1,a),n(2,o[1-t]),n(3,c),n(4,l),n(5,this.integrationParams)],`${e.id}-integration-${t}`)),f=o.map(e=>this.packKernel.createBindGroup(this.device,[n(0,e),n(1,u),n(2,d)])),h=[0,1].map(e=>this.finalizeKernel.createBindGroup(this.device,[n(0,o[1-e]),n(1,o[e]),n(2,a),n(3,this.integrationParams)])),g=function(e,t){if(0===e.constraints.length)return null;if(e.constraints.length%2!=0)throw new Error("Secondary-motion distance constraints must contain particle pairs");const i=e.constraints.length/2;if(e.constraintRest.length!==i||e.constraintColors.length!==i)throw new Error("Secondary-motion constraint arrays have inconsistent lengths");const n=Array.from({length:i},(e,t)=>t).sort((t,i)=>e.constraintColors[t]-e.constraintColors[i]),r=new Uint32Array(e.constraints.length),s=new Float32Array(i),o=[];let a=-1;for(let c=0;c<i;c++){const i=n[c],l=e.constraintColors[i],d=e.constraints[2*i],u=e.constraints[2*i+1];if(d>=t||u>=t)throw new Error("Secondary-motion constraint references an invalid particle");r[2*c]=d,r[2*c+1]=u,s[c]=e.constraintRest[i],l!==a?(o.push({offset:c,count:1}),a=l):o[o.length-1].count++}return{pairs:r,rest:s,batches:o}}(e,t),m=g?this.createStorageBuffer(`${e.id}-constraints`,g.pairs):null,v=g?this.createStorageBuffer(`${e.id}-constraint-rest`,g.rest):null,y=[];if(g&&m&&v)for(const t of g.batches){const i=this.device.createBuffer({label:`${e.id}-constraint-params`,size:16,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),r=new ArrayBuffer(16),s=new DataView(r);s.setUint32(0,t.offset,!0),s.setUint32(4,t.count,!0),s.setFloat32(8,this.constraintStiffness,!0),this.device.queue.writeBuffer(i,0,r),y.push({count:t.count,params:i,bindGroups:o.map(e=>this.constraintKernel.createBindGroup(this.device,[n(0,e),n(1,l),n(2,m),n(3,v),n(4,i)]))})}const x={id:e.id,particleCount:t,renderVertexCount:i,positions:o,velocities:a,targets:c,inverseMass:l,renderPositions:d,renderVertexIndices:u,integrationBindGroups:p,packBindGroups:f,finalizeBindGroups:h,constraints:m,constraintRest:v,constraintBatches:y,state:0,disposed:!1,renderGeometry:null};this.regions.set(e.id,x);const P=this;return{id:e.id,particleCount:t,renderVertexCount:i,get positionBuffer(){return x.positions[x.state]},setRenderGeometry(e){x.renderGeometry=e},updateTargets(e){P.writeTargets(x.targets,e,t)},reset(t=e.particlePositions){P.resetRegion(x,t)},dispose(){P.removeRegion(x)}}}execute(e){if(this.assertActive(),e.device!==this.device)throw new Error("Secondary-motion runtime was created for a different GPU device");if(this.autoAdvance){const e=.001*performance.now();null!=this.lastExecuteSeconds&&this.advance(e-this.lastExecuteSeconds),this.lastExecuteSeconds=e}const i=this.pendingSteps;if(this.pendingSteps=0,0===this.regions.size)return;const n=e.encoder.beginComputePass({label:"secondary-motion-simulation"});for(let e=0;e<i;e++)for(const e of this.regions.values())if(!e.disposed&&0!==e.particleCount){if(n.setPipeline(this.integrationKernel.pipeline),n.setBindGroup(0,e.integrationBindGroups[e.state]),n.dispatchWorkgroups(Math.ceil(e.particleCount/t)),e.state=0===e.state?1:0,e.constraintBatches.length>0){n.setPipeline(this.constraintKernel.pipeline);for(let i=0;i<this.constraintIterations;i++)for(const i of e.constraintBatches)n.setBindGroup(0,i.bindGroups[e.state]),n.dispatchWorkgroups(Math.ceil(i.count/t))}n.setPipeline(this.collisionKernel.pipeline),n.setBindGroup(0,this.getCollisionBindGroup(e)),n.dispatchWorkgroups(Math.ceil(e.particleCount/t)),n.setPipeline(this.finalizeKernel.pipeline),n.setBindGroup(0,e.finalizeBindGroups[e.state]),n.dispatchWorkgroups(Math.ceil(e.particleCount/t))}for(const e of this.regions.values())e.disposed||0===e.particleCount||null==e.renderGeometry||(n.setPipeline(this.packKernel.pipeline),n.setBindGroup(0,e.packBindGroups[e.state]),n.dispatchWorkgroups(Math.ceil(e.renderVertexCount/t)));n.end();for(const t of this.regions.values())if(!t.disposed&&null!=t.renderGeometry){const i=e.getGeometryVertexBuffer(t.renderGeometry,"position");e.encoder.copyBufferToBuffer(t.renderPositions,0,i,0,12*t.renderVertexCount)}}dispose(){if(!this.disposed){for(const e of Array.from(this.regions.values()))this.removeRegion(e);this.integrationParams.destroy(),this.collisionSpheres.destroy(),this.previousCollisionSpheres.destroy(),this.collisionParams.destroy(),this.collisionBindGroups.clear(),this.disposed=!0}}resetRegion(e,t){this.writeTargets(e.targets,t,e.particleCount);const i=function(e,t){if(e.length!==3*t)throw new Error(`Expected ${3*t} secondary-motion position values, received ${e.length}`);return r(e)}(t,e.particleCount);for(const t of e.positions)this.device.queue.writeBuffer(t,0,i);this.device.queue.writeBuffer(e.velocities,0,new Float32Array(4*e.particleCount)),e.state=0,this.fixedStep.reset(),this.pendingSteps=0}writeTargets(e,t,i){if(t.length!==3*i)throw new Error(`Expected ${3*i} secondary-motion target values, received ${t.length}`);this.device.queue.writeBuffer(e,0,t)}removeRegion(e){if(!e.disposed){e.disposed=!0,this.regions.delete(e.id);for(const t of e.positions)this.collisionBindGroups.delete(t);for(const t of e.positions)t.destroy();e.velocities.destroy(),e.targets.destroy(),e.inverseMass.destroy(),e.renderPositions.destroy(),e.renderVertexIndices.destroy(),e.constraints?.destroy(),e.constraintRest?.destroy();for(const t of e.constraintBatches)t.params.destroy()}}createStorageBuffer(e,t){const i=this.device.createBuffer({label:e,size:Math.max(4,t.byteLength+3&-4),usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST|GPUBufferUsage.COPY_SRC});return t.byteLength>0&&this.device.queue.writeBuffer(i,0,t.buffer,t.byteOffset,t.byteLength),i}getCollisionBindGroup(e){const t=e.positions[e.state];let i=this.collisionBindGroups.get(t);return null==i&&(i=this.collisionKernel.createBindGroup(this.device,[n(0,t),n(1,e.positions[1-e.state]),n(2,e.inverseMass),n(3,this.collisionSpheres),n(4,this.previousCollisionSpheres),n(5,this.collisionParams)],`${e.id}-sphere-collision-${e.state}`),this.collisionBindGroups.set(t,i)),i}assertActive(){if(this.disposed)throw new Error("Secondary-motion runtime is disposed")}writeFrameVector(e,t){this.integrationParamsView.setFloat32(e,t?.[0]??0,!0),this.integrationParamsView.setFloat32(e+4,t?.[1]??0,!0),this.integrationParamsView.setFloat32(e+8,t?.[2]??0,!0)}}export function createSecondaryMotionRenderGraphExtension(e,t="SecondaryMotionSimulation"){const i=i=>{i.addPass({name:t,execute:t=>e.execute(t)})};return i.stage="beforeScene",i}export class SecondaryMotionFixedStep{constructor(e={}){this.accumulator=0,this.step=1/(e.frequency??60),this.maxSubsteps=e.maxSubsteps??4,this.maxFrameDelta=e.maxFrameDelta??.1}consume(e){if(!Number.isFinite(e)||e<0||e>this.maxFrameDelta)return this.accumulator=0,0;this.accumulator=Math.min(this.accumulator+e,this.step*this.maxSubsteps);const t=Math.min(Math.floor(this.accumulator/this.step),this.maxSubsteps);return this.accumulator-=t*this.step,t}reset(){this.accumulator=0}}export function selectSecondaryMotionLod(e,t,i){return t?i?"hero":e<8?"near":e<20?"mid":e<40?"far":"sleep":"sleep"}export function secondaryMotionTransformDiscontinuous(e,t,i=2,n=.5){if(e.length<7||t.length<7)return!0;const r=t[0]-e[0],s=t[1]-e[1],o=t[2]-e[2];if(r*r+s*s+o*o>i*i)return!0;return Math.abs(e[3]*t[3]+e[4]*t[4]+e[5]*t[5]+e[6]*t[6])<n}function i(e,t){return{binding:e,visibility:GPUShaderStage.COMPUTE,buffer:{type:t?"read-only-storage":"storage"}}}function n(e,t){return{binding:e,resource:{buffer:t}}}function r(e){const t=new Float32Array(e.length/3*4);for(let i=0,n=0;i<e.length;i+=3,n+=4)t[n]=e[i],t[n+1]=e[i+1],t[n+2]=e[i+2],t[n+3]=1;return t}const s="\nstruct IntegrationParams {\n deltaTime: f32,\n damping: f32,\n attachment: f32,\n _padding: f32,\n gravity: vec3<f32>,\n _padding2: f32,\n frameLinearAcceleration: vec3<f32>,\n _padding3: f32,\n frameAngularVelocity: vec3<f32>,\n _padding4: f32,\n frameAngularAcceleration: vec3<f32>,\n maxFrameAcceleration: f32,\n}\n@group(0) @binding(0) var<storage, read> currentPositions: array<vec4<f32>>;\n@group(0) @binding(1) var<storage, read_write> velocities: array<vec4<f32>>;\n@group(0) @binding(2) var<storage, read_write> outputPositions: array<vec4<f32>>;\nstruct PackedPosition { x: f32, y: f32, z: f32 }\n@group(0) @binding(3) var<storage, read> targetPositions: array<PackedPosition>;\n@group(0) @binding(4) var<storage, read> inverseMass: array<f32>;\n@group(0) @binding(5) var<uniform> params: IntegrationParams;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n let index = invocation.x;\n if (index >= arrayLength(&inverseMass)) { return; }\n let packedTarget = targetPositions[index];\n let targetPosition = vec3<f32>(packedTarget.x, packedTarget.y, packedTarget.z);\n if (inverseMass[index] <= 0.0) {\n velocities[index] = vec4(0.0);\n outputPositions[index] = vec4(targetPosition, 1.0);\n return;\n }\n let current = currentPositions[index].xyz;\n // Fully simulated cloth should be driven through its structural connection\n // to the animated roots. Pulling every free particle toward its skinned rest\n // pose makes collision sandwich it against the character like shrink-wrap.\n let animationInfluence = 1.0 - clamp(inverseMass[index], 0.0, 1.0);\n let targetAcceleration = (targetPosition - current) * params.attachment * animationInfluence;\n let previousVelocity = velocities[index].xyz;\n let rotationalAcceleration =\n cross(params.frameAngularAcceleration, current) +\n cross(params.frameAngularVelocity, cross(params.frameAngularVelocity, current)) +\n 2.0 * cross(params.frameAngularVelocity, previousVelocity);\n var frameAcceleration = params.frameLinearAcceleration + rotationalAcceleration;\n let frameAccelerationLength = length(frameAcceleration);\n if (frameAccelerationLength > params.maxFrameAcceleration) {\n frameAcceleration *= params.maxFrameAcceleration / frameAccelerationLength;\n }\n let acceleration = params.gravity + targetAcceleration - frameAcceleration;\n let velocity = previousVelocity * params.damping + acceleration * params.deltaTime;\n let next = current + velocity * params.deltaTime;\n velocities[index] = vec4(velocity, 0.0);\n outputPositions[index] = vec4(next, 1.0);\n}\n",o="\nstruct ConstraintParams {\n offset: u32,\n count: u32,\n stiffness: f32,\n _padding: f32,\n}\n@group(0) @binding(0) var<storage, read_write> positions: array<vec4<f32>>;\n@group(0) @binding(1) var<storage, read> inverseMass: array<f32>;\n@group(0) @binding(2) var<storage, read> pairs: array<vec2<u32>>;\n@group(0) @binding(3) var<storage, read> restLengths: array<f32>;\n@group(0) @binding(4) var<uniform> params: ConstraintParams;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n if (invocation.x >= params.count) { return; }\n let constraintIndex = params.offset + invocation.x;\n let pair = pairs[constraintIndex];\n let weightA = inverseMass[pair.x];\n let weightB = inverseMass[pair.y];\n let weight = weightA + weightB;\n if (weight <= 0.0) { return; }\n let delta = positions[pair.y].xyz - positions[pair.x].xyz;\n let length = length(delta);\n if (length <= 0.000001) { return; }\n let correction = delta * ((length - restLengths[constraintIndex]) / length) * params.stiffness;\n positions[pair.x] = vec4(positions[pair.x].xyz + correction * (weightA / weight), 1.0);\n positions[pair.y] = vec4(positions[pair.y].xyz - correction * (weightB / weight), 1.0);\n}\n",a="\nstruct PackedPosition { x: f32, y: f32, z: f32 }\n@group(0) @binding(0) var<storage, read> positions: array<vec4<f32>>;\n@group(0) @binding(1) var<storage, read> particleByRenderVertex: array<u32>;\n@group(0) @binding(2) var<storage, read_write> output: array<PackedPosition>;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n let index = invocation.x;\n if (index >= arrayLength(&particleByRenderVertex)) { return; }\n let position = positions[particleByRenderVertex[index]];\n output[index].x = position.x; output[index].y = position.y; output[index].z = position.z;\n}\n",c="\nstruct CollisionParams { count: u32, _padding1: u32, _padding2: u32, _padding3: u32 }\n@group(0) @binding(0) var<storage, read_write> positions: array<vec4<f32>>;\nstruct Ellipsoid { center: vec4<f32>, radii: vec4<f32>, rotation: vec4<f32> }\n@group(0) @binding(1) var<storage, read> previousPositions: array<vec4<f32>>;\n@group(0) @binding(2) var<storage, read> inverseMass: array<f32>;\n@group(0) @binding(3) var<storage, read> ellipsoids: array<Ellipsoid>;\n@group(0) @binding(4) var<storage, read> previousEllipsoids: array<Ellipsoid>;\n@group(0) @binding(5) var<uniform> params: CollisionParams;\n\nfn rotateByQuaternion(value: vec3<f32>, rotation: vec4<f32>) -> vec3<f32> {\n return value + 2.0 * cross(rotation.xyz, cross(rotation.xyz, value) + rotation.w * value);\n}\n\nfn toEllipsoidSpace(value: vec3<f32>, shape: Ellipsoid) -> vec3<f32> {\n return rotateByQuaternion(value - shape.center.xyz, vec4<f32>(-shape.rotation.xyz, shape.rotation.w)) / shape.radii.xyz;\n}\n\nfn fromEllipsoidSpace(value: vec3<f32>, shape: Ellipsoid) -> vec3<f32> {\n return shape.center.xyz + rotateByQuaternion(value * shape.radii.xyz, shape.rotation);\n}\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n let index = invocation.x;\n if (index >= arrayLength(&inverseMass) || inverseMass[index] <= 0.0) { return; }\n var position = positions[index].xyz;\n for (var collider = 0u; collider < params.count; collider++) {\n let shape = ellipsoids[collider];\n if (all(shape.radii.xyz > vec3<f32>(0.0))) {\n let scaled = toEllipsoidSpace(position, shape);\n let scaledLengthSquared = dot(scaled, scaled);\n if (scaledLengthSquared < 1.0) {\n let direction = select(vec3<f32>(0.0, 0.0, 1.0), scaled * inverseSqrt(scaledLengthSquared), scaledLengthSquared > 0.00000001);\n position = fromEllipsoidSpace(direction * 1.06, shape);\n } else {\n let previousShape = previousEllipsoids[collider];\n let start = toEllipsoidSpace(previousPositions[index].xyz, previousShape);\n let travel = scaled - start;\n let a = dot(travel, travel);\n let b = 2.0 * dot(start, travel);\n let c = dot(start, start) - 1.0;\n let discriminant = b * b - 4.0 * a * c;\n if (a > 0.00000001 && discriminant >= 0.0) {\n let hit = (-b - sqrt(discriminant)) / (2.0 * a);\n if (hit > 0.0001 && hit <= 1.0) {\n let direction = normalize(start + travel * hit);\n position = fromEllipsoidSpace(direction * 1.06, shape);\n }\n }\n }\n }\n }\n positions[index] = vec4(position, 1.0);\n}\n",l="\nstruct IntegrationParams {\n deltaTime: f32,\n damping: f32,\n attachment: f32,\n _padding: f32,\n gravity: vec3<f32>,\n _padding2: f32,\n}\n@group(0) @binding(0) var<storage, read> previousPositions: array<vec4<f32>>;\n@group(0) @binding(1) var<storage, read> constrainedPositions: array<vec4<f32>>;\n@group(0) @binding(2) var<storage, read_write> velocities: array<vec4<f32>>;\n@group(0) @binding(3) var<uniform> params: IntegrationParams;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n let index = invocation.x;\n if (index >= arrayLength(&velocities)) { return; }\n velocities[index] = vec4(\n (constrainedPositions[index].xyz - previousPositions[index].xyz) / params.deltaTime,\n 0.0,\n );\n}\n";/*
|
|
1
|
+
import{ComputeKernel as e}from"@hology/webgpu-renderer";const t=64;export class WebGpuSecondaryMotionRuntime{constructor(t,n={}){this.device=t,this.collisionColliderData=new Float32Array(96),this.collisionCollidersInitialized=!1,this.collisionBindGroups=new Map,this.integrationParamsData=new ArrayBuffer(80),this.integrationParamsView=new DataView(this.integrationParamsData),this.regions=new Map,this.pendingSteps=0,this.disposed=!1,this.lastExecuteSeconds=null,this.fixedStep=new SecondaryMotionFixedStep(n),this.constraintIterations=Math.max(0,Math.trunc(n.constraintIterations??4)),this.constraintStiffness=Math.max(0,Math.min(1,n.constraintStiffness??.9)),this.autoAdvance=n.autoAdvance??!0,this.integrationKernel=new e(t,{label:"secondary-motion-integrate",code:o,bindGroupLayoutEntries:[i(0,!0),i(1,!1),i(2,!1),i(3,!0),i(4,!0),{binding:5,visibility:GPUShaderStage.COMPUTE,buffer:{type:"uniform"}}]}),this.constraintKernel=new e(t,{label:"secondary-motion-distance-constraints",code:s,bindGroupLayoutEntries:[i(0,!1),i(1,!0),i(2,!0),i(3,!0),{binding:4,visibility:GPUShaderStage.COMPUTE,buffer:{type:"uniform"}}]}),this.packKernel=new e(t,{label:"secondary-motion-pack-render-positions",code:a,bindGroupLayoutEntries:[i(0,!0),i(1,!0),i(2,!1)]}),this.finalizeKernel=new e(t,{label:"secondary-motion-finalize-velocity",code:l,bindGroupLayoutEntries:[i(0,!0),i(1,!0),i(2,!1),{binding:3,visibility:GPUShaderStage.COMPUTE,buffer:{type:"uniform"}}]}),this.collisionKernel=new e(t,{label:"secondary-motion-sphere-collision",code:c,bindGroupLayoutEntries:[i(0,!1),i(1,!0),i(2,!0),i(3,!0),i(4,!0),i(5,!1),{binding:6,visibility:GPUShaderStage.COMPUTE,buffer:{type:"uniform"}}]}),this.integrationParams=t.createBuffer({label:"secondary-motion-integration-params",size:80,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),this.collisionSpheres=t.createBuffer({label:"secondary-motion-collision-spheres",size:384,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),this.previousCollisionSpheres=t.createBuffer({label:"secondary-motion-previous-collision-spheres",size:384,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),this.collisionParams=t.createBuffer({label:"secondary-motion-collision-params",size:16,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),t.queue.writeBuffer(this.collisionParams,0,new Uint32Array(4));const r=this.integrationParamsView,d=n.gravity??[0,-9.81,0];r.setFloat32(0,this.fixedStep.step,!0),r.setFloat32(4,Math.max(0,Math.min(1,n.damping??.985)),!0),r.setFloat32(8,Math.max(0,n.attachment??12),!0),r.setFloat32(16,d[0],!0),r.setFloat32(20,d[1],!0),r.setFloat32(24,d[2],!0),r.setFloat32(76,Math.max(1,4*Math.hypot(d[0],d[1],d[2])),!0),t.queue.writeBuffer(this.integrationParams,0,this.integrationParamsData)}setFrameMotion(e){this.writeFrameVector(32,e.linearAcceleration),this.writeFrameVector(48,e.angularVelocity),this.writeFrameVector(64,e.angularAcceleration),this.device.queue.writeBuffer(this.integrationParams,32,this.integrationParamsData,32,48)}setSphereColliders(e){this.setEllipsoidColliders(e.map(e=>({center:e.center,radii:[e.radius,e.radius,e.radius]})))}setEllipsoidColliders(e){const t=Math.min(e.length,8),i=new Float32Array(96);for(let n=0;n<t;n++){const t=e[n],r=12*n;i[r]=t.center[0],i[r+1]=t.center[1],i[r+2]=t.center[2],i[r+4]=Math.max(0,t.radii[0]),i[r+5]=Math.max(0,t.radii[1]),i[r+6]=Math.max(0,t.radii[2]),i[r+8]=t.rotation?.[0]??0,i[r+9]=t.rotation?.[1]??0,i[r+10]=t.rotation?.[2]??0,i[r+11]=t.rotation?.[3]??1}this.collisionCollidersInitialized?this.device.queue.writeBuffer(this.previousCollisionSpheres,0,this.collisionColliderData):(this.device.queue.writeBuffer(this.previousCollisionSpheres,0,i),this.collisionCollidersInitialized=!0),this.collisionColliderData.set(i),this.device.queue.writeBuffer(this.collisionSpheres,0,this.collisionColliderData),this.device.queue.writeBuffer(this.collisionParams,0,new Uint32Array([t,0,0,0]))}advance(e){const t=this.fixedStep.consume(e);return this.pendingSteps=Math.min(this.pendingSteps+t,this.fixedStep.maxSubsteps),t}addRegion(e){if(this.assertActive(),this.regions.has(e.id))throw new Error(`Secondary-motion region "${e.id}" is already registered`);const t=e.particleInverseMass.length,i=e.renderVertexIndices.length;if(e.particlePositions.length!==3*t)throw new Error("Secondary-motion particle positions must contain three values per particle");const o=r(e.particlePositions),s=[this.createStorageBuffer(`${e.id}-positions-a`,o),this.createStorageBuffer(`${e.id}-positions-b`,o)],a=this.createStorageBuffer(`${e.id}-velocities`,new Float32Array(4*t)),c=this.createStorageBuffer(`${e.id}-targets`,e.particlePositions),l=this.createStorageBuffer(`${e.id}-inverse-mass`,e.particleInverseMass),d=this.createStorageBuffer(`${e.id}-render-positions`,new Float32Array(3*i)),u=this.createStorageBuffer(`${e.id}-render-vertex-indices`,e.renderVertexIndices),p=[0,1].map(t=>this.integrationKernel.createBindGroup(this.device,[n(0,s[t]),n(1,a),n(2,s[1-t]),n(3,c),n(4,l),n(5,this.integrationParams)],`${e.id}-integration-${t}`)),f=s.map(e=>this.packKernel.createBindGroup(this.device,[n(0,e),n(1,u),n(2,d)])),h=[0,1].map(e=>this.finalizeKernel.createBindGroup(this.device,[n(0,s[1-e]),n(1,s[e]),n(2,a),n(3,this.integrationParams)])),g=function(e,t){if(0===e.constraints.length)return null;if(e.constraints.length%2!=0)throw new Error("Secondary-motion distance constraints must contain particle pairs");const i=e.constraints.length/2;if(e.constraintRest.length!==i||e.constraintColors.length!==i)throw new Error("Secondary-motion constraint arrays have inconsistent lengths");const n=Array.from({length:i},(e,t)=>t).sort((t,i)=>e.constraintColors[t]-e.constraintColors[i]),r=new Uint32Array(e.constraints.length),o=new Float32Array(i),s=[];let a=-1;for(let c=0;c<i;c++){const i=n[c],l=e.constraintColors[i],d=e.constraints[2*i],u=e.constraints[2*i+1];if(d>=t||u>=t)throw new Error("Secondary-motion constraint references an invalid particle");r[2*c]=d,r[2*c+1]=u,o[c]=e.constraintRest[i],l!==a?(s.push({offset:c,count:1}),a=l):s[s.length-1].count++}return{pairs:r,rest:o,batches:s}}(e,t),m=g?this.createStorageBuffer(`${e.id}-constraints`,g.pairs):null,v=g?this.createStorageBuffer(`${e.id}-constraint-rest`,g.rest):null,y=[];if(g&&m&&v)for(const t of g.batches){const i=this.device.createBuffer({label:`${e.id}-constraint-params`,size:16,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),r=new ArrayBuffer(16),o=new DataView(r);o.setUint32(0,t.offset,!0),o.setUint32(4,t.count,!0),o.setFloat32(8,this.constraintStiffness,!0),this.device.queue.writeBuffer(i,0,r),y.push({count:t.count,params:i,bindGroups:s.map(e=>this.constraintKernel.createBindGroup(this.device,[n(0,e),n(1,l),n(2,m),n(3,v),n(4,i)]))})}const x={id:e.id,particleCount:t,renderVertexCount:i,positions:s,velocities:a,targets:c,inverseMass:l,renderPositions:d,renderVertexIndices:u,integrationBindGroups:p,packBindGroups:f,finalizeBindGroups:h,constraints:m,constraintRest:v,constraintBatches:y,state:0,disposed:!1,renderGeometry:null};this.regions.set(e.id,x);const P=this;return{id:e.id,particleCount:t,renderVertexCount:i,get positionBuffer(){return x.positions[x.state]},setRenderGeometry(e){x.renderGeometry=e},updateTargets(e){P.writeTargets(x.targets,e,t)},reset(t=e.particlePositions){P.resetRegion(x,t)},dispose(){P.removeRegion(x)}}}execute(e){if(this.assertActive(),e.device!==this.device)throw new Error("Secondary-motion runtime was created for a different GPU device");if(this.autoAdvance){const e=.001*performance.now();null!=this.lastExecuteSeconds&&this.advance(e-this.lastExecuteSeconds),this.lastExecuteSeconds=e}const i=this.pendingSteps;if(this.pendingSteps=0,0===this.regions.size)return;const n=e.encoder.beginComputePass({label:"secondary-motion-simulation"});for(let e=0;e<i;e++)for(const e of this.regions.values())if(!e.disposed&&0!==e.particleCount){if(n.setPipeline(this.integrationKernel.pipeline),n.setBindGroup(0,e.integrationBindGroups[e.state]),n.dispatchWorkgroups(Math.ceil(e.particleCount/t)),e.state=0===e.state?1:0,e.constraintBatches.length>0){n.setPipeline(this.constraintKernel.pipeline);for(let i=0;i<this.constraintIterations;i++)for(const i of e.constraintBatches)n.setBindGroup(0,i.bindGroups[e.state]),n.dispatchWorkgroups(Math.ceil(i.count/t))}n.setPipeline(this.finalizeKernel.pipeline),n.setBindGroup(0,e.finalizeBindGroups[e.state]),n.dispatchWorkgroups(Math.ceil(e.particleCount/t)),n.setPipeline(this.collisionKernel.pipeline),n.setBindGroup(0,this.getCollisionBindGroup(e)),n.dispatchWorkgroups(Math.ceil(e.particleCount/t))}for(const e of this.regions.values())e.disposed||0===e.particleCount||null==e.renderGeometry||(n.setPipeline(this.packKernel.pipeline),n.setBindGroup(0,e.packBindGroups[e.state]),n.dispatchWorkgroups(Math.ceil(e.renderVertexCount/t)));n.end();for(const t of this.regions.values())if(!t.disposed&&null!=t.renderGeometry){const i=e.getGeometryVertexBuffer(t.renderGeometry,"position");e.encoder.copyBufferToBuffer(t.renderPositions,0,i,0,12*t.renderVertexCount)}}dispose(){if(!this.disposed){for(const e of Array.from(this.regions.values()))this.removeRegion(e);this.integrationParams.destroy(),this.collisionSpheres.destroy(),this.previousCollisionSpheres.destroy(),this.collisionParams.destroy(),this.collisionBindGroups.clear(),this.disposed=!0}}resetRegion(e,t){this.writeTargets(e.targets,t,e.particleCount);const i=function(e,t){if(e.length!==3*t)throw new Error(`Expected ${3*t} secondary-motion position values, received ${e.length}`);return r(e)}(t,e.particleCount);for(const t of e.positions)this.device.queue.writeBuffer(t,0,i);this.device.queue.writeBuffer(e.velocities,0,new Float32Array(4*e.particleCount)),e.state=0,this.fixedStep.reset(),this.pendingSteps=0}writeTargets(e,t,i){if(t.length!==3*i)throw new Error(`Expected ${3*i} secondary-motion target values, received ${t.length}`);this.device.queue.writeBuffer(e,0,t)}removeRegion(e){if(!e.disposed){e.disposed=!0,this.regions.delete(e.id);for(const t of e.positions)this.collisionBindGroups.delete(t);for(const t of e.positions)t.destroy();e.velocities.destroy(),e.targets.destroy(),e.inverseMass.destroy(),e.renderPositions.destroy(),e.renderVertexIndices.destroy(),e.constraints?.destroy(),e.constraintRest?.destroy();for(const t of e.constraintBatches)t.params.destroy()}}createStorageBuffer(e,t){const i=this.device.createBuffer({label:e,size:Math.max(4,t.byteLength+3&-4),usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST|GPUBufferUsage.COPY_SRC});return t.byteLength>0&&this.device.queue.writeBuffer(i,0,t.buffer,t.byteOffset,t.byteLength),i}getCollisionBindGroup(e){const t=e.positions[e.state];let i=this.collisionBindGroups.get(t);return null==i&&(i=this.collisionKernel.createBindGroup(this.device,[n(0,t),n(1,e.positions[1-e.state]),n(2,e.inverseMass),n(3,this.collisionSpheres),n(4,this.previousCollisionSpheres),n(5,e.velocities),n(6,this.collisionParams)],`${e.id}-sphere-collision-${e.state}`),this.collisionBindGroups.set(t,i)),i}assertActive(){if(this.disposed)throw new Error("Secondary-motion runtime is disposed")}writeFrameVector(e,t){this.integrationParamsView.setFloat32(e,t?.[0]??0,!0),this.integrationParamsView.setFloat32(e+4,t?.[1]??0,!0),this.integrationParamsView.setFloat32(e+8,t?.[2]??0,!0)}}export function createSecondaryMotionRenderGraphExtension(e,t="SecondaryMotionSimulation"){const i=i=>{i.addPass({name:t,execute:t=>e.execute(t)})};return i.stage="beforeScene",i}export class SecondaryMotionFixedStep{constructor(e={}){this.accumulator=0,this.step=1/(e.frequency??60),this.maxSubsteps=e.maxSubsteps??4,this.maxFrameDelta=e.maxFrameDelta??.1}consume(e){if(!Number.isFinite(e)||e<0||e>this.maxFrameDelta)return this.accumulator=0,0;this.accumulator=Math.min(this.accumulator+e,this.step*this.maxSubsteps);const t=Math.min(Math.floor(this.accumulator/this.step),this.maxSubsteps);return this.accumulator-=t*this.step,t}reset(){this.accumulator=0}}export function selectSecondaryMotionLod(e,t,i){return t?i?"hero":e<8?"near":e<20?"mid":e<40?"far":"sleep":"sleep"}export function secondaryMotionTransformDiscontinuous(e,t,i=2,n=.5){if(e.length<7||t.length<7)return!0;const r=t[0]-e[0],o=t[1]-e[1],s=t[2]-e[2];if(r*r+o*o+s*s>i*i)return!0;return Math.abs(e[3]*t[3]+e[4]*t[4]+e[5]*t[5]+e[6]*t[6])<n}function i(e,t){return{binding:e,visibility:GPUShaderStage.COMPUTE,buffer:{type:t?"read-only-storage":"storage"}}}function n(e,t){return{binding:e,resource:{buffer:t}}}function r(e){const t=new Float32Array(e.length/3*4);for(let i=0,n=0;i<e.length;i+=3,n+=4)t[n]=e[i],t[n+1]=e[i+1],t[n+2]=e[i+2],t[n+3]=1;return t}const o="\nstruct IntegrationParams {\n deltaTime: f32,\n damping: f32,\n attachment: f32,\n _padding: f32,\n gravity: vec3<f32>,\n _padding2: f32,\n frameLinearAcceleration: vec3<f32>,\n _padding3: f32,\n frameAngularVelocity: vec3<f32>,\n _padding4: f32,\n frameAngularAcceleration: vec3<f32>,\n maxFrameAcceleration: f32,\n}\n@group(0) @binding(0) var<storage, read> currentPositions: array<vec4<f32>>;\n@group(0) @binding(1) var<storage, read_write> velocities: array<vec4<f32>>;\n@group(0) @binding(2) var<storage, read_write> outputPositions: array<vec4<f32>>;\nstruct PackedPosition { x: f32, y: f32, z: f32 }\n@group(0) @binding(3) var<storage, read> targetPositions: array<PackedPosition>;\n@group(0) @binding(4) var<storage, read> inverseMass: array<f32>;\n@group(0) @binding(5) var<uniform> params: IntegrationParams;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n let index = invocation.x;\n if (index >= arrayLength(&inverseMass)) { return; }\n let packedTarget = targetPositions[index];\n let targetPosition = vec3<f32>(packedTarget.x, packedTarget.y, packedTarget.z);\n if (inverseMass[index] <= 0.0) {\n velocities[index] = vec4(0.0);\n outputPositions[index] = vec4(targetPosition, 1.0);\n return;\n }\n let current = currentPositions[index].xyz;\n // Fully simulated cloth should be driven through its structural connection\n // to the animated roots. Pulling every free particle toward its skinned rest\n // pose makes collision sandwich it against the character like shrink-wrap.\n let animationInfluence = 1.0 - clamp(inverseMass[index], 0.0, 1.0);\n let targetAcceleration = (targetPosition - current) * params.attachment * animationInfluence;\n let previousVelocity = velocities[index].xyz;\n let rotationalAcceleration =\n cross(params.frameAngularAcceleration, current) +\n cross(params.frameAngularVelocity, cross(params.frameAngularVelocity, current)) +\n 2.0 * cross(params.frameAngularVelocity, previousVelocity);\n var frameAcceleration = params.frameLinearAcceleration + rotationalAcceleration;\n let frameAccelerationLength = length(frameAcceleration);\n if (frameAccelerationLength > params.maxFrameAcceleration) {\n frameAcceleration *= params.maxFrameAcceleration / frameAccelerationLength;\n }\n let acceleration = params.gravity + targetAcceleration - frameAcceleration;\n let velocity = previousVelocity * params.damping + acceleration * params.deltaTime;\n let next = current + velocity * params.deltaTime;\n velocities[index] = vec4(velocity, 0.0);\n outputPositions[index] = vec4(next, 1.0);\n}\n",s="\nstruct ConstraintParams {\n offset: u32,\n count: u32,\n stiffness: f32,\n _padding: f32,\n}\n@group(0) @binding(0) var<storage, read_write> positions: array<vec4<f32>>;\n@group(0) @binding(1) var<storage, read> inverseMass: array<f32>;\n@group(0) @binding(2) var<storage, read> pairs: array<vec2<u32>>;\n@group(0) @binding(3) var<storage, read> restLengths: array<f32>;\n@group(0) @binding(4) var<uniform> params: ConstraintParams;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n if (invocation.x >= params.count) { return; }\n let constraintIndex = params.offset + invocation.x;\n let pair = pairs[constraintIndex];\n let weightA = inverseMass[pair.x];\n let weightB = inverseMass[pair.y];\n let weight = weightA + weightB;\n if (weight <= 0.0) { return; }\n let delta = positions[pair.y].xyz - positions[pair.x].xyz;\n let length = length(delta);\n if (length <= 0.000001) { return; }\n let correction = delta * ((length - restLengths[constraintIndex]) / length) * params.stiffness;\n positions[pair.x] = vec4(positions[pair.x].xyz + correction * (weightA / weight), 1.0);\n positions[pair.y] = vec4(positions[pair.y].xyz - correction * (weightB / weight), 1.0);\n}\n",a="\nstruct PackedPosition { x: f32, y: f32, z: f32 }\n@group(0) @binding(0) var<storage, read> positions: array<vec4<f32>>;\n@group(0) @binding(1) var<storage, read> particleByRenderVertex: array<u32>;\n@group(0) @binding(2) var<storage, read_write> output: array<PackedPosition>;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n let index = invocation.x;\n if (index >= arrayLength(&particleByRenderVertex)) { return; }\n let position = positions[particleByRenderVertex[index]];\n output[index].x = position.x; output[index].y = position.y; output[index].z = position.z;\n}\n",c="\nstruct CollisionParams { count: u32, _padding1: u32, _padding2: u32, _padding3: u32 }\n@group(0) @binding(0) var<storage, read_write> positions: array<vec4<f32>>;\nstruct Ellipsoid { center: vec4<f32>, radii: vec4<f32>, rotation: vec4<f32> }\n@group(0) @binding(1) var<storage, read> previousPositions: array<vec4<f32>>;\n@group(0) @binding(2) var<storage, read> inverseMass: array<f32>;\n@group(0) @binding(3) var<storage, read> ellipsoids: array<Ellipsoid>;\n@group(0) @binding(4) var<storage, read> previousEllipsoids: array<Ellipsoid>;\n@group(0) @binding(5) var<storage, read_write> velocities: array<vec4<f32>>;\n@group(0) @binding(6) var<uniform> params: CollisionParams;\n\nfn rotateByQuaternion(value: vec3<f32>, rotation: vec4<f32>) -> vec3<f32> {\n return value + 2.0 * cross(rotation.xyz, cross(rotation.xyz, value) + rotation.w * value);\n}\n\nfn toEllipsoidSpace(value: vec3<f32>, shape: Ellipsoid) -> vec3<f32> {\n return rotateByQuaternion(value - shape.center.xyz, vec4<f32>(-shape.rotation.xyz, shape.rotation.w)) / shape.radii.xyz;\n}\n\nfn fromEllipsoidSpace(value: vec3<f32>, shape: Ellipsoid) -> vec3<f32> {\n return shape.center.xyz + rotateByQuaternion(value * shape.radii.xyz, shape.rotation);\n}\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n let index = invocation.x;\n if (index >= arrayLength(&inverseMass) || inverseMass[index] <= 0.0) { return; }\n var position = positions[index].xyz;\n var velocity = velocities[index].xyz;\n for (var collider = 0u; collider < params.count; collider++) {\n let shape = ellipsoids[collider];\n if (all(shape.radii.xyz > vec3<f32>(0.0))) {\n let scaled = toEllipsoidSpace(position, shape);\n let scaledLengthSquared = dot(scaled, scaled);\n if (scaledLengthSquared < 1.0) {\n let direction = select(vec3<f32>(0.0, 0.0, 1.0), scaled * inverseSqrt(scaledLengthSquared), scaledLengthSquared > 0.00000001);\n position = fromEllipsoidSpace(direction * 1.02, shape);\n let normal = normalize(rotateByQuaternion(direction / shape.radii.xyz, shape.rotation));\n velocity -= normal * min(dot(velocity, normal), 0.0);\n } else {\n let previousShape = previousEllipsoids[collider];\n let start = toEllipsoidSpace(previousPositions[index].xyz, previousShape);\n let travel = scaled - start;\n let a = dot(travel, travel);\n let b = 2.0 * dot(start, travel);\n let c = dot(start, start) - 1.0;\n let discriminant = b * b - 4.0 * a * c;\n if (a > 0.00000001 && discriminant >= 0.0) {\n let hit = (-b - sqrt(discriminant)) / (2.0 * a);\n if (hit > 0.0001 && hit <= 1.0) {\n let direction = normalize(start + travel * hit);\n position = fromEllipsoidSpace(direction * 1.02, shape);\n let normal = normalize(rotateByQuaternion(direction / shape.radii.xyz, shape.rotation));\n velocity -= normal * min(dot(velocity, normal), 0.0);\n }\n }\n }\n }\n }\n positions[index] = vec4(position, 1.0);\n velocities[index] = vec4(velocity, 0.0);\n}\n",l="\nstruct IntegrationParams {\n deltaTime: f32,\n damping: f32,\n attachment: f32,\n _padding: f32,\n gravity: vec3<f32>,\n _padding2: f32,\n}\n@group(0) @binding(0) var<storage, read> previousPositions: array<vec4<f32>>;\n@group(0) @binding(1) var<storage, read> constrainedPositions: array<vec4<f32>>;\n@group(0) @binding(2) var<storage, read_write> velocities: array<vec4<f32>>;\n@group(0) @binding(3) var<uniform> params: IntegrationParams;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) invocation: vec3<u32>) {\n let index = invocation.x;\n if (index >= arrayLength(&velocities)) { return; }\n velocities[index] = vec4(\n (constrainedPositions[index].xyz - previousPositions[index].xyz) / params.deltaTime,\n 0.0,\n );\n}\n";/*
|
|
2
2
|
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
3
|
* See the LICENSE.md file for details.
|
|
4
4
|
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{describe as e,expect as t,it as n,vi as o}from"vitest";import{Group as a,Scene as s}from"three";import{AssetResourceLoader as r}from"../scene/asset-resource-loader.js";o.mock("../scene/materializer.js",()=>({applyMaterial:o.fn(),getConvertedFbxToGlbAssignedMaterialTextureFlipY:o.fn()})),o.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=o.fn()),e[t]),set:(e,t,n)=>(e[t]=n,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),e("AssetResourceLoader converted FBX hierarchy",()=>{n("unwraps the synthetic glTF scene around a Three-converted FBX root",async()=>{const e=new r;e.setDataDir("");const n=new s,i=new a;i.name="AuthoredFbxRoot",n.add(i),e.glbLoader={loadAsync:o.fn().mockResolvedValue({scene:n,animations:[]})};const l=await e.loadByAsset({id:"converted-fbx",name:"Converted FBX",type:"mesh",fileKey:"model.glb",fileFormat:"glb",originalFileKey:"model.fbx"});t(l.scene).toBe(i),t(i.parent).toBeNull()}),n("keeps the synthetic scene for an ordinary GLB",async()=>{const e=new r;e.setDataDir("");const n=new s;n.add(new a),e.glbLoader={loadAsync:o.fn().mockResolvedValue({scene:n,animations:[]})};const i=await e.loadByAsset({id:"native-glb",name:"Native GLB",type:"mesh",fileKey:"model.glb",fileFormat:"glb"});t(i.scene).toBe(n)})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{Subject as e}from"rxjs";import{Group as t,Object3D as n,Quaternion as s,Scene as o,Vector3 as a}from"three";import{describe as i,expect as r,it as c,vi as d}from"vitest";d.hoisted(()=>{if("undefined"==typeof HTMLCanvasElement)return;const e=new Proxy({},{get:(e,t)=>(t in e||(e[t]=("string"!=typeof t||!t.startsWith("is"))&&d.fn()),e[t]),set:(e,t,n)=>(e[t]=n,!0)});Object.defineProperty(HTMLCanvasElement.prototype,"getContext",{configurable:!0,value:()=>e}),"undefined"==typeof AudioBuffer&&Object.defineProperty(globalThis,"AudioBuffer",{configurable:!0,value:class{}})}),d.mock("../gameplay/actors/builtin/index.js",()=>({default:{}})),d.mock("../effects/vfx/vfx-actor.js",()=>({VfxActor:class{}})),d.mock("../effects/vfx/vfx-param.js",()=>({VisualEffect:class{}})),d.mock("../rendering.js",()=>({RenderingView:class{},setRenderingPaused:d.fn()}));import{PhysicsBodyType as f}from"../gameplay/services/physics/physics-system.js";import{SceneMaterializer as l}from"../scene/materializer.js";function p(e){return{name:e.id,...e}}i("SceneMaterializer dynamic transforms",()=>{c("preserves a nested dynamic object world transform when moving it to the scene root",async()=>{const i=new o,c=function(t){const n={getObjects:()=>[],onCreate:()=>{},onUpdate:()=>{},onRemove:()=>{}},s={onCreate:new e,onDelete:new e,onUpdate:new e,getAsset:d.fn(async()=>null),getAssets:d.fn(async()=>[])};return new l(t,n,s,{},{renderer:{shadowMap:{needsUpdate:!1}}},[],[],{create:async()=>null,initActor:async()=>{}})}(i);c.inEditor=!1;const m=[.2,.7,-.1],y=[0,0,0],u=p({id:"parent",type:"group",position:[10,4,-7],rotation:m,scale:[2,3,4],children:[p({id:"dynamic-child",type:"shape_mesh",shape:"box",position:[3,-2,5],rotation:y,scale:[.5,1.5,2],physics:{type:f.dynamic}})]});await c.materialize(u);const g=i.getObjectByName("dynamic-child");r(g?.parent).toBe(i);const h=new t;h.position.set(10,4,-7),h.rotation.set(...m),h.scale.set(2,3,4);const w=new n;w.position.set(3,-2,5),w.rotation.set(...y),w.scale.set(.5,1.5,2),h.add(w);const v=w.getWorldPosition(new a),j=w.getWorldQuaternion(new s),b=w.getWorldScale(new a);r(g.position.distanceTo(v)).toBeLessThan(1e-6),r(1-Math.abs(g.quaternion.dot(j))).toBeLessThan(1e-6),r(g.scale.distanceTo(b)).toBeLessThan(1e-6)})});/*
|
|
2
|
+
* Copyright (©) 2026 Hology Interactive AB. All rights reserved.
|
|
3
|
+
* See the LICENSE.md file for details.
|
|
4
|
+
*/
|