main repo

This commit is contained in:
Basilosaurusrex
2025-11-24 18:09:40 +01:00
parent b636ee5e70
commit f027651f9b
34146 changed files with 4436636 additions and 0 deletions

View File

@@ -0,0 +1,539 @@
import {
Line3,
Mesh,
Plane,
Vector3
} from 'three';
import { ConvexGeometry } from '../geometries/ConvexGeometry.js';
const _v1 = new Vector3();
/**
* This class can be used to subdivide a convex Geometry object into pieces.
*
* Use the function prepareBreakableObject to prepare a Mesh object to be broken.
* Then, call the various functions to subdivide the object (subdivideByImpact, cutByPlane).
* Sub-objects that are product of subdivision don't need prepareBreakableObject to be called on them.
*
* Requisites for the object:
* - Mesh object must have a buffer geometry and a material.
* - Vertex normals must be planar (not smoothed).
* - The geometry must be convex (this is not checked in the library). You can create convex
* geometries with {@link ConvexGeometry}. The {@link BoxGeometry}, {@link SphereGeometry} and other
* convex primitives can also be used.
*
* Note: This lib adds member variables to object's userData member (see prepareBreakableObject function)
* Use with caution and read the code when using with other libs.
*
* @three_import import { ConvexObjectBreaker } from 'three/addons/misc/ConvexObjectBreaker.js';
*/
class ConvexObjectBreaker {
/**
* Constructs a new convex object breaker.
*
* @param {number} [minSizeForBreak=1.4] - Min size a debris can have to break.
* @param {number} [smallDelta=0.0001] - Max distance to consider that a point belongs to a plane.
*/
constructor( minSizeForBreak = 1.4, smallDelta = 0.0001 ) {
this.minSizeForBreak = minSizeForBreak;
this.smallDelta = smallDelta;
this.tempLine1 = new Line3();
this.tempPlane1 = new Plane();
this.tempPlane2 = new Plane();
this.tempPlane_Cut = new Plane();
this.tempCM1 = new Vector3();
this.tempCM2 = new Vector3();
this.tempVector3 = new Vector3();
this.tempVector3_2 = new Vector3();
this.tempVector3_3 = new Vector3();
this.tempVector3_P0 = new Vector3();
this.tempVector3_P1 = new Vector3();
this.tempVector3_P2 = new Vector3();
this.tempVector3_N0 = new Vector3();
this.tempVector3_N1 = new Vector3();
this.tempVector3_AB = new Vector3();
this.tempVector3_CB = new Vector3();
this.tempResultObjects = { object1: null, object2: null };
this.segments = [];
const n = 30 * 30;
for ( let i = 0; i < n; i ++ ) this.segments[ i ] = false;
}
/**
* Must be called for all 3D objects that should be breakable.
*
* @param {Object3D} object - The 3D object. It must have a convex geometry.
* @param {number} mass - The 3D object's mass in kg. Must be greater than `0`.
* @param {Vector3} velocity - The 3D object's velocity.
* @param {Vector3} angularVelocity - The 3D object's angular velocity.
* @param {boolean} breakable - Whether the 3D object is breakable or not.
*/
prepareBreakableObject( object, mass, velocity, angularVelocity, breakable ) {
// object is a Object3d (normally a Mesh), must have a buffer geometry, and it must be convex.
// Its material property is propagated to its children (sub-pieces)
// mass must be > 0
const userData = object.userData;
userData.mass = mass;
userData.velocity = velocity.clone();
userData.angularVelocity = angularVelocity.clone();
userData.breakable = breakable;
}
/**
* Subdivides the given 3D object into pieces by an impact (meaning another object hits
* the given 3D object at a certain surface point).
*
* @param {Object3D} object - The 3D object to subdivide.
* @param {Vector3} pointOfImpact - The point of impact.
* @param {Vector3} normal - The impact normal.
* @param {number} maxRadialIterations - Iterations for radial cuts.
* @param {number} maxRandomIterations - Max random iterations for not-radial cuts.
* @return {Array<Object3D>} The array of pieces.
*/
subdivideByImpact( object, pointOfImpact, normal, maxRadialIterations, maxRandomIterations ) {
const debris = [];
const tempPlane1 = this.tempPlane1;
const tempPlane2 = this.tempPlane2;
this.tempVector3.addVectors( pointOfImpact, normal );
tempPlane1.setFromCoplanarPoints( pointOfImpact, object.position, this.tempVector3 );
const maxTotalIterations = maxRandomIterations + maxRadialIterations;
const scope = this;
function subdivideRadial( subObject, startAngle, endAngle, numIterations ) {
if ( Math.random() < numIterations * 0.05 || numIterations > maxTotalIterations ) {
debris.push( subObject );
return;
}
let angle = Math.PI;
if ( numIterations === 0 ) {
tempPlane2.normal.copy( tempPlane1.normal );
tempPlane2.constant = tempPlane1.constant;
} else {
if ( numIterations <= maxRadialIterations ) {
angle = ( endAngle - startAngle ) * ( 0.2 + 0.6 * Math.random() ) + startAngle;
// Rotate tempPlane2 at impact point around normal axis and the angle
scope.tempVector3_2.copy( object.position ).sub( pointOfImpact ).applyAxisAngle( normal, angle ).add( pointOfImpact );
tempPlane2.setFromCoplanarPoints( pointOfImpact, scope.tempVector3, scope.tempVector3_2 );
} else {
angle = ( ( 0.5 * ( numIterations & 1 ) ) + 0.2 * ( 2 - Math.random() ) ) * Math.PI;
// Rotate tempPlane2 at object position around normal axis and the angle
scope.tempVector3_2.copy( pointOfImpact ).sub( subObject.position ).applyAxisAngle( normal, angle ).add( subObject.position );
scope.tempVector3_3.copy( normal ).add( subObject.position );
tempPlane2.setFromCoplanarPoints( subObject.position, scope.tempVector3_3, scope.tempVector3_2 );
}
}
// Perform the cut
scope.cutByPlane( subObject, tempPlane2, scope.tempResultObjects );
const obj1 = scope.tempResultObjects.object1;
const obj2 = scope.tempResultObjects.object2;
if ( obj1 ) {
subdivideRadial( obj1, startAngle, angle, numIterations + 1 );
}
if ( obj2 ) {
subdivideRadial( obj2, angle, endAngle, numIterations + 1 );
}
}
subdivideRadial( object, 0, 2 * Math.PI, 0 );
return debris;
}
/**
* Subdivides the given 3D object into pieces by a plane.
*
* @param {Object3D} object - The 3D object to subdivide.
* @param {Plane} plane - The plane to cut the 3D object.
* @param {{object1:?Mesh,object2:?Mesh}} output - An object that stores the pieces.
* @return {number} The number of pieces.
*/
cutByPlane( object, plane, output ) {
// Returns breakable objects in output.object1 and output.object2 members, the resulting 2 pieces of the cut.
// object2 can be null if the plane doesn't cut the object.
// object1 can be null only in case of internal error
// Returned value is number of pieces, 0 for error.
const geometry = object.geometry;
const coords = geometry.attributes.position.array;
const normals = geometry.attributes.normal.array;
const numPoints = coords.length / 3;
let numFaces = numPoints / 3;
let indices = geometry.getIndex();
if ( indices ) {
indices = indices.array;
numFaces = indices.length / 3;
}
function getVertexIndex( faceIdx, vert ) {
// vert = 0, 1 or 2.
const idx = faceIdx * 3 + vert;
return indices ? indices[ idx ] : idx;
}
const points1 = [];
const points2 = [];
const delta = this.smallDelta;
// Reset segments mark
const numPointPairs = numPoints * numPoints;
for ( let i = 0; i < numPointPairs; i ++ ) this.segments[ i ] = false;
const p0 = this.tempVector3_P0;
const p1 = this.tempVector3_P1;
const n0 = this.tempVector3_N0;
const n1 = this.tempVector3_N1;
// Iterate through the faces to mark edges shared by coplanar faces
for ( let i = 0; i < numFaces - 1; i ++ ) {
const a1 = getVertexIndex( i, 0 );
const b1 = getVertexIndex( i, 1 );
const c1 = getVertexIndex( i, 2 );
// Assuming all 3 vertices have the same normal
n0.set( normals[ a1 ], normals[ a1 ] + 1, normals[ a1 ] + 2 );
for ( let j = i + 1; j < numFaces; j ++ ) {
const a2 = getVertexIndex( j, 0 );
const b2 = getVertexIndex( j, 1 );
const c2 = getVertexIndex( j, 2 );
// Assuming all 3 vertices have the same normal
n1.set( normals[ a2 ], normals[ a2 ] + 1, normals[ a2 ] + 2 );
const coplanar = 1 - n0.dot( n1 ) < delta;
if ( coplanar ) {
if ( a1 === a2 || a1 === b2 || a1 === c2 ) {
if ( b1 === a2 || b1 === b2 || b1 === c2 ) {
this.segments[ a1 * numPoints + b1 ] = true;
this.segments[ b1 * numPoints + a1 ] = true;
} else {
this.segments[ c1 * numPoints + a1 ] = true;
this.segments[ a1 * numPoints + c1 ] = true;
}
} else if ( b1 === a2 || b1 === b2 || b1 === c2 ) {
this.segments[ c1 * numPoints + b1 ] = true;
this.segments[ b1 * numPoints + c1 ] = true;
}
}
}
}
// Transform the plane to object local space
const localPlane = this.tempPlane_Cut;
object.updateMatrix();
ConvexObjectBreaker.transformPlaneToLocalSpace( plane, object.matrix, localPlane );
// Iterate through the faces adding points to both pieces
for ( let i = 0; i < numFaces; i ++ ) {
const va = getVertexIndex( i, 0 );
const vb = getVertexIndex( i, 1 );
const vc = getVertexIndex( i, 2 );
for ( let segment = 0; segment < 3; segment ++ ) {
const i0 = segment === 0 ? va : ( segment === 1 ? vb : vc );
const i1 = segment === 0 ? vb : ( segment === 1 ? vc : va );
const segmentState = this.segments[ i0 * numPoints + i1 ];
if ( segmentState ) continue; // The segment already has been processed in another face
// Mark segment as processed (also inverted segment)
this.segments[ i0 * numPoints + i1 ] = true;
this.segments[ i1 * numPoints + i0 ] = true;
p0.set( coords[ 3 * i0 ], coords[ 3 * i0 + 1 ], coords[ 3 * i0 + 2 ] );
p1.set( coords[ 3 * i1 ], coords[ 3 * i1 + 1 ], coords[ 3 * i1 + 2 ] );
// mark: 1 for negative side, 2 for positive side, 3 for coplanar point
let mark0 = 0;
let d = localPlane.distanceToPoint( p0 );
if ( d > delta ) {
mark0 = 2;
points2.push( p0.clone() );
} else if ( d < - delta ) {
mark0 = 1;
points1.push( p0.clone() );
} else {
mark0 = 3;
points1.push( p0.clone() );
points2.push( p0.clone() );
}
// mark: 1 for negative side, 2 for positive side, 3 for coplanar point
let mark1 = 0;
d = localPlane.distanceToPoint( p1 );
if ( d > delta ) {
mark1 = 2;
points2.push( p1.clone() );
} else if ( d < - delta ) {
mark1 = 1;
points1.push( p1.clone() );
} else {
mark1 = 3;
points1.push( p1.clone() );
points2.push( p1.clone() );
}
if ( ( mark0 === 1 && mark1 === 2 ) || ( mark0 === 2 && mark1 === 1 ) ) {
// Intersection of segment with the plane
this.tempLine1.start.copy( p0 );
this.tempLine1.end.copy( p1 );
let intersection = new Vector3();
intersection = localPlane.intersectLine( this.tempLine1, intersection );
if ( intersection === null ) {
// Shouldn't happen
console.error( 'Internal error: segment does not intersect plane.' );
output.segmentedObject1 = null;
output.segmentedObject2 = null;
return 0;
}
points1.push( intersection );
points2.push( intersection.clone() );
}
}
}
// Calculate debris mass (very fast and imprecise):
const newMass = object.userData.mass * 0.5;
// Calculate debris Center of Mass (again fast and imprecise)
this.tempCM1.set( 0, 0, 0 );
let radius1 = 0;
const numPoints1 = points1.length;
if ( numPoints1 > 0 ) {
for ( let i = 0; i < numPoints1; i ++ ) this.tempCM1.add( points1[ i ] );
this.tempCM1.divideScalar( numPoints1 );
for ( let i = 0; i < numPoints1; i ++ ) {
const p = points1[ i ];
p.sub( this.tempCM1 );
radius1 = Math.max( radius1, p.x, p.y, p.z );
}
this.tempCM1.add( object.position );
}
this.tempCM2.set( 0, 0, 0 );
let radius2 = 0;
const numPoints2 = points2.length;
if ( numPoints2 > 0 ) {
for ( let i = 0; i < numPoints2; i ++ ) this.tempCM2.add( points2[ i ] );
this.tempCM2.divideScalar( numPoints2 );
for ( let i = 0; i < numPoints2; i ++ ) {
const p = points2[ i ];
p.sub( this.tempCM2 );
radius2 = Math.max( radius2, p.x, p.y, p.z );
}
this.tempCM2.add( object.position );
}
let object1 = null;
let object2 = null;
let numObjects = 0;
if ( numPoints1 > 4 ) {
object1 = new Mesh( new ConvexGeometry( points1 ), object.material );
object1.position.copy( this.tempCM1 );
object1.quaternion.copy( object.quaternion );
this.prepareBreakableObject( object1, newMass, object.userData.velocity, object.userData.angularVelocity, 2 * radius1 > this.minSizeForBreak );
numObjects ++;
}
if ( numPoints2 > 4 ) {
object2 = new Mesh( new ConvexGeometry( points2 ), object.material );
object2.position.copy( this.tempCM2 );
object2.quaternion.copy( object.quaternion );
this.prepareBreakableObject( object2, newMass, object.userData.velocity, object.userData.angularVelocity, 2 * radius2 > this.minSizeForBreak );
numObjects ++;
}
output.object1 = object1;
output.object2 = object2;
return numObjects;
}
// internal helpers
static transformFreeVector( v, m ) {
// input:
// vector interpreted as a free vector
// THREE.Matrix4 orthogonal matrix (matrix without scale)
const x = v.x, y = v.y, z = v.z;
const e = m.elements;
v.x = e[ 0 ] * x + e[ 4 ] * y + e[ 8 ] * z;
v.y = e[ 1 ] * x + e[ 5 ] * y + e[ 9 ] * z;
v.z = e[ 2 ] * x + e[ 6 ] * y + e[ 10 ] * z;
return v;
}
static transformFreeVectorInverse( v, m ) {
// input:
// vector interpreted as a free vector
// THREE.Matrix4 orthogonal matrix (matrix without scale)
const x = v.x, y = v.y, z = v.z;
const e = m.elements;
v.x = e[ 0 ] * x + e[ 1 ] * y + e[ 2 ] * z;
v.y = e[ 4 ] * x + e[ 5 ] * y + e[ 6 ] * z;
v.z = e[ 8 ] * x + e[ 9 ] * y + e[ 10 ] * z;
return v;
}
static transformTiedVectorInverse( v, m ) {
// input:
// vector interpreted as a tied (ordinary) vector
// THREE.Matrix4 orthogonal matrix (matrix without scale)
const x = v.x, y = v.y, z = v.z;
const e = m.elements;
v.x = e[ 0 ] * x + e[ 1 ] * y + e[ 2 ] * z - e[ 12 ];
v.y = e[ 4 ] * x + e[ 5 ] * y + e[ 6 ] * z - e[ 13 ];
v.z = e[ 8 ] * x + e[ 9 ] * y + e[ 10 ] * z - e[ 14 ];
return v;
}
static transformPlaneToLocalSpace( plane, m, resultPlane ) {
resultPlane.normal.copy( plane.normal );
resultPlane.constant = plane.constant;
const referencePoint = ConvexObjectBreaker.transformTiedVectorInverse( plane.coplanarPoint( _v1 ), m );
ConvexObjectBreaker.transformFreeVectorInverse( resultPlane.normal, m );
// recalculate constant (like in setFromNormalAndCoplanarPoint)
resultPlane.constant = - referencePoint.dot( resultPlane.normal );
}
}
export { ConvexObjectBreaker };

View File

@@ -0,0 +1,506 @@
import {
ClampToEdgeWrapping,
DataTexture,
FloatType,
NearestFilter,
RGBAFormat,
ShaderMaterial,
WebGLRenderTarget
} from 'three';
import { FullScreenQuad } from '../postprocessing/Pass.js';
/**
* GPUComputationRenderer, based on SimulationRenderer by @zz85.
*
* The GPUComputationRenderer uses the concept of variables. These variables are RGBA float textures that hold 4 floats
* for each compute element (texel).
*
* Each variable has a fragment shader that defines the computation made to obtain the variable in question.
* You can use as many variables you need, and make dependencies so you can use textures of other variables in the shader
* (the sampler uniforms are added automatically) Most of the variables will need themselves as dependency.
*
* The renderer has actually two render targets per variable, to make ping-pong. Textures from the current frame are used
* as inputs to render the textures of the next frame.
*
* The render targets of the variables can be used as input textures for your visualization shaders.
*
* Variable names should be valid identifiers and should not collide with THREE GLSL used identifiers.
* a common approach could be to use 'texture' prefixing the variable name; i.e texturePosition, textureVelocity...
*
* The size of the computation (sizeX * sizeY) is defined as 'resolution' automatically in the shader. For example:
* ```
* #DEFINE resolution vec2( 1024.0, 1024.0 )
* ```
* Basic use:
* ```js
* // Initialization...
*
* // Create computation renderer
* const gpuCompute = new GPUComputationRenderer( 1024, 1024, renderer );
*
* // Create initial state float textures
* const pos0 = gpuCompute.createTexture();
* const vel0 = gpuCompute.createTexture();
* // and fill in here the texture data...
*
* // Add texture variables
* const velVar = gpuCompute.addVariable( "textureVelocity", fragmentShaderVel, vel0 );
* const posVar = gpuCompute.addVariable( "texturePosition", fragmentShaderPos, pos0 );
*
* // Add variable dependencies
* gpuCompute.setVariableDependencies( velVar, [ velVar, posVar ] );
* gpuCompute.setVariableDependencies( posVar, [ velVar, posVar ] );
*
* // Add custom uniforms
* velVar.material.uniforms.time = { value: 0.0 };
*
* // Check for completeness
* const error = gpuCompute.init();
* if ( error !== null ) {
* console.error( error );
* }
*
* // In each frame...
*
* // Compute!
* gpuCompute.compute();
*
* // Update texture uniforms in your visualization materials with the gpu renderer output
* myMaterial.uniforms.myTexture.value = gpuCompute.getCurrentRenderTarget( posVar ).texture;
*
* // Do your rendering
* renderer.render( myScene, myCamera );
* ```
*
* Also, you can use utility functions to create ShaderMaterial and perform computations (rendering between textures)
* Note that the shaders can have multiple input textures.
*
* ```js
* const myFilter1 = gpuCompute.createShaderMaterial( myFilterFragmentShader1, { theTexture: { value: null } } );
* const myFilter2 = gpuCompute.createShaderMaterial( myFilterFragmentShader2, { theTexture: { value: null } } );
*
* const inputTexture = gpuCompute.createTexture();
*
* // Fill in here inputTexture...
*
* myFilter1.uniforms.theTexture.value = inputTexture;
*
* const myRenderTarget = gpuCompute.createRenderTarget();
* myFilter2.uniforms.theTexture.value = myRenderTarget.texture;
*
* const outputRenderTarget = gpuCompute.createRenderTarget();
*
* // Now use the output texture where you want:
* myMaterial.uniforms.map.value = outputRenderTarget.texture;
*
* // And compute each frame, before rendering to screen:
* gpuCompute.doRenderTarget( myFilter1, myRenderTarget );
* gpuCompute.doRenderTarget( myFilter2, outputRenderTarget );
* ```
*
* @three_import import { GPUComputationRenderer } from 'three/addons/misc/GPUComputationRenderer.js';
*/
class GPUComputationRenderer {
/**
* Constructs a new GPU computation renderer.
*
* @param {number} sizeX - Computation problem size is always 2d: sizeX * sizeY elements.
* @param {number} sizeY - Computation problem size is always 2d: sizeX * sizeY elements.
* @param {WebGLRenderer} renderer - The renderer.
*/
constructor( sizeX, sizeY, renderer ) {
this.variables = [];
this.currentTextureIndex = 0;
let dataType = FloatType;
const passThruUniforms = {
passThruTexture: { value: null }
};
const passThruShader = createShaderMaterial( getPassThroughFragmentShader(), passThruUniforms );
const quad = new FullScreenQuad( passThruShader );
/**
* Sets the data type of the internal textures.
*
* @param {(FloatType|HalfFloatType)} type - The type to set.
* @return {GPUComputationRenderer} A reference to this renderer.
*/
this.setDataType = function ( type ) {
dataType = type;
return this;
};
/**
* Adds a compute variable to the renderer.
*
* @param {string} variableName - The variable name.
* @param {string} computeFragmentShader - The compute (fragment) shader source.
* @param {Texture} initialValueTexture - The initial value texture.
* @return {Object} The compute variable.
*/
this.addVariable = function ( variableName, computeFragmentShader, initialValueTexture ) {
const material = this.createShaderMaterial( computeFragmentShader );
const variable = {
name: variableName,
initialValueTexture: initialValueTexture,
material: material,
dependencies: null,
renderTargets: [],
wrapS: null,
wrapT: null,
minFilter: NearestFilter,
magFilter: NearestFilter
};
this.variables.push( variable );
return variable;
};
/**
* Sets variable dependencies.
*
* @param {Object} variable - The compute variable.
* @param {Array<Object>} dependencies - Other compute variables that represents the dependencies.
*/
this.setVariableDependencies = function ( variable, dependencies ) {
variable.dependencies = dependencies;
};
/**
* Initializes the renderer.
*
* @return {?string} Returns `null` if no errors are detected. Otherwise returns the error message.
*/
this.init = function () {
if ( renderer.capabilities.maxVertexTextures === 0 ) {
return 'No support for vertex shader textures.';
}
for ( let i = 0; i < this.variables.length; i ++ ) {
const variable = this.variables[ i ];
// Creates rendertargets and initialize them with input texture
variable.renderTargets[ 0 ] = this.createRenderTarget( sizeX, sizeY, variable.wrapS, variable.wrapT, variable.minFilter, variable.magFilter );
variable.renderTargets[ 1 ] = this.createRenderTarget( sizeX, sizeY, variable.wrapS, variable.wrapT, variable.minFilter, variable.magFilter );
this.renderTexture( variable.initialValueTexture, variable.renderTargets[ 0 ] );
this.renderTexture( variable.initialValueTexture, variable.renderTargets[ 1 ] );
// Adds dependencies uniforms to the ShaderMaterial
const material = variable.material;
const uniforms = material.uniforms;
if ( variable.dependencies !== null ) {
for ( let d = 0; d < variable.dependencies.length; d ++ ) {
const depVar = variable.dependencies[ d ];
if ( depVar.name !== variable.name ) {
// Checks if variable exists
let found = false;
for ( let j = 0; j < this.variables.length; j ++ ) {
if ( depVar.name === this.variables[ j ].name ) {
found = true;
break;
}
}
if ( ! found ) {
return 'Variable dependency not found. Variable=' + variable.name + ', dependency=' + depVar.name;
}
}
uniforms[ depVar.name ] = { value: null };
material.fragmentShader = '\nuniform sampler2D ' + depVar.name + ';\n' + material.fragmentShader;
}
}
}
this.currentTextureIndex = 0;
return null;
};
/**
* Executes the compute. This method is usually called in the animation loop.
*/
this.compute = function () {
const currentTextureIndex = this.currentTextureIndex;
const nextTextureIndex = this.currentTextureIndex === 0 ? 1 : 0;
for ( let i = 0, il = this.variables.length; i < il; i ++ ) {
const variable = this.variables[ i ];
// Sets texture dependencies uniforms
if ( variable.dependencies !== null ) {
const uniforms = variable.material.uniforms;
for ( let d = 0, dl = variable.dependencies.length; d < dl; d ++ ) {
const depVar = variable.dependencies[ d ];
uniforms[ depVar.name ].value = depVar.renderTargets[ currentTextureIndex ].texture;
}
}
// Performs the computation for this variable
this.doRenderTarget( variable.material, variable.renderTargets[ nextTextureIndex ] );
}
this.currentTextureIndex = nextTextureIndex;
};
/**
* Returns the current render target for the given compute variable.
*
* @param {Object} variable - The compute variable.
* @return {WebGLRenderTarget} The current render target.
*/
this.getCurrentRenderTarget = function ( variable ) {
return variable.renderTargets[ this.currentTextureIndex ];
};
/**
* Returns the alternate render target for the given compute variable.
*
* @param {Object} variable - The compute variable.
* @return {WebGLRenderTarget} The alternate render target.
*/
this.getAlternateRenderTarget = function ( variable ) {
return variable.renderTargets[ this.currentTextureIndex === 0 ? 1 : 0 ];
};
/**
* Frees all internal resources. Call this method if you don't need the
* renderer anymore.
*/
this.dispose = function () {
quad.dispose();
const variables = this.variables;
for ( let i = 0; i < variables.length; i ++ ) {
const variable = variables[ i ];
if ( variable.initialValueTexture ) variable.initialValueTexture.dispose();
const renderTargets = variable.renderTargets;
for ( let j = 0; j < renderTargets.length; j ++ ) {
const renderTarget = renderTargets[ j ];
renderTarget.dispose();
}
}
};
function addResolutionDefine( materialShader ) {
materialShader.defines.resolution = 'vec2( ' + sizeX.toFixed( 1 ) + ', ' + sizeY.toFixed( 1 ) + ' )';
}
/**
* Adds a resolution defined for the given material shader.
*
* @param {Object} materialShader - The material shader.
*/
this.addResolutionDefine = addResolutionDefine;
// The following functions can be used to compute things manually
function createShaderMaterial( computeFragmentShader, uniforms ) {
uniforms = uniforms || {};
const material = new ShaderMaterial( {
name: 'GPUComputationShader',
uniforms: uniforms,
vertexShader: getPassThroughVertexShader(),
fragmentShader: computeFragmentShader
} );
addResolutionDefine( material );
return material;
}
this.createShaderMaterial = createShaderMaterial;
/**
* Creates a new render target from the given parameters.
*
* @param {number} sizeXTexture - The width of the render target.
* @param {number} sizeYTexture - The height of the render target.
* @param {number} wrapS - The wrapS value.
* @param {number} wrapT - The wrapS value.
* @param {number} minFilter - The minFilter value.
* @param {number} magFilter - The magFilter value.
* @return {WebGLRenderTarget} The new render target.
*/
this.createRenderTarget = function ( sizeXTexture, sizeYTexture, wrapS, wrapT, minFilter, magFilter ) {
sizeXTexture = sizeXTexture || sizeX;
sizeYTexture = sizeYTexture || sizeY;
wrapS = wrapS || ClampToEdgeWrapping;
wrapT = wrapT || ClampToEdgeWrapping;
minFilter = minFilter || NearestFilter;
magFilter = magFilter || NearestFilter;
const renderTarget = new WebGLRenderTarget( sizeXTexture, sizeYTexture, {
wrapS: wrapS,
wrapT: wrapT,
minFilter: minFilter,
magFilter: magFilter,
format: RGBAFormat,
type: dataType,
depthBuffer: false
} );
return renderTarget;
};
/**
* Creates a new data texture.
*
* @return {DataTexture} The new data texture.
*/
this.createTexture = function () {
const data = new Float32Array( sizeX * sizeY * 4 );
const texture = new DataTexture( data, sizeX, sizeY, RGBAFormat, FloatType );
texture.needsUpdate = true;
return texture;
};
/**
* Renders the given texture into the given render target.
*
* @param {Texture} input - The input.
* @param {WebGLRenderTarget} output - The output.
*/
this.renderTexture = function ( input, output ) {
passThruUniforms.passThruTexture.value = input;
this.doRenderTarget( passThruShader, output );
passThruUniforms.passThruTexture.value = null;
};
/**
* Renders the given material into the given render target
* with a full-screen pass.
*
* @param {Material} material - The material.
* @param {WebGLRenderTarget} output - The output.
*/
this.doRenderTarget = function ( material, output ) {
const currentRenderTarget = renderer.getRenderTarget();
const currentXrEnabled = renderer.xr.enabled;
const currentShadowAutoUpdate = renderer.shadowMap.autoUpdate;
renderer.xr.enabled = false; // Avoid camera modification
renderer.shadowMap.autoUpdate = false; // Avoid re-computing shadows
quad.material = material;
renderer.setRenderTarget( output );
quad.render( renderer );
quad.material = passThruShader;
renderer.xr.enabled = currentXrEnabled;
renderer.shadowMap.autoUpdate = currentShadowAutoUpdate;
renderer.setRenderTarget( currentRenderTarget );
};
// Shaders
function getPassThroughVertexShader() {
return 'void main() {\n' +
'\n' +
' gl_Position = vec4( position, 1.0 );\n' +
'\n' +
'}\n';
}
function getPassThroughFragmentShader() {
return 'uniform sampler2D passThruTexture;\n' +
'\n' +
'void main() {\n' +
'\n' +
' vec2 uv = gl_FragCoord.xy / resolution.xy;\n' +
'\n' +
' gl_FragColor = texture2D( passThruTexture, uv );\n' +
'\n' +
'}\n';
}
}
}
export { GPUComputationRenderer };

78
node_modules/three/examples/jsm/misc/Gyroscope.js generated vendored Normal file
View File

@@ -0,0 +1,78 @@
import {
Object3D,
Quaternion,
Vector3
} from 'three';
const _translationObject = new Vector3();
const _quaternionObject = new Quaternion();
const _scaleObject = new Vector3();
const _translationWorld = new Vector3();
const _quaternionWorld = new Quaternion();
const _scaleWorld = new Vector3();
/**
* A special type of 3D object that takes a position from the scene graph hierarchy
* but uses its local rotation as world rotation. It works like real-world gyroscope -
* you can move it around using hierarchy while its orientation stays fixed with
* respect to the world.
*
* @augments Object3D
* @three_import import { Gyroscope } from 'three/addons/misc/Gyroscope.js';
*/
class Gyroscope extends Object3D {
/**
* Constructs a new gyroscope.
*/
constructor() {
super();
}
updateMatrixWorld( force ) {
this.matrixAutoUpdate && this.updateMatrix();
// update matrixWorld
if ( this.matrixWorldNeedsUpdate || force ) {
if ( this.parent !== null ) {
this.matrixWorld.multiplyMatrices( this.parent.matrixWorld, this.matrix );
this.matrixWorld.decompose( _translationWorld, _quaternionWorld, _scaleWorld );
this.matrix.decompose( _translationObject, _quaternionObject, _scaleObject );
this.matrixWorld.compose( _translationWorld, _quaternionObject, _scaleWorld );
} else {
this.matrixWorld.copy( this.matrix );
}
this.matrixWorldNeedsUpdate = false;
force = true;
}
// update children
for ( let i = 0, l = this.children.length; i < l; i ++ ) {
this.children[ i ].updateMatrixWorld( force );
}
}
}
export { Gyroscope };

391
node_modules/three/examples/jsm/misc/MD2Character.js generated vendored Normal file
View File

@@ -0,0 +1,391 @@
import {
AnimationMixer,
Box3,
Mesh,
MeshLambertMaterial,
Object3D,
TextureLoader,
UVMapping,
SRGBColorSpace
} from 'three';
import { MD2Loader } from '../loaders/MD2Loader.js';
/**
* This class represents a management component for animated MD2
* character assets.
*
* @three_import import { MD2Character } from 'three/addons/misc/MD2Character.js';
*/
class MD2Character {
/**
* Constructs a new MD2 character.
*/
constructor() {
/**
* The mesh scale.
*
* @type {number}
* @default 1
*/
this.scale = 1;
/**
* The FPS
*
* @type {number}
* @default 6
*/
this.animationFPS = 6;
/**
* The root 3D object
*
* @type {Object3D}
*/
this.root = new Object3D();
/**
* The body mesh.
*
* @type {?Mesh}
* @default null
*/
this.meshBody = null;
/**
* The weapon mesh.
*
* @type {?Mesh}
* @default null
*/
this.meshWeapon = null;
/**
* The body skins.
*
* @type {Array<Texture>}
*/
this.skinsBody = [];
/**
* The weapon skins.
*
* @type {Array<Texture>}
*/
this.skinsWeapon = [];
/**
* The weapon meshes.
*
* @type {Array<Mesh>}
*/
this.weapons = [];
/**
* The name of the active animation clip.
*
* @type {?string}
* @default null
*/
this.activeAnimationClipName = null;
/**
* The animation mixer.
*
* @type {?AnimationMixer}
* @default null
*/
this.mixer = null;
/**
* The `onLoad` callback function.
*
* @type {Function}
*/
this.onLoadComplete = function () {};
// internal
this.loadCounter = 0;
}
/**
* Loads the character model for the given config.
*
* @param {Object} config - The config which defines the model and textures paths.
*/
loadParts( config ) {
const scope = this;
function createPart( geometry, skinMap ) {
const materialWireframe = new MeshLambertMaterial( { color: 0xffaa00, wireframe: true } );
const materialTexture = new MeshLambertMaterial( { color: 0xffffff, wireframe: false, map: skinMap } );
//
const mesh = new Mesh( geometry, materialTexture );
mesh.rotation.y = - Math.PI / 2;
mesh.castShadow = true;
mesh.receiveShadow = true;
//
mesh.materialTexture = materialTexture;
mesh.materialWireframe = materialWireframe;
return mesh;
}
function loadTextures( baseUrl, textureUrls ) {
const textureLoader = new TextureLoader();
const textures = [];
for ( let i = 0; i < textureUrls.length; i ++ ) {
textures[ i ] = textureLoader.load( baseUrl + textureUrls[ i ], checkLoadingComplete );
textures[ i ].mapping = UVMapping;
textures[ i ].name = textureUrls[ i ];
textures[ i ].colorSpace = SRGBColorSpace;
}
return textures;
}
function checkLoadingComplete() {
scope.loadCounter -= 1;
if ( scope.loadCounter === 0 ) scope.onLoadComplete();
}
this.loadCounter = config.weapons.length * 2 + config.skins.length + 1;
const weaponsTextures = [];
for ( let i = 0; i < config.weapons.length; i ++ ) weaponsTextures[ i ] = config.weapons[ i ][ 1 ];
// SKINS
this.skinsBody = loadTextures( config.baseUrl + 'skins/', config.skins );
this.skinsWeapon = loadTextures( config.baseUrl + 'skins/', weaponsTextures );
// BODY
const loader = new MD2Loader();
loader.load( config.baseUrl + config.body, function ( geo ) {
const boundingBox = new Box3();
boundingBox.setFromBufferAttribute( geo.attributes.position );
scope.root.position.y = - scope.scale * boundingBox.min.y;
const mesh = createPart( geo, scope.skinsBody[ 0 ] );
mesh.scale.set( scope.scale, scope.scale, scope.scale );
scope.root.add( mesh );
scope.meshBody = mesh;
scope.meshBody.clipOffset = 0;
scope.activeAnimationClipName = mesh.geometry.animations[ 0 ].name;
scope.mixer = new AnimationMixer( mesh );
checkLoadingComplete();
} );
// WEAPONS
const generateCallback = function ( index, name ) {
return function ( geo ) {
const mesh = createPart( geo, scope.skinsWeapon[ index ] );
mesh.scale.set( scope.scale, scope.scale, scope.scale );
mesh.visible = false;
mesh.name = name;
scope.root.add( mesh );
scope.weapons[ index ] = mesh;
scope.meshWeapon = mesh;
checkLoadingComplete();
};
};
for ( let i = 0; i < config.weapons.length; i ++ ) {
loader.load( config.baseUrl + config.weapons[ i ][ 0 ], generateCallback( i, config.weapons[ i ][ 0 ] ) );
}
}
/**
* Sets the animation playback rate.
*
* @param {number} rate - The playback rate to set.
*/
setPlaybackRate( rate ) {
if ( rate !== 0 ) {
this.mixer.timeScale = 1 / rate;
} else {
this.mixer.timeScale = 0;
}
}
/**
* Sets the wireframe material flag.
*
* @param {boolean} wireframeEnabled - Whether to enable wireframe rendering or not.
*/
setWireframe( wireframeEnabled ) {
if ( wireframeEnabled ) {
if ( this.meshBody ) this.meshBody.material = this.meshBody.materialWireframe;
if ( this.meshWeapon ) this.meshWeapon.material = this.meshWeapon.materialWireframe;
} else {
if ( this.meshBody ) this.meshBody.material = this.meshBody.materialTexture;
if ( this.meshWeapon ) this.meshWeapon.material = this.meshWeapon.materialTexture;
}
}
/**
* Sets the skin defined by the given skin index. This will result in a different texture
* for the body mesh.
*
* @param {number} index - The skin index.
*/
setSkin( index ) {
if ( this.meshBody && this.meshBody.material.wireframe === false ) {
this.meshBody.material.map = this.skinsBody[ index ];
}
}
/**
* Sets the weapon defined by the given weapon index. This will result in a different weapon
* hold by the character.
*
* @param {number} index - The weapon index.
*/
setWeapon( index ) {
for ( let i = 0; i < this.weapons.length; i ++ ) this.weapons[ i ].visible = false;
const activeWeapon = this.weapons[ index ];
if ( activeWeapon ) {
activeWeapon.visible = true;
this.meshWeapon = activeWeapon;
this.syncWeaponAnimation();
}
}
/**
* Sets the defined animation clip as the active animation.
*
* @param {string} clipName - The name of the animation clip.
*/
setAnimation( clipName ) {
if ( this.meshBody ) {
if ( this.meshBody.activeAction ) {
this.meshBody.activeAction.stop();
this.meshBody.activeAction = null;
}
const action = this.mixer.clipAction( clipName, this.meshBody );
if ( action ) {
this.meshBody.activeAction = action.play();
}
}
this.activeClipName = clipName;
this.syncWeaponAnimation();
}
/**
* Synchronizes the weapon with the body animation.
*/
syncWeaponAnimation() {
const clipName = this.activeClipName;
if ( this.meshWeapon ) {
if ( this.meshWeapon.activeAction ) {
this.meshWeapon.activeAction.stop();
this.meshWeapon.activeAction = null;
}
const action = this.mixer.clipAction( clipName, this.meshWeapon );
if ( action ) {
this.meshWeapon.activeAction = action.syncWith( this.meshBody.activeAction ).play();
}
}
}
/**
* Updates the animations of the mesh. Must be called inside the animation loop.
*
* @param {number} delta - The delta time in seconds.
*/
update( delta ) {
if ( this.mixer ) this.mixer.update( delta );
}
}
export { MD2Character };

View File

@@ -0,0 +1,740 @@
import {
Box3,
MathUtils,
MeshLambertMaterial,
Object3D,
TextureLoader,
UVMapping,
SRGBColorSpace
} from 'three';
import { MD2Loader } from '../loaders/MD2Loader.js';
import { MorphBlendMesh } from '../misc/MorphBlendMesh.js';
/**
* This class represents a management component for animated MD2
* character assets. It provides a larger API compared to {@link MD2Character}.
*
* @three_import import { MD2CharacterComplex } from 'three/addons/misc/MD2CharacterComplex.js';
*/
class MD2CharacterComplex {
/**
* Constructs a new MD2 character.
*/
constructor() {
/**
* The mesh scale.
*
* @type {number}
* @default 1
*/
this.scale = 1;
/**
* The FPS
*
* @type {number}
* @default 6
*/
this.animationFPS = 6;
/**
* The transition frames.
*
* @type {number}
* @default 15
*/
this.transitionFrames = 15;
/**
* The character's maximum speed.
*
* @type {number}
* @default 275
*/
this.maxSpeed = 275;
/**
* The character's maximum reverse speed.
*
* @type {number}
* @default - 275
*/
this.maxReverseSpeed = - 275;
/**
* The character's front acceleration.
*
* @type {number}
* @default 600
*/
this.frontAcceleration = 600;
/**
* The character's back acceleration.
*
* @type {number}
* @default 600
*/
this.backAcceleration = 600;
/**
* The character's front deceleration.
*
* @type {number}
* @default 600
*/
this.frontDeceleration = 600;
/**
* The character's angular speed.
*
* @type {number}
* @default 2.5
*/
this.angularSpeed = 2.5;
/**
* The root 3D object
*
* @type {Object3D}
*/
this.root = new Object3D();
/**
* The body mesh.
*
* @type {?Mesh}
* @default null
*/
this.meshBody = null;
/**
* The weapon mesh.
*
* @type {?Mesh}
* @default null
*/
this.meshWeapon = null;
/**
* The movement controls.
*
* @type {?Object}
* @default null
*/
this.controls = null;
/**
* The body skins.
*
* @type {Array<Texture>}
*/
this.skinsBody = [];
/**
* The weapon skins.
*
* @type {Array<Texture>}
*/
this.skinsWeapon = [];
/**
* The weapon meshes.
*
* @type {Array<Mesh>}
*/
this.weapons = [];
/**
* The current skin.
*
* @type {Texture}
* @default undefined
*/
this.currentSkin = undefined;
//
this.onLoadComplete = function () {};
// internals
this.meshes = [];
this.animations = {};
this.loadCounter = 0;
// internal movement control variables
this.speed = 0;
this.bodyOrientation = 0;
this.walkSpeed = this.maxSpeed;
this.crouchSpeed = this.maxSpeed * 0.5;
// internal animation parameters
this.activeAnimation = null;
this.oldAnimation = null;
// API
}
/**
* Toggles shadow casting and receiving on the character's meshes.
*
* @param {boolean} enable - Whether to enable shadows or not.
*/
enableShadows( enable ) {
for ( let i = 0; i < this.meshes.length; i ++ ) {
this.meshes[ i ].castShadow = enable;
this.meshes[ i ].receiveShadow = enable;
}
}
/**
* Toggles visibility on the character's meshes.
*
* @param {boolean} enable - Whether the character is visible or not.
*/
setVisible( enable ) {
for ( let i = 0; i < this.meshes.length; i ++ ) {
this.meshes[ i ].visible = enable;
this.meshes[ i ].visible = enable;
}
}
/**
* Shares certain resources from a different character model.
*
* @param {MD2CharacterComplex} original - The original MD2 character.
*/
shareParts( original ) {
this.animations = original.animations;
this.walkSpeed = original.walkSpeed;
this.crouchSpeed = original.crouchSpeed;
this.skinsBody = original.skinsBody;
this.skinsWeapon = original.skinsWeapon;
// BODY
const mesh = this._createPart( original.meshBody.geometry, this.skinsBody[ 0 ] );
mesh.scale.set( this.scale, this.scale, this.scale );
this.root.position.y = original.root.position.y;
this.root.add( mesh );
this.meshBody = mesh;
this.meshes.push( mesh );
// WEAPONS
for ( let i = 0; i < original.weapons.length; i ++ ) {
const meshWeapon = this._createPart( original.weapons[ i ].geometry, this.skinsWeapon[ i ] );
meshWeapon.scale.set( this.scale, this.scale, this.scale );
meshWeapon.visible = false;
meshWeapon.name = original.weapons[ i ].name;
this.root.add( meshWeapon );
this.weapons[ i ] = meshWeapon;
this.meshWeapon = meshWeapon;
this.meshes.push( meshWeapon );
}
}
/**
* Loads the character model for the given config.
*
* @param {Object} config - The config which defines the model and textures paths.
*/
loadParts( config ) {
const scope = this;
function loadTextures( baseUrl, textureUrls ) {
const textureLoader = new TextureLoader();
const textures = [];
for ( let i = 0; i < textureUrls.length; i ++ ) {
textures[ i ] = textureLoader.load( baseUrl + textureUrls[ i ], checkLoadingComplete );
textures[ i ].mapping = UVMapping;
textures[ i ].name = textureUrls[ i ];
textures[ i ].colorSpace = SRGBColorSpace;
}
return textures;
}
function checkLoadingComplete() {
scope.loadCounter -= 1;
if ( scope.loadCounter === 0 ) scope.onLoadComplete();
}
this.animations = config.animations;
this.walkSpeed = config.walkSpeed;
this.crouchSpeed = config.crouchSpeed;
this.loadCounter = config.weapons.length * 2 + config.skins.length + 1;
const weaponsTextures = [];
for ( let i = 0; i < config.weapons.length; i ++ ) weaponsTextures[ i ] = config.weapons[ i ][ 1 ];
// SKINS
this.skinsBody = loadTextures( config.baseUrl + 'skins/', config.skins );
this.skinsWeapon = loadTextures( config.baseUrl + 'skins/', weaponsTextures );
// BODY
const loader = new MD2Loader();
loader.load( config.baseUrl + config.body, function ( geo ) {
const boundingBox = new Box3();
boundingBox.setFromBufferAttribute( geo.attributes.position );
scope.root.position.y = - scope.scale * boundingBox.min.y;
const mesh = scope._createPart( geo, scope.skinsBody[ 0 ] );
mesh.scale.set( scope.scale, scope.scale, scope.scale );
scope.root.add( mesh );
scope.meshBody = mesh;
scope.meshes.push( mesh );
checkLoadingComplete();
} );
// WEAPONS
const generateCallback = function ( index, name ) {
return function ( geo ) {
const mesh = scope._createPart( geo, scope.skinsWeapon[ index ] );
mesh.scale.set( scope.scale, scope.scale, scope.scale );
mesh.visible = false;
mesh.name = name;
scope.root.add( mesh );
scope.weapons[ index ] = mesh;
scope.meshWeapon = mesh;
scope.meshes.push( mesh );
checkLoadingComplete();
};
};
for ( let i = 0; i < config.weapons.length; i ++ ) {
loader.load( config.baseUrl + config.weapons[ i ][ 0 ], generateCallback( i, config.weapons[ i ][ 0 ] ) );
}
}
/**
* Sets the animation playback rate.
*
* @param {number} rate - The playback rate to set.
*/
setPlaybackRate( rate ) {
if ( this.meshBody ) this.meshBody.duration = this.meshBody.baseDuration / rate;
if ( this.meshWeapon ) this.meshWeapon.duration = this.meshWeapon.baseDuration / rate;
}
/**
* Sets the wireframe material flag.
*
* @param {boolean} wireframeEnabled - Whether to enable wireframe rendering or not.
*/
setWireframe( wireframeEnabled ) {
if ( wireframeEnabled ) {
if ( this.meshBody ) this.meshBody.material = this.meshBody.materialWireframe;
if ( this.meshWeapon ) this.meshWeapon.material = this.meshWeapon.materialWireframe;
} else {
if ( this.meshBody ) this.meshBody.material = this.meshBody.materialTexture;
if ( this.meshWeapon ) this.meshWeapon.material = this.meshWeapon.materialTexture;
}
}
/**
* Sets the skin defined by the given skin index. This will result in a different texture
* for the body mesh.
*
* @param {number} index - The skin index.
*/
setSkin( index ) {
if ( this.meshBody && this.meshBody.material.wireframe === false ) {
this.meshBody.material.map = this.skinsBody[ index ];
this.currentSkin = index;
}
}
/**
* Sets the weapon defined by the given weapon index. This will result in a different weapon
* hold by the character.
*
* @param {number} index - The weapon index.
*/
setWeapon( index ) {
for ( let i = 0; i < this.weapons.length; i ++ ) this.weapons[ i ].visible = false;
const activeWeapon = this.weapons[ index ];
if ( activeWeapon ) {
activeWeapon.visible = true;
this.meshWeapon = activeWeapon;
if ( this.activeAnimation ) {
activeWeapon.playAnimation( this.activeAnimation );
this.meshWeapon.setAnimationTime( this.activeAnimation, this.meshBody.getAnimationTime( this.activeAnimation ) );
}
}
}
/**
* Sets the defined animation clip as the active animation.
*
* @param {string} animationName - The name of the animation clip.
*/
setAnimation( animationName ) {
if ( animationName === this.activeAnimation || ! animationName ) return;
if ( this.meshBody ) {
this.meshBody.setAnimationWeight( animationName, 0 );
this.meshBody.playAnimation( animationName );
this.oldAnimation = this.activeAnimation;
this.activeAnimation = animationName;
this.blendCounter = this.transitionFrames;
}
if ( this.meshWeapon ) {
this.meshWeapon.setAnimationWeight( animationName, 0 );
this.meshWeapon.playAnimation( animationName );
}
}
update( delta ) {
if ( this.controls ) this.updateMovementModel( delta );
if ( this.animations ) {
this.updateBehaviors();
this.updateAnimations( delta );
}
}
/**
* Updates the animations of the mesh. Must be called inside the animation loop.
*
* @param {number} delta - The delta time in seconds.
*/
updateAnimations( delta ) {
let mix = 1;
if ( this.blendCounter > 0 ) {
mix = ( this.transitionFrames - this.blendCounter ) / this.transitionFrames;
this.blendCounter -= 1;
}
if ( this.meshBody ) {
this.meshBody.update( delta );
this.meshBody.setAnimationWeight( this.activeAnimation, mix );
this.meshBody.setAnimationWeight( this.oldAnimation, 1 - mix );
}
if ( this.meshWeapon ) {
this.meshWeapon.update( delta );
this.meshWeapon.setAnimationWeight( this.activeAnimation, mix );
this.meshWeapon.setAnimationWeight( this.oldAnimation, 1 - mix );
}
}
/**
* Updates the animation state based on the control inputs.
*/
updateBehaviors() {
const controls = this.controls;
const animations = this.animations;
let moveAnimation, idleAnimation;
// crouch vs stand
if ( controls.crouch ) {
moveAnimation = animations[ 'crouchMove' ];
idleAnimation = animations[ 'crouchIdle' ];
} else {
moveAnimation = animations[ 'move' ];
idleAnimation = animations[ 'idle' ];
}
// actions
if ( controls.jump ) {
moveAnimation = animations[ 'jump' ];
idleAnimation = animations[ 'jump' ];
}
if ( controls.attack ) {
if ( controls.crouch ) {
moveAnimation = animations[ 'crouchAttack' ];
idleAnimation = animations[ 'crouchAttack' ];
} else {
moveAnimation = animations[ 'attack' ];
idleAnimation = animations[ 'attack' ];
}
}
// set animations
if ( controls.moveForward || controls.moveBackward || controls.moveLeft || controls.moveRight ) {
if ( this.activeAnimation !== moveAnimation ) {
this.setAnimation( moveAnimation );
}
}
if ( Math.abs( this.speed ) < 0.2 * this.maxSpeed && ! ( controls.moveLeft || controls.moveRight || controls.moveForward || controls.moveBackward ) ) {
if ( this.activeAnimation !== idleAnimation ) {
this.setAnimation( idleAnimation );
}
}
// set animation direction
if ( controls.moveForward ) {
if ( this.meshBody ) {
this.meshBody.setAnimationDirectionForward( this.activeAnimation );
this.meshBody.setAnimationDirectionForward( this.oldAnimation );
}
if ( this.meshWeapon ) {
this.meshWeapon.setAnimationDirectionForward( this.activeAnimation );
this.meshWeapon.setAnimationDirectionForward( this.oldAnimation );
}
}
if ( controls.moveBackward ) {
if ( this.meshBody ) {
this.meshBody.setAnimationDirectionBackward( this.activeAnimation );
this.meshBody.setAnimationDirectionBackward( this.oldAnimation );
}
if ( this.meshWeapon ) {
this.meshWeapon.setAnimationDirectionBackward( this.activeAnimation );
this.meshWeapon.setAnimationDirectionBackward( this.oldAnimation );
}
}
}
/**
* Transforms the character model based on the control input.
*
* @param {number} delta - The delta time in seconds.
*/
updateMovementModel( delta ) {
function exponentialEaseOut( k ) {
return k === 1 ? 1 : - Math.pow( 2, - 10 * k ) + 1;
}
const controls = this.controls;
// speed based on controls
if ( controls.crouch ) this.maxSpeed = this.crouchSpeed;
else this.maxSpeed = this.walkSpeed;
this.maxReverseSpeed = - this.maxSpeed;
if ( controls.moveForward ) this.speed = MathUtils.clamp( this.speed + delta * this.frontAcceleration, this.maxReverseSpeed, this.maxSpeed );
if ( controls.moveBackward ) this.speed = MathUtils.clamp( this.speed - delta * this.backAcceleration, this.maxReverseSpeed, this.maxSpeed );
// orientation based on controls
// (don't just stand while turning)
const dir = 1;
if ( controls.moveLeft ) {
this.bodyOrientation += delta * this.angularSpeed;
this.speed = MathUtils.clamp( this.speed + dir * delta * this.frontAcceleration, this.maxReverseSpeed, this.maxSpeed );
}
if ( controls.moveRight ) {
this.bodyOrientation -= delta * this.angularSpeed;
this.speed = MathUtils.clamp( this.speed + dir * delta * this.frontAcceleration, this.maxReverseSpeed, this.maxSpeed );
}
// speed decay
if ( ! ( controls.moveForward || controls.moveBackward ) ) {
if ( this.speed > 0 ) {
const k = exponentialEaseOut( this.speed / this.maxSpeed );
this.speed = MathUtils.clamp( this.speed - k * delta * this.frontDeceleration, 0, this.maxSpeed );
} else {
const k = exponentialEaseOut( this.speed / this.maxReverseSpeed );
this.speed = MathUtils.clamp( this.speed + k * delta * this.backAcceleration, this.maxReverseSpeed, 0 );
}
}
// displacement
const forwardDelta = this.speed * delta;
this.root.position.x += Math.sin( this.bodyOrientation ) * forwardDelta;
this.root.position.z += Math.cos( this.bodyOrientation ) * forwardDelta;
// steering
this.root.rotation.y = this.bodyOrientation;
}
// internal
_createPart( geometry, skinMap ) {
const materialWireframe = new MeshLambertMaterial( { color: 0xffaa00, wireframe: true } );
const materialTexture = new MeshLambertMaterial( { color: 0xffffff, wireframe: false, map: skinMap } );
//
const mesh = new MorphBlendMesh( geometry, materialTexture );
mesh.rotation.y = - Math.PI / 2;
//
mesh.materialTexture = materialTexture;
mesh.materialWireframe = materialWireframe;
//
mesh.autoCreateAnimations( this.animationFPS );
return mesh;
}
}
export { MD2CharacterComplex };

119
node_modules/three/examples/jsm/misc/MorphAnimMesh.js generated vendored Normal file
View File

@@ -0,0 +1,119 @@
import {
AnimationClip,
AnimationMixer,
Mesh
} from 'three';
/**
* A special type of an animated mesh with a simple interface
* for animation playback. It allows to playback just one animation
* without any transitions or fading between animation changes.
*
* @augments Mesh
* @three_import import { MorphAnimMesh } from 'three/addons/misc/MorphAnimMesh.js';
*/
class MorphAnimMesh extends Mesh {
/**
* Constructs a new morph anim mesh.
*
* @param {BufferGeometry} [geometry] - The mesh geometry.
* @param {Material|Array<Material>} [material] - The mesh material.
*/
constructor( geometry, material ) {
super( geometry, material );
this.type = 'MorphAnimMesh';
/**
* The internal animation mixer.
*
* @type {AnimationMixer}
*/
this.mixer = new AnimationMixer( this );
/**
* The current active animation action.
*
* @type {?AnimationAction}
* @default null
*/
this.activeAction = null;
}
/**
* Sets the animation playback direction to "forward".
*/
setDirectionForward() {
this.mixer.timeScale = 1.0;
}
/**
* Sets the animation playback direction to "backward".
*/
setDirectionBackward() {
this.mixer.timeScale = - 1.0;
}
/**
* Plays the defined animation clip. The implementation assumes the animation
* clips are stored in {@link Object3D#animations} or the geometry.
*
* @param {string} label - The name of the animation clip.
* @param {number} fps - The FPS of the animation clip.
*/
playAnimation( label, fps ) {
if ( this.activeAction ) {
this.activeAction.stop();
this.activeAction = null;
}
const clip = AnimationClip.findByName( this, label );
if ( clip ) {
const action = this.mixer.clipAction( clip );
action.timeScale = ( clip.tracks.length * fps ) / clip.duration;
this.activeAction = action.play();
} else {
throw new Error( 'THREE.MorphAnimMesh: animations[' + label + '] undefined in .playAnimation()' );
}
}
/**
* Updates the animations of the mesh. Must be called inside the animation loop.
*
* @param {number} delta - The delta time in seconds.
*/
updateAnimation( delta ) {
this.mixer.update( delta );
}
copy( source, recursive ) {
super.copy( source, recursive );
this.mixer = new AnimationMixer( this );
return this;
}
}
export { MorphAnimMesh };

425
node_modules/three/examples/jsm/misc/MorphBlendMesh.js generated vendored Normal file
View File

@@ -0,0 +1,425 @@
import {
MathUtils,
Mesh
} from 'three';
/**
* A special type of an animated mesh with a more advanced interface
* for animation playback. Unlike {@link MorphAnimMesh}. It allows to
* playback more than one morph animation at the same time but without
* fading options.
*
* @augments Mesh
* @three_import import { MorphBlendMesh } from 'three/addons/misc/MorphBlendMesh.js';
*/
class MorphBlendMesh extends Mesh {
/**
* Constructs a new morph blend mesh.
*
* @param {BufferGeometry} [geometry] - The mesh geometry.
* @param {Material|Array<Material>} [material] - The mesh material.
*/
constructor( geometry, material ) {
super( geometry, material );
/**
* A dictionary of animations.
*
* @type {Object<string,Object>}
*/
this.animationsMap = {};
/**
* A list of animations.
*
* @type {Array<Object>}
*/
this.animationsList = [];
// prepare default animation
// (all frames played together in 1 second)
const numFrames = Object.keys( this.morphTargetDictionary ).length;
const name = '__default';
const startFrame = 0;
const endFrame = numFrames - 1;
const fps = numFrames / 1;
this.createAnimation( name, startFrame, endFrame, fps );
this.setAnimationWeight( name, 1 );
}
/**
* Creates a new animation.
*
* @param {string} name - The animation name.
* @param {number} start - The start time.
* @param {number} end - The end time.
* @param {number} fps - The FPS.
*/
createAnimation( name, start, end, fps ) {
const animation = {
start: start,
end: end,
length: end - start + 1,
fps: fps,
duration: ( end - start ) / fps,
lastFrame: 0,
currentFrame: 0,
active: false,
time: 0,
direction: 1,
weight: 1,
directionBackwards: false,
mirroredLoop: false
};
this.animationsMap[ name ] = animation;
this.animationsList.push( animation );
}
/**
* Automatically creates animations based on the values in
* {@link Mesh#morphTargetDictionary}.
*
* @param {number} fps - The FPS of all animations.
*/
autoCreateAnimations( fps ) {
const pattern = /([a-z]+)_?(\d+)/i;
let firstAnimation;
const frameRanges = {};
let i = 0;
for ( const key in this.morphTargetDictionary ) {
const chunks = key.match( pattern );
if ( chunks && chunks.length > 1 ) {
const name = chunks[ 1 ];
if ( ! frameRanges[ name ] ) frameRanges[ name ] = { start: Infinity, end: - Infinity };
const range = frameRanges[ name ];
if ( i < range.start ) range.start = i;
if ( i > range.end ) range.end = i;
if ( ! firstAnimation ) firstAnimation = name;
}
i ++;
}
for ( const name in frameRanges ) {
const range = frameRanges[ name ];
this.createAnimation( name, range.start, range.end, fps );
}
this.firstAnimation = firstAnimation;
}
/**
* Sets the animation playback direction to "forward" for the
* defined animation.
*
* @param {string} name - The animation name.
*/
setAnimationDirectionForward( name ) {
const animation = this.animationsMap[ name ];
if ( animation ) {
animation.direction = 1;
animation.directionBackwards = false;
}
}
/**
* Sets the animation playback direction to "backward" for the
* defined animation.
*
* @param {string} name - The animation name.
*/
setAnimationDirectionBackward( name ) {
const animation = this.animationsMap[ name ];
if ( animation ) {
animation.direction = - 1;
animation.directionBackwards = true;
}
}
/**
* Sets the FPS to the given value for the defined animation.
*
* @param {string} name - The animation name.
* @param {number} fps - The FPS to set.
*/
setAnimationFPS( name, fps ) {
const animation = this.animationsMap[ name ];
if ( animation ) {
animation.fps = fps;
animation.duration = ( animation.end - animation.start ) / animation.fps;
}
}
/**
* Sets the duration to the given value for the defined animation.
*
* @param {string} name - The animation name.
* @param {number} duration - The duration to set.
*/
setAnimationDuration( name, duration ) {
const animation = this.animationsMap[ name ];
if ( animation ) {
animation.duration = duration;
animation.fps = ( animation.end - animation.start ) / animation.duration;
}
}
/**
* Sets the weight to the given value for the defined animation.
*
* @param {string} name - The animation name.
* @param {number} weight - The weight to set.
*/
setAnimationWeight( name, weight ) {
const animation = this.animationsMap[ name ];
if ( animation ) {
animation.weight = weight;
}
}
/**
* Sets the time to the given value for the defined animation.
*
* @param {string} name - The animation name.
* @param {number} time - The time to set.
*/
setAnimationTime( name, time ) {
const animation = this.animationsMap[ name ];
if ( animation ) {
animation.time = time;
}
}
/**
* Returns the time for the defined animation.
*
* @param {string} name - The animation name.
* @return {number} The time.
*/
getAnimationTime( name ) {
let time = 0;
const animation = this.animationsMap[ name ];
if ( animation ) {
time = animation.time;
}
return time;
}
/**
* Returns the duration for the defined animation.
*
* @param {string} name - The animation name.
* @return {number} The duration.
*/
getAnimationDuration( name ) {
let duration = - 1;
const animation = this.animationsMap[ name ];
if ( animation ) {
duration = animation.duration;
}
return duration;
}
/**
* Plays the defined animation.
*
* @param {string} name - The animation name.
*/
playAnimation( name ) {
const animation = this.animationsMap[ name ];
if ( animation ) {
animation.time = 0;
animation.active = true;
} else {
console.warn( 'THREE.MorphBlendMesh: animation[' + name + '] undefined in .playAnimation()' );
}
}
/**
* Stops the defined animation.
*
* @param {string} name - The animation name.
*/
stopAnimation( name ) {
const animation = this.animationsMap[ name ];
if ( animation ) {
animation.active = false;
}
}
/**
* Updates the animations of the mesh.
*
* @param {number} delta - The delta time in seconds.
*/
update( delta ) {
for ( let i = 0, il = this.animationsList.length; i < il; i ++ ) {
const animation = this.animationsList[ i ];
if ( ! animation.active ) continue;
const frameTime = animation.duration / animation.length;
animation.time += animation.direction * delta;
if ( animation.mirroredLoop ) {
if ( animation.time > animation.duration || animation.time < 0 ) {
animation.direction *= - 1;
if ( animation.time > animation.duration ) {
animation.time = animation.duration;
animation.directionBackwards = true;
}
if ( animation.time < 0 ) {
animation.time = 0;
animation.directionBackwards = false;
}
}
} else {
animation.time = animation.time % animation.duration;
if ( animation.time < 0 ) animation.time += animation.duration;
}
const keyframe = animation.start + MathUtils.clamp( Math.floor( animation.time / frameTime ), 0, animation.length - 1 );
const weight = animation.weight;
if ( keyframe !== animation.currentFrame ) {
this.morphTargetInfluences[ animation.lastFrame ] = 0;
this.morphTargetInfluences[ animation.currentFrame ] = 1 * weight;
this.morphTargetInfluences[ keyframe ] = 0;
animation.lastFrame = animation.currentFrame;
animation.currentFrame = keyframe;
}
let mix = ( animation.time % frameTime ) / frameTime;
if ( animation.directionBackwards ) mix = 1 - mix;
if ( animation.currentFrame !== animation.lastFrame ) {
this.morphTargetInfluences[ animation.currentFrame ] = mix * weight;
this.morphTargetInfluences[ animation.lastFrame ] = ( 1 - mix ) * weight;
} else {
this.morphTargetInfluences[ animation.currentFrame ] = weight;
}
}
}
}
export { MorphBlendMesh };

View File

@@ -0,0 +1,364 @@
import { DoubleSide, FloatType, HalfFloatType, Mesh, MeshBasicMaterial, MeshPhongMaterial, PlaneGeometry, Scene, WebGLRenderTarget } from 'three';
import { potpack } from '../libs/potpack.module.js';
/**
* Progressive Light Map Accumulator, by [zalo]{@link https://github.com/zalo/}.
*
* To use, simply construct a `ProgressiveLightMap` object,
* `plmap.addObjectsToLightMap(object)` an array of semi-static
* objects and lights to the class once, and then call
* `plmap.update(camera)` every frame to begin accumulating
* lighting samples.
*
* This should begin accumulating lightmaps which apply to
* your objects, so you can start jittering lighting to achieve
* the texture-space effect you're looking for.
*
* This class can only be used with {@link WebGLRenderer}.
* When using {@link WebGPURenderer}, import from `ProgressiveLightMapGPU.js`.
*
* @three_import import { ProgressiveLightMap } from 'three/addons/misc/ProgressiveLightMap.js';
*/
class ProgressiveLightMap {
/**
* Constructs a new progressive light map.
*
* @param {WebGLRenderer} renderer - The renderer.
* @param {number} [res=1024] - The side-long dimension of the total lightmap.
*/
constructor( renderer, res = 1024 ) {
/**
* The renderer.
*
* @type {WebGLRenderer}
*/
this.renderer = renderer;
/**
* The side-long dimension of the total lightmap.
*
* @type {number}
* @default 1024
*/
this.res = res;
// internals
this.lightMapContainers = [];
this.scene = new Scene();
this.buffer1Active = false;
this.firstUpdate = true;
this.labelMesh = null;
this.blurringPlane = null;
// Create the Progressive LightMap Texture
const format = /(Android|iPad|iPhone|iPod)/g.test( navigator.userAgent ) ? HalfFloatType : FloatType;
this.progressiveLightMap1 = new WebGLRenderTarget( this.res, this.res, { type: format } );
this.progressiveLightMap2 = new WebGLRenderTarget( this.res, this.res, { type: format } );
this.progressiveLightMap2.texture.channel = 1;
// Inject some spicy new logic into a standard phong material
this.uvMat = new MeshPhongMaterial();
this.uvMat.uniforms = {};
this.uvMat.onBeforeCompile = ( shader ) => {
// Vertex Shader: Set Vertex Positions to the Unwrapped UV Positions
shader.vertexShader =
'attribute vec2 uv1;\n' +
'#define USE_LIGHTMAP\n' +
'#define LIGHTMAP_UV uv1\n' +
shader.vertexShader.slice( 0, - 1 ) +
' gl_Position = vec4((LIGHTMAP_UV - 0.5) * 2.0, 1.0, 1.0); }';
// Fragment Shader: Set Pixels to average in the Previous frame's Shadows
const bodyStart = shader.fragmentShader.indexOf( 'void main() {' );
shader.fragmentShader =
'#define USE_LIGHTMAP\n' +
shader.fragmentShader.slice( 0, bodyStart ) +
' uniform sampler2D previousShadowMap;\n uniform float averagingWindow;\n' +
shader.fragmentShader.slice( bodyStart - 1, - 1 ) +
`\nvec3 texelOld = texture2D(previousShadowMap, vLightMapUv).rgb;
gl_FragColor.rgb = mix(texelOld, gl_FragColor.rgb, 1.0/averagingWindow);
}`;
// Set the Previous Frame's Texture Buffer and Averaging Window
shader.uniforms.previousShadowMap = { value: this.progressiveLightMap1.texture };
shader.uniforms.averagingWindow = { value: 100 };
this.uvMat.uniforms = shader.uniforms;
// Set the new Shader to this
this.uvMat.userData.shader = shader;
};
}
/**
* Sets these objects' materials' lightmaps and modifies their uv1's.
*
* @param {Array<Object3D>} objects - An array of objects and lights to set up your lightmap.
*/
addObjectsToLightMap( objects ) {
// Prepare list of UV bounding boxes for packing later...
this.uv_boxes = []; const padding = 3 / this.res;
for ( let ob = 0; ob < objects.length; ob ++ ) {
const object = objects[ ob ];
// If this object is a light, simply add it to the internal scene
if ( object.isLight ) {
this.scene.attach( object ); continue;
}
if ( object.geometry.hasAttribute( 'uv' ) === false ) {
console.warn( 'THREE.ProgressiveLightMap: All lightmap objects need uvs.' ); continue;
}
if ( this.blurringPlane === null ) {
this._initializeBlurPlane( this.res, this.progressiveLightMap1 );
}
// Apply the lightmap to the object
object.material.lightMap = this.progressiveLightMap2.texture;
object.material.dithering = true;
object.castShadow = true;
object.receiveShadow = true;
object.renderOrder = 1000 + ob;
// Prepare UV boxes for potpack
// TODO: Size these by object surface area
this.uv_boxes.push( { w: 1 + ( padding * 2 ),
h: 1 + ( padding * 2 ), index: ob } );
this.lightMapContainers.push( { basicMat: object.material, object: object } );
}
// Pack the objects' lightmap UVs into the same global space
const dimensions = potpack( this.uv_boxes );
this.uv_boxes.forEach( ( box ) => {
const uv1 = objects[ box.index ].geometry.getAttribute( 'uv' ).clone();
for ( let i = 0; i < uv1.array.length; i += uv1.itemSize ) {
uv1.array[ i ] = ( uv1.array[ i ] + box.x + padding ) / dimensions.w;
uv1.array[ i + 1 ] = ( uv1.array[ i + 1 ] + box.y + padding ) / dimensions.h;
}
objects[ box.index ].geometry.setAttribute( 'uv1', uv1 );
objects[ box.index ].geometry.getAttribute( 'uv1' ).needsUpdate = true;
} );
}
/**
* This function renders each mesh one at a time into their respective surface maps.
*
* @param {Camera} camera - The camera the scene is rendered with.
* @param {number} [blendWindow=100] - When >1, samples will accumulate over time.
* @param {boolean} [blurEdges=true] - Whether to fix UV Edges via blurring.
*/
update( camera, blendWindow = 100, blurEdges = true ) {
if ( this.blurringPlane === null ) {
return;
}
// Store the original Render Target
const oldTarget = this.renderer.getRenderTarget();
// The blurring plane applies blur to the seams of the lightmap
this.blurringPlane.visible = blurEdges;
// Steal the Object3D from the real world to our special dimension
for ( let l = 0; l < this.lightMapContainers.length; l ++ ) {
this.lightMapContainers[ l ].object.oldScene =
this.lightMapContainers[ l ].object.parent;
this.scene.attach( this.lightMapContainers[ l ].object );
}
// Initialize everything
if ( this.firstUpdate === true ) {
this.renderer.compile( this.scene, camera );
this.firstUpdate = false;
}
// Set each object's material to the UV Unwrapped Surface Mapping Version
for ( let l = 0; l < this.lightMapContainers.length; l ++ ) {
this.uvMat.uniforms.averagingWindow = { value: blendWindow };
this.lightMapContainers[ l ].object.material = this.uvMat;
this.lightMapContainers[ l ].object.oldFrustumCulled =
this.lightMapContainers[ l ].object.frustumCulled;
this.lightMapContainers[ l ].object.frustumCulled = false;
}
// Ping-pong two surface buffers for reading/writing
const activeMap = this.buffer1Active ? this.progressiveLightMap1 : this.progressiveLightMap2;
const inactiveMap = this.buffer1Active ? this.progressiveLightMap2 : this.progressiveLightMap1;
// Render the object's surface maps
this.renderer.setRenderTarget( activeMap );
this.uvMat.uniforms.previousShadowMap = { value: inactiveMap.texture };
this.blurringPlane.material.uniforms.previousShadowMap = { value: inactiveMap.texture };
this.buffer1Active = ! this.buffer1Active;
this.renderer.render( this.scene, camera );
// Restore the object's Real-time Material and add it back to the original world
for ( let l = 0; l < this.lightMapContainers.length; l ++ ) {
this.lightMapContainers[ l ].object.frustumCulled =
this.lightMapContainers[ l ].object.oldFrustumCulled;
this.lightMapContainers[ l ].object.material = this.lightMapContainers[ l ].basicMat;
this.lightMapContainers[ l ].object.oldScene.attach( this.lightMapContainers[ l ].object );
}
// Restore the original Render Target
this.renderer.setRenderTarget( oldTarget );
}
/**
* Draws the lightmap in the main scene. Call this after adding the objects to it.
*
* @param {boolean} visible - Whether the debug plane should be visible
* @param {Vector3} [position] - Where the debug plane should be drawn
*/
showDebugLightmap( visible, position = undefined ) {
if ( this.lightMapContainers.length === 0 ) {
console.warn( 'THREE.ProgressiveLightMap: Call .showDebugLightmap() after adding the objects.' );
return;
}
if ( this.labelMesh === null ) {
const labelMaterial = new MeshBasicMaterial( { map: this.progressiveLightMap1.texture, side: DoubleSide } );
const labelGeometry = new PlaneGeometry( 100, 100 );
this.labelMesh = new Mesh( labelGeometry, labelMaterial );
this.labelMesh.position.y = 250;
this.lightMapContainers[ 0 ].object.parent.add( this.labelMesh );
}
if ( position !== undefined ) {
this.labelMesh.position.copy( position );
}
this.labelMesh.visible = visible;
}
/**
* Creates the Blurring Plane.
*
* @private
* @param {number} res - The square resolution of this object's lightMap.
* @param {?WebGLRenderTarget} [lightMap] - The lightmap to initialize the plane with.
*/
_initializeBlurPlane( res, lightMap = null ) {
const blurMaterial = new MeshBasicMaterial();
blurMaterial.uniforms = { previousShadowMap: { value: null },
pixelOffset: { value: 1.0 / res },
polygonOffset: true, polygonOffsetFactor: - 1, polygonOffsetUnits: 3.0 };
blurMaterial.onBeforeCompile = ( shader ) => {
// Vertex Shader: Set Vertex Positions to the Unwrapped UV Positions
shader.vertexShader =
'#define USE_UV\n' +
shader.vertexShader.slice( 0, - 1 ) +
' gl_Position = vec4((uv - 0.5) * 2.0, 1.0, 1.0); }';
// Fragment Shader: Set Pixels to 9-tap box blur the current frame's Shadows
const bodyStart = shader.fragmentShader.indexOf( 'void main() {' );
shader.fragmentShader =
'#define USE_UV\n' +
shader.fragmentShader.slice( 0, bodyStart ) +
' uniform sampler2D previousShadowMap;\n uniform float pixelOffset;\n' +
shader.fragmentShader.slice( bodyStart - 1, - 1 ) +
` gl_FragColor.rgb = (
texture2D(previousShadowMap, vUv + vec2( pixelOffset, 0.0 )).rgb +
texture2D(previousShadowMap, vUv + vec2( 0.0 , pixelOffset)).rgb +
texture2D(previousShadowMap, vUv + vec2( 0.0 , -pixelOffset)).rgb +
texture2D(previousShadowMap, vUv + vec2(-pixelOffset, 0.0 )).rgb +
texture2D(previousShadowMap, vUv + vec2( pixelOffset, pixelOffset)).rgb +
texture2D(previousShadowMap, vUv + vec2(-pixelOffset, pixelOffset)).rgb +
texture2D(previousShadowMap, vUv + vec2( pixelOffset, -pixelOffset)).rgb +
texture2D(previousShadowMap, vUv + vec2(-pixelOffset, -pixelOffset)).rgb)/8.0;
}`;
// Set the LightMap Accumulation Buffer
shader.uniforms.previousShadowMap = { value: lightMap.texture };
shader.uniforms.pixelOffset = { value: 0.5 / res };
blurMaterial.uniforms = shader.uniforms;
// Set the new Shader to this
blurMaterial.userData.shader = shader;
};
this.blurringPlane = new Mesh( new PlaneGeometry( 1, 1 ), blurMaterial );
this.blurringPlane.name = 'Blurring Plane';
this.blurringPlane.frustumCulled = false;
this.blurringPlane.renderOrder = 0;
this.blurringPlane.material.depthWrite = false;
this.scene.add( this.blurringPlane );
}
/**
* Frees all internal resources.
*/
dispose() {
this.progressiveLightMap1.dispose();
this.progressiveLightMap2.dispose();
this.uvMat.dispose();
if ( this.blurringPlane !== null ) {
this.blurringPlane.geometry.dispose();
this.blurringPlane.material.dispose();
}
if ( this.labelMesh !== null ) {
this.labelMesh.geometry.dispose();
this.labelMesh.material.dispose();
}
}
}
export { ProgressiveLightMap };

View File

@@ -0,0 +1,316 @@
import { DoubleSide, FloatType, HalfFloatType, PlaneGeometry, Mesh, RenderTarget, Scene, MeshPhongNodeMaterial, NodeMaterial } from 'three/webgpu';
import { add, float, mix, output, sub, texture, uniform, uv, vec2, vec4 } from 'three/tsl';
import { potpack } from '../libs/potpack.module.js';
/**
* Progressive Light Map Accumulator, by [zalo]{@link https://github.com/zalo/}.
*
* To use, simply construct a `ProgressiveLightMap` object,
* `plmap.addObjectsToLightMap(object)` an array of semi-static
* objects and lights to the class once, and then call
* `plmap.update(camera)` every frame to begin accumulating
* lighting samples.
*
* This should begin accumulating lightmaps which apply to
* your objects, so you can start jittering lighting to achieve
* the texture-space effect you're looking for.
*
* This class can only be used with {@link WebGPURenderer}.
* When using {@link WebGLRenderer}, import from `ProgressiveLightMap.js`.
*
* @three_import import { ProgressiveLightMap } from 'three/addons/misc/ProgressiveLightMapGPU.js';
*/
class ProgressiveLightMap {
/**
* @param {WebGPURenderer} renderer - The renderer.
* @param {number} [resolution=1024] - The side-long dimension of the total lightmap.
*/
constructor( renderer, resolution = 1024 ) {
/**
* The renderer.
*
* @type {WebGPURenderer}
*/
this.renderer = renderer;
/**
* The side-long dimension of the total lightmap.
*
* @type {number}
* @default 1024
*/
this.resolution = resolution;
this._lightMapContainers = [];
this._scene = new Scene();
this._buffer1Active = false;
this._labelMesh = null;
this._blurringPlane = null;
// Create the Progressive LightMap Texture
const type = /(Android|iPad|iPhone|iPod)/g.test( navigator.userAgent ) ? HalfFloatType : FloatType;
this._progressiveLightMap1 = new RenderTarget( this.resolution, this.resolution, { type: type } );
this._progressiveLightMap2 = new RenderTarget( this.resolution, this.resolution, { type: type } );
this._progressiveLightMap2.texture.channel = 1;
// uniforms
this._averagingWindow = uniform( 100 );
this._previousShadowMap = texture( this._progressiveLightMap1.texture );
// materials
const uvNode = uv( 1 ).flipY();
this._uvMat = new MeshPhongNodeMaterial();
this._uvMat.vertexNode = vec4( sub( uvNode, vec2( 0.5 ) ).mul( 2 ), 1, 1 );
this._uvMat.outputNode = vec4( mix( this._previousShadowMap.sample( uv( 1 ) ), output, float( 1 ).div( this._averagingWindow ) ) );
}
/**
* Sets these objects' materials' lightmaps and modifies their uv1's.
*
* @param {Array<Object3D>} objects - An array of objects and lights to set up your lightmap.
*/
addObjectsToLightMap( objects ) {
// Prepare list of UV bounding boxes for packing later...
const uv_boxes = [];
const padding = 3 / this.resolution;
for ( let ob = 0; ob < objects.length; ob ++ ) {
const object = objects[ ob ];
// If this object is a light, simply add it to the internal scene
if ( object.isLight ) {
this._scene.attach( object ); continue;
}
if ( object.geometry.hasAttribute( 'uv' ) === false ) {
console.warn( 'THREE.ProgressiveLightMap: All lightmap objects need uvs.' ); continue;
}
if ( this._blurringPlane === null ) {
this._initializeBlurPlane();
}
// Apply the lightmap to the object
object.material.lightMap = this._progressiveLightMap2.texture;
object.material.dithering = true;
object.castShadow = true;
object.receiveShadow = true;
object.renderOrder = 1000 + ob;
// Prepare UV boxes for potpack (potpack will update x and y)
// TODO: Size these by object surface area
uv_boxes.push( { w: 1 + ( padding * 2 ), h: 1 + ( padding * 2 ), index: ob, x: 0, y: 0 } );
this._lightMapContainers.push( { basicMat: object.material, object: object } );
}
// Pack the objects' lightmap UVs into the same global space
const dimensions = potpack( uv_boxes );
uv_boxes.forEach( ( box ) => {
const uv1 = objects[ box.index ].geometry.getAttribute( 'uv' ).clone();
for ( let i = 0; i < uv1.array.length; i += uv1.itemSize ) {
uv1.array[ i ] = ( uv1.array[ i ] + box.x + padding ) / dimensions.w;
uv1.array[ i + 1 ] = 1 - ( ( uv1.array[ i + 1 ] + box.y + padding ) / dimensions.h );
}
objects[ box.index ].geometry.setAttribute( 'uv1', uv1 );
objects[ box.index ].geometry.getAttribute( 'uv1' ).needsUpdate = true;
} );
}
/**
* Frees all internal resources.
*/
dispose() {
this._progressiveLightMap1.dispose();
this._progressiveLightMap2.dispose();
this._uvMat.dispose();
if ( this._blurringPlane !== null ) {
this._blurringPlane.geometry.dispose();
this._blurringPlane.material.dispose();
}
if ( this._labelMesh !== null ) {
this._labelMesh.geometry.dispose();
this._labelMesh.material.dispose();
}
}
/**
* This function renders each mesh one at a time into their respective surface maps.
*
* @param {Camera} camera - The camera the scene is rendered with.
* @param {number} [blendWindow=100] - When >1, samples will accumulate over time.
* @param {boolean} [blurEdges=true] - Whether to fix UV Edges via blurring.
*/
update( camera, blendWindow = 100, blurEdges = true ) {
if ( this._blurringPlane === null ) {
return;
}
// Store the original Render Target
const currentRenderTarget = this.renderer.getRenderTarget();
// The blurring plane applies blur to the seams of the lightmap
this._blurringPlane.visible = blurEdges;
// Steal the Object3D from the real world to our special dimension
for ( let l = 0; l < this._lightMapContainers.length; l ++ ) {
this._lightMapContainers[ l ].object.oldScene = this._lightMapContainers[ l ].object.parent;
this._scene.attach( this._lightMapContainers[ l ].object );
}
// Set each object's material to the UV Unwrapped Surface Mapping Version
for ( let l = 0; l < this._lightMapContainers.length; l ++ ) {
this._averagingWindow.value = blendWindow;
this._lightMapContainers[ l ].object.material = this._uvMat;
this._lightMapContainers[ l ].object.oldFrustumCulled = this._lightMapContainers[ l ].object.frustumCulled;
this._lightMapContainers[ l ].object.frustumCulled = false;
}
// Ping-pong two surface buffers for reading/writing
const activeMap = this._buffer1Active ? this._progressiveLightMap1 : this._progressiveLightMap2;
const inactiveMap = this._buffer1Active ? this._progressiveLightMap2 : this._progressiveLightMap1;
// Render the object's surface maps
this.renderer.setRenderTarget( activeMap );
this._previousShadowMap.value = inactiveMap.texture;
this._buffer1Active = ! this._buffer1Active;
this.renderer.render( this._scene, camera );
// Restore the object's Real-time Material and add it back to the original world
for ( let l = 0; l < this._lightMapContainers.length; l ++ ) {
this._lightMapContainers[ l ].object.frustumCulled = this._lightMapContainers[ l ].object.oldFrustumCulled;
this._lightMapContainers[ l ].object.material = this._lightMapContainers[ l ].basicMat;
this._lightMapContainers[ l ].object.oldScene.attach( this._lightMapContainers[ l ].object );
}
// Restore the original Render Target
this.renderer.setRenderTarget( currentRenderTarget );
}
/**
* Draws the lightmap in the main scene. Call this after adding the objects to it.
*
* @param {boolean} visible - Whether the debug plane should be visible
* @param {Vector3} [position] - Where the debug plane should be drawn
*/
showDebugLightmap( visible, position = null ) {
if ( this._lightMapContainers.length === 0 ) {
console.warn( 'THREE.ProgressiveLightMap: Call .showDebugLightmap() after adding the objects.' );
return;
}
if ( this._labelMesh === null ) {
const labelMaterial = new NodeMaterial();
labelMaterial.colorNode = texture( this._progressiveLightMap1.texture ).sample( uv().flipY() );
labelMaterial.side = DoubleSide;
const labelGeometry = new PlaneGeometry( 100, 100 );
this._labelMesh = new Mesh( labelGeometry, labelMaterial );
this._labelMesh.position.y = 250;
this._lightMapContainers[ 0 ].object.parent.add( this._labelMesh );
}
if ( position !== null ) {
this._labelMesh.position.copy( position );
}
this._labelMesh.visible = visible;
}
/**
* Creates the Blurring Plane.
*
* @private
*/
_initializeBlurPlane() {
const blurMaterial = new NodeMaterial();
blurMaterial.polygonOffset = true;
blurMaterial.polygonOffsetFactor = - 1;
blurMaterial.polygonOffsetUnits = 3;
blurMaterial.vertexNode = vec4( sub( uv(), vec2( 0.5 ) ).mul( 2 ), 1, 1 );
const uvNode = uv().flipY().toVar();
const pixelOffset = float( 0.5 ).div( float( this.resolution ) ).toVar();
const color = add(
this._previousShadowMap.sample( uvNode.add( vec2( pixelOffset, 0 ) ) ),
this._previousShadowMap.sample( uvNode.add( vec2( 0, pixelOffset ) ) ),
this._previousShadowMap.sample( uvNode.add( vec2( 0, pixelOffset.negate() ) ) ),
this._previousShadowMap.sample( uvNode.add( vec2( pixelOffset.negate(), 0 ) ) ),
this._previousShadowMap.sample( uvNode.add( vec2( pixelOffset, pixelOffset ) ) ),
this._previousShadowMap.sample( uvNode.add( vec2( pixelOffset.negate(), pixelOffset ) ) ),
this._previousShadowMap.sample( uvNode.add( vec2( pixelOffset, pixelOffset.negate() ) ) ),
this._previousShadowMap.sample( uvNode.add( vec2( pixelOffset.negate(), pixelOffset.negate() ) ) ),
).div( 8 );
blurMaterial.fragmentNode = color;
this._blurringPlane = new Mesh( new PlaneGeometry( 1, 1 ), blurMaterial );
this._blurringPlane.name = 'Blurring Plane';
this._blurringPlane.frustumCulled = false;
this._blurringPlane.renderOrder = 0;
this._blurringPlane.material.depthWrite = false;
this._scene.add( this._blurringPlane );
}
}
export { ProgressiveLightMap };

623
node_modules/three/examples/jsm/misc/RollerCoaster.js generated vendored Normal file
View File

@@ -0,0 +1,623 @@
import {
BufferAttribute,
BufferGeometry,
Color,
Quaternion,
Raycaster,
SRGBColorSpace,
Vector3
} from 'three';
/**
* A procedural roller coaster geometry.
*
* @augments BufferGeometry
* @three_import import { RollerCoasterGeometry } from 'three/addons/misc/RollerCoaster.js';
*/
class RollerCoasterGeometry extends BufferGeometry {
/**
* Constructs a new geometry.
*
* @param {Curve} curve - The curve to generate the geometry along.
* @param {number} divisions - The number of divisions which defines the detail of the geometry.
*/
constructor( curve, divisions ) {
super();
const vertices = [];
const normals = [];
const colors = [];
const color1 = [ 1, 1, 1 ];
const color2 = [ 1, 1, 0 ];
const up = new Vector3( 0, 1, 0 );
const forward = new Vector3();
const right = new Vector3();
const quaternion = new Quaternion();
const prevQuaternion = new Quaternion();
prevQuaternion.setFromAxisAngle( up, Math.PI / 2 );
const point = new Vector3();
const prevPoint = new Vector3();
prevPoint.copy( curve.getPointAt( 0 ) );
// shapes
const step = [
new Vector3( - 0.225, 0, 0 ),
new Vector3( 0, - 0.050, 0 ),
new Vector3( 0, - 0.175, 0 ),
new Vector3( 0, - 0.050, 0 ),
new Vector3( 0.225, 0, 0 ),
new Vector3( 0, - 0.175, 0 )
];
const PI2 = Math.PI * 2;
let sides = 5;
const tube1 = [];
for ( let i = 0; i < sides; i ++ ) {
const angle = ( i / sides ) * PI2;
tube1.push( new Vector3( Math.sin( angle ) * 0.06, Math.cos( angle ) * 0.06, 0 ) );
}
sides = 6;
const tube2 = [];
for ( let i = 0; i < sides; i ++ ) {
const angle = ( i / sides ) * PI2;
tube2.push( new Vector3( Math.sin( angle ) * 0.025, Math.cos( angle ) * 0.025, 0 ) );
}
const vector = new Vector3();
const normal = new Vector3();
function drawShape( shape, color ) {
normal.set( 0, 0, - 1 ).applyQuaternion( quaternion );
for ( let j = 0; j < shape.length; j ++ ) {
vector.copy( shape[ j ] );
vector.applyQuaternion( quaternion );
vector.add( point );
vertices.push( vector.x, vector.y, vector.z );
normals.push( normal.x, normal.y, normal.z );
colors.push( color[ 0 ], color[ 1 ], color[ 2 ] );
}
normal.set( 0, 0, 1 ).applyQuaternion( quaternion );
for ( let j = shape.length - 1; j >= 0; j -- ) {
vector.copy( shape[ j ] );
vector.applyQuaternion( quaternion );
vector.add( point );
vertices.push( vector.x, vector.y, vector.z );
normals.push( normal.x, normal.y, normal.z );
colors.push( color[ 0 ], color[ 1 ], color[ 2 ] );
}
}
const vector1 = new Vector3();
const vector2 = new Vector3();
const vector3 = new Vector3();
const vector4 = new Vector3();
const normal1 = new Vector3();
const normal2 = new Vector3();
const normal3 = new Vector3();
const normal4 = new Vector3();
function extrudeShape( shape, offset, color ) {
for ( let j = 0, jl = shape.length; j < jl; j ++ ) {
const point1 = shape[ j ];
const point2 = shape[ ( j + 1 ) % jl ];
vector1.copy( point1 ).add( offset );
vector1.applyQuaternion( quaternion );
vector1.add( point );
vector2.copy( point2 ).add( offset );
vector2.applyQuaternion( quaternion );
vector2.add( point );
vector3.copy( point2 ).add( offset );
vector3.applyQuaternion( prevQuaternion );
vector3.add( prevPoint );
vector4.copy( point1 ).add( offset );
vector4.applyQuaternion( prevQuaternion );
vector4.add( prevPoint );
vertices.push( vector1.x, vector1.y, vector1.z );
vertices.push( vector2.x, vector2.y, vector2.z );
vertices.push( vector4.x, vector4.y, vector4.z );
vertices.push( vector2.x, vector2.y, vector2.z );
vertices.push( vector3.x, vector3.y, vector3.z );
vertices.push( vector4.x, vector4.y, vector4.z );
//
normal1.copy( point1 );
normal1.applyQuaternion( quaternion );
normal1.normalize();
normal2.copy( point2 );
normal2.applyQuaternion( quaternion );
normal2.normalize();
normal3.copy( point2 );
normal3.applyQuaternion( prevQuaternion );
normal3.normalize();
normal4.copy( point1 );
normal4.applyQuaternion( prevQuaternion );
normal4.normalize();
normals.push( normal1.x, normal1.y, normal1.z );
normals.push( normal2.x, normal2.y, normal2.z );
normals.push( normal4.x, normal4.y, normal4.z );
normals.push( normal2.x, normal2.y, normal2.z );
normals.push( normal3.x, normal3.y, normal3.z );
normals.push( normal4.x, normal4.y, normal4.z );
colors.push( color[ 0 ], color[ 1 ], color[ 2 ] );
colors.push( color[ 0 ], color[ 1 ], color[ 2 ] );
colors.push( color[ 0 ], color[ 1 ], color[ 2 ] );
colors.push( color[ 0 ], color[ 1 ], color[ 2 ] );
colors.push( color[ 0 ], color[ 1 ], color[ 2 ] );
colors.push( color[ 0 ], color[ 1 ], color[ 2 ] );
}
}
const offset = new Vector3();
for ( let i = 1; i <= divisions; i ++ ) {
point.copy( curve.getPointAt( i / divisions ) );
up.set( 0, 1, 0 );
forward.subVectors( point, prevPoint ).normalize();
right.crossVectors( up, forward ).normalize();
up.crossVectors( forward, right );
const angle = Math.atan2( forward.x, forward.z );
quaternion.setFromAxisAngle( up, angle );
if ( i % 2 === 0 ) {
drawShape( step, color2 );
}
extrudeShape( tube1, offset.set( 0, - 0.125, 0 ), color2 );
extrudeShape( tube2, offset.set( 0.2, 0, 0 ), color1 );
extrudeShape( tube2, offset.set( - 0.2, 0, 0 ), color1 );
prevPoint.copy( point );
prevQuaternion.copy( quaternion );
}
// console.log( vertices.length );
this.setAttribute( 'position', new BufferAttribute( new Float32Array( vertices ), 3 ) );
this.setAttribute( 'normal', new BufferAttribute( new Float32Array( normals ), 3 ) );
this.setAttribute( 'color', new BufferAttribute( new Float32Array( colors ), 3 ) );
}
}
/**
* A procedural roller coaster lifters geometry.
*
* @augments BufferGeometry
* @three_import import { RollerCoasterLiftersGeometry } from 'three/addons/misc/RollerCoaster.js';
*/
class RollerCoasterLiftersGeometry extends BufferGeometry {
/**
* Constructs a new geometry.
*
* @param {Curve} curve - The curve to generate the geometry along.
* @param {number} divisions - The number of divisions which defines the detail of the geometry.
*/
constructor( curve, divisions ) {
super();
const vertices = [];
const normals = [];
const quaternion = new Quaternion();
const up = new Vector3( 0, 1, 0 );
const point = new Vector3();
const tangent = new Vector3();
// shapes
const tube1 = [
new Vector3( 0, 0.05, - 0.05 ),
new Vector3( 0, 0.05, 0.05 ),
new Vector3( 0, - 0.05, 0 )
];
const tube2 = [
new Vector3( - 0.05, 0, 0.05 ),
new Vector3( - 0.05, 0, - 0.05 ),
new Vector3( 0.05, 0, 0 )
];
const tube3 = [
new Vector3( 0.05, 0, - 0.05 ),
new Vector3( 0.05, 0, 0.05 ),
new Vector3( - 0.05, 0, 0 )
];
const vector1 = new Vector3();
const vector2 = new Vector3();
const vector3 = new Vector3();
const vector4 = new Vector3();
const normal1 = new Vector3();
const normal2 = new Vector3();
const normal3 = new Vector3();
const normal4 = new Vector3();
function extrudeShape( shape, fromPoint, toPoint ) {
for ( let j = 0, jl = shape.length; j < jl; j ++ ) {
const point1 = shape[ j ];
const point2 = shape[ ( j + 1 ) % jl ];
vector1.copy( point1 );
vector1.applyQuaternion( quaternion );
vector1.add( fromPoint );
vector2.copy( point2 );
vector2.applyQuaternion( quaternion );
vector2.add( fromPoint );
vector3.copy( point2 );
vector3.applyQuaternion( quaternion );
vector3.add( toPoint );
vector4.copy( point1 );
vector4.applyQuaternion( quaternion );
vector4.add( toPoint );
vertices.push( vector1.x, vector1.y, vector1.z );
vertices.push( vector2.x, vector2.y, vector2.z );
vertices.push( vector4.x, vector4.y, vector4.z );
vertices.push( vector2.x, vector2.y, vector2.z );
vertices.push( vector3.x, vector3.y, vector3.z );
vertices.push( vector4.x, vector4.y, vector4.z );
//
normal1.copy( point1 );
normal1.applyQuaternion( quaternion );
normal1.normalize();
normal2.copy( point2 );
normal2.applyQuaternion( quaternion );
normal2.normalize();
normal3.copy( point2 );
normal3.applyQuaternion( quaternion );
normal3.normalize();
normal4.copy( point1 );
normal4.applyQuaternion( quaternion );
normal4.normalize();
normals.push( normal1.x, normal1.y, normal1.z );
normals.push( normal2.x, normal2.y, normal2.z );
normals.push( normal4.x, normal4.y, normal4.z );
normals.push( normal2.x, normal2.y, normal2.z );
normals.push( normal3.x, normal3.y, normal3.z );
normals.push( normal4.x, normal4.y, normal4.z );
}
}
const fromPoint = new Vector3();
const toPoint = new Vector3();
for ( let i = 1; i <= divisions; i ++ ) {
point.copy( curve.getPointAt( i / divisions ) );
tangent.copy( curve.getTangentAt( i / divisions ) );
const angle = Math.atan2( tangent.x, tangent.z );
quaternion.setFromAxisAngle( up, angle );
//
if ( point.y > 10 ) {
fromPoint.set( - 0.75, - 0.35, 0 );
fromPoint.applyQuaternion( quaternion );
fromPoint.add( point );
toPoint.set( 0.75, - 0.35, 0 );
toPoint.applyQuaternion( quaternion );
toPoint.add( point );
extrudeShape( tube1, fromPoint, toPoint );
fromPoint.set( - 0.7, - 0.3, 0 );
fromPoint.applyQuaternion( quaternion );
fromPoint.add( point );
toPoint.set( - 0.7, - point.y, 0 );
toPoint.applyQuaternion( quaternion );
toPoint.add( point );
extrudeShape( tube2, fromPoint, toPoint );
fromPoint.set( 0.7, - 0.3, 0 );
fromPoint.applyQuaternion( quaternion );
fromPoint.add( point );
toPoint.set( 0.7, - point.y, 0 );
toPoint.applyQuaternion( quaternion );
toPoint.add( point );
extrudeShape( tube3, fromPoint, toPoint );
} else {
fromPoint.set( 0, - 0.2, 0 );
fromPoint.applyQuaternion( quaternion );
fromPoint.add( point );
toPoint.set( 0, - point.y, 0 );
toPoint.applyQuaternion( quaternion );
toPoint.add( point );
extrudeShape( tube3, fromPoint, toPoint );
}
}
this.setAttribute( 'position', new BufferAttribute( new Float32Array( vertices ), 3 ) );
this.setAttribute( 'normal', new BufferAttribute( new Float32Array( normals ), 3 ) );
}
}
/**
* A procedural roller coaster shadow geometry.
*
* @augments BufferGeometry
* @three_import import { RollerCoasterShadowGeometry } from 'three/addons/misc/RollerCoaster.js';
*/
class RollerCoasterShadowGeometry extends BufferGeometry {
/**
* Constructs a new geometry.
*
* @param {Curve} curve - The curve to generate the geometry along.
* @param {number} divisions - The number of divisions which defines the detail of the geometry.
*/
constructor( curve, divisions ) {
super();
const vertices = [];
const up = new Vector3( 0, 1, 0 );
const forward = new Vector3();
const quaternion = new Quaternion();
const prevQuaternion = new Quaternion();
prevQuaternion.setFromAxisAngle( up, Math.PI / 2 );
const point = new Vector3();
const prevPoint = new Vector3();
prevPoint.copy( curve.getPointAt( 0 ) );
prevPoint.y = 0;
const vector1 = new Vector3();
const vector2 = new Vector3();
const vector3 = new Vector3();
const vector4 = new Vector3();
for ( let i = 1; i <= divisions; i ++ ) {
point.copy( curve.getPointAt( i / divisions ) );
point.y = 0;
forward.subVectors( point, prevPoint );
const angle = Math.atan2( forward.x, forward.z );
quaternion.setFromAxisAngle( up, angle );
vector1.set( - 0.3, 0, 0 );
vector1.applyQuaternion( quaternion );
vector1.add( point );
vector2.set( 0.3, 0, 0 );
vector2.applyQuaternion( quaternion );
vector2.add( point );
vector3.set( 0.3, 0, 0 );
vector3.applyQuaternion( prevQuaternion );
vector3.add( prevPoint );
vector4.set( - 0.3, 0, 0 );
vector4.applyQuaternion( prevQuaternion );
vector4.add( prevPoint );
vertices.push( vector1.x, vector1.y, vector1.z );
vertices.push( vector2.x, vector2.y, vector2.z );
vertices.push( vector4.x, vector4.y, vector4.z );
vertices.push( vector2.x, vector2.y, vector2.z );
vertices.push( vector3.x, vector3.y, vector3.z );
vertices.push( vector4.x, vector4.y, vector4.z );
prevPoint.copy( point );
prevQuaternion.copy( quaternion );
}
this.setAttribute( 'position', new BufferAttribute( new Float32Array( vertices ), 3 ) );
}
}
/**
* A procedural sky geometry.
*
* @augments BufferGeometry
* @three_import import { SkyGeometry } from 'three/addons/misc/RollerCoaster.js';
*/
class SkyGeometry extends BufferGeometry {
/**
* Constructs a new geometry.
*/
constructor() {
super();
const vertices = [];
for ( let i = 0; i < 100; i ++ ) {
const x = Math.random() * 800 - 400;
const y = Math.random() * 50 + 50;
const z = Math.random() * 800 - 400;
const size = Math.random() * 40 + 20;
vertices.push( x - size, y, z - size );
vertices.push( x + size, y, z - size );
vertices.push( x - size, y, z + size );
vertices.push( x + size, y, z - size );
vertices.push( x + size, y, z + size );
vertices.push( x - size, y, z + size );
}
this.setAttribute( 'position', new BufferAttribute( new Float32Array( vertices ), 3 ) );
}
}
/**
* A procedural trees geometry.
*
* @augments BufferGeometry
* @three_import import { TreesGeometry } from 'three/addons/misc/RollerCoaster.js';
*/
class TreesGeometry extends BufferGeometry {
/**
* Constructs a new geometry.
*
* @param {Mesh} landscape - A mesh representing the landscape. Trees will be positioned
* randomly on the landscape's surface.
*/
constructor( landscape ) {
super();
const vertices = [];
const colors = [];
const raycaster = new Raycaster();
raycaster.ray.direction.set( 0, - 1, 0 );
const _color = new Color();
for ( let i = 0; i < 2000; i ++ ) {
const x = Math.random() * 500 - 250;
const z = Math.random() * 500 - 250;
raycaster.ray.origin.set( x, 50, z );
const intersections = raycaster.intersectObject( landscape );
if ( intersections.length === 0 ) continue;
const y = intersections[ 0 ].point.y;
const height = Math.random() * 5 + 0.5;
let angle = Math.random() * Math.PI * 2;
vertices.push( x + Math.sin( angle ), y, z + Math.cos( angle ) );
vertices.push( x, y + height, z );
vertices.push( x + Math.sin( angle + Math.PI ), y, z + Math.cos( angle + Math.PI ) );
angle += Math.PI / 2;
vertices.push( x + Math.sin( angle ), y, z + Math.cos( angle ) );
vertices.push( x, y + height, z );
vertices.push( x + Math.sin( angle + Math.PI ), y, z + Math.cos( angle + Math.PI ) );
const random = Math.random() * 0.1;
for ( let j = 0; j < 6; j ++ ) {
_color.setRGB( 0.2 + random, 0.4 + random, 0, SRGBColorSpace );
colors.push( _color.r, _color.g, _color.b );
}
}
this.setAttribute( 'position', new BufferAttribute( new Float32Array( vertices ), 3 ) );
this.setAttribute( 'color', new BufferAttribute( new Float32Array( colors ), 3 ) );
}
}
export { RollerCoasterGeometry, RollerCoasterLiftersGeometry, RollerCoasterShadowGeometry, SkyGeometry, TreesGeometry };

256
node_modules/three/examples/jsm/misc/TubePainter.js generated vendored Normal file
View File

@@ -0,0 +1,256 @@
import {
BufferAttribute,
BufferGeometry,
Color,
DynamicDrawUsage,
Matrix4,
Mesh,
MeshStandardMaterial,
Vector3
} from 'three';
/**
* @classdesc This module can be used to paint tube-like meshes
* along a sequence of points. This module is used in a XR
* painter demo.
*
* ```js
* const painter = new TubePainter();
* scene.add( painter.mesh );
* ```
*
* @name TubePainter
* @class
* @three_import import { TubePainter } from 'three/addons/misc/TubePainter.js';
*/
function TubePainter() {
const BUFFER_SIZE = 1000000 * 3;
const positions = new BufferAttribute( new Float32Array( BUFFER_SIZE ), 3 );
positions.usage = DynamicDrawUsage;
const normals = new BufferAttribute( new Float32Array( BUFFER_SIZE ), 3 );
normals.usage = DynamicDrawUsage;
const colors = new BufferAttribute( new Float32Array( BUFFER_SIZE ), 3 );
colors.usage = DynamicDrawUsage;
const geometry = new BufferGeometry();
geometry.setAttribute( 'position', positions );
geometry.setAttribute( 'normal', normals );
geometry.setAttribute( 'color', colors );
geometry.drawRange.count = 0;
const material = new MeshStandardMaterial( {
vertexColors: true
} );
const mesh = new Mesh( geometry, material );
mesh.frustumCulled = false;
//
function getPoints( size ) {
const PI2 = Math.PI * 2;
const sides = 10;
const array = [];
const radius = 0.01 * size;
for ( let i = 0; i < sides; i ++ ) {
const angle = ( i / sides ) * PI2;
array.push( new Vector3( Math.sin( angle ) * radius, Math.cos( angle ) * radius, 0 ) );
}
return array;
}
//
const vector1 = new Vector3();
const vector2 = new Vector3();
const vector3 = new Vector3();
const vector4 = new Vector3();
const color = new Color( 0xffffff );
let size = 1;
function stroke( position1, position2, matrix1, matrix2 ) {
if ( position1.distanceToSquared( position2 ) === 0 ) return;
let count = geometry.drawRange.count;
const points = getPoints( size );
for ( let i = 0, il = points.length; i < il; i ++ ) {
const vertex1 = points[ i ];
const vertex2 = points[ ( i + 1 ) % il ];
// positions
vector1.copy( vertex1 ).applyMatrix4( matrix2 ).add( position2 );
vector2.copy( vertex2 ).applyMatrix4( matrix2 ).add( position2 );
vector3.copy( vertex2 ).applyMatrix4( matrix1 ).add( position1 );
vector4.copy( vertex1 ).applyMatrix4( matrix1 ).add( position1 );
vector1.toArray( positions.array, ( count + 0 ) * 3 );
vector2.toArray( positions.array, ( count + 1 ) * 3 );
vector4.toArray( positions.array, ( count + 2 ) * 3 );
vector2.toArray( positions.array, ( count + 3 ) * 3 );
vector3.toArray( positions.array, ( count + 4 ) * 3 );
vector4.toArray( positions.array, ( count + 5 ) * 3 );
// normals
vector1.copy( vertex1 ).applyMatrix4( matrix2 ).normalize();
vector2.copy( vertex2 ).applyMatrix4( matrix2 ).normalize();
vector3.copy( vertex2 ).applyMatrix4( matrix1 ).normalize();
vector4.copy( vertex1 ).applyMatrix4( matrix1 ).normalize();
vector1.toArray( normals.array, ( count + 0 ) * 3 );
vector2.toArray( normals.array, ( count + 1 ) * 3 );
vector4.toArray( normals.array, ( count + 2 ) * 3 );
vector2.toArray( normals.array, ( count + 3 ) * 3 );
vector3.toArray( normals.array, ( count + 4 ) * 3 );
vector4.toArray( normals.array, ( count + 5 ) * 3 );
// colors
color.toArray( colors.array, ( count + 0 ) * 3 );
color.toArray( colors.array, ( count + 1 ) * 3 );
color.toArray( colors.array, ( count + 2 ) * 3 );
color.toArray( colors.array, ( count + 3 ) * 3 );
color.toArray( colors.array, ( count + 4 ) * 3 );
color.toArray( colors.array, ( count + 5 ) * 3 );
count += 6;
}
geometry.drawRange.count = count;
}
//
const up = new Vector3( 0, 1, 0 );
const point1 = new Vector3();
const point2 = new Vector3();
const matrix1 = new Matrix4();
const matrix2 = new Matrix4();
function moveTo( position ) {
point1.copy( position );
matrix1.lookAt( point2, point1, up );
point2.copy( position );
matrix2.copy( matrix1 );
}
function lineTo( position ) {
point1.copy( position );
matrix1.lookAt( point2, point1, up );
stroke( point1, point2, matrix1, matrix2 );
point2.copy( point1 );
matrix2.copy( matrix1 );
}
function setSize( value ) {
size = value;
}
//
let count = 0;
function update() {
const start = count;
const end = geometry.drawRange.count;
if ( start === end ) return;
positions.addUpdateRange( start * 3, ( end - start ) * 3 );
positions.needsUpdate = true;
normals.addUpdateRange( start * 3, ( end - start ) * 3 );
normals.needsUpdate = true;
colors.addUpdateRange( start * 3, ( end - start ) * 3 );
colors.needsUpdate = true;
count = geometry.drawRange.count;
}
return {
/**
* The "painted" tube mesh. Must be added to the scene.
*
* @name TubePainter#mesh
* @type {Mesh}
*/
mesh: mesh,
/**
* Moves the current painting position to the given value.
*
* @method
* @name TubePainter#moveTo
* @param {Vector3} position The new painting position.
*/
moveTo: moveTo,
/**
* Draw a stroke from the current position to the given one.
* This method extends the tube while drawing with the XR
* controllers.
*
* @method
* @name TubePainter#lineTo
* @param {Vector3} position The destination position.
*/
lineTo: lineTo,
/**
* Sets the size of newly rendered tube segments.
*
* @method
* @name TubePainter#setSize
* @param {number} size The size.
*/
setSize: setSize,
/**
* Updates the internal geometry buffers so the new painted
* segments are rendered.
*
* @method
* @name TubePainter#update
*/
update: update
};
}
export { TubePainter };

521
node_modules/three/examples/jsm/misc/Volume.js generated vendored Normal file
View File

@@ -0,0 +1,521 @@
import {
Matrix3,
Matrix4,
Vector3
} from 'three';
import { VolumeSlice } from '../misc/VolumeSlice.js';
/**
* This class had been written to handle the output of the {@link NRRDLoader}.
* It contains a volume of data and information about it. For now it only handles 3 dimensional data.
*
* @three_import import { Volume } from 'three/addons/misc/Volume.js';
*/
class Volume {
/**
* Constructs a new volume.
*
* @param {number} [xLength] - Width of the volume.
* @param {number} [yLength] - Length of the volume.
* @param {number} [zLength] - Depth of the volume.
* @param {string} [type] - The type of data (uint8, uint16, ...).
* @param {ArrayBuffer} [arrayBuffer] - The buffer with volume data.
*/
constructor( xLength, yLength, zLength, type, arrayBuffer ) {
if ( xLength !== undefined ) {
/**
* Width of the volume in the IJK coordinate system.
*
* @type {number}
* @default 1
*/
this.xLength = Number( xLength ) || 1;
/**
* Height of the volume in the IJK coordinate system.
*
* @type {number}
* @default 1
*/
this.yLength = Number( yLength ) || 1;
/**
* Depth of the volume in the IJK coordinate system.
*
* @type {number}
* @default 1
*/
this.zLength = Number( zLength ) || 1;
/**
* The order of the Axis dictated by the NRRD header
*
* @type {Array<string>}
*/
this.axisOrder = [ 'x', 'y', 'z' ];
/**
* The data of the volume.
*
* @type {TypedArray}
*/
this.data;
switch ( type ) {
case 'Uint8' :
case 'uint8' :
case 'uchar' :
case 'unsigned char' :
case 'uint8_t' :
this.data = new Uint8Array( arrayBuffer );
break;
case 'Int8' :
case 'int8' :
case 'signed char' :
case 'int8_t' :
this.data = new Int8Array( arrayBuffer );
break;
case 'Int16' :
case 'int16' :
case 'short' :
case 'short int' :
case 'signed short' :
case 'signed short int' :
case 'int16_t' :
this.data = new Int16Array( arrayBuffer );
break;
case 'Uint16' :
case 'uint16' :
case 'ushort' :
case 'unsigned short' :
case 'unsigned short int' :
case 'uint16_t' :
this.data = new Uint16Array( arrayBuffer );
break;
case 'Int32' :
case 'int32' :
case 'int' :
case 'signed int' :
case 'int32_t' :
this.data = new Int32Array( arrayBuffer );
break;
case 'Uint32' :
case 'uint32' :
case 'uint' :
case 'unsigned int' :
case 'uint32_t' :
this.data = new Uint32Array( arrayBuffer );
break;
case 'longlong' :
case 'long long' :
case 'long long int' :
case 'signed long long' :
case 'signed long long int' :
case 'int64' :
case 'int64_t' :
case 'ulonglong' :
case 'unsigned long long' :
case 'unsigned long long int' :
case 'uint64' :
case 'uint64_t' :
throw new Error( 'Error in Volume constructor : this type is not supported in JavaScript' );
break;
case 'Float32' :
case 'float32' :
case 'float' :
this.data = new Float32Array( arrayBuffer );
break;
case 'Float64' :
case 'float64' :
case 'double' :
this.data = new Float64Array( arrayBuffer );
break;
default :
this.data = new Uint8Array( arrayBuffer );
}
if ( this.data.length !== this.xLength * this.yLength * this.zLength ) {
throw new Error( 'Error in Volume constructor, lengths are not matching arrayBuffer size' );
}
}
/**
* Spacing to apply to the volume from IJK to RAS coordinate system
*
* @type {Array<number>}
*/
this.spacing = [ 1, 1, 1 ];
/**
* Offset of the volume in the RAS coordinate system
*
* @type {Array<number>}
*/
this.offset = [ 0, 0, 0 ];
/**
* The IJK to RAS matrix.
*
* @type {Martrix3}
*/
this.matrix = new Matrix3();
this.matrix.identity();
/**
* The RAS to IJK matrix.
*
* @type {Martrix3}
*/
this.inverseMatrix = new Matrix3();
let lowerThreshold = - Infinity;
Object.defineProperty( this, 'lowerThreshold', {
get: function () {
return lowerThreshold;
},
/**
* The voxels with values under this threshold won't appear in the slices.
* If changed, geometryNeedsUpdate is automatically set to true on all the slices associated to this volume.
*
* @name Volume#lowerThreshold
* @type {number}
* @param {number} value
*/
set: function ( value ) {
lowerThreshold = value;
this.sliceList.forEach( function ( slice ) {
slice.geometryNeedsUpdate = true;
} );
}
} );
let upperThreshold = Infinity;
Object.defineProperty( this, 'upperThreshold', {
get: function () {
return upperThreshold;
},
/**
* The voxels with values over this threshold won't appear in the slices.
* If changed, geometryNeedsUpdate is automatically set to true on all the slices associated to this volume
*
* @name Volume#upperThreshold
* @type {number}
* @param {number} value
*/
set: function ( value ) {
upperThreshold = value;
this.sliceList.forEach( function ( slice ) {
slice.geometryNeedsUpdate = true;
} );
}
} );
/**
* The list of all the slices associated to this volume
*
* @type {Array<VolumeSlice>}
*/
this.sliceList = [];
/**
* Whether to use segmentation mode or not.
* It can load 16-bits nrrds correctly.
*
* @type {boolean}
* @default false
*/
this.segmentation = false;
/**
* This array holds the dimensions of the volume in the RAS space
*
* @type {Array<number>}
*/
this.RASDimensions = [];
}
/**
* Shortcut for data[access(i,j,k)].
*
* @param {number} i - First coordinate.
* @param {number} j - Second coordinate.
* @param {number} k - Third coordinate.
* @returns {number} The value in the data array.
*/
getData( i, j, k ) {
return this.data[ k * this.xLength * this.yLength + j * this.xLength + i ];
}
/**
* Compute the index in the data array corresponding to the given coordinates in IJK system.
*
* @param {number} i - First coordinate.
* @param {number} j - Second coordinate.
* @param {number} k - Third coordinate.
* @returns {number} The index.
*/
access( i, j, k ) {
return k * this.xLength * this.yLength + j * this.xLength + i;
}
/**
* Retrieve the IJK coordinates of the voxel corresponding of the given index in the data.
*
* @param {number} index - Index of the voxel.
* @returns {Array<number>} The IJK coordinates as `[x,y,z]`.
*/
reverseAccess( index ) {
const z = Math.floor( index / ( this.yLength * this.xLength ) );
const y = Math.floor( ( index - z * this.yLength * this.xLength ) / this.xLength );
const x = index - z * this.yLength * this.xLength - y * this.xLength;
return [ x, y, z ];
}
/**
* Apply a function to all the voxels, be careful, the value will be replaced.
*
* @param {Function} functionToMap A function to apply to every voxel, will be called with the following parameters:
* value of the voxel, index of the voxel, the data (TypedArray).
* @param {Object} context - You can specify a context in which call the function, default if this Volume.
* @returns {Volume} A reference to this instance.
*/
map( functionToMap, context ) {
const length = this.data.length;
context = context || this;
for ( let i = 0; i < length; i ++ ) {
this.data[ i ] = functionToMap.call( context, this.data[ i ], i, this.data );
}
return this;
}
/**
* Compute the orientation of the slice and returns all the information relative to the geometry such as sliceAccess,
* the plane matrix (orientation and position in RAS coordinate) and the dimensions of the plane in both coordinate system.
*
* @param {('x'|'y'|'z')} axis - The normal axis to the slice.
* @param {number} RASIndex - The index of the slice.
* @returns {Object} An object containing all the useful information on the geometry of the slice.
*/
extractPerpendicularPlane( axis, RASIndex ) {
let firstSpacing,
secondSpacing,
positionOffset,
IJKIndex;
const axisInIJK = new Vector3(),
firstDirection = new Vector3(),
secondDirection = new Vector3(),
planeMatrix = ( new Matrix4() ).identity(),
volume = this;
const dimensions = new Vector3( this.xLength, this.yLength, this.zLength );
switch ( axis ) {
case 'x' :
axisInIJK.set( 1, 0, 0 );
firstDirection.set( 0, 0, - 1 );
secondDirection.set( 0, - 1, 0 );
firstSpacing = this.spacing[ this.axisOrder.indexOf( 'z' ) ];
secondSpacing = this.spacing[ this.axisOrder.indexOf( 'y' ) ];
IJKIndex = new Vector3( RASIndex, 0, 0 );
planeMatrix.multiply( ( new Matrix4() ).makeRotationY( Math.PI / 2 ) );
positionOffset = ( volume.RASDimensions[ 0 ] - 1 ) / 2;
planeMatrix.setPosition( new Vector3( RASIndex - positionOffset, 0, 0 ) );
break;
case 'y' :
axisInIJK.set( 0, 1, 0 );
firstDirection.set( 1, 0, 0 );
secondDirection.set( 0, 0, 1 );
firstSpacing = this.spacing[ this.axisOrder.indexOf( 'x' ) ];
secondSpacing = this.spacing[ this.axisOrder.indexOf( 'z' ) ];
IJKIndex = new Vector3( 0, RASIndex, 0 );
planeMatrix.multiply( ( new Matrix4() ).makeRotationX( - Math.PI / 2 ) );
positionOffset = ( volume.RASDimensions[ 1 ] - 1 ) / 2;
planeMatrix.setPosition( new Vector3( 0, RASIndex - positionOffset, 0 ) );
break;
case 'z' :
default :
axisInIJK.set( 0, 0, 1 );
firstDirection.set( 1, 0, 0 );
secondDirection.set( 0, - 1, 0 );
firstSpacing = this.spacing[ this.axisOrder.indexOf( 'x' ) ];
secondSpacing = this.spacing[ this.axisOrder.indexOf( 'y' ) ];
IJKIndex = new Vector3( 0, 0, RASIndex );
positionOffset = ( volume.RASDimensions[ 2 ] - 1 ) / 2;
planeMatrix.setPosition( new Vector3( 0, 0, RASIndex - positionOffset ) );
break;
}
if ( ! this.segmentation ) {
firstDirection.applyMatrix4( volume.inverseMatrix ).normalize();
secondDirection.applyMatrix4( volume.inverseMatrix ).normalize();
axisInIJK.applyMatrix4( volume.inverseMatrix ).normalize();
}
firstDirection.arglet = 'i';
secondDirection.arglet = 'j';
const iLength = Math.floor( Math.abs( firstDirection.dot( dimensions ) ) );
const jLength = Math.floor( Math.abs( secondDirection.dot( dimensions ) ) );
const planeWidth = Math.abs( iLength * firstSpacing );
const planeHeight = Math.abs( jLength * secondSpacing );
IJKIndex = Math.abs( Math.round( IJKIndex.applyMatrix4( volume.inverseMatrix ).dot( axisInIJK ) ) );
const base = [ new Vector3( 1, 0, 0 ), new Vector3( 0, 1, 0 ), new Vector3( 0, 0, 1 ) ];
const iDirection = [ firstDirection, secondDirection, axisInIJK ].find( function ( x ) {
return Math.abs( x.dot( base[ 0 ] ) ) > 0.9;
} );
const jDirection = [ firstDirection, secondDirection, axisInIJK ].find( function ( x ) {
return Math.abs( x.dot( base[ 1 ] ) ) > 0.9;
} );
const kDirection = [ firstDirection, secondDirection, axisInIJK ].find( function ( x ) {
return Math.abs( x.dot( base[ 2 ] ) ) > 0.9;
} );
function sliceAccess( i, j ) {
const si = ( iDirection === axisInIJK ) ? IJKIndex : ( iDirection.arglet === 'i' ? i : j );
const sj = ( jDirection === axisInIJK ) ? IJKIndex : ( jDirection.arglet === 'i' ? i : j );
const sk = ( kDirection === axisInIJK ) ? IJKIndex : ( kDirection.arglet === 'i' ? i : j );
// invert indices if necessary
const accessI = ( iDirection.dot( base[ 0 ] ) > 0 ) ? si : ( volume.xLength - 1 ) - si;
const accessJ = ( jDirection.dot( base[ 1 ] ) > 0 ) ? sj : ( volume.yLength - 1 ) - sj;
const accessK = ( kDirection.dot( base[ 2 ] ) > 0 ) ? sk : ( volume.zLength - 1 ) - sk;
return volume.access( accessI, accessJ, accessK );
}
return {
iLength: iLength,
jLength: jLength,
sliceAccess: sliceAccess,
matrix: planeMatrix,
planeWidth: planeWidth,
planeHeight: planeHeight
};
}
/**
* Returns a slice corresponding to the given axis and index.
* The coordinate are given in the Right Anterior Superior coordinate format.
*
* @param {('x'|'y'|'z')} axis - The normal axis to the slice.
* @param {number} index - The index of the slice.
* @returns {VolumeSlice} The extracted slice.
*/
extractSlice( axis, index ) {
const slice = new VolumeSlice( this, index, axis );
this.sliceList.push( slice );
return slice;
}
/**
* Call repaint on all the slices extracted from this volume.
*
* @see {@link VolumeSlice#repaint}
* @returns {Volume} A reference to this volume.
*/
repaintAllSlices() {
this.sliceList.forEach( function ( slice ) {
slice.repaint();
} );
return this;
}
/**
* Compute the minimum and the maximum of the data in the volume.
*
* @returns {Array<number>} The min/max data as `[min,max]`.
*/
computeMinMax() {
let min = Infinity;
let max = - Infinity;
// buffer the length
const datasize = this.data.length;
let i = 0;
for ( i = 0; i < datasize; i ++ ) {
if ( ! isNaN( this.data[ i ] ) ) {
const value = this.data[ i ];
min = Math.min( min, value );
max = Math.max( max, value );
}
}
this.min = min;
this.max = max;
return [ min, max ];
}
}
export { Volume };

276
node_modules/three/examples/jsm/misc/VolumeSlice.js generated vendored Normal file
View File

@@ -0,0 +1,276 @@
import {
ClampToEdgeWrapping,
DoubleSide,
LinearFilter,
Mesh,
MeshBasicMaterial,
PlaneGeometry,
Texture,
SRGBColorSpace
} from 'three';
/**
* This class has been made to hold a slice of a volume data.
*
* @see {@link Volume}
* @three_import import { VolumeSlice } from 'three/addons/misc/VolumeSlice.js';
*/
class VolumeSlice {
/**
* Constructs a new volume slice.
*
* @param {Volume} volume - The associated volume.
* @param {number} [index=0] - The index of the slice.
* @param {('x'|'y'|'z')} [axis='z'] - For now only 'x', 'y' or 'z' but later it will change to a normal vector.
*/
constructor( volume, index = 0, axis = 'z' ) {
const slice = this;
/**
* The associated volume.
*
* @type {Volume}
*/
this.volume = volume;
Object.defineProperty( this, 'index', {
get: function () {
return index;
},
/**
* The index of the slice, if changed, will automatically call updateGeometry at the next repaint.
*
* @name VolumeSlice#index
* @type {number}
* @default 0
* @param {number} value
* @return {number}
*/
set: function ( value ) {
index = value;
slice.geometryNeedsUpdate = true;
return index;
}
} );
/**
* The normal axis.
*
* @type {('x'|'y'|'z')}
*/
this.axis = axis;
/**
* The final canvas used for the texture.
*
* @type {HTMLCanvasElement}
*/
this.canvas = document.createElement( 'canvas' );
/**
* The rendering context of the canvas.
*
* @type {CanvasRenderingContext2D}
*/
this.ctx;
/**
* The intermediary canvas used to paint the data.
*
* @type {HTMLCanvasElement}
*/
this.canvasBuffer = document.createElement( 'canvas' );
/**
* The rendering context of the canvas buffer,
*
* @type {CanvasRenderingContext2D}
*/
this.ctxBuffer;
this.updateGeometry();
const canvasMap = new Texture( this.canvas );
canvasMap.minFilter = LinearFilter;
canvasMap.generateMipmaps = false;
canvasMap.wrapS = canvasMap.wrapT = ClampToEdgeWrapping;
canvasMap.colorSpace = SRGBColorSpace;
const material = new MeshBasicMaterial( { map: canvasMap, side: DoubleSide, transparent: true } );
/**
* The mesh ready to get used in the scene.
*
* @type {Mesh}
*/
this.mesh = new Mesh( this.geometry, material );
this.mesh.matrixAutoUpdate = false;
/**
* If set to `true`, `updateGeometry()` will be triggered at the next repaint.
*
* @type {boolean}
* @default true
*/
this.geometryNeedsUpdate = true;
this.repaint();
/**
* Width of slice in the original coordinate system, corresponds to the width of the buffer canvas.
*
* @type {number}
* @default 0
*/
this.iLength = 0;
/**
* Height of slice in the original coordinate system, corresponds to the height of the buffer canvas.
*
* @type {number}
* @default 0
*/
this.jLength = 0;
/**
* Function that allow the slice to access right data.
*
* @type {?Function}
* @see {@link Volume#extractPerpendicularPlane}
*/
this.sliceAccess = null;
}
/**
* Refresh the texture and the geometry if geometryNeedsUpdate is set to `true`.
*/
repaint() {
if ( this.geometryNeedsUpdate ) {
this.updateGeometry();
}
const iLength = this.iLength,
jLength = this.jLength,
sliceAccess = this.sliceAccess,
volume = this.volume,
canvas = this.canvasBuffer,
ctx = this.ctxBuffer;
// get the imageData and pixel array from the canvas
const imgData = ctx.getImageData( 0, 0, iLength, jLength );
const data = imgData.data;
const volumeData = volume.data;
const upperThreshold = volume.upperThreshold;
const lowerThreshold = volume.lowerThreshold;
const windowLow = volume.windowLow;
const windowHigh = volume.windowHigh;
// manipulate some pixel elements
let pixelCount = 0;
if ( volume.dataType === 'label' ) {
console.error( 'THREE.VolumeSlice.repaint: label are not supported yet' );
// This part is currently useless but will be used when colortables will be handled
// for ( let j = 0; j < jLength; j ++ ) {
// for ( let i = 0; i < iLength; i ++ ) {
// let label = volumeData[ sliceAccess( i, j ) ];
// label = label >= this.colorMap.length ? ( label % this.colorMap.length ) + 1 : label;
// const color = this.colorMap[ label ];
// data[ 4 * pixelCount ] = ( color >> 24 ) & 0xff;
// data[ 4 * pixelCount + 1 ] = ( color >> 16 ) & 0xff;
// data[ 4 * pixelCount + 2 ] = ( color >> 8 ) & 0xff;
// data[ 4 * pixelCount + 3 ] = color & 0xff;
// pixelCount ++;
// }
// }
} else {
for ( let j = 0; j < jLength; j ++ ) {
for ( let i = 0; i < iLength; i ++ ) {
let value = volumeData[ sliceAccess( i, j ) ];
let alpha = 0xff;
//apply threshold
alpha = upperThreshold >= value ? ( lowerThreshold <= value ? alpha : 0 ) : 0;
//apply window level
value = Math.floor( 255 * ( value - windowLow ) / ( windowHigh - windowLow ) );
value = value > 255 ? 255 : ( value < 0 ? 0 : value | 0 );
data[ 4 * pixelCount ] = value;
data[ 4 * pixelCount + 1 ] = value;
data[ 4 * pixelCount + 2 ] = value;
data[ 4 * pixelCount + 3 ] = alpha;
pixelCount ++;
}
}
}
ctx.putImageData( imgData, 0, 0 );
this.ctx.drawImage( canvas, 0, 0, iLength, jLength, 0, 0, this.canvas.width, this.canvas.height );
this.mesh.material.map.needsUpdate = true;
}
/**
* Refresh the geometry according to axis and index.
* @see {@link Volume#extractPerpendicularPlane}
*/
updateGeometry() {
const extracted = this.volume.extractPerpendicularPlane( this.axis, this.index );
this.sliceAccess = extracted.sliceAccess;
this.jLength = extracted.jLength;
this.iLength = extracted.iLength;
this.matrix = extracted.matrix;
this.canvas.width = extracted.planeWidth;
this.canvas.height = extracted.planeHeight;
this.canvasBuffer.width = this.iLength;
this.canvasBuffer.height = this.jLength;
this.ctx = this.canvas.getContext( '2d' );
this.ctxBuffer = this.canvasBuffer.getContext( '2d' );
if ( this.geometry ) this.geometry.dispose(); // dispose existing geometry
this.geometry = new PlaneGeometry( extracted.planeWidth, extracted.planeHeight );
if ( this.mesh ) {
this.mesh.geometry = this.geometry;
//reset mesh matrix
this.mesh.matrix.identity();
this.mesh.applyMatrix4( this.matrix );
}
this.geometryNeedsUpdate = false;
}
}
export { VolumeSlice };