three-rwx-loader
Version:
three.js RWX (AW) loader
1,791 lines (1,167 loc) • 78.7 kB
JavaScript
/**
* @author Julien 'Blaxar' Bardagi <blaxar.waldarax@gmail.com>
*/
import {
FileLoader,
Loader,
Mesh,
Vector2,
Vector3,
Matrix4,
Vector4,
MathUtils,
MeshPhongMaterial,
MeshBasicMaterial,
BufferGeometry,
Quaternion,
Plane,
TextureLoader,
NearestFilter,
LinearMipmapNearestFilter,
RepeatWrapping,
ClampToEdgeWrapping,
MirroredRepeatWrapping,
LinearSRGBColorSpace,
SRGBColorSpace,
FrontSide,
DoubleSide,
Group,
BufferAttribute,
EdgesGeometry,
LineSegments,
LineBasicMaterial
} from 'three';
import { Earcut } from 'three/src/extras/Earcut.js';
import { SweepContext } from 'poly2tri';
const LightSampling = {
FACET: 1,
VERTEX: 2
};
const GeometrySampling = {
POINTCLOUD: 1,
WIREFRAME: 2,
SOLID: 3
};
const TextureMode = {
LIT: 1,
FORESHORTEN: 2,
FILTER: 3
};
const MaterialMode = {
NONE: 0,
NULL: 1,
DOUBLE: 2
};
const TextureAddressMode = {
WRAP: 0,
MIRROR: 1,
CLAMP: 2
};
const signTag = 100;
const pictureTag = 200;
const glossRatio = 0.1;
const defaultAlphaTest = 0.2;
const defaultSurface = [ 0.69, 0.0, 0.0 ]; // Ambience (recommended AW 2.2), Diffusion, Specularity
const urlRegex = /^https?:\/\/.*$/i;
const extensionRegex = /^(.*)(\.[^\\]+)$/i;
const isAlphaExtensionRegex = /^\.(tiff|png|webp|gif)$/i;
const firstClumpName = 'rwx-first-clump';
const scaleGroupName = 'rwx-scale-group';
// Perform polygon triangulation by projecting vertices on a 2D plane first
function triangulateFaces( vertices, uvs, loop, objectName, forceEarcut = false, verboseWarning = false ) {
// Mostly crediting @neeh for their answer: https://stackoverflow.com/a/42402681
const _ctr = new Vector3();
let _basis = new Matrix4();
const _plane = new Plane();
const _q = new Quaternion();
const _y = new Vector3();
const _x = new Vector3();
const X = new Vector3( 1.0, 0.0, 0.0 );
const Z = new Vector3( 0.0, 0.0, 1.0 );
let _tmp = new Vector3();
const vertexIdMap = [];
// Compute centroid
_ctr.setScalar( 0.0 );
let l = loop.length;
for ( let i = 0; i < l; i ++ ) {
_ctr.add( new Vector3( vertices[ loop[ i ] * 3 ], vertices[ loop[ i ] * 3 + 1 ], vertices[ loop[ i ] * 3 + 2 ] ) );
vertexIdMap.push( loop[ i ] );
}
_ctr.multiplyScalar( 1.0 / l );
let loopNormal = new Vector3( 0.0, 0.0, 0.0 );
// Compute loop normal using Newell's Method
for ( let i = 0; i < l; i ++ ) {
const currentVertex = new Vector3( vertices[ loop[ i ] * 3 ], vertices[ loop[ i ] * 3 + 1 ], vertices[ loop[ i ] * 3 + 2 ] );
let nextVertex = new Vector3(
vertices[ loop[ ( ( i + 1 ) % l ) ] * 3 ],
vertices[ loop[ ( ( i + 1 ) % l ) ] * 3 + 1 ],
vertices[ loop[ ( ( i + 1 ) % l ) ] * 3 + 2 ]
);
loopNormal.x += ( currentVertex.y - nextVertex.y ) * ( currentVertex.z + nextVertex.z );
loopNormal.y += ( currentVertex.z - nextVertex.z ) * ( currentVertex.x + nextVertex.x );
loopNormal.z += ( currentVertex.x - nextVertex.x ) * ( currentVertex.y + nextVertex.y );
}
loopNormal.normalize();
const coplanarVertex = new Vector3( vertices[ loop[ 0 ] * 3 ], vertices[ loop[ 0 ] * 3 + 1 ], vertices[ loop[ 0 ] * 3 + 2 ] );
_plane.setFromNormalAndCoplanarPoint( loopNormal, coplanarVertex );
let _z = _plane.normal;
// Compute basis
_q.setFromUnitVectors( Z, _z );
_x.copy( X ).applyQuaternion( _q );
_y.crossVectors( _x, _z );
_y.normalize();
_basis.makeBasis( _x, _y, _z );
_basis.setPosition( _ctr );
// Project the 3D vertices on the 2D plane
let poly2triData = [];
let earcutData = [];
for ( let i = 0; i < l; i ++ ) {
const currentVertex = new Vector3( vertices[ loop[ i ] * 3 ], vertices[ loop[ i ] * 3 + 1 ], vertices[ loop[ i ] * 3 + 2 ] );
_tmp.subVectors( currentVertex, _ctr );
if ( ! forceEarcut ) poly2triData.push( { x: _tmp.dot( _x ), y: _tmp.dot( _y ), id: vertexIdMap[ i ] } );
earcutData.push( _tmp.dot( _x ), _tmp.dot( _y ) );
}
let faces = [];
if ( ! forceEarcut ) {
try {
// By default: try to use poly2tri (Delaunay triangulation), as it lays better result than Earcut in our case when it succeeds
const swctx = new SweepContext( poly2triData );
swctx.triangulate();
const triangles = swctx.getTriangles();
for ( const tri of triangles ) {
faces.push( tri.getPoint( 0 ).id, tri.getPoint( 1 ).id, tri.getPoint( 2 ).id );
}
return faces;
} catch ( e ) {
// Can't use poly2tri in this case... fallback to Earcut
if ( verboseWarning ) console.warn( 'Could not use poly2tri here for ' + objectName + ' (falling back to Earcut): ' + e );
}
}
// Return faces correctly mapping original vertex IDs
faces = Earcut.triangulate( earcutData, null, 2 ).map( id => vertexIdMap[ id ] );
return faces;
}
function makeMaskPromise( bmpURI, threeMat, loader, textureColorSpace = LinearSRGBColorSpace,
textureWrapping = RepeatWrapping, textureFiltering = true ) {
return new Promise( ( resolveMask ) => {
loader.load( bmpURI, ( maskTexture ) => {
// If the height is nicely divisible by the width: it's an animated mask
if ( maskTexture.image.height !== maskTexture.image.width && maskTexture.image.height % maskTexture.image.width === 0 ) {
threeMat.userData.rwx.maskAnimation = { yTiles: maskTexture.image.height / maskTexture.image.width,
yHeight: maskTexture.image.width / maskTexture.image.height,
step: 0 };
maskTexture.offset.y = ( 1.0 - threeMat.userData.rwx.maskAnimation.yHeight );
maskTexture.repeat.set( 1, threeMat.userData.rwx.maskAnimation.yHeight );
}
maskTexture.wrapS = textureWrapping;
maskTexture.wrapT = textureWrapping;
maskTexture.colorSpace = textureColorSpace;
if ( ! textureFiltering ) {
maskTexture.minFilter = LinearMipmapNearestFilter;
maskTexture.magFilter = NearestFilter;
}
threeMat.alphaMap = maskTexture;
threeMat.needsUpdate = true;
resolveMask( maskTexture );
} );
} );
}
function applyTextureToMat( threeMat, folder, textureName, textureExtension = '.jpg', maskName = null,
maskExtension = '.zip', specularName = null, normalName = null, fflate = null, loadingPromises = [], textureColorSpace = SRGBColorSpace,
textureWrapping = RepeatWrapping, alphaTest = defaultAlphaTest, textureFiltering = true, allowURLs = true ) {
let loader = new TextureLoader();
let texturePath = null;
const urlRes = urlRegex.exec( textureName );
const extRes = extensionRegex.exec( textureName );
if ( urlRes ) {
if ( ! allowURLs ) {
console.error( `Full URLs not allowed for texture and mask paths, skipping ${textureName}` );
return;
}
texturePath = textureName;
} else {
// If texture.jpg is requested, make sure we don't load texture.jpg.jpg
if ( extRes ) {
textureExtension = '';
}
texturePath = folder + '/' + textureName + textureExtension;
}
if ( extRes && isAlphaExtensionRegex.test( extRes[ 2 ] ) ) {
threeMat.alphaTest = alphaTest;
threeMat.transparent = true;
}
loadingPromises.push( new Promise( ( resolveTex, rejectTex ) => {
loader.load( texturePath, ( texture ) => {
texture.wrapS = textureWrapping;
texture.wrapT = textureWrapping;
texture.colorSpace = textureColorSpace;
threeMat.map = texture;
threeMat.needsUpdate = true;
if ( ! textureFiltering ) {
texture.minFilter = LinearMipmapNearestFilter;
texture.magFilter = NearestFilter;
}
// If the height is nicely divisible by the width: it's an animated texture
if ( texture.image.height !== texture.image.width && texture.image.height % texture.image.width === 0 ) {
threeMat.userData.rwx.animation = { yTiles: texture.image.height / texture.image.width,
yHeight: texture.image.width / texture.image.height,
step: 0 };
texture.offset.y = ( 1.0 - threeMat.userData.rwx.animation.yHeight );
texture.repeat.set( 1, threeMat.userData.rwx.animation.yHeight );
}
resolveTex( texture );
}, null, ( e ) => {
rejectTex( e );
} );
} ) );
if ( maskName != null ) {
threeMat.alphaTest = alphaTest;
threeMat.transparent = true;
const urlRes = urlRegex.exec( maskName );
const extRes = extensionRegex.exec( maskName );
if ( extRes ) {
maskExtension = extRes[ 2 ];
}
// We try to extract the bmp mask from the archive
let maskBaseName = extRes ? extRes[ 1 ] : maskName;
if ( urlRes ) {
if ( ! allowURLs ) {
console.error( `Full URLs not allowed for texture and mask paths, skipping ${maskName}` );
return;
}
// Trim everything before the filename in the URL
maskBaseName = maskBaseName.slice( maskBaseName.lastIndexOf( '/' ) + 1 );
}
if ( maskExtension.toLowerCase() == '.zip' && fflate != null ) {
// If mask path is a full URL: use it verbatim;
// Otherwise, if it's a single name: do not append the extension to it if
// it's already embedded in the original string.
const zipPath = urlRes ? maskName : folder + '/' + maskBaseName + maskExtension;
loadingPromises.push( new Promise( ( resolve, reject ) => {
const zipLoader = new FileLoader();
zipLoader.setResponseType( 'arraybuffer' );
// We load the mask asynchronously using fflate (if available)
zipLoader.load( zipPath, ( data ) => {
const zipData = fflate.unzipSync( new Uint8Array( data ) );
let filename = null;
// Find the bmp file within the archive, we need to be case insensitive
for ( const key of Object.keys( zipData ) ) {
if ( key.toLowerCase() == ( maskBaseName.toLowerCase() + '.bmp' ) ) {
// Found the file
filename = key;
break;
}
}
if ( ! filename ) {
// No .bmp mask file found within the archive: can't proceed further
reject( new Error( `No .bmp file candidate found within '${zipPath}'` ) );
}
const buffer = zipData[ filename ];
// Load the bmp image into a data uri string
let bmpURI = 'data:image/bmp;base64,';
const chunkSize = 4056;
let dataStr = '';
// Chunking the buffer to maximize browser compatibility and avoid exceeding some size limit
// during string creation when using 'String.fromCharCode'
for ( let i = 0; i < buffer.length; i += chunkSize ) {
dataStr = dataStr.concat( String.fromCharCode.apply( null, new Uint16Array( buffer.slice( i, i + chunkSize ) ) ) );
}
bmpURI = bmpURI.concat( btoa( dataStr ) );
makeMaskPromise( bmpURI, threeMat, loader, LinearSRGBColorSpace, textureWrapping, textureFiltering ).then( ( mask ) => {
resolve( mask );
} )
.catch( ( e ) => {
reject( e );
} );
}, null, ( e ) => {
reject( e );
} );
} ) );
} else if ( maskExtension != '.zip' ) {
const bmpPath = urlRes ? maskName : folder + '/' + maskBaseName + maskExtension;
loadingPromises.push( makeMaskPromise( bmpPath, threeMat, loader, LinearSRGBColorSpace, textureWrapping, textureFiltering ) );
}
}
// Load normal map
if ( normalName != null ) {
const urlRes = urlRegex.exec( normalName );
const normalPath = urlRes ? normalName : folder + '/' + normalName + textureExtension;
loadingPromises.push( new Promise( ( resolve, reject ) => {
loader.load( normalPath, ( normalMap ) => {
normalMap.wrapS = textureWrapping;
normalMap.wrapT = textureWrapping;
threeMat.normalMap = normalMap;
threeMat.needsUpdate = true;
resolve( normalMap );
}, null, reject );
} ) );
}
// Load specular map
if ( specularName != null ) {
const urlRes = urlRegex.exec( specularName );
const specularPath = urlRes ? specularName : folder + '/' + specularName + textureExtension;
loadingPromises.push( new Promise( ( resolve, reject ) => {
loader.load( specularPath, ( specularMap ) => {
specularMap.wrapS = textureWrapping;
specularMap.wrapT = textureWrapping;
threeMat.specularMap = specularMap;
threeMat.needsUpdate = true;
resolve( specularMap );
}, null, reject );
} ) );
}
}
function makeThreeMaterial( rwxMaterial, folder, textureExtension = '.jpg', maskExtension = '.zip',
fflate = null, useBasicMaterial = false, textureColorSpace = SRGBColorSpace, alphaTest = defaultAlphaTest,
allowURLs = true ) {
let materialDict = { name: rwxMaterial.getMatSignature() };
if ( rwxMaterial.materialmode == MaterialMode.NULL ) {
materialDict[ 'side' ] = FrontSide;
} else if ( rwxMaterial.materialmode == MaterialMode.DOUBLE ) {
materialDict[ 'side' ] = DoubleSide;
} else if ( rwxMaterial.materialmode == MaterialMode.NONE ) {
materialDict[ 'visible' ] = false;
}
if ( rwxMaterial.opacity < 1.0 ) {
materialDict[ 'transparent' ] = true;
}
if ( rwxMaterial.geometrysampling < GeometrySampling.SOLID ) {
// For the time being: we treat 'wireframe' and 'pointcloud' the same, as 'pointcloud' is not yet trivially
// supported
materialDict[ 'wireframe' ] = true;
} else {
materialDict[ 'wireframe' ] = false;
}
const textureFiltering = rwxMaterial.texturemodes.includes( TextureMode.FILTER );
let textureWrapping = ClampToEdgeWrapping;
if ( rwxMaterial.textureaddressmode == TextureAddressMode.WRAP ) {
textureWrapping = RepeatWrapping;
} else if ( rwxMaterial.textureaddressmode == TextureAddressMode.MIRROR ) {
textureWrapping = MirroredRepeatWrapping;
}
// Missing Lit mode means we should not take the surface values into account
const surface = rwxMaterial.texturemodes.includes( TextureMode.LIT ) ? rwxMaterial.surface : defaultSurface;
if ( ! useBasicMaterial ) {
if ( rwxMaterial.lightsampling == LightSampling.FACET ) {
materialDict[ 'flatShading' ] = true;
} else if ( rwxMaterial.lightsampling == LightSampling.VERTEX ) {
materialDict[ 'flatShading' ] = false;
}
// The specular value in a Phong material is expressed using an hexadecimal value
// holding on 3 bytes, each representing a different color channel.
// Without any prior knowledge: we safely assume a white light instead
const whiteSpecular = Math.trunc( surface[ 2 ] * glossRatio * 255 );
materialDict[ 'specular' ] = ( whiteSpecular << 16 ) + ( whiteSpecular << 8 ) + whiteSpecular;
// Same thing for the emissive value
const whiteEmissive = Math.trunc( surface[ 1 ] );
materialDict[ 'emissive' ] = ( whiteEmissive << 16 ) + ( whiteEmissive << 8 ) + whiteEmissive;
materialDict[ 'shininess' ] = 30; // '30' is the demo's default Phong material shininess value
}
materialDict[ 'opacity' ] = rwxMaterial.opacity;
let threeMat = useBasicMaterial ? new MeshBasicMaterial( materialDict ) : new MeshPhongMaterial( materialDict );
threeMat.userData.rwx = { material: rwxMaterial.clone() };
let loadingPromises = [];
threeMat.userData.collision = rwxMaterial.collision;
threeMat.userData.ratio = rwxMaterial.ratio;
const brightnessRatio = Math.max( ...surface );
if ( rwxMaterial.texture == null ) {
threeMat.color.set( rwxMaterial.getColorHexValue() );
threeMat.color.multiplyScalar( brightnessRatio );
} else {
threeMat.color.set( rwxMaterial.tint ? rwxMaterial.getColorHexValue() : 0xffffff );
threeMat.color.multiplyScalar( brightnessRatio );
applyTextureToMat( threeMat, folder, rwxMaterial.texture, textureExtension, rwxMaterial.mask,
maskExtension, rwxMaterial.specular, rwxMaterial.normal, fflate, loadingPromises, textureColorSpace, textureWrapping, alphaTest, textureFiltering,
allowURLs );
}
threeMat.needsUpdate = true;
return {
threeMat: threeMat,
loadingPromises: loadingPromises,
};
}
function clearGeometry( ctx ) {
if ( ctx.currentBufferFaceCount > 0 ) {
commitBufferGeometryGroup( ctx );
}
ctx.currentBufferGeometry = new BufferGeometry();
ctx.currentBufferVertices = [];
ctx.currentBufferUVs = [];
ctx.currentBufferFaces = [];
ctx.currentBufferFaceCount = 0;
ctx.currentBufferGroupFirstFaceID = 0;
ctx.previousMaterialID = null;
}
function hasCurrentInvalidNormals( ctx, faces ) {
if ( ctx.currentBufferFaces.length > 0 ) {
const tmpBufferGeometry = new BufferGeometry();
const tmpFaces = [];
tmpFaces.push( ...ctx.currentBufferFaces, ...faces );
tmpBufferGeometry.setAttribute( 'position', new BufferAttribute( new Float32Array( ctx.currentBufferVertices ), 3 ) );
tmpBufferGeometry.setIndex( tmpFaces );
tmpBufferGeometry.computeVertexNormals();
// Look for normal value issues
tmpBufferGeometry.computeVertexNormals();
if ( tmpBufferGeometry.hasAttribute( 'normal' ) ) {
const normals = tmpBufferGeometry.getAttribute( 'normal' ).array;
const normalVector = new Vector3();
for ( let i = 0; i < normals.length; i += 3 ) {
// Those vertices are not targetted by the provided faces: skip
if ( ! faces.includes( i / 3 ) ) continue;
normalVector.set( normals[ i ], normals[ i + 1 ], normals[ i + 2 ] );
if ( normalVector.lengthSq() == 0.0 ) {
return true; // ctx.materialTracker.currentRWXMaterial.lightsampling = LightSampling.FACET;
}
}
}
}
return false;
}
function makeMeshToCurrentGroup( ctx ) {
if ( ctx.currentBufferFaceCount > 0 ) {
commitBufferGeometryGroup( ctx );
}
if ( ctx.currentBufferFaces.length > 0 ) {
ctx.currentBufferGeometry.setAttribute( 'position', new BufferAttribute( new Float32Array( ctx.currentBufferVertices ), 3 ) );
ctx.currentBufferGeometry.setAttribute( 'uv', new BufferAttribute( new Float32Array( ctx.currentBufferUVs ), 2 ) );
ctx.currentBufferGeometry.setIndex( ctx.currentBufferFaces );
ctx.currentBufferGeometry.uvsNeedUpdate = true;
ctx.currentBufferGeometry.computeVertexNormals();
ctx.loadingPromises = ctx.loadingPromises.concat( ctx.materialTracker.getCommitedMaterialList().map( res => res.loadingPromises ) );
const mesh = new Mesh( ctx.currentBufferGeometry, ctx.materialTracker.getCommitedMaterialList().map( res => res.threeMat ) );
/* Keep track of tagged materials for this mesh */
mesh.userData.taggedMaterials = ctx.taggedMaterials;
ctx.currentGroup.add( mesh );
clearMaterialTag( ctx );
clearMaterialRatio( ctx );
ctx.taggedMaterials = {};
}
}
function commitBufferGeometryGroup( ctx ) {
// Make new material group out of existing data
if ( ctx.currentBufferFaceCount ) {
ctx.currentBufferGeometry.addGroup( ctx.currentBufferGroupFirstFaceID, ctx.currentBufferFaceCount * 3, ctx.previousMaterialID );
}
ctx.materialTracker.commitMaterials();
// Set everything ready for the next group to start
ctx.previousMaterialID = ctx.materialTracker.getCurrentMaterialID();
ctx.currentBufferGroupFirstFaceID = ctx.currentBufferGroupFirstFaceID + ctx.currentBufferFaceCount * 3;
ctx.currentBufferFaceCount = 0;
}
function addTriangle( ctx, a, b, c ) {
if ( ctx.materialTracker.getCurrentMaterialID() !== ctx.previousMaterialID ) {
commitBufferGeometryGroup( ctx );
}
// Add new face
ctx.currentBufferFaceCount ++;
ctx.currentBufferFaces.push( a, b, c );
}
function addQuad( ctx, a, b, c, d ) {
if ( ctx.materialTracker.getCurrentMaterialID() !== ctx.previousMaterialID ) {
commitBufferGeometryGroup( ctx );
}
if ( true && ctx.materialTracker.currentRWXMaterial.geometrysampling == GeometrySampling.WIREFRAME ) {
// We need to use a whole different geometry logic to handle wireframe quads the way the AW client does:
// by only rendering the outter edges
const tmpBufferGeometry = new BufferGeometry();
tmpBufferGeometry.setAttribute( 'position', new BufferAttribute( new Float32Array( [
ctx.currentBufferVertices[ a * 3 ], ctx.currentBufferVertices[ a * 3 + 1 ], ctx.currentBufferVertices[ a * 3 + 2 ],
ctx.currentBufferVertices[ b * 3 ], ctx.currentBufferVertices[ b * 3 + 1 ], ctx.currentBufferVertices[ b * 3 + 2 ],
ctx.currentBufferVertices[ c * 3 ], ctx.currentBufferVertices[ c * 3 + 1 ], ctx.currentBufferVertices[ c * 3 + 2 ],
ctx.currentBufferVertices[ a * 3 ], ctx.currentBufferVertices[ a * 3 + 1 ], ctx.currentBufferVertices[ a * 3 + 2 ],
ctx.currentBufferVertices[ c * 3 ], ctx.currentBufferVertices[ c * 3 + 1 ], ctx.currentBufferVertices[ c * 3 + 2 ],
ctx.currentBufferVertices[ d * 3 ], ctx.currentBufferVertices[ d * 3 + 1 ], ctx.currentBufferVertices[ d * 3 + 2 ]
] ), 3 ) );
tmpBufferGeometry.computeVertexNormals();
const lines = new LineSegments( new EdgesGeometry( tmpBufferGeometry ),
new LineBasicMaterial( { color: ctx.materialTracker.currentRWXMaterial.getColorHexValue() } ) );
ctx.currentGroup.add( lines );
} else {
// Evaluate which diagonal we should cut with
const vertexA = new Vector3( ctx.currentBufferVertices[ a * 3 ],
ctx.currentBufferVertices[ a * 3 + 1 ],
ctx.currentBufferVertices[ a * 3 + 2 ] );
const vertexB = new Vector3( ctx.currentBufferVertices[ b * 3 ],
ctx.currentBufferVertices[ b * 3 + 1 ],
ctx.currentBufferVertices[ b * 3 + 2 ] );
const vertexC = new Vector3( ctx.currentBufferVertices[ c * 3 ],
ctx.currentBufferVertices[ c * 3 + 1 ],
ctx.currentBufferVertices[ c * 3 + 2 ] );
const vertexD = new Vector3( ctx.currentBufferVertices[ d * 3 ],
ctx.currentBufferVertices[ d * 3 + 1 ],
ctx.currentBufferVertices[ d * 3 + 2 ] );
const cutAC = vertexA.distanceToSquared( vertexC ) > vertexB.distanceToSquared( vertexD );
// Add two new faces
ctx.currentBufferFaceCount += 2;
const faces = cutAC ? [ a, b, c, a, c, d ] : [ a, b, d, b, c, d ];
if ( ctx.correctInvalidNormals && hasCurrentInvalidNormals( ctx, faces ) && ctx.materialTracker.currentRWXMaterial != LightSampling.FACET ) {
// Duplicate vertices to avoid issues
const vertexIds = [];
for ( const id of [ a, b, c, d ] ) {
vertexIds.push( ctx.currentBufferVertices.length / 3 );
ctx.currentBufferVertices.push(
ctx.currentBufferVertices[ id * 3 ],
ctx.currentBufferVertices[ id * 3 + 1 ],
ctx.currentBufferVertices[ id * 3 + 2 ]
);
ctx.currentBufferUVs.push(
ctx.currentBufferUVs[ id * 2 ],
ctx.currentBufferUVs[ id * 2 + 1 ]
);
}
if ( cutAC ) {
ctx.currentBufferFaces.push( vertexIds[ 0 ], vertexIds[ 1 ], vertexIds[ 2 ],
vertexIds[ 0 ], vertexIds[ 2 ], vertexIds[ 3 ] );
} else {
ctx.currentBufferFaces.push( vertexIds[ 0 ], vertexIds[ 1 ], vertexIds[ 3 ],
vertexIds[ 1 ], vertexIds[ 2 ], vertexIds[ 3 ] );
}
ctx.currentBufferGeometry.computeVertexNormals();
} else {
ctx.currentBufferFaces.push( ...faces );
}
}
}
function addPolygon( ctx, indices ) {
// Apparently: polygons should always behave according to the facet light sampling mode (despite being told otherwise).
const previousLightSampling = ctx.materialTracker.currentRWXMaterial.lightsampling;
ctx.materialTracker.currentRWXMaterial.lightsampling = LightSampling.FACET;
if ( ctx.materialTracker.getCurrentMaterialID() !== ctx.previousMaterialID ) {
commitBufferGeometryGroup( ctx );
}
const newFaces =
triangulateFaces( ctx.currentBufferVertices, ctx.currentBufferUVs, indices, ctx.objectName, ctx.forceEarcut, ctx.verboseWarning );
for ( let lf = 0; lf < newFaces.length; lf += 3 ) {
const a = newFaces[ lf ];
const b = newFaces[ lf + 1 ];
const c = newFaces[ lf + 2 ];
// Add new face
ctx.currentBufferFaceCount ++;
ctx.currentBufferFaces.push( a, b, c );
}
ctx.materialTracker.currentRWXMaterial.lightsampling = previousLightSampling;
}
function makeVertexCircle( h, r, n, v = null ) {
if ( n < 3 ) {
throw new Error( 'Need at least 3 sides to make a vertex circle' );
}
let positions = [];
let uvs = [];
let vec = new Vector3();
const deltaRad = Math.PI * 2 / n;
const axis = new Vector3( 0, 1, 0 );
vec.add( new Vector3( r, 0, 0 ) );
for ( let i = 0; i < n; i ++ ) {
positions.push( vec.x, vec.y + h, vec.z );
vec.applyAxisAngle( axis, deltaRad );
if ( v === null ) {
// No reference V value provided for UVs: assuming a circular cutout in the texture
uvs.push( ( Math.cos( deltaRad * i ) + 1 ) / 2, ( Math.sin( deltaRad * i ) + 1 ) / 2 );
} else {
// V value provided: picking UVs along U axis with fixed V
uvs.push( 1 / n * i, v );
}
}
return [ positions, uvs ];
}
function addBlock( ctx, w, h, d ) {
let bufferGeometry = new BufferGeometry();
let material = ctx.materialTracker.getCurrentMaterial().threeMat;
if ( material.flatShading !== undefined && ! material.flatShading ) {
material = material.clone();
material.flatShading = true;
}
// 8 vertices to make a block
const positions = [
- w / 2, h / 2, - d / 2,
w / 2, h / 2, - d / 2,
w / 2, h / 2, d / 2,
- w / 2, h / 2, d / 2,
- w / 2, - h / 2, - d / 2,
w / 2, - h / 2, - d / 2,
w / 2, - h / 2, d / 2,
- w / 2, - h / 2, d / 2
];
const uvs = [
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
1.0, 1.0,
0.0, 1.0,
0.0, 0.0,
1.0, 0.0
];
bufferGeometry.setAttribute( 'position', new BufferAttribute( new Float32Array( positions ), 3 ) );
bufferGeometry.setAttribute( 'uv', new BufferAttribute( new Float32Array( uvs ), 2 ) );
// 6 squared faces to make a block, each made of 2 triangles (so 12 in total)
bufferGeometry.setIndex( [ 0, 3, 1, 1, 3, 2,
0, 4, 3, 3, 4, 7,
3, 6, 2, 3, 7, 6,
6, 7, 5, 5, 7, 4,
1, 5, 0, 0, 5, 4,
2, 5, 1, 6, 5, 2 ] );
// For the sake of having every mesh with the same internal structure,
// we create a geometry group for the material
bufferGeometry.addGroup( 0, 36, 0 );
bufferGeometry.uvsNeedUpdate = true;
bufferGeometry.computeVertexNormals();
let mesh = new Mesh( bufferGeometry, [ material ] );
mesh.userData.taggedMaterials = {};
mesh.applyMatrix4( ctx.currentTransform );
ctx.currentGroup.add( mesh );
}
function addCone( ctx, h, r, nbSides ) {
if ( nbSides < 3 ) {
// Silently skip if the cone doesn't have enough faces on its base
return;
}
let bufferGeometry = new BufferGeometry();
let [ positions, uvs ] = makeVertexCircle( 0, r, nbSides );
// We add the pointy top of the cone
positions.push( 0, h, 0 );
uvs.push( 0.5, 0.5 );
bufferGeometry.setAttribute( 'position', new BufferAttribute( new Float32Array( positions ), 3 ) );
bufferGeometry.setAttribute( 'uv', new BufferAttribute( new Float32Array( uvs ), 2 ) );
let index = [];
// We weave faces across the circle (starting from the pointy top) to make a cone
for ( let i = 0; i < nbSides; i ++ ) {
index.push( nbSides, i, ( i + 1 ) % nbSides );
}
bufferGeometry.setIndex( index );
// For the sake of having every mesh with the same internal structure,
// we create a geometry group for the material
bufferGeometry.addGroup( 0, nbSides * 3, 0 );
bufferGeometry.uvsNeedUpdate = true;
bufferGeometry.computeVertexNormals();
let mesh = new Mesh( bufferGeometry, [ ctx.materialTracker.getCurrentMaterial().threeMat ] );
mesh.userData.taggedMaterials = {};
mesh.applyMatrix4( ctx.currentTransform );
ctx.currentGroup.add( mesh );
}
function addCylinder( ctx, h, br, tr, nbSides ) {
if ( nbSides < 3 ) {
// Silently skip if the cylinder doesn't have enough faces on its base
return;
}
// Bottom vertex circle
let [ positions, uvs ] = makeVertexCircle( 0, br, nbSides, 1.0 );
const topData = makeVertexCircle( h, tr, nbSides, 0.0 );
// Top vertex circle
positions.push( ...topData[ 0 ] );
uvs.push( ...topData[ 1 ] );
let bufferGeometry = new BufferGeometry();
bufferGeometry.setAttribute( 'position', new BufferAttribute( new Float32Array( positions ), 3 ) );
bufferGeometry.setAttribute( 'uv', new BufferAttribute( new Float32Array( uvs ), 2 ) );
const firstTopID = nbSides;
let index = [];
// We weave faces across both circles (up and down) to make a cylinder
for ( let i = 0; i < nbSides; i ++ ) {
index.push( firstTopID + i, i, ( i + 1 ) % nbSides );
index.push( firstTopID + i, ( i + 1 ) % nbSides, firstTopID + ( ( i + 1 ) % nbSides ) );
}
bufferGeometry.setIndex( index );
// For the sake of having every mesh with the same internal structure,
// we create a geometry group for the material
bufferGeometry.addGroup( 0, nbSides * 6, 0 );
bufferGeometry.uvsNeedUpdate = true;
bufferGeometry.computeVertexNormals();
let mesh = new Mesh( bufferGeometry, [ ctx.materialTracker.getCurrentMaterial().threeMat ] );
mesh.userData.taggedMaterials = {};
mesh.applyMatrix4( ctx.currentTransform );
ctx.currentGroup.add( mesh );
}
function addDisc( ctx, h, r, nbSides ) {
if ( nbSides < 3 ) {
// Silently skip if the disc doesn't have enough faces on its base
return;
}
let bufferGeometry = new BufferGeometry();
let [ positions, uvs ] = makeVertexCircle( h, r, nbSides );
bufferGeometry.setAttribute( 'position', new BufferAttribute( new Float32Array( positions ), 3 ) );
bufferGeometry.setAttribute( 'uv', new BufferAttribute( new Float32Array( uvs ), 2 ) );
let index = [];
// We weave faces across all the circle (always using the first point) to make a disc
for ( let i = 1; i < nbSides; i ++ ) {
index.push( 0, i, ( i + 1 ) % nbSides );
}
bufferGeometry.setIndex( index );
// For the sake of having every mesh with the same internal structure,
// we create a geometry group for the material
bufferGeometry.addGroup( 0, nbSides * 3, 0 );
bufferGeometry.uvsNeedUpdate = true;
bufferGeometry.computeVertexNormals();
let mesh = new Mesh( bufferGeometry, [ ctx.materialTracker.getCurrentMaterial().threeMat ] );
mesh.applyMatrix4( ctx.currentTransform );
mesh.userData.taggedMaterials = {};
ctx.currentGroup.add( mesh );
}
function addHemisphere( ctx, r, n ) {
if ( n < 2 ) {
// Silently skip if the hemisphere doesn't have enough density
return;
}
const nbSides = n * 4;
const nbSegments = n;
const deltaRad = Math.PI / ( nbSegments * 2 );
// Bottom vertex circle
let [ positions, uvs ] = makeVertexCircle( 0, r, nbSides, 1.0 );
let previousLevelID = 0;
let currentLevelID = 0;
let index = [];
// Now that we have the base of the hemisphere: we build up from there to the top
for ( let h = 1; h < nbSegments; h ++ ) {
currentLevelID = previousLevelID + nbSides;
const nH = Math.sin( deltaRad * h );
const levelData = makeVertexCircle( nH * r, Math.cos( deltaRad * h ) * r, nbSides, nH );
positions.push( ...levelData[ 0 ] );
uvs.push( ...levelData[ 1 ] );
// We weave faces across both circles (up and down) to make a cylinder
for ( let i = 0; i < nbSides; i ++ ) {
index.push( currentLevelID + i, previousLevelID + i, previousLevelID + ( ( i + 1 ) % nbSides ),
currentLevelID + i, previousLevelID + ( ( i + 1 ) % nbSides ),
currentLevelID + ( ( i + 1 ) % nbSides ) );
}
previousLevelID = currentLevelID;
}
// We add the pointy top of the hemisphere
positions.push( 0, r, 0 );
uvs.push( 0.5, 0.0 );
const topID = positions.length / 3 - 1;
// We weave faces across the circle (starting from the pointy top) to make a cone
for ( let i = 0; i < nbSides; i ++ ) {
index.push( topID, previousLevelID + i, previousLevelID + ( ( i + 1 ) % nbSides ) );
}
let bufferGeometry = new BufferGeometry();
bufferGeometry.setAttribute( 'position', new BufferAttribute( new Float32Array( positions ), 3 ) );
bufferGeometry.setAttribute( 'uv', new BufferAttribute( new Float32Array( uvs ), 2 ) );
bufferGeometry.setIndex( index );
// For the sake of having every mesh with the same internal structure,
// we create a geometry group for the material
bufferGeometry.addGroup( 0, bufferGeometry.getIndex().count, 0 );
bufferGeometry.uvsNeedUpdate = true;
bufferGeometry.computeVertexNormals();
let mesh = new Mesh( bufferGeometry, [ ctx.materialTracker.getCurrentMaterial().threeMat ] );
mesh.userData.taggedMaterials = {};
mesh.applyMatrix4( ctx.currentTransform );
ctx.currentGroup.add( mesh );
}
function addSphere( ctx, r, n ) {
if ( n < 2 ) {
// Silently skip if the sphere doesn't have enough density
return;
}
const nbSides = n * 4;
const nbSegments = n;
const deltaRad = Math.PI / ( nbSegments * 2 );
// We add the pointy bottom of the sphere
const positions = [ 0, - r, 0 ];
const uvs = [ 0.5, 0.0 ];
// Bottom vertex circle (above pointy bottom)
let h = - nbSegments + 1;
const nH = Math.sin( deltaRad * h );
let levelData = makeVertexCircle( nH * r, Math.cos( deltaRad * h ) * r, nbSides, nH );
positions.push( ...levelData[ 0 ] );
uvs.push( ...levelData[ 1 ] );
let previousLevelID = 0;
let currentLevelID = 1;
const index = [];
// We weave faces across the circle (starting from the pointy bottom) to make a cone
for ( let i = 0; i < nbSides; i ++ ) {
index.push( previousLevelID, currentLevelID + ( i + 1 ) % nbSides, currentLevelID + i );
}
previousLevelID = currentLevelID;
// Now that we have the base of the sphere: we build up from there to the top
for ( h ++; h < nbSegments; h ++ ) {
currentLevelID = previousLevelID + nbSides;
const nH = Math.sin( deltaRad * h );
levelData = makeVertexCircle( nH * r, Math.cos( deltaRad * h ) * r, nbSides, nH );
positions.push( ...levelData[ 0 ] );
uvs.push( ...levelData[ 1 ] );
// We weave faces across both circles (up and down) to make a cylinder
for ( let i = 0; i < nbSides; i ++ ) {
index.push( currentLevelID + i, previousLevelID + i, previousLevelID + ( ( i + 1 ) % nbSides ),
currentLevelID + i, previousLevelID + ( ( i + 1 ) % nbSides ),
currentLevelID + ( ( i + 1 ) % nbSides ) );
}
previousLevelID = currentLevelID;
}
// We add the pointy top of the sphere
positions.push( 0, r, 0 );
uvs.push( 0.5, 0.0 );
currentLevelID += nbSides;
// We weave faces across the circle (starting from the pointy top) to make a cone
for ( let i = 0; i < nbSides; i ++ ) {
index.push( currentLevelID, previousLevelID + i, previousLevelID + ( ( i + 1 ) % nbSides ) );
}
let bufferGeometry = new BufferGeometry();
bufferGeometry.setAttribute( 'position', new BufferAttribute( new Float32Array( positions ), 3 ) );
bufferGeometry.setAttribute( 'uv', new BufferAttribute( new Float32Array( uvs ), 2 ) );
bufferGeometry.setIndex( index );
// For the sake of having every mesh with the same internal structure,
// we create a geometry group for the material
bufferGeometry.addGroup( 0, bufferGeometry.getIndex().count, 0 );
bufferGeometry.uvsNeedUpdate = true;
bufferGeometry.computeVertexNormals();
let mesh = new Mesh( bufferGeometry, [ ctx.materialTracker.getCurrentMaterial().threeMat ] );
mesh.userData.taggedMaterials = {};
mesh.applyMatrix4( ctx.currentTransform );
ctx.currentGroup.add( mesh );
}
function pushCurrentGroup( ctx ) {
let group = new Group();
group.userData.rwx = {};
group.applyMatrix4( ctx.currentTransform );
ctx.currentGroup.add( group );
ctx.currentGroup = group;
ctx.currentTransform = new Matrix4();
}
function popCurrentGroup( ctx ) {
ctx.currentTransform = ctx.currentGroup.matrix.clone();
ctx.currentGroup = ctx.currentGroup.parent;
}
function pushCurrentMaterial( ctx ) {
ctx.materialStack.push( ctx.materialTracker.currentRWXMaterial );
ctx.materialTracker.currentRWXMaterial = ctx.materialTracker.currentRWXMaterial.clone();
ctx.materialTracker.clearCurrentMaterialList( ctx.materialTracker.currentRWXMaterial );
}
function popCurrentMaterial( ctx ) {
ctx.materialTracker.currentRWXMaterial = ctx.materialStack.pop();
ctx.materialTracker.clearCurrentMaterialList( ctx.materialTracker.currentRWXMaterial );
}
function saveCurrentTransform( ctx ) {
ctx.transformSaves.push( ctx.currentTransform.clone() );
}
function loadCurrentTransform( ctx ) {
if ( ctx.transformSaves.length > 0 ) {
ctx.currentTransform = ctx.transformSaves.pop();
} else {
ctx.currentTransform = new Matrix4();
}
}
function commitMaterialTag( ctx, tag ) {
ctx.materialTracker.currentRWXMaterial.tag = tag;
if ( ctx.taggedMaterials[ tag.toString() ] === undefined ) {
// If there is no material under that tag yet, we need to initiliaze the entry
// with an empty array
ctx.taggedMaterials[ tag.toString() ] = [];
}
// We need to keep track of the position of the tagged material within the material list
// held by the mesh, we don't have said mesh yet but we already know the position from which
// the material will be accessible, thanks to the material manager, see makeMeshToCurrentGroup(...)
// to see how said mesh is finally defined
if ( ! ctx.taggedMaterials[ tag.toString() ].includes( ctx.materialTracker.getCurrentMaterialID() ) ) {
ctx.taggedMaterials[ tag.toString() ].push( ctx.materialTracker.getCurrentMaterialID() );
}
}
function clearMaterialTag( ctx ) {
ctx.materialTracker.currentRWXMaterial.tag = 0;
}
function setMaterialRatio( ctx, a, b, c, d = null ) {
// The point here is to evaluate the aspect ratio of the surface to write a sign on,
// we first need to list all the information we will need: vertex positions an UVs.
const aPos = new Vector3( ctx.currentBufferVertices[ a * 3 ],
ctx.currentBufferVertices[ a * 3 + 1 ],
ctx.currentBufferVertices[ a * 3 + 2 ] );
const bPos = new Vector3( ctx.currentBufferVertices[ b * 3 ],
ctx.currentBufferVertices[ b * 3 + 1 ],
ctx.currentBufferVertices[ b * 3 + 2 ] );
const cPos = new Vector3( ctx.currentBufferVertices[ c * 3 ],
ctx.currentBufferVertices[ c * 3 + 1 ],
ctx.currentBufferVertices[ c * 3 + 2 ] );
const aUV = new Vector2( ctx.currentBufferUVs[ a * 2 ],
ctx.currentBufferUVs[ a * 2 + 1 ] );
const bUV = new Vector2( ctx.currentBufferUVs[ b * 2 ],
ctx.currentBufferUVs[ b * 2 + 1 ] );
const cUV = new Vector2( ctx.currentBufferUVs[ c * 2 ],
ctx.currentBufferUVs[ c * 2 + 1 ] );
// From there: we compute useful UV boundaries to better evaluate the dimensions later.
const maxU = Math.max( aUV.x, bUV.x, cUV.x );
const maxV = Math.max( aUV.y, bUV.y, cUV.y );
const minU = Math.min( aUV.x, bUV.x, cUV.x );
const minV = Math.min( aUV.y, bUV.y, cUV.y );
const midU = ( maxU + minU ) / 2;
const scaleU = 1 / ( maxU - minU );
const scaleV = 1 / ( maxV - minV );
// We work under the assumption that the triangle we are dealing with has a straight angle,
// but also that the UVs are reasonably aligned with it.
// We first assume that the edge (a) <--> (b) is the hypotenuse and c is said straight angle.
// By virtue of having a straight angle: the longest edge of the triangle cannot be anything
// else than the hypotenuse, so we know how to correct ourselves if need be.
let width = 1;
let height = 1;
let straightAngleVertex = cPos;
let straightAngleUV = cUV;
let hypEnds = [ aPos, bPos ];
let hypEndUVs = [ aUV, bUV ];
let sqrdHyp = aPos.distanceToSquared( bPos );
const acSqrdHyp = aPos.distanceToSquared( cPos );
const bcSqrdHyp = bPos.distanceToSquared( cPos );
// If (a) <--> (c) is longer than (a) <--> (b): the latter is no longer our best guest,
// for the hypotenuse, so (a) <--> (c) is now considered to be the hypotenuse and (b) is the
// new straight angle.
if ( acSqrdHyp > sqrdHyp ) {
sqrdHyp = acSqrdHyp;
straightAngleVertex = bPos;
hypEnds = [ aPos, cPos ];
straightAngleUV = bUV;
hypEndUVs = [ aUV, cUV ];
}
// If (b) <--> (c) is longer than (a) <--> (c): the latter is no longer our best guest,
// for the hypotenuse, so (b) <--> (c) is now considered to be the hypotenuse and (a) is the
// new straight angle.
if ( bcSqrdHyp > sqrdHyp ) {
sqrdHyp = bcSqrdHyp;
straightAngleVertex = aPos;
hypEnds = [ bPos, cPos ];
straightAngleUV = aUV;
hypEndUVs = [ bUV, cUV ];
}
// At this stage: we have a better idea of what the triangle looks like, but we still
// need to determine how flipped it is in regards to the UV canvas.
// We can determine which other vertices to use respectively for width and height by peeking at
// the UV coordinates of one of them, the trick is that a vertex here can only be the furthest
// away on U (width) or the furthest away on V (height) but not both at the same time.
if ( straightAngleUV.x < midU ) {
// Straight angle is on the left side
if ( hypEndUVs[ 0 ].x > midU ) {
// First end of the hypotenuse gives the width as it holds the furthest U value,
// the other end gives the height as it (by deduction) holds the furthest V value
width = straightAngleVertex.distanceTo( hypEnds[ 0 ] );
height = straightAngleVertex.distanceTo( hypEnds[ 1 ] );
} else {
// The other way around here
width = straightAngleVertex.distanceTo( hypEnds[ 1 ] );
height = straightAngleVertex.distanceTo( hypEnds[ 0 ] );
}
}
if ( straightAngleUV.x > midU ) {
// Straight angle is on the right side
if ( hypEndUVs[ 0 ].x < midU ) {
width = straightAngleVertex.distanceTo( hypEnds[ 0 ] );
height = straightAngleVertex.distanceTo( hypEnds[ 1 ] );
} else {
width = straightAngleVertex.distanceTo( hypEnds[ 1 ] );
height = straightAngleVertex.distanceTo( hypEnds[ 0 ] );
}
}
// The width and height values still need to be scaled to match the full UV canvas size
const ratio = ( width * scaleU ) / ( height * scaleV );
ctx.materialTracker.currentRWXMaterial.ratio = ratio;
// To avoid generating multiple (unmatching) materials in case the ratios we get accross
// quads/triangles of a single surface were to differ: we check if a previous ratio was
// hinted at us, if it falls relatively close to the newly computed ratio: we take the
// hint instead.
if ( d === null ) {
if ( ctx.triangleRatioHint === null ) {
ctx.triangleRatioHint = ratio;
} else {
ctx.materialTracker.currentRWXMaterial.ratio = ctx.triangleRatioHint;
}
} else {
if ( ctx.quadRatioHint === null ) {
ctx.quadRatioHint = ratio;
} else {
ctx.materialTracker.currentRWXMaterial.ratio = ctx.quadRatioHint;
}
}
}
function clearMaterialRatio( ctx ) {
ctx.materialTracker.currentRWXMaterial.ratio = 1.0;
}
// Utility function to merge all group and subgroup geometries into on single buffer, all while taking materials into account
function mergeGeometryRecursive( group, ctx, transform = group.matrix ) {
group.children.forEach( ( child ) => {
let localTransform = new Matrix4();
localTransform.copy( transform );
localTransform.multiply( child.matrix );
if ( child instanceof Mesh && ctx.meshFilter( child ) ) {
// We first need to set up the new BufferGeometry groups
let geometryGroups = [];
const geometryIndices = child.geometry.getIndex().array;
if ( typeof child.material[ Symbol.iterator ] === 'function' ) {
// There's likely multiple materials to deal with, so we fetch the original geometry groups
child.geometry.groups.forEach( ( g ) => {
// Each group in the original geometry from the child needs to be exported,
// we take into account the already-registered geometry and materials
// from the context, so that we can compute offsets and match the
// final layout of the mesh (and final material IDs as well)
geometryGroups.push( {
start: g.start + ctx.indices.length,
count: g.count,
materialIndex: g.materialIndex + ctx.materials.length
} );
} );
// Adjust user data for tagged materials, all indices must also be offset
const taggedMaterials = child.userData.taggedMaterials;
if ( taggedMaterials !== undefined ) {
for ( const [ tag, ids ] of Object.entries( taggedMaterials ) ) {
if ( ctx.taggedMaterials[ tag ] === undefined ) {
ctx.taggedMaterials[ tag ] = [];
}
ids.forEach( ( id ) => {
ctx.taggedMaterials[ tag ].push( id + ctx.materials.length );
} );
}
}
// Add the materials from the child to the final material list
ctx.materials.push( ...child.material );
} else {
// There's only one single material, but it will still need its own geometry group
// in the final mesh
geometryGroups.push( {
start: ctx.indices.length,
count: geometryIndices.length,
materialIndex: ctx.materials.length
} );
ctx.materials.push( child.material );
}
const originalVertices = child.geometry.getAttribute( 'position' ).array;
const vertexOffset = ctx.positions.length / 3;
// Import the current geometry (vertices and faces) from the child into the final buffer,
// apply local transformations if any
let i = 0;
for ( let l = originalVertices.length / 3; i < l; i ++ ) {
let tmpVertex = new Vector4( originalVertices[ i * 3 ], originalVertices[ i * 3 + 1 ], originalVertices[ i * 3 + 2 ] );
tmpVertex.applyMatrix4( localTransform );
ctx.positions.push( tmpVertex.x );
ctx.positions.push( tmpVertex.y );
ctx.positions.push( tmpVertex.z );
}
// Do not forget the UVs either
if ( child.geometry.getAttribute( 'uv' ) === undefined ) {
const uvs = new Array( i * 2 ).fill( 0.0 );
ctx.uvs.push( ...uvs );
} else {
ctx.uvs.push( ...child.geometry.getAttribute( 'uv' ).array );
}
ctx.indices.push( ...geometryIndices.map( ( value ) => {
return value + vertexOffset;
} ) );
// Since the new BufferGeometry groups are all set, we can import them into the
// final buffer geometry
geometryGroups.forEach( ( g ) => {
ctx.bufferGeometry.addGroup( g.start, g.count, g.materialIndex );
} );
} else if ( child instanceof Group ) {
/* Recursive case */
mergeGeometryRecursive( child, ctx, localTransform );
}
} );
}
function flattenGroup( group, filter = () => true ) {
let ctx = {
bufferGeometry: new BufferGeometry(),
positions: [],
uvs: [],
indices: [],
materials: [],
taggedMaterials: {},
meshFilter: filter
};
mergeGeometryRecursive( group, ctx );
/* Ready data for final BufferGeometry */
ctx.bufferGeometry.setAttribute( 'position', new BufferAttribute( new Float32Array( ctx.positions ), 3 ) );
ctx.bufferGeometry.setAttribute( 'uv', new BufferAttribute( new Float32Array( ctx.uvs ), 2 ) );
ctx.bufferGeometry.setIndex( ctx.indices );
ctx.bufferGeometry.uvsNeedUpdate = true;
ctx.bufferGeometry.computeVertexNormals();
let finalMesh = new Mesh( ctx.bufferGeometry, ctx.materials );
finalMesh.userData.rwx = group.userData.rwx;
finalMesh.userData.taggedMaterials = ctx.taggedMaterials;
return finalMesh;
}
class RWXMaterial {
constructor() {
// Material related properties start here
this.color = [ 0.0, 0.0, 0.0 ]; // Red, Green, Blue
this.surface = defaultSurface.slice( 0, 3 );
this.opacity = 1.0;
this.lightsampling = LightSampling.FACET;
this.geometrysampling = GeometrySampling.SOLID;
this.texturemodes = [
TextureMode.LIT,
TextureMode.FORESHORTEN,
TextureMode.FILTER,
]; // There's possibly more than one mode enabled at a time (hence why we use an array)
this.materialmode = MaterialMode.NULL; // Neither NONE nor DOUBLE: we only render one side of the polygon
this.texture = null;
this.tint = false;
this.mask = null;
this.normalMap = null;
this.specularMap = null;
this.textureaddressmode = TextureAddressMode.WRAP;
this.collision = true;
// End of material related properties
this.tag = 0;
this.ratio = 1.0;
}
// Make a deep copy of the RWX material instance
clone() {
let cloned = Object.assign( Object.create( Object.getPrototypeOf( this ) ),
this );
cloned.color = [ ...this.color ];
cloned.surface = [ ...this.surface ];
cloned.texturemodes = [ ...this.texturemodes ];
return cloned;
}
getColorHexValue() {
return ( Math.trunc( this.color[ 0 ] * 255 ) << 16 ) + ( Math.trunc( this
.color[ 1 ] * 255 ) << 8 ) + Math.trunc( this.color[ 2 ] * 255 );
}
getMatSignature() {
const color = this.color[ 0 ].toFixed( 3 ) + this.color[ 1 ].toFixed( 3 ) + this.color[ 2 ].toFixed( 3 );
const surface = this.surface[ 0 ].toFixed( 3 ) + this.surface[ 1 ].toFixed( 3 ) + this.surface[ 2 ].toFixed( 3 );
const opacity = this.opacity.toFixed( 3 );
const lightSampling = this.lightsampling.toString();
const geometrySampling = this.geometrysampling.toString();
let textureMode = '';
this.texturemodes.forEach( ( tm ) => {
textureMode += tm.toString();
} );
const materialMode = this.materialmode.toString();
const texture = this.texture === null ? '' : this.texture;
const mask = this.mask === null ? '' : this.mask;
const normal = this.normalMap === null ? '' : this.normalMap;
const specular = this.specularMap === null ? '' : this.specularMap;
const textureAddressMode = this.textureaddressmode.toString();
const collision = this.collision.toString();
const tag = this.tag.toString();
const ratio = this.ratio.toFixed( 2 );
return `${color}_${surface}_${opacity}_${lightSampling}_${geometrySampling}_${textureMode}_${materialMode}` +
`_${texture}_${this.tint}_${mask}_${specular}_${normal}_${textureAddressMode}_${collision}_${tag}_${ratio}`;
}
}
// Take RWXMaterials as input, convert them to three.js materials
class RWXMaterialManager {
constructor( folder, textureExtension = '.jpg', maskExtension =
'.zip', fflate = null, useBasicMaterial = false,
textureColorSpace = SRGBColorSpace, alphaTest = defaultAlphaTest,
allowURLs = false ) {
this.threeMaterialMap = new Map();
this.folder = folder;
this.textureExtension = textureExtension;
this.maskExtension = maskExtension;
this.fflate = fflate;
this.useBasicMaterial = useBasicMaterial;
this.textureColorSpace = textureColorSpace;
this.alphaTest = alphaTest;
this.allowURLs = allowURLs;
}
addRWXMaterial( newRWXMaterial, signature = null ) {
// If no custom signature is provided: we use the one from the material itself
const finalSignature = signature || newRWXMaterial.getMatSignature();
const threeMaterial = makeThreeMaterial( newRWXMaterial,
this.folder, this.textureExtension, this.maskExtension, this.fflate,
this.useBasicMaterial, this.textureColorSpace, this.alphaTest,
this.allowURLs );
threeMaterial.signature = finalSignature;
this.threeMaterialMap.set( finalSignature, threeMaterial );
}
hasThreeMaterialPack( signature ) {
return this.threeMaterialMap.has( signature );
}
getThreeMaterialPack( signature ) {
return this.threeMaterialMap.get( signature );
}
removeThreeM