Archived
Private
Public Access
1
0

Initial commit

This commit is contained in:
2022-09-04 12:45:01 +02:00
commit f4a01d6a69
11601 changed files with 4206660 additions and 0 deletions

View File

@@ -0,0 +1,485 @@
( function () {
/**
* @fileoverview This class can be used to subdivide a convex Geometry object into pieces.
*
* Usage:
*
* Use the function prepareBreakableObject to prepare a THREE.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:
*
* - THREE.Mesh object must have a BufferGeometry (not 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 THREE.ConvexGeometry. The BoxGeometry, 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.
*
* @param {double} minSizeForBreak Min size a debris can have to break.
* @param {double} smallDelta Max distance to consider that a point belongs to a plane.
*
*/
const _v1 = new THREE.Vector3();
class ConvexObjectBreaker {
constructor( minSizeForBreak = 1.4, smallDelta = 0.0001 ) {
this.minSizeForBreak = minSizeForBreak;
this.smallDelta = smallDelta;
this.tempLine1 = new THREE.Line3();
this.tempPlane1 = new THREE.Plane();
this.tempPlane2 = new THREE.Plane();
this.tempPlane_Cut = new THREE.Plane();
this.tempCM1 = new THREE.Vector3();
this.tempCM2 = new THREE.Vector3();
this.tempVector3 = new THREE.Vector3();
this.tempVector3_2 = new THREE.Vector3();
this.tempVector3_3 = new THREE.Vector3();
this.tempVector3_P0 = new THREE.Vector3();
this.tempVector3_P1 = new THREE.Vector3();
this.tempVector3_P2 = new THREE.Vector3();
this.tempVector3_N0 = new THREE.Vector3();
this.tempVector3_N1 = new THREE.Vector3();
this.tempVector3_AB = new THREE.Vector3();
this.tempVector3_CB = new THREE.Vector3();
this.tempResultObjects = {
object1: null,
object2: null
};
this.segments = [];
const n = 30 * 30;
for ( let i = 0; i < n; i ++ ) this.segments[ i ] = false;
}
prepareBreakableObject( object, mass, velocity, angularVelocity, breakable ) {
// object is a Object3d (normally a THREE.Mesh), must have a BufferGeometry, and it must be convex.
// Its material property is propagated to its children (sub-pieces)
// mass must be > 0
if ( ! object.geometry.isBufferGeometry ) {
console.error( 'THREE.ConvexObjectBreaker.prepareBreakableObject(): Parameter object must have a BufferGeometry.' );
}
const userData = object.userData;
userData.mass = mass;
userData.velocity = velocity.clone();
userData.angularVelocity = angularVelocity.clone();
userData.breakable = breakable;
}
/*
* @param {int} maxRadialIterations Iterations for radial cuts.
* @param {int} maxRandomIterations Max random iterations for not-radial cuts
*
* Returns 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;
}
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 THREE.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 THREE.Mesh( new THREE.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 THREE.Mesh( new THREE.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;
}
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 );
}
}
THREE.ConvexObjectBreaker = ConvexObjectBreaker;
} )();

View File

@@ -0,0 +1,360 @@
( function () {
/**
* 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:
*
* // 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, pos0 );
* const posVar = gpuCompute.addVariable( "texturePosition", fragmentShaderPos, vel0 );
*
* // 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 THREE.ShaderMaterial and perform computations (rendering between textures)
* Note that the shaders can have multiple input textures.
*
* 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 );
*
*
*
* @param {int} sizeX Computation problem size is always 2d: sizeX * sizeY elements.
* @param {int} sizeY Computation problem size is always 2d: sizeX * sizeY elements.
* @param {WebGLRenderer} renderer The renderer
*/
class GPUComputationRenderer {
constructor( sizeX, sizeY, renderer ) {
this.variables = [];
this.currentTextureIndex = 0;
let dataType = THREE.FloatType;
const scene = new THREE.Scene();
const camera = new THREE.Camera();
camera.position.z = 1;
const passThruUniforms = {
passThruTexture: {
value: null
}
};
const passThruShader = createShaderMaterial( getPassThroughFragmentShader(), passThruUniforms );
const mesh = new THREE.Mesh( new THREE.PlaneGeometry( 2, 2 ), passThruShader );
scene.add( mesh );
this.setDataType = function ( type ) {
dataType = type;
return this;
};
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: THREE.NearestFilter,
magFilter: THREE.NearestFilter
};
this.variables.push( variable );
return variable;
};
this.setVariableDependencies = function ( variable, dependencies ) {
variable.dependencies = dependencies;
};
this.init = function () {
if ( renderer.capabilities.isWebGL2 === false && renderer.extensions.has( 'OES_texture_float' ) === false ) {
return 'No OES_texture_float support for float textures.';
}
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 THREE.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;
};
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;
};
this.getCurrentRenderTarget = function ( variable ) {
return variable.renderTargets[ this.currentTextureIndex ];
};
this.getAlternateRenderTarget = function ( variable ) {
return variable.renderTargets[ this.currentTextureIndex === 0 ? 1 : 0 ];
};
function addResolutionDefine( materialShader ) {
materialShader.defines.resolution = 'vec2( ' + sizeX.toFixed( 1 ) + ', ' + sizeY.toFixed( 1 ) + ' )';
}
this.addResolutionDefine = addResolutionDefine; // The following functions can be used to compute things manually
function createShaderMaterial( computeFragmentShader, uniforms ) {
uniforms = uniforms || {};
const material = new THREE.ShaderMaterial( {
uniforms: uniforms,
vertexShader: getPassThroughVertexShader(),
fragmentShader: computeFragmentShader
} );
addResolutionDefine( material );
return material;
}
this.createShaderMaterial = createShaderMaterial;
this.createRenderTarget = function ( sizeXTexture, sizeYTexture, wrapS, wrapT, minFilter, magFilter ) {
sizeXTexture = sizeXTexture || sizeX;
sizeYTexture = sizeYTexture || sizeY;
wrapS = wrapS || THREE.ClampToEdgeWrapping;
wrapT = wrapT || THREE.ClampToEdgeWrapping;
minFilter = minFilter || THREE.NearestFilter;
magFilter = magFilter || THREE.NearestFilter;
const renderTarget = new THREE.WebGLRenderTarget( sizeXTexture, sizeYTexture, {
wrapS: wrapS,
wrapT: wrapT,
minFilter: minFilter,
magFilter: magFilter,
format: THREE.RGBAFormat,
type: dataType,
depthBuffer: false
} );
return renderTarget;
};
this.createTexture = function () {
const data = new Float32Array( sizeX * sizeY * 4 );
const texture = new THREE.DataTexture( data, sizeX, sizeY, THREE.RGBAFormat, THREE.FloatType );
texture.needsUpdate = true;
return texture;
};
this.renderTexture = function ( input, output ) {
// Takes a texture, and render out in rendertarget
// input = Texture
// output = RenderTarget
passThruUniforms.passThruTexture.value = input;
this.doRenderTarget( passThruShader, output );
passThruUniforms.passThruTexture.value = null;
};
this.doRenderTarget = function ( material, output ) {
const currentRenderTarget = renderer.getRenderTarget();
mesh.material = material;
renderer.setRenderTarget( output );
renderer.render( scene, camera );
mesh.material = passThruShader;
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';
}
}
}
THREE.GPUComputationRenderer = GPUComputationRenderer;
} )();

View File

@@ -0,0 +1,60 @@
( function () {
const _translationObject = new THREE.Vector3();
const _quaternionObject = new THREE.Quaternion();
const _scaleObject = new THREE.Vector3();
const _translationWorld = new THREE.Vector3();
const _quaternionWorld = new THREE.Quaternion();
const _scaleWorld = new THREE.Vector3();
class Gyroscope extends THREE.Object3D {
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 );
}
}
}
THREE.Gyroscope = Gyroscope;
} )();

View File

@@ -0,0 +1,244 @@
( function () {
class MD2Character {
constructor() {
this.scale = 1;
this.animationFPS = 6;
this.root = new THREE.Object3D();
this.meshBody = null;
this.meshWeapon = null;
this.skinsBody = [];
this.skinsWeapon = [];
this.weapons = [];
this.activeAnimation = null;
this.mixer = null;
this.onLoadComplete = function () {};
this.loadCounter = 0;
}
loadParts( config ) {
const scope = this;
function createPart( geometry, skinMap ) {
const materialWireframe = new THREE.MeshLambertMaterial( {
color: 0xffaa00,
wireframe: true
} );
const materialTexture = new THREE.MeshLambertMaterial( {
color: 0xffffff,
wireframe: false,
map: skinMap
} ); //
const mesh = new THREE.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 THREE.TextureLoader();
const textures = [];
for ( let i = 0; i < textureUrls.length; i ++ ) {
textures[ i ] = textureLoader.load( baseUrl + textureUrls[ i ], checkLoadingComplete );
textures[ i ].mapping = THREE.UVMapping;
textures[ i ].name = textureUrls[ i ];
textures[ i ].encoding = THREE.sRGBEncoding;
}
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 THREE.MD2Loader();
loader.load( config.baseUrl + config.body, function ( geo ) {
const boundingBox = new THREE.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 THREE.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 ] ) );
}
}
setPlaybackRate( rate ) {
if ( rate !== 0 ) {
this.mixer.timeScale = 1 / rate;
} else {
this.mixer.timeScale = 0;
}
}
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;
}
}
setSkin( index ) {
if ( this.meshBody && this.meshBody.material.wireframe === false ) {
this.meshBody.material.map = this.skinsBody[ 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();
}
}
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();
}
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();
}
}
}
update( delta ) {
if ( this.mixer ) this.mixer.update( delta );
}
}
THREE.MD2Character = MD2Character;
} )();

View File

@@ -0,0 +1,494 @@
( function () {
class MD2CharacterComplex {
constructor() {
this.scale = 1; // animation parameters
this.animationFPS = 6;
this.transitionFrames = 15; // movement model parameters
this.maxSpeed = 275;
this.maxReverseSpeed = - 275;
this.frontAcceleration = 600;
this.backAcceleration = 600;
this.frontDecceleration = 600;
this.angularSpeed = 2.5; // rig
this.root = new THREE.Object3D();
this.meshBody = null;
this.meshWeapon = null;
this.controls = null; // skins
this.skinsBody = [];
this.skinsWeapon = [];
this.weapons = [];
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
}
enableShadows( enable ) {
for ( let i = 0; i < this.meshes.length; i ++ ) {
this.meshes[ i ].castShadow = enable;
this.meshes[ i ].receiveShadow = enable;
}
}
setVisible( enable ) {
for ( let i = 0; i < this.meshes.length; i ++ ) {
this.meshes[ i ].visible = enable;
this.meshes[ i ].visible = enable;
}
}
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 );
}
}
loadParts( config ) {
const scope = this;
function loadTextures( baseUrl, textureUrls ) {
const textureLoader = new THREE.TextureLoader();
const textures = [];
for ( let i = 0; i < textureUrls.length; i ++ ) {
textures[ i ] = textureLoader.load( baseUrl + textureUrls[ i ], checkLoadingComplete );
textures[ i ].mapping = THREE.UVMapping;
textures[ i ].name = textureUrls[ i ];
textures[ i ].encoding = THREE.sRGBEncoding;
}
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 THREE.MD2Loader();
loader.load( config.baseUrl + config.body, function ( geo ) {
const boundingBox = new THREE.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 ] ) );
}
}
setPlaybackRate( rate ) {
if ( this.meshBody ) this.meshBody.duration = this.meshBody.baseDuration / rate;
if ( this.meshWeapon ) this.meshWeapon.duration = this.meshWeapon.baseDuration / rate;
}
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;
}
}
setSkin( index ) {
if ( this.meshBody && this.meshBody.material.wireframe === false ) {
this.meshBody.material.map = this.skinsBody[ index ];
this.currentSkin = 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 ) );
}
}
}
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 );
}
}
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 );
}
}
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 );
}
}
}
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 = THREE.MathUtils.clamp( this.speed + delta * this.frontAcceleration, this.maxReverseSpeed, this.maxSpeed );
if ( controls.moveBackward ) this.speed = THREE.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 = THREE.MathUtils.clamp( this.speed + dir * delta * this.frontAcceleration, this.maxReverseSpeed, this.maxSpeed );
}
if ( controls.moveRight ) {
this.bodyOrientation -= delta * this.angularSpeed;
this.speed = THREE.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 = THREE.MathUtils.clamp( this.speed - k * delta * this.frontDecceleration, 0, this.maxSpeed );
} else {
const k = exponentialEaseOut( this.speed / this.maxReverseSpeed );
this.speed = THREE.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 THREE.MeshLambertMaterial( {
color: 0xffaa00,
wireframe: true
} );
const materialTexture = new THREE.MeshLambertMaterial( {
color: 0xffffff,
wireframe: false,
map: skinMap
} ); //
const mesh = new THREE.MorphBlendMesh( geometry, materialTexture );
mesh.rotation.y = - Math.PI / 2; //
mesh.materialTexture = materialTexture;
mesh.materialWireframe = materialWireframe; //
mesh.autoCreateAnimations( this.animationFPS );
return mesh;
}
}
THREE.MD2CharacterComplex = MD2CharacterComplex;
} )();

View File

@@ -0,0 +1,69 @@
( function () {
class MorphAnimMesh extends THREE.Mesh {
constructor( geometry, material ) {
super( geometry, material );
this.type = 'MorphAnimMesh';
this.mixer = new THREE.AnimationMixer( this );
this.activeAction = null;
}
setDirectionForward() {
this.mixer.timeScale = 1.0;
}
setDirectionBackward() {
this.mixer.timeScale = - 1.0;
}
playAnimation( label, fps ) {
if ( this.activeAction ) {
this.activeAction.stop();
this.activeAction = null;
}
const clip = THREE.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()' );
}
}
updateAnimation( delta ) {
this.mixer.update( delta );
}
copy( source ) {
super.copy( source );
this.mixer = new THREE.AnimationMixer( this );
return this;
}
}
THREE.MorphAnimMesh = MorphAnimMesh;
} )();

View File

@@ -0,0 +1,292 @@
( function () {
class MorphBlendMesh extends THREE.Mesh {
constructor( geometry, material ) {
super( geometry, material );
this.animationsMap = {};
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 );
}
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 );
}
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;
}
setAnimationDirectionForward( name ) {
const animation = this.animationsMap[ name ];
if ( animation ) {
animation.direction = 1;
animation.directionBackwards = false;
}
}
setAnimationDirectionBackward( name ) {
const animation = this.animationsMap[ name ];
if ( animation ) {
animation.direction = - 1;
animation.directionBackwards = true;
}
}
setAnimationFPS( name, fps ) {
const animation = this.animationsMap[ name ];
if ( animation ) {
animation.fps = fps;
animation.duration = ( animation.end - animation.start ) / animation.fps;
}
}
setAnimationDuration( name, duration ) {
const animation = this.animationsMap[ name ];
if ( animation ) {
animation.duration = duration;
animation.fps = ( animation.end - animation.start ) / animation.duration;
}
}
setAnimationWeight( name, weight ) {
const animation = this.animationsMap[ name ];
if ( animation ) {
animation.weight = weight;
}
}
setAnimationTime( name, time ) {
const animation = this.animationsMap[ name ];
if ( animation ) {
animation.time = time;
}
}
getAnimationTime( name ) {
let time = 0;
const animation = this.animationsMap[ name ];
if ( animation ) {
time = animation.time;
}
return time;
}
getAnimationDuration( name ) {
let duration = - 1;
const animation = this.animationsMap[ name ];
if ( animation ) {
duration = animation.duration;
}
return duration;
}
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()' );
}
}
stopAnimation( name ) {
const animation = this.animationsMap[ name ];
if ( animation ) {
animation.active = false;
}
}
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 + THREE.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;
}
}
}
}
THREE.MorphBlendMesh = MorphBlendMesh;
} )();

View File

@@ -0,0 +1,331 @@
( function () {
/**
* Progressive Light Map Accumulator, by [zalo](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.
*
* @param {WebGLRenderer} renderer A WebGL Rendering Context
* @param {number} res The side-long dimension of you total lightmap
*/
class ProgressiveLightMap {
constructor( renderer, res = 1024 ) {
this.renderer = renderer;
this.res = res;
this.lightMapContainers = [];
this.compiled = false;
this.scene = new THREE.Scene();
this.scene.background = null;
this.tinyTarget = new THREE.WebGLRenderTarget( 1, 1 );
this.buffer1Active = false;
this.firstUpdate = true;
this.warned = false; // Create the Progressive LightMap Texture
const format = /(Android|iPad|iPhone|iPod)/g.test( navigator.userAgent ) ? THREE.HalfFloatType : THREE.FloatType;
this.progressiveLightMap1 = new THREE.WebGLRenderTarget( this.res, this.res, {
type: format
} );
this.progressiveLightMap2 = new THREE.WebGLRenderTarget( this.res, this.res, {
type: format
} ); // Inject some spicy new logic into a standard phong material
this.uvMat = new THREE.MeshPhongMaterial();
this.uvMat.uniforms = {};
this.uvMat.onBeforeCompile = shader => {
// Vertex Shader: Set Vertex Positions to the Unwrapped UV Positions
shader.vertexShader = '#define USE_LIGHTMAP\n' + shader.vertexShader.slice( 0, - 1 ) + ' gl_Position = vec4((uv2 - 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 = 'varying vec2 vUv2;\n' + shader.fragmentShader.slice( 0, bodyStart ) + ' uniform sampler2D previousShadowMap;\n uniform float averagingWindow;\n' + shader.fragmentShader.slice( bodyStart - 1, - 1 ) + `\nvec3 texelOld = texture2D(previousShadowMap, vUv2).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;
this.compiled = true;
};
}
/**
* Sets these objects' materials' lightmaps and modifies their uv2's.
* @param {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' ) ) {
console.warn( '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
} );
this.compiled = false;
} // Pack the objects' lightmap UVs into the same global space
const dimensions = potpack( this.uv_boxes );
this.uv_boxes.forEach( box => {
const uv2 = objects[ box.index ].geometry.getAttribute( 'uv' ).clone();
for ( let i = 0; i < uv2.array.length; i += uv2.itemSize ) {
uv2.array[ i ] = ( uv2.array[ i ] + box.x + padding ) / dimensions.w;
uv2.array[ i + 1 ] = ( uv2.array[ i + 1 ] + box.y + padding ) / dimensions.h;
}
objects[ box.index ].geometry.setAttribute( 'uv2', uv2 );
objects[ box.index ].geometry.getAttribute( 'uv2' ).needsUpdate = true;
} );
}
/**
* This function renders each mesh one at a time into their respective surface maps
* @param {Camera} camera Standard Rendering Camera
* @param {number} blendWindow When >1, samples will accumulate over time.
* @param {boolean} blurEdges 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 );
} // Render once normally to initialize everything
if ( this.firstUpdate ) {
this.renderer.setRenderTarget( this.tinyTarget ); // Tiny for Speed
this.renderer.render( 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 );
}
/** DEBUG
* Draw 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 ) {
if ( ! this.warned ) {
console.warn( 'Call this after adding the objects!' );
this.warned = true;
}
return;
}
if ( this.labelMesh == null ) {
this.labelMaterial = new THREE.MeshBasicMaterial( {
map: this.progressiveLightMap1.texture,
side: THREE.DoubleSide
} );
this.labelPlane = new THREE.PlaneGeometry( 100, 100 );
this.labelMesh = new THREE.Mesh( this.labelPlane, this.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;
}
/**
* INTERNAL Creates the Blurring Plane
* @param {number} res The square resolution of this object's lightMap.
* @param {WebGLRenderTexture} lightMap The lightmap to initialize the plane with.
*/
_initializeBlurPlane( res, lightMap = null ) {
const blurMaterial = new THREE.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.compiled = true;
};
this.blurringPlane = new THREE.Mesh( new THREE.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 );
}
}
THREE.ProgressiveLightMap = ProgressiveLightMap;
} )();

View File

@@ -0,0 +1,437 @@
( function () {
class RollerCoasterGeometry extends THREE.BufferGeometry {
constructor( curve, divisions ) {
super();
const vertices = [];
const normals = [];
const colors = [];
const color1 = [ 1, 1, 1 ];
const color2 = [ 1, 1, 0 ];
const up = new THREE.Vector3( 0, 1, 0 );
const forward = new THREE.Vector3();
const right = new THREE.Vector3();
const quaternion = new THREE.Quaternion();
const prevQuaternion = new THREE.Quaternion();
prevQuaternion.setFromAxisAngle( up, Math.PI / 2 );
const point = new THREE.Vector3();
const prevPoint = new THREE.Vector3();
prevPoint.copy( curve.getPointAt( 0 ) ); // shapes
const step = [ new THREE.Vector3( - 0.225, 0, 0 ), new THREE.Vector3( 0, - 0.050, 0 ), new THREE.Vector3( 0, - 0.175, 0 ), new THREE.Vector3( 0, - 0.050, 0 ), new THREE.Vector3( 0.225, 0, 0 ), new THREE.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 THREE.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 THREE.Vector3( Math.sin( angle ) * 0.025, Math.cos( angle ) * 0.025, 0 ) );
}
const vector = new THREE.Vector3();
const normal = new THREE.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 THREE.Vector3();
const vector2 = new THREE.Vector3();
const vector3 = new THREE.Vector3();
const vector4 = new THREE.Vector3();
const normal1 = new THREE.Vector3();
const normal2 = new THREE.Vector3();
const normal3 = new THREE.Vector3();
const normal4 = new THREE.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 THREE.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 THREE.BufferAttribute( new Float32Array( vertices ), 3 ) );
this.setAttribute( 'normal', new THREE.BufferAttribute( new Float32Array( normals ), 3 ) );
this.setAttribute( 'color', new THREE.BufferAttribute( new Float32Array( colors ), 3 ) );
}
}
class RollerCoasterLiftersGeometry extends THREE.BufferGeometry {
constructor( curve, divisions ) {
super();
const vertices = [];
const normals = [];
const quaternion = new THREE.Quaternion();
const up = new THREE.Vector3( 0, 1, 0 );
const point = new THREE.Vector3();
const tangent = new THREE.Vector3(); // shapes
const tube1 = [ new THREE.Vector3( 0, 0.05, - 0.05 ), new THREE.Vector3( 0, 0.05, 0.05 ), new THREE.Vector3( 0, - 0.05, 0 ) ];
const tube2 = [ new THREE.Vector3( - 0.05, 0, 0.05 ), new THREE.Vector3( - 0.05, 0, - 0.05 ), new THREE.Vector3( 0.05, 0, 0 ) ];
const tube3 = [ new THREE.Vector3( 0.05, 0, - 0.05 ), new THREE.Vector3( 0.05, 0, 0.05 ), new THREE.Vector3( - 0.05, 0, 0 ) ];
const vector1 = new THREE.Vector3();
const vector2 = new THREE.Vector3();
const vector3 = new THREE.Vector3();
const vector4 = new THREE.Vector3();
const normal1 = new THREE.Vector3();
const normal2 = new THREE.Vector3();
const normal3 = new THREE.Vector3();
const normal4 = new THREE.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 THREE.Vector3();
const toPoint = new THREE.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 THREE.BufferAttribute( new Float32Array( vertices ), 3 ) );
this.setAttribute( 'normal', new THREE.BufferAttribute( new Float32Array( normals ), 3 ) );
}
}
class RollerCoasterShadowGeometry extends THREE.BufferGeometry {
constructor( curve, divisions ) {
super();
const vertices = [];
const up = new THREE.Vector3( 0, 1, 0 );
const forward = new THREE.Vector3();
const quaternion = new THREE.Quaternion();
const prevQuaternion = new THREE.Quaternion();
prevQuaternion.setFromAxisAngle( up, Math.PI / 2 );
const point = new THREE.Vector3();
const prevPoint = new THREE.Vector3();
prevPoint.copy( curve.getPointAt( 0 ) );
prevPoint.y = 0;
const vector1 = new THREE.Vector3();
const vector2 = new THREE.Vector3();
const vector3 = new THREE.Vector3();
const vector4 = new THREE.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 THREE.BufferAttribute( new Float32Array( vertices ), 3 ) );
}
}
class SkyGeometry extends THREE.BufferGeometry {
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 THREE.BufferAttribute( new Float32Array( vertices ), 3 ) );
}
}
class TreesGeometry extends THREE.BufferGeometry {
constructor( landscape ) {
super();
const vertices = [];
const colors = [];
const raycaster = new THREE.Raycaster();
raycaster.ray.direction.set( 0, - 1, 0 );
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 ++ ) {
colors.push( 0.2 + random, 0.4 + random, 0 );
}
}
this.setAttribute( 'position', new THREE.BufferAttribute( new Float32Array( vertices ), 3 ) );
this.setAttribute( 'color', new THREE.BufferAttribute( new Float32Array( colors ), 3 ) );
}
}
THREE.RollerCoasterGeometry = RollerCoasterGeometry;
THREE.RollerCoasterLiftersGeometry = RollerCoasterLiftersGeometry;
THREE.RollerCoasterShadowGeometry = RollerCoasterShadowGeometry;
THREE.SkyGeometry = SkyGeometry;
THREE.TreesGeometry = TreesGeometry;
} )();

View File

@@ -0,0 +1,161 @@
( function () {
function TubePainter() {
const BUFFER_SIZE = 1000000 * 3;
const positions = new THREE.BufferAttribute( new Float32Array( BUFFER_SIZE ), 3 );
positions.usage = THREE.DynamicDrawUsage;
const normals = new THREE.BufferAttribute( new Float32Array( BUFFER_SIZE ), 3 );
normals.usage = THREE.DynamicDrawUsage;
const colors = new THREE.BufferAttribute( new Float32Array( BUFFER_SIZE ), 3 );
colors.usage = THREE.DynamicDrawUsage;
const geometry = new THREE.BufferGeometry();
geometry.setAttribute( 'position', positions );
geometry.setAttribute( 'normal', normals );
geometry.setAttribute( 'color', colors );
geometry.drawRange.count = 0;
const material = new THREE.MeshStandardMaterial( {
vertexColors: true
} );
const mesh = new THREE.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 THREE.Vector3( Math.sin( angle ) * radius, Math.cos( angle ) * radius, 0 ) );
}
return array;
} //
const vector1 = new THREE.Vector3();
const vector2 = new THREE.Vector3();
const vector3 = new THREE.Vector3();
const vector4 = new THREE.Vector3();
const color = new THREE.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 THREE.Vector3( 0, 1, 0 );
const point1 = new THREE.Vector3();
const point2 = new THREE.Vector3();
const matrix1 = new THREE.Matrix4();
const matrix2 = new THREE.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.updateRange.offset = start * 3;
positions.updateRange.count = ( end - start ) * 3;
positions.needsUpdate = true;
normals.updateRange.offset = start * 3;
normals.updateRange.count = ( end - start ) * 3;
normals.needsUpdate = true;
colors.updateRange.offset = start * 3;
colors.updateRange.count = ( end - start ) * 3;
colors.needsUpdate = true;
count = geometry.drawRange.count;
}
return {
mesh: mesh,
moveTo: moveTo,
lineTo: lineTo,
setSize: setSize,
update: update
};
}
THREE.TubePainter = TubePainter;
} )();

View File

@@ -0,0 +1,458 @@
( function () {
/**
* This class had been written to handle the output of the NRRD loader.
* It contains a volume of data and informations about it.
* For now it only handles 3 dimensional data.
* See the webgl_loader_nrrd.html example and the loaderNRRD.js file to see how to use this class.
* @class
* @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
*/
function Volume( xLength, yLength, zLength, type, arrayBuffer ) {
if ( arguments.length > 0 ) {
/**
* @member {number} xLength Width of the volume in the IJK coordinate system
*/
this.xLength = Number( xLength ) || 1;
/**
* @member {number} yLength Height of the volume in the IJK coordinate system
*/
this.yLength = Number( yLength ) || 1;
/**
* @member {number} zLength Depth of the volume in the IJK coordinate system
*/
this.zLength = Number( zLength ) || 1;
/**
* @member {Array<string>} The order of the Axis dictated by the NRRD header
*/
this.axisOrder = [ 'x', 'y', 'z' ];
/**
* @member {TypedArray} data Data of the volume
*/
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' );
}
}
/**
* @member {Array} spacing Spacing to apply to the volume from IJK to RAS coordinate system
*/
this.spacing = [ 1, 1, 1 ];
/**
* @member {Array} offset Offset of the volume in the RAS coordinate system
*/
this.offset = [ 0, 0, 0 ];
/**
* @member {Martrix3} matrix The IJK to RAS matrix
*/
this.matrix = new THREE.Matrix3();
this.matrix.identity();
/**
* @member {Martrix3} inverseMatrix The RAS to IJK matrix
*/
/**
* @member {number} 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
*/
let lowerThreshold = - Infinity;
Object.defineProperty( this, 'lowerThreshold', {
get: function () {
return lowerThreshold;
},
set: function ( value ) {
lowerThreshold = value;
this.sliceList.forEach( function ( slice ) {
slice.geometryNeedsUpdate = true;
} );
}
} );
/**
* @member {number} 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
*/
let upperThreshold = Infinity;
Object.defineProperty( this, 'upperThreshold', {
get: function () {
return upperThreshold;
},
set: function ( value ) {
upperThreshold = value;
this.sliceList.forEach( function ( slice ) {
slice.geometryNeedsUpdate = true;
} );
}
} );
/**
* @member {Array} sliceList The list of all the slices associated to this volume
*/
this.sliceList = [];
/**
* @member {Array} RASDimensions This array holds the dimensions of the volume in the RAS space
*/
}
Volume.prototype = {
constructor: Volume,
/**
* @member {Function} getData Shortcut for data[access(i,j,k)]
* @memberof Volume
* @param {number} i First coordinate
* @param {number} j Second coordinate
* @param {number} k Third coordinate
* @returns {number} value in the data array
*/
getData: function ( i, j, k ) {
return this.data[ k * this.xLength * this.yLength + j * this.xLength + i ];
},
/**
* @member {Function} access compute the index in the data array corresponding to the given coordinates in IJK system
* @memberof Volume
* @param {number} i First coordinate
* @param {number} j Second coordinate
* @param {number} k Third coordinate
* @returns {number} index
*/
access: function ( i, j, k ) {
return k * this.xLength * this.yLength + j * this.xLength + i;
},
/**
* @member {Function} reverseAccess Retrieve the IJK coordinates of the voxel corresponding of the given index in the data
* @memberof Volume
* @param {number} index index of the voxel
* @returns {Array} [x,y,z]
*/
reverseAccess: function ( 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 ];
},
/**
* @member {Function} map Apply a function to all the voxels, be careful, the value will be replaced
* @memberof Volume
* @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} this
*/
map: function ( 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;
},
/**
* @member {Function} extractPerpendicularPlane 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.
* @memberof Volume
* @param {string} axis the normal axis to the slice 'x' 'y' or 'z'
* @param {number} index the index of the slice
* @returns {Object} an object containing all the usefull information on the geometry of the slice
*/
extractPerpendicularPlane: function ( axis, RASIndex ) {
let firstSpacing, secondSpacing, positionOffset, IJKIndex;
const axisInIJK = new THREE.Vector3(),
firstDirection = new THREE.Vector3(),
secondDirection = new THREE.Vector3(),
planeMatrix = new THREE.Matrix4().identity(),
volume = this;
const dimensions = new THREE.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 THREE.Vector3( RASIndex, 0, 0 );
planeMatrix.multiply( new THREE.Matrix4().makeRotationY( Math.PI / 2 ) );
positionOffset = ( volume.RASDimensions[ 0 ] - 1 ) / 2;
planeMatrix.setPosition( new THREE.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 THREE.Vector3( 0, RASIndex, 0 );
planeMatrix.multiply( new THREE.Matrix4().makeRotationX( - Math.PI / 2 ) );
positionOffset = ( volume.RASDimensions[ 1 ] - 1 ) / 2;
planeMatrix.setPosition( new THREE.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 THREE.Vector3( 0, 0, RASIndex );
positionOffset = ( volume.RASDimensions[ 2 ] - 1 ) / 2;
planeMatrix.setPosition( new THREE.Vector3( 0, 0, RASIndex - positionOffset ) );
break;
}
firstDirection.applyMatrix4( volume.inverseMatrix ).normalize();
firstDirection.arglet = 'i';
secondDirection.applyMatrix4( volume.inverseMatrix ).normalize();
secondDirection.arglet = 'j';
axisInIJK.applyMatrix4( volume.inverseMatrix ).normalize();
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 THREE.Vector3( 1, 0, 0 ), new THREE.Vector3( 0, 1, 0 ), new THREE.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
};
},
/**
* @member {Function} extractSlice Returns a slice corresponding to the given axis and index
* The coordinate are given in the Right Anterior Superior coordinate format
* @memberof Volume
* @param {string} axis the normal axis to the slice 'x' 'y' or 'z'
* @param {number} index the index of the slice
* @returns {VolumeSlice} the extracted slice
*/
extractSlice: function ( axis, index ) {
const slice = new THREE.VolumeSlice( this, index, axis );
this.sliceList.push( slice );
return slice;
},
/**
* @member {Function} repaintAllSlices Call repaint on all the slices extracted from this volume
* @see THREE.VolumeSlice.repaint
* @memberof Volume
* @returns {Volume} this
*/
repaintAllSlices: function () {
this.sliceList.forEach( function ( slice ) {
slice.repaint();
} );
return this;
},
/**
* @member {Function} computeMinMax Compute the minimum and the maximum of the data in the volume
* @memberof Volume
* @returns {Array} [min,max]
*/
computeMinMax: function () {
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 ];
}
};
THREE.Volume = Volume;
} )();

View File

@@ -0,0 +1,222 @@
( function () {
/**
* This class has been made to hold a slice of a volume data
* @class
* @param {Volume} volume The associated volume
* @param {number} [index=0] The index of the slice
* @param {string} [axis='z'] For now only 'x', 'y' or 'z' but later it will change to a normal vector
* @see Volume
*/
function VolumeSlice( volume, index, axis ) {
const slice = this;
/**
* @member {Volume} volume The associated volume
*/
this.volume = volume;
/**
* @member {Number} index The index of the slice, if changed, will automatically call updateGeometry at the next repaint
*/
index = index || 0;
Object.defineProperty( this, 'index', {
get: function () {
return index;
},
set: function ( value ) {
index = value;
slice.geometryNeedsUpdate = true;
return index;
}
} );
/**
* @member {String} axis The normal axis
*/
this.axis = axis || 'z';
/**
* @member {HTMLCanvasElement} canvas The final canvas used for the texture
*/
/**
* @member {CanvasRenderingContext2D} ctx Context of the canvas
*/
this.canvas = document.createElement( 'canvas' );
/**
* @member {HTMLCanvasElement} canvasBuffer The intermediary canvas used to paint the data
*/
/**
* @member {CanvasRenderingContext2D} ctxBuffer Context of the canvas buffer
*/
this.canvasBuffer = document.createElement( 'canvas' );
this.updateGeometry();
const canvasMap = new THREE.Texture( this.canvas );
canvasMap.minFilter = THREE.LinearFilter;
canvasMap.wrapS = canvasMap.wrapT = THREE.ClampToEdgeWrapping;
const material = new THREE.MeshBasicMaterial( {
map: canvasMap,
side: THREE.DoubleSide,
transparent: true
} );
/**
* @member {Mesh} mesh The mesh ready to get used in the scene
*/
this.mesh = new THREE.Mesh( this.geometry, material );
this.mesh.matrixAutoUpdate = false;
/**
* @member {Boolean} geometryNeedsUpdate If set to true, updateGeometry will be triggered at the next repaint
*/
this.geometryNeedsUpdate = true;
this.repaint();
/**
* @member {Number} iLength Width of slice in the original coordinate system, corresponds to the width of the buffer canvas
*/
/**
* @member {Number} jLength Height of slice in the original coordinate system, corresponds to the height of the buffer canvas
*/
/**
* @member {Function} sliceAccess Function that allow the slice to access right data
* @see Volume.extractPerpendicularPlane
* @param {Number} i The first coordinate
* @param {Number} j The second coordinate
* @returns {Number} the index corresponding to the voxel in volume.data of the given position in the slice
*/
}
VolumeSlice.prototype = {
constructor: VolumeSlice,
/**
* @member {Function} repaint Refresh the texture and the geometry if geometryNeedsUpdate is set to true
* @memberof VolumeSlice
*/
repaint: function () {
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' ) {
//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;
},
/**
* @member {Function} Refresh the geometry according to axis and index
* @see Volume.extractPerpendicularPlane
* @memberof VolumeSlice
*/
updateGeometry: function () {
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 THREE.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;
}
};
THREE.VolumeSlice = VolumeSlice;
} )();