first commit

This commit is contained in:
2025-08-19 10:19:29 +08:00
commit 3b61e84a7f
3014 changed files with 2640574 additions and 0 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,521 @@
import {
BufferGeometry,
Color,
FileLoader,
Float32BufferAttribute,
Group,
Loader,
Mesh,
MeshPhongMaterial
} from 'three';
import * as fflate from '../libs/fflate.module.js';
/**
* Description: Early release of an AMF Loader following the pattern of the
* example loaders in the three.js project.
*
* Usage:
* const loader = new AMFLoader();
* loader.load('/path/to/project.amf', function(objecttree) {
* scene.add(objecttree);
* });
*
* Materials now supported, material colors supported
* Zip support, requires fflate
* No constellation support (yet)!
*
*/
class AMFLoader extends Loader {
constructor( manager ) {
super( manager );
}
load( url, onLoad, onProgress, onError ) {
const scope = this;
const loader = new FileLoader( scope.manager );
loader.setPath( scope.path );
loader.setResponseType( 'arraybuffer' );
loader.setRequestHeader( scope.requestHeader );
loader.setWithCredentials( scope.withCredentials );
loader.load( url, function ( text ) {
try {
onLoad( scope.parse( text ) );
} catch ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
scope.manager.itemError( url );
}
}, onProgress, onError );
}
parse( data ) {
function loadDocument( data ) {
let view = new DataView( data );
const magic = String.fromCharCode( view.getUint8( 0 ), view.getUint8( 1 ) );
if ( magic === 'PK' ) {
let zip = null;
let file = null;
console.log( 'THREE.AMFLoader: Loading Zip' );
try {
zip = fflate.unzipSync( new Uint8Array( data ) );
} catch ( e ) {
if ( e instanceof ReferenceError ) {
console.log( 'THREE.AMFLoader: fflate missing and file is compressed.' );
return null;
}
}
for ( file in zip ) {
if ( file.toLowerCase().slice( - 4 ) === '.amf' ) {
break;
}
}
console.log( 'THREE.AMFLoader: Trying to load file asset: ' + file );
view = new DataView( zip[ file ].buffer );
}
const fileText = new TextDecoder().decode( view );
const xmlData = new DOMParser().parseFromString( fileText, 'application/xml' );
if ( xmlData.documentElement.nodeName.toLowerCase() !== 'amf' ) {
console.log( 'THREE.AMFLoader: Error loading AMF - no AMF document found.' );
return null;
}
return xmlData;
}
function loadDocumentScale( node ) {
let scale = 1.0;
let unit = 'millimeter';
if ( node.documentElement.attributes.unit !== undefined ) {
unit = node.documentElement.attributes.unit.value.toLowerCase();
}
const scaleUnits = {
millimeter: 1.0,
inch: 25.4,
feet: 304.8,
meter: 1000.0,
micron: 0.001
};
if ( scaleUnits[ unit ] !== undefined ) {
scale = scaleUnits[ unit ];
}
console.log( 'THREE.AMFLoader: Unit scale: ' + scale );
return scale;
}
function loadMaterials( node ) {
let matName = 'AMF Material';
const matId = node.attributes.id.textContent;
let color = { r: 1.0, g: 1.0, b: 1.0, a: 1.0 };
let loadedMaterial = null;
for ( let i = 0; i < node.childNodes.length; i ++ ) {
const matChildEl = node.childNodes[ i ];
if ( matChildEl.nodeName === 'metadata' && matChildEl.attributes.type !== undefined ) {
if ( matChildEl.attributes.type.value === 'name' ) {
matName = matChildEl.textContent;
}
} else if ( matChildEl.nodeName === 'color' ) {
color = loadColor( matChildEl );
}
}
loadedMaterial = new MeshPhongMaterial( {
flatShading: true,
color: new Color( color.r, color.g, color.b ),
name: matName
} );
if ( color.a !== 1.0 ) {
loadedMaterial.transparent = true;
loadedMaterial.opacity = color.a;
}
return { id: matId, material: loadedMaterial };
}
function loadColor( node ) {
const color = { r: 1.0, g: 1.0, b: 1.0, a: 1.0 };
for ( let i = 0; i < node.childNodes.length; i ++ ) {
const matColor = node.childNodes[ i ];
if ( matColor.nodeName === 'r' ) {
color.r = matColor.textContent;
} else if ( matColor.nodeName === 'g' ) {
color.g = matColor.textContent;
} else if ( matColor.nodeName === 'b' ) {
color.b = matColor.textContent;
} else if ( matColor.nodeName === 'a' ) {
color.a = matColor.textContent;
}
}
return color;
}
function loadMeshVolume( node ) {
const volume = { name: '', triangles: [], materialid: null };
let currVolumeNode = node.firstElementChild;
if ( node.attributes.materialid !== undefined ) {
volume.materialId = node.attributes.materialid.nodeValue;
}
while ( currVolumeNode ) {
if ( currVolumeNode.nodeName === 'metadata' ) {
if ( currVolumeNode.attributes.type !== undefined ) {
if ( currVolumeNode.attributes.type.value === 'name' ) {
volume.name = currVolumeNode.textContent;
}
}
} else if ( currVolumeNode.nodeName === 'triangle' ) {
const v1 = currVolumeNode.getElementsByTagName( 'v1' )[ 0 ].textContent;
const v2 = currVolumeNode.getElementsByTagName( 'v2' )[ 0 ].textContent;
const v3 = currVolumeNode.getElementsByTagName( 'v3' )[ 0 ].textContent;
volume.triangles.push( v1, v2, v3 );
}
currVolumeNode = currVolumeNode.nextElementSibling;
}
return volume;
}
function loadMeshVertices( node ) {
const vertArray = [];
const normalArray = [];
let currVerticesNode = node.firstElementChild;
while ( currVerticesNode ) {
if ( currVerticesNode.nodeName === 'vertex' ) {
let vNode = currVerticesNode.firstElementChild;
while ( vNode ) {
if ( vNode.nodeName === 'coordinates' ) {
const x = vNode.getElementsByTagName( 'x' )[ 0 ].textContent;
const y = vNode.getElementsByTagName( 'y' )[ 0 ].textContent;
const z = vNode.getElementsByTagName( 'z' )[ 0 ].textContent;
vertArray.push( x, y, z );
} else if ( vNode.nodeName === 'normal' ) {
const nx = vNode.getElementsByTagName( 'nx' )[ 0 ].textContent;
const ny = vNode.getElementsByTagName( 'ny' )[ 0 ].textContent;
const nz = vNode.getElementsByTagName( 'nz' )[ 0 ].textContent;
normalArray.push( nx, ny, nz );
}
vNode = vNode.nextElementSibling;
}
}
currVerticesNode = currVerticesNode.nextElementSibling;
}
return { 'vertices': vertArray, 'normals': normalArray };
}
function loadObject( node ) {
const objId = node.attributes.id.textContent;
const loadedObject = { name: 'amfobject', meshes: [] };
let currColor = null;
let currObjNode = node.firstElementChild;
while ( currObjNode ) {
if ( currObjNode.nodeName === 'metadata' ) {
if ( currObjNode.attributes.type !== undefined ) {
if ( currObjNode.attributes.type.value === 'name' ) {
loadedObject.name = currObjNode.textContent;
}
}
} else if ( currObjNode.nodeName === 'color' ) {
currColor = loadColor( currObjNode );
} else if ( currObjNode.nodeName === 'mesh' ) {
let currMeshNode = currObjNode.firstElementChild;
const mesh = { vertices: [], normals: [], volumes: [], color: currColor };
while ( currMeshNode ) {
if ( currMeshNode.nodeName === 'vertices' ) {
const loadedVertices = loadMeshVertices( currMeshNode );
mesh.normals = mesh.normals.concat( loadedVertices.normals );
mesh.vertices = mesh.vertices.concat( loadedVertices.vertices );
} else if ( currMeshNode.nodeName === 'volume' ) {
mesh.volumes.push( loadMeshVolume( currMeshNode ) );
}
currMeshNode = currMeshNode.nextElementSibling;
}
loadedObject.meshes.push( mesh );
}
currObjNode = currObjNode.nextElementSibling;
}
return { 'id': objId, 'obj': loadedObject };
}
const xmlData = loadDocument( data );
let amfName = '';
let amfAuthor = '';
const amfScale = loadDocumentScale( xmlData );
const amfMaterials = {};
const amfObjects = {};
const childNodes = xmlData.documentElement.childNodes;
let i, j;
for ( i = 0; i < childNodes.length; i ++ ) {
const child = childNodes[ i ];
if ( child.nodeName === 'metadata' ) {
if ( child.attributes.type !== undefined ) {
if ( child.attributes.type.value === 'name' ) {
amfName = child.textContent;
} else if ( child.attributes.type.value === 'author' ) {
amfAuthor = child.textContent;
}
}
} else if ( child.nodeName === 'material' ) {
const loadedMaterial = loadMaterials( child );
amfMaterials[ loadedMaterial.id ] = loadedMaterial.material;
} else if ( child.nodeName === 'object' ) {
const loadedObject = loadObject( child );
amfObjects[ loadedObject.id ] = loadedObject.obj;
}
}
const sceneObject = new Group();
const defaultMaterial = new MeshPhongMaterial( {
name: Loader.DEFAULT_MATERIAL_NAME,
color: 0xaaaaff,
flatShading: true
} );
sceneObject.name = amfName;
sceneObject.userData.author = amfAuthor;
sceneObject.userData.loader = 'AMF';
for ( const id in amfObjects ) {
const part = amfObjects[ id ];
const meshes = part.meshes;
const newObject = new Group();
newObject.name = part.name || '';
for ( i = 0; i < meshes.length; i ++ ) {
let objDefaultMaterial = defaultMaterial;
const mesh = meshes[ i ];
const vertices = new Float32BufferAttribute( mesh.vertices, 3 );
let normals = null;
if ( mesh.normals.length ) {
normals = new Float32BufferAttribute( mesh.normals, 3 );
}
if ( mesh.color ) {
const color = mesh.color;
objDefaultMaterial = defaultMaterial.clone();
objDefaultMaterial.color = new Color( color.r, color.g, color.b );
if ( color.a !== 1.0 ) {
objDefaultMaterial.transparent = true;
objDefaultMaterial.opacity = color.a;
}
}
const volumes = mesh.volumes;
for ( j = 0; j < volumes.length; j ++ ) {
const volume = volumes[ j ];
const newGeometry = new BufferGeometry();
let material = objDefaultMaterial;
newGeometry.setIndex( volume.triangles );
newGeometry.setAttribute( 'position', vertices.clone() );
if ( normals ) {
newGeometry.setAttribute( 'normal', normals.clone() );
}
if ( amfMaterials[ volume.materialId ] !== undefined ) {
material = amfMaterials[ volume.materialId ];
}
newGeometry.scale( amfScale, amfScale, amfScale );
newObject.add( new Mesh( newGeometry, material.clone() ) );
}
}
sceneObject.add( newObject );
}
return sceneObject;
}
}
export { AMFLoader };

View File

@ -0,0 +1,437 @@
import {
AnimationClip,
Bone,
FileLoader,
Loader,
Quaternion,
QuaternionKeyframeTrack,
Skeleton,
Vector3,
VectorKeyframeTrack
} from 'three';
/**
* Description: reads BVH files and outputs a single Skeleton and an AnimationClip
*
* Currently only supports bvh files containing a single root.
*
*/
class BVHLoader extends Loader {
constructor( manager ) {
super( manager );
this.animateBonePositions = true;
this.animateBoneRotations = true;
}
load( url, onLoad, onProgress, onError ) {
const scope = this;
const loader = new FileLoader( scope.manager );
loader.setPath( scope.path );
loader.setRequestHeader( scope.requestHeader );
loader.setWithCredentials( scope.withCredentials );
loader.load( url, function ( text ) {
try {
onLoad( scope.parse( text ) );
} catch ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
scope.manager.itemError( url );
}
}, onProgress, onError );
}
parse( text ) {
/*
reads a string array (lines) from a BVH file
and outputs a skeleton structure including motion data
returns thee root node:
{ name: '', channels: [], children: [] }
*/
function readBvh( lines ) {
// read model structure
if ( nextLine( lines ) !== 'HIERARCHY' ) {
console.error( 'THREE.BVHLoader: HIERARCHY expected.' );
}
const list = []; // collects flat array of all bones
const root = readNode( lines, nextLine( lines ), list );
// read motion data
if ( nextLine( lines ) !== 'MOTION' ) {
console.error( 'THREE.BVHLoader: MOTION expected.' );
}
// number of frames
let tokens = nextLine( lines ).split( /[\s]+/ );
const numFrames = parseInt( tokens[ 1 ] );
if ( isNaN( numFrames ) ) {
console.error( 'THREE.BVHLoader: Failed to read number of frames.' );
}
// frame time
tokens = nextLine( lines ).split( /[\s]+/ );
const frameTime = parseFloat( tokens[ 2 ] );
if ( isNaN( frameTime ) ) {
console.error( 'THREE.BVHLoader: Failed to read frame time.' );
}
// read frame data line by line
for ( let i = 0; i < numFrames; i ++ ) {
tokens = nextLine( lines ).split( /[\s]+/ );
readFrameData( tokens, i * frameTime, root );
}
return list;
}
/*
Recursively reads data from a single frame into the bone hierarchy.
The passed bone hierarchy has to be structured in the same order as the BVH file.
keyframe data is stored in bone.frames.
- data: splitted string array (frame values), values are shift()ed so
this should be empty after parsing the whole hierarchy.
- frameTime: playback time for this keyframe.
- bone: the bone to read frame data from.
*/
function readFrameData( data, frameTime, bone ) {
// end sites have no motion data
if ( bone.type === 'ENDSITE' ) return;
// add keyframe
const keyframe = {
time: frameTime,
position: new Vector3(),
rotation: new Quaternion()
};
bone.frames.push( keyframe );
const quat = new Quaternion();
const vx = new Vector3( 1, 0, 0 );
const vy = new Vector3( 0, 1, 0 );
const vz = new Vector3( 0, 0, 1 );
// parse values for each channel in node
for ( let i = 0; i < bone.channels.length; i ++ ) {
switch ( bone.channels[ i ] ) {
case 'Xposition':
keyframe.position.x = parseFloat( data.shift().trim() );
break;
case 'Yposition':
keyframe.position.y = parseFloat( data.shift().trim() );
break;
case 'Zposition':
keyframe.position.z = parseFloat( data.shift().trim() );
break;
case 'Xrotation':
quat.setFromAxisAngle( vx, parseFloat( data.shift().trim() ) * Math.PI / 180 );
keyframe.rotation.multiply( quat );
break;
case 'Yrotation':
quat.setFromAxisAngle( vy, parseFloat( data.shift().trim() ) * Math.PI / 180 );
keyframe.rotation.multiply( quat );
break;
case 'Zrotation':
quat.setFromAxisAngle( vz, parseFloat( data.shift().trim() ) * Math.PI / 180 );
keyframe.rotation.multiply( quat );
break;
default:
console.warn( 'THREE.BVHLoader: Invalid channel type.' );
}
}
// parse child nodes
for ( let i = 0; i < bone.children.length; i ++ ) {
readFrameData( data, frameTime, bone.children[ i ] );
}
}
/*
Recursively parses the HIERACHY section of the BVH file
- lines: all lines of the file. lines are consumed as we go along.
- firstline: line containing the node type and name e.g. 'JOINT hip'
- list: collects a flat list of nodes
returns: a BVH node including children
*/
function readNode( lines, firstline, list ) {
const node = { name: '', type: '', frames: [] };
list.push( node );
// parse node type and name
let tokens = firstline.split( /[\s]+/ );
if ( tokens[ 0 ].toUpperCase() === 'END' && tokens[ 1 ].toUpperCase() === 'SITE' ) {
node.type = 'ENDSITE';
node.name = 'ENDSITE'; // bvh end sites have no name
} else {
node.name = tokens[ 1 ];
node.type = tokens[ 0 ].toUpperCase();
}
if ( nextLine( lines ) !== '{' ) {
console.error( 'THREE.BVHLoader: Expected opening { after type & name' );
}
// parse OFFSET
tokens = nextLine( lines ).split( /[\s]+/ );
if ( tokens[ 0 ] !== 'OFFSET' ) {
console.error( 'THREE.BVHLoader: Expected OFFSET but got: ' + tokens[ 0 ] );
}
if ( tokens.length !== 4 ) {
console.error( 'THREE.BVHLoader: Invalid number of values for OFFSET.' );
}
const offset = new Vector3(
parseFloat( tokens[ 1 ] ),
parseFloat( tokens[ 2 ] ),
parseFloat( tokens[ 3 ] )
);
if ( isNaN( offset.x ) || isNaN( offset.y ) || isNaN( offset.z ) ) {
console.error( 'THREE.BVHLoader: Invalid values of OFFSET.' );
}
node.offset = offset;
// parse CHANNELS definitions
if ( node.type !== 'ENDSITE' ) {
tokens = nextLine( lines ).split( /[\s]+/ );
if ( tokens[ 0 ] !== 'CHANNELS' ) {
console.error( 'THREE.BVHLoader: Expected CHANNELS definition.' );
}
const numChannels = parseInt( tokens[ 1 ] );
node.channels = tokens.splice( 2, numChannels );
node.children = [];
}
// read children
while ( true ) {
const line = nextLine( lines );
if ( line === '}' ) {
return node;
} else {
node.children.push( readNode( lines, line, list ) );
}
}
}
/*
recursively converts the internal bvh node structure to a Bone hierarchy
source: the bvh root node
list: pass an empty array, collects a flat list of all converted THREE.Bones
returns the root Bone
*/
function toTHREEBone( source, list ) {
const bone = new Bone();
list.push( bone );
bone.position.add( source.offset );
bone.name = source.name;
if ( source.type !== 'ENDSITE' ) {
for ( let i = 0; i < source.children.length; i ++ ) {
bone.add( toTHREEBone( source.children[ i ], list ) );
}
}
return bone;
}
/*
builds a AnimationClip from the keyframe data saved in each bone.
bone: bvh root node
returns: a AnimationClip containing position and quaternion tracks
*/
function toTHREEAnimation( bones ) {
const tracks = [];
// create a position and quaternion animation track for each node
for ( let i = 0; i < bones.length; i ++ ) {
const bone = bones[ i ];
if ( bone.type === 'ENDSITE' )
continue;
// track data
const times = [];
const positions = [];
const rotations = [];
for ( let j = 0; j < bone.frames.length; j ++ ) {
const frame = bone.frames[ j ];
times.push( frame.time );
// the animation system animates the position property,
// so we have to add the joint offset to all values
positions.push( frame.position.x + bone.offset.x );
positions.push( frame.position.y + bone.offset.y );
positions.push( frame.position.z + bone.offset.z );
rotations.push( frame.rotation.x );
rotations.push( frame.rotation.y );
rotations.push( frame.rotation.z );
rotations.push( frame.rotation.w );
}
if ( scope.animateBonePositions ) {
tracks.push( new VectorKeyframeTrack( bone.name + '.position', times, positions ) );
}
if ( scope.animateBoneRotations ) {
tracks.push( new QuaternionKeyframeTrack( bone.name + '.quaternion', times, rotations ) );
}
}
return new AnimationClip( 'animation', - 1, tracks );
}
/*
returns the next non-empty line in lines
*/
function nextLine( lines ) {
let line;
// skip empty lines
while ( ( line = lines.shift().trim() ).length === 0 ) { }
return line;
}
const scope = this;
const lines = text.split( /[\r\n]+/g );
const bones = readBvh( lines );
const threeBones = [];
toTHREEBone( bones[ 0 ], threeBones );
const threeClip = toTHREEAnimation( bones );
return {
skeleton: new Skeleton( threeBones ),
clip: threeClip
};
}
}
export { BVHLoader };

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,318 @@
import {
CompressedTextureLoader,
RGBAFormat,
RGBA_S3TC_DXT3_Format,
RGBA_S3TC_DXT5_Format,
RGB_ETC1_Format,
RGB_S3TC_DXT1_Format,
RGB_BPTC_SIGNED_Format,
RGB_BPTC_UNSIGNED_Format
} from 'three';
class DDSLoader extends CompressedTextureLoader {
constructor( manager ) {
super( manager );
}
parse( buffer, loadMipmaps ) {
const dds = { mipmaps: [], width: 0, height: 0, format: null, mipmapCount: 1 };
// Adapted from @toji's DDS utils
// https://github.com/toji/webgl-texture-utils/blob/master/texture-util/dds.js
// All values and structures referenced from:
// http://msdn.microsoft.com/en-us/library/bb943991.aspx/
const DDS_MAGIC = 0x20534444;
// const DDSD_CAPS = 0x1;
// const DDSD_HEIGHT = 0x2;
// const DDSD_WIDTH = 0x4;
// const DDSD_PITCH = 0x8;
// const DDSD_PIXELFORMAT = 0x1000;
const DDSD_MIPMAPCOUNT = 0x20000;
// const DDSD_LINEARSIZE = 0x80000;
// const DDSD_DEPTH = 0x800000;
// const DDSCAPS_COMPLEX = 0x8;
// const DDSCAPS_MIPMAP = 0x400000;
// const DDSCAPS_TEXTURE = 0x1000;
const DDSCAPS2_CUBEMAP = 0x200;
const DDSCAPS2_CUBEMAP_POSITIVEX = 0x400;
const DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800;
const DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000;
const DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000;
const DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000;
const DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000;
// const DDSCAPS2_VOLUME = 0x200000;
// const DDPF_ALPHAPIXELS = 0x1;
// const DDPF_ALPHA = 0x2;
// const DDPF_FOURCC = 0x4;
// const DDPF_RGB = 0x40;
// const DDPF_YUV = 0x200;
// const DDPF_LUMINANCE = 0x20000;
const DXGI_FORMAT_BC6H_UF16 = 95;
const DXGI_FORMAT_BC6H_SF16 = 96;
function fourCCToInt32( value ) {
return value.charCodeAt( 0 ) +
( value.charCodeAt( 1 ) << 8 ) +
( value.charCodeAt( 2 ) << 16 ) +
( value.charCodeAt( 3 ) << 24 );
}
function int32ToFourCC( value ) {
return String.fromCharCode(
value & 0xff,
( value >> 8 ) & 0xff,
( value >> 16 ) & 0xff,
( value >> 24 ) & 0xff
);
}
function loadARGBMip( buffer, dataOffset, width, height ) {
const dataLength = width * height * 4;
const srcBuffer = new Uint8Array( buffer, dataOffset, dataLength );
const byteArray = new Uint8Array( dataLength );
let dst = 0;
let src = 0;
for ( let y = 0; y < height; y ++ ) {
for ( let x = 0; x < width; x ++ ) {
const b = srcBuffer[ src ]; src ++;
const g = srcBuffer[ src ]; src ++;
const r = srcBuffer[ src ]; src ++;
const a = srcBuffer[ src ]; src ++;
byteArray[ dst ] = r; dst ++; //r
byteArray[ dst ] = g; dst ++; //g
byteArray[ dst ] = b; dst ++; //b
byteArray[ dst ] = a; dst ++; //a
}
}
return byteArray;
}
const FOURCC_DXT1 = fourCCToInt32( 'DXT1' );
const FOURCC_DXT3 = fourCCToInt32( 'DXT3' );
const FOURCC_DXT5 = fourCCToInt32( 'DXT5' );
const FOURCC_ETC1 = fourCCToInt32( 'ETC1' );
const FOURCC_DX10 = fourCCToInt32( 'DX10' );
const headerLengthInt = 31; // The header length in 32 bit ints
const extendedHeaderLengthInt = 5; // The extended header length in 32 bit ints
// Offsets into the header array
const off_magic = 0;
const off_size = 1;
const off_flags = 2;
const off_height = 3;
const off_width = 4;
const off_mipmapCount = 7;
// const off_pfFlags = 20;
const off_pfFourCC = 21;
const off_RGBBitCount = 22;
const off_RBitMask = 23;
const off_GBitMask = 24;
const off_BBitMask = 25;
const off_ABitMask = 26;
// const off_caps = 27;
const off_caps2 = 28;
// const off_caps3 = 29;
// const off_caps4 = 30;
// If fourCC = DX10, the extended header starts after 32
const off_dxgiFormat = 0;
// Parse header
const header = new Int32Array( buffer, 0, headerLengthInt );
if ( header[ off_magic ] !== DDS_MAGIC ) {
console.error( 'THREE.DDSLoader.parse: Invalid magic number in DDS header.' );
return dds;
}
let blockBytes;
const fourCC = header[ off_pfFourCC ];
let isRGBAUncompressed = false;
let dataOffset = header[ off_size ] + 4;
switch ( fourCC ) {
case FOURCC_DXT1:
blockBytes = 8;
dds.format = RGB_S3TC_DXT1_Format;
break;
case FOURCC_DXT3:
blockBytes = 16;
dds.format = RGBA_S3TC_DXT3_Format;
break;
case FOURCC_DXT5:
blockBytes = 16;
dds.format = RGBA_S3TC_DXT5_Format;
break;
case FOURCC_ETC1:
blockBytes = 8;
dds.format = RGB_ETC1_Format;
break;
case FOURCC_DX10:
dataOffset += extendedHeaderLengthInt * 4;
const extendedHeader = new Int32Array( buffer, ( headerLengthInt + 1 ) * 4, extendedHeaderLengthInt );
const dxgiFormat = extendedHeader[ off_dxgiFormat ];
switch ( dxgiFormat ) {
case DXGI_FORMAT_BC6H_SF16: {
blockBytes = 16;
dds.format = RGB_BPTC_SIGNED_Format;
break;
}
case DXGI_FORMAT_BC6H_UF16: {
blockBytes = 16;
dds.format = RGB_BPTC_UNSIGNED_Format;
break;
}
default: {
console.error( 'THREE.DDSLoader.parse: Unsupported DXGI_FORMAT code ', dxgiFormat );
return dds;
}
}
break;
default:
if ( header[ off_RGBBitCount ] === 32
&& header[ off_RBitMask ] & 0xff0000
&& header[ off_GBitMask ] & 0xff00
&& header[ off_BBitMask ] & 0xff
&& header[ off_ABitMask ] & 0xff000000 ) {
isRGBAUncompressed = true;
blockBytes = 64;
dds.format = RGBAFormat;
} else {
console.error( 'THREE.DDSLoader.parse: Unsupported FourCC code ', int32ToFourCC( fourCC ) );
return dds;
}
}
dds.mipmapCount = 1;
if ( header[ off_flags ] & DDSD_MIPMAPCOUNT && loadMipmaps !== false ) {
dds.mipmapCount = Math.max( 1, header[ off_mipmapCount ] );
}
const caps2 = header[ off_caps2 ];
dds.isCubemap = caps2 & DDSCAPS2_CUBEMAP ? true : false;
if ( dds.isCubemap && (
! ( caps2 & DDSCAPS2_CUBEMAP_POSITIVEX ) ||
! ( caps2 & DDSCAPS2_CUBEMAP_NEGATIVEX ) ||
! ( caps2 & DDSCAPS2_CUBEMAP_POSITIVEY ) ||
! ( caps2 & DDSCAPS2_CUBEMAP_NEGATIVEY ) ||
! ( caps2 & DDSCAPS2_CUBEMAP_POSITIVEZ ) ||
! ( caps2 & DDSCAPS2_CUBEMAP_NEGATIVEZ )
) ) {
console.error( 'THREE.DDSLoader.parse: Incomplete cubemap faces' );
return dds;
}
dds.width = header[ off_width ];
dds.height = header[ off_height ];
// Extract mipmaps buffers
const faces = dds.isCubemap ? 6 : 1;
for ( let face = 0; face < faces; face ++ ) {
let width = dds.width;
let height = dds.height;
for ( let i = 0; i < dds.mipmapCount; i ++ ) {
let byteArray, dataLength;
if ( isRGBAUncompressed ) {
byteArray = loadARGBMip( buffer, dataOffset, width, height );
dataLength = byteArray.length;
} else {
dataLength = Math.max( 4, width ) / 4 * Math.max( 4, height ) / 4 * blockBytes;
byteArray = new Uint8Array( buffer, dataOffset, dataLength );
}
const mipmap = { 'data': byteArray, 'width': width, 'height': height };
dds.mipmaps.push( mipmap );
dataOffset += dataLength;
width = Math.max( width >> 1, 1 );
height = Math.max( height >> 1, 1 );
}
}
return dds;
}
}
export { DDSLoader };

View File

@ -0,0 +1,613 @@
import {
BufferAttribute,
BufferGeometry,
Color,
FileLoader,
Loader,
LinearSRGBColorSpace,
SRGBColorSpace
} from 'three';
const _taskCache = new WeakMap();
class DRACOLoader extends Loader {
constructor( manager ) {
super( manager );
this.decoderPath = '';
this.decoderConfig = {};
this.decoderBinary = null;
this.decoderPending = null;
this.workerLimit = 4;
this.workerPool = [];
this.workerNextTaskID = 1;
this.workerSourceURL = '';
this.defaultAttributeIDs = {
position: 'POSITION',
normal: 'NORMAL',
color: 'COLOR',
uv: 'TEX_COORD'
};
this.defaultAttributeTypes = {
position: 'Float32Array',
normal: 'Float32Array',
color: 'Float32Array',
uv: 'Float32Array'
};
}
setDecoderPath( path ) {
this.decoderPath = path;
return this;
}
setDecoderConfig( config ) {
this.decoderConfig = config;
return this;
}
setWorkerLimit( workerLimit ) {
this.workerLimit = workerLimit;
return this;
}
load( url, onLoad, onProgress, onError ) {
const loader = new FileLoader( this.manager );
loader.setPath( this.path );
loader.setResponseType( 'arraybuffer' );
loader.setRequestHeader( this.requestHeader );
loader.setWithCredentials( this.withCredentials );
loader.load( url, ( buffer ) => {
this.parse( buffer, onLoad, onError );
}, onProgress, onError );
}
parse( buffer, onLoad, onError = ()=>{} ) {
this.decodeDracoFile( buffer, onLoad, null, null, SRGBColorSpace ).catch( onError );
}
decodeDracoFile( buffer, callback, attributeIDs, attributeTypes, vertexColorSpace = LinearSRGBColorSpace, onError = () => {} ) {
const taskConfig = {
attributeIDs: attributeIDs || this.defaultAttributeIDs,
attributeTypes: attributeTypes || this.defaultAttributeTypes,
useUniqueIDs: !! attributeIDs,
vertexColorSpace: vertexColorSpace,
};
return this.decodeGeometry( buffer, taskConfig ).then( callback ).catch( onError );
}
decodeGeometry( buffer, taskConfig ) {
const taskKey = JSON.stringify( taskConfig );
// Check for an existing task using this buffer. A transferred buffer cannot be transferred
// again from this thread.
if ( _taskCache.has( buffer ) ) {
const cachedTask = _taskCache.get( buffer );
if ( cachedTask.key === taskKey ) {
return cachedTask.promise;
} else if ( buffer.byteLength === 0 ) {
// Technically, it would be possible to wait for the previous task to complete,
// transfer the buffer back, and decode again with the second configuration. That
// is complex, and I don't know of any reason to decode a Draco buffer twice in
// different ways, so this is left unimplemented.
throw new Error(
'THREE.DRACOLoader: Unable to re-decode a buffer with different ' +
'settings. Buffer has already been transferred.'
);
}
}
//
let worker;
const taskID = this.workerNextTaskID ++;
const taskCost = buffer.byteLength;
// Obtain a worker and assign a task, and construct a geometry instance
// when the task completes.
const geometryPending = this._getWorker( taskID, taskCost )
.then( ( _worker ) => {
worker = _worker;
return new Promise( ( resolve, reject ) => {
worker._callbacks[ taskID ] = { resolve, reject };
worker.postMessage( { type: 'decode', id: taskID, taskConfig, buffer }, [ buffer ] );
// this.debug();
} );
} )
.then( ( message ) => this._createGeometry( message.geometry ) );
// Remove task from the task list.
// Note: replaced '.finally()' with '.catch().then()' block - iOS 11 support (#19416)
geometryPending
.catch( () => true )
.then( () => {
if ( worker && taskID ) {
this._releaseTask( worker, taskID );
// this.debug();
}
} );
// Cache the task result.
_taskCache.set( buffer, {
key: taskKey,
promise: geometryPending
} );
return geometryPending;
}
_createGeometry( geometryData ) {
const geometry = new BufferGeometry();
if ( geometryData.index ) {
geometry.setIndex( new BufferAttribute( geometryData.index.array, 1 ) );
}
for ( let i = 0; i < geometryData.attributes.length; i ++ ) {
const result = geometryData.attributes[ i ];
const name = result.name;
const array = result.array;
const itemSize = result.itemSize;
const attribute = new BufferAttribute( array, itemSize );
if ( name === 'color' ) {
this._assignVertexColorSpace( attribute, result.vertexColorSpace );
attribute.normalized = ( array instanceof Float32Array ) === false;
}
geometry.setAttribute( name, attribute );
}
return geometry;
}
_assignVertexColorSpace( attribute, inputColorSpace ) {
// While .drc files do not specify colorspace, the only 'official' tooling
// is PLY and OBJ converters, which use sRGB. We'll assume sRGB when a .drc
// file is passed into .load() or .parse(). GLTFLoader uses internal APIs
// to decode geometry, and vertex colors are already Linear-sRGB in there.
if ( inputColorSpace !== SRGBColorSpace ) return;
const _color = new Color();
for ( let i = 0, il = attribute.count; i < il; i ++ ) {
_color.fromBufferAttribute( attribute, i ).convertSRGBToLinear();
attribute.setXYZ( i, _color.r, _color.g, _color.b );
}
}
_loadLibrary( url, responseType ) {
const loader = new FileLoader( this.manager );
loader.setPath( this.decoderPath );
loader.setResponseType( responseType );
loader.setWithCredentials( this.withCredentials );
return new Promise( ( resolve, reject ) => {
loader.load( url, resolve, undefined, reject );
} );
}
preload() {
this._initDecoder();
return this;
}
_initDecoder() {
if ( this.decoderPending ) return this.decoderPending;
const useJS = typeof WebAssembly !== 'object' || this.decoderConfig.type === 'js';
const librariesPending = [];
if ( useJS ) {
librariesPending.push( this._loadLibrary( 'draco_decoder.js', 'text' ) );
} else {
librariesPending.push( this._loadLibrary( 'draco_wasm_wrapper.js', 'text' ) );
librariesPending.push( this._loadLibrary( 'draco_decoder.wasm', 'arraybuffer' ) );
}
this.decoderPending = Promise.all( librariesPending )
.then( ( libraries ) => {
const jsContent = libraries[ 0 ];
if ( ! useJS ) {
this.decoderConfig.wasmBinary = libraries[ 1 ];
}
const fn = DRACOWorker.toString();
const body = [
'/* draco decoder */',
jsContent,
'',
'/* worker */',
fn.substring( fn.indexOf( '{' ) + 1, fn.lastIndexOf( '}' ) )
].join( '\n' );
this.workerSourceURL = URL.createObjectURL( new Blob( [ body ] ) );
} );
return this.decoderPending;
}
_getWorker( taskID, taskCost ) {
return this._initDecoder().then( () => {
if ( this.workerPool.length < this.workerLimit ) {
const worker = new Worker( this.workerSourceURL );
worker._callbacks = {};
worker._taskCosts = {};
worker._taskLoad = 0;
worker.postMessage( { type: 'init', decoderConfig: this.decoderConfig } );
worker.onmessage = function ( e ) {
const message = e.data;
switch ( message.type ) {
case 'decode':
worker._callbacks[ message.id ].resolve( message );
break;
case 'error':
worker._callbacks[ message.id ].reject( message );
break;
default:
console.error( 'THREE.DRACOLoader: Unexpected message, "' + message.type + '"' );
}
};
this.workerPool.push( worker );
} else {
this.workerPool.sort( function ( a, b ) {
return a._taskLoad > b._taskLoad ? - 1 : 1;
} );
}
const worker = this.workerPool[ this.workerPool.length - 1 ];
worker._taskCosts[ taskID ] = taskCost;
worker._taskLoad += taskCost;
return worker;
} );
}
_releaseTask( worker, taskID ) {
worker._taskLoad -= worker._taskCosts[ taskID ];
delete worker._callbacks[ taskID ];
delete worker._taskCosts[ taskID ];
}
debug() {
console.log( 'Task load: ', this.workerPool.map( ( worker ) => worker._taskLoad ) );
}
dispose() {
for ( let i = 0; i < this.workerPool.length; ++ i ) {
this.workerPool[ i ].terminate();
}
this.workerPool.length = 0;
if ( this.workerSourceURL !== '' ) {
URL.revokeObjectURL( this.workerSourceURL );
}
return this;
}
}
/* WEB WORKER */
function DRACOWorker() {
let decoderConfig;
let decoderPending;
onmessage = function ( e ) {
const message = e.data;
switch ( message.type ) {
case 'init':
decoderConfig = message.decoderConfig;
decoderPending = new Promise( function ( resolve/*, reject*/ ) {
decoderConfig.onModuleLoaded = function ( draco ) {
// Module is Promise-like. Wrap before resolving to avoid loop.
resolve( { draco: draco } );
};
DracoDecoderModule( decoderConfig ); // eslint-disable-line no-undef
} );
break;
case 'decode':
const buffer = message.buffer;
const taskConfig = message.taskConfig;
decoderPending.then( ( module ) => {
const draco = module.draco;
const decoder = new draco.Decoder();
try {
const geometry = decodeGeometry( draco, decoder, new Int8Array( buffer ), taskConfig );
const buffers = geometry.attributes.map( ( attr ) => attr.array.buffer );
if ( geometry.index ) buffers.push( geometry.index.array.buffer );
self.postMessage( { type: 'decode', id: message.id, geometry }, buffers );
} catch ( error ) {
console.error( error );
self.postMessage( { type: 'error', id: message.id, error: error.message } );
} finally {
draco.destroy( decoder );
}
} );
break;
}
};
function decodeGeometry( draco, decoder, array, taskConfig ) {
const attributeIDs = taskConfig.attributeIDs;
const attributeTypes = taskConfig.attributeTypes;
let dracoGeometry;
let decodingStatus;
const geometryType = decoder.GetEncodedGeometryType( array );
if ( geometryType === draco.TRIANGULAR_MESH ) {
dracoGeometry = new draco.Mesh();
decodingStatus = decoder.DecodeArrayToMesh( array, array.byteLength, dracoGeometry );
} else if ( geometryType === draco.POINT_CLOUD ) {
dracoGeometry = new draco.PointCloud();
decodingStatus = decoder.DecodeArrayToPointCloud( array, array.byteLength, dracoGeometry );
} else {
throw new Error( 'THREE.DRACOLoader: Unexpected geometry type.' );
}
if ( ! decodingStatus.ok() || dracoGeometry.ptr === 0 ) {
throw new Error( 'THREE.DRACOLoader: Decoding failed: ' + decodingStatus.error_msg() );
}
const geometry = { index: null, attributes: [] };
// Gather all vertex attributes.
for ( const attributeName in attributeIDs ) {
const attributeType = self[ attributeTypes[ attributeName ] ];
let attribute;
let attributeID;
// A Draco file may be created with default vertex attributes, whose attribute IDs
// are mapped 1:1 from their semantic name (POSITION, NORMAL, ...). Alternatively,
// a Draco file may contain a custom set of attributes, identified by known unique
// IDs. glTF files always do the latter, and `.drc` files typically do the former.
if ( taskConfig.useUniqueIDs ) {
attributeID = attributeIDs[ attributeName ];
attribute = decoder.GetAttributeByUniqueId( dracoGeometry, attributeID );
} else {
attributeID = decoder.GetAttributeId( dracoGeometry, draco[ attributeIDs[ attributeName ] ] );
if ( attributeID === - 1 ) continue;
attribute = decoder.GetAttribute( dracoGeometry, attributeID );
}
const attributeResult = decodeAttribute( draco, decoder, dracoGeometry, attributeName, attributeType, attribute );
if ( attributeName === 'color' ) {
attributeResult.vertexColorSpace = taskConfig.vertexColorSpace;
}
geometry.attributes.push( attributeResult );
}
// Add index.
if ( geometryType === draco.TRIANGULAR_MESH ) {
geometry.index = decodeIndex( draco, decoder, dracoGeometry );
}
draco.destroy( dracoGeometry );
return geometry;
}
function decodeIndex( draco, decoder, dracoGeometry ) {
const numFaces = dracoGeometry.num_faces();
const numIndices = numFaces * 3;
const byteLength = numIndices * 4;
const ptr = draco._malloc( byteLength );
decoder.GetTrianglesUInt32Array( dracoGeometry, byteLength, ptr );
const index = new Uint32Array( draco.HEAPF32.buffer, ptr, numIndices ).slice();
draco._free( ptr );
return { array: index, itemSize: 1 };
}
function decodeAttribute( draco, decoder, dracoGeometry, attributeName, attributeType, attribute ) {
const numComponents = attribute.num_components();
const numPoints = dracoGeometry.num_points();
const numValues = numPoints * numComponents;
const byteLength = numValues * attributeType.BYTES_PER_ELEMENT;
const dataType = getDracoDataType( draco, attributeType );
const ptr = draco._malloc( byteLength );
decoder.GetAttributeDataArrayForAllPoints( dracoGeometry, attribute, dataType, byteLength, ptr );
const array = new attributeType( draco.HEAPF32.buffer, ptr, numValues ).slice();
draco._free( ptr );
return {
name: attributeName,
array: array,
itemSize: numComponents
};
}
function getDracoDataType( draco, attributeType ) {
switch ( attributeType ) {
case Float32Array: return draco.DT_FLOAT32;
case Int8Array: return draco.DT_INT8;
case Int16Array: return draco.DT_INT16;
case Int32Array: return draco.DT_INT32;
case Uint8Array: return draco.DT_UINT8;
case Uint16Array: return draco.DT_UINT16;
case Uint32Array: return draco.DT_UINT32;
}
}
}
export { DRACOLoader };

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,183 @@
import {
FileLoader,
Loader,
ShapePath
} from '../../three.module.min.js';
class FontLoader extends Loader {
constructor( manager ) {
super( manager );
}
load( url, onLoad, onProgress, onError ) {
const scope = this;
const loader = new FileLoader( this.manager );
loader.setPath( this.path );
loader.setRequestHeader( this.requestHeader );
loader.setWithCredentials( this.withCredentials );
loader.load( url, function ( text ) {
const font = scope.parse( JSON.parse( text ) );
if ( onLoad ) onLoad( font );
}, onProgress, onError );
}
parse( json ) {
return new Font( json );
}
}
//
class Font {
constructor( data ) {
this.isFont = true;
this.type = 'Font';
this.data = data;
}
generateShapes( text, size = 100 ) {
const shapes = [];
const paths = createPaths( text, size, this.data );
for ( let p = 0, pl = paths.length; p < pl; p ++ ) {
shapes.push( ...paths[ p ].toShapes() );
}
return shapes;
}
}
function createPaths( text, size, data ) {
const chars = Array.from( text );
const scale = size / data.resolution;
const line_height = ( data.boundingBox.yMax - data.boundingBox.yMin + data.underlineThickness ) * scale;
const paths = [];
let offsetX = 0, offsetY = 0;
for ( let i = 0; i < chars.length; i ++ ) {
const char = chars[ i ];
if ( char === '\n' ) {
offsetX = 0;
offsetY -= line_height;
} else {
const ret = createPath( char, scale, offsetX, offsetY, data );
offsetX += ret.offsetX;
paths.push( ret.path );
}
}
return paths;
}
function createPath( char, scale, offsetX, offsetY, data ) {
const glyph = data.glyphs[ char ] || data.glyphs[ '?' ];
if ( ! glyph ) {
console.error( 'THREE.Font: character "' + char + '" does not exists in font family ' + data.familyName + '.' );
return;
}
const path = new ShapePath();
let x, y, cpx, cpy, cpx1, cpy1, cpx2, cpy2;
if ( glyph.o ) {
const outline = glyph._cachedOutline || ( glyph._cachedOutline = glyph.o.split( ' ' ) );
for ( let i = 0, l = outline.length; i < l; ) {
const action = outline[ i ++ ];
switch ( action ) {
case 'm': // moveTo
x = outline[ i ++ ] * scale + offsetX;
y = outline[ i ++ ] * scale + offsetY;
path.moveTo( x, y );
break;
case 'l': // lineTo
x = outline[ i ++ ] * scale + offsetX;
y = outline[ i ++ ] * scale + offsetY;
path.lineTo( x, y );
break;
case 'q': // quadraticCurveTo
cpx = outline[ i ++ ] * scale + offsetX;
cpy = outline[ i ++ ] * scale + offsetY;
cpx1 = outline[ i ++ ] * scale + offsetX;
cpy1 = outline[ i ++ ] * scale + offsetY;
path.quadraticCurveTo( cpx1, cpy1, cpx, cpy );
break;
case 'b': // bezierCurveTo
cpx = outline[ i ++ ] * scale + offsetX;
cpy = outline[ i ++ ] * scale + offsetY;
cpx1 = outline[ i ++ ] * scale + offsetX;
cpy1 = outline[ i ++ ] * scale + offsetY;
cpx2 = outline[ i ++ ] * scale + offsetX;
cpy2 = outline[ i ++ ] * scale + offsetY;
path.bezierCurveTo( cpx1, cpy1, cpx2, cpy2, cpx, cpy );
break;
}
}
}
return { offsetX: glyph.ha * scale, path: path };
}
export { FontLoader, Font };

View File

@ -0,0 +1,261 @@
import {
BufferGeometry,
FileLoader,
Float32BufferAttribute,
Group,
LineBasicMaterial,
LineSegments,
Loader
} from 'three';
/**
* GCodeLoader is used to load gcode files usually used for 3D printing or CNC applications.
*
* Gcode files are composed by commands used by machines to create objects.
*
* @class GCodeLoader
* @param {Manager} manager Loading manager.
*/
class GCodeLoader extends Loader {
constructor( manager ) {
super( manager );
this.splitLayer = false;
}
load( url, onLoad, onProgress, onError ) {
const scope = this;
const loader = new FileLoader( scope.manager );
loader.setPath( scope.path );
loader.setRequestHeader( scope.requestHeader );
loader.setWithCredentials( scope.withCredentials );
loader.load( url, function ( text ) {
try {
onLoad( scope.parse( text ) );
} catch ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
scope.manager.itemError( url );
}
}, onProgress, onError );
}
parse( data ) {
let state = { x: 0, y: 0, z: 0, e: 0, f: 0, extruding: false, relative: false };
const layers = [];
let currentLayer = undefined;
const pathMaterial = new LineBasicMaterial( { color: 0xFF0000 } );
pathMaterial.name = 'path';
const extrudingMaterial = new LineBasicMaterial( { color: 0x00FF00 } );
extrudingMaterial.name = 'extruded';
function newLayer( line ) {
currentLayer = { vertex: [], pathVertex: [], z: line.z };
layers.push( currentLayer );
}
//Create lie segment between p1 and p2
function addSegment( p1, p2 ) {
if ( currentLayer === undefined ) {
newLayer( p1 );
}
if ( state.extruding ) {
currentLayer.vertex.push( p1.x, p1.y, p1.z );
currentLayer.vertex.push( p2.x, p2.y, p2.z );
} else {
currentLayer.pathVertex.push( p1.x, p1.y, p1.z );
currentLayer.pathVertex.push( p2.x, p2.y, p2.z );
}
}
function delta( v1, v2 ) {
return state.relative ? v2 : v2 - v1;
}
function absolute( v1, v2 ) {
return state.relative ? v1 + v2 : v2;
}
const lines = data.replace( /;.+/g, '' ).split( '\n' );
for ( let i = 0; i < lines.length; i ++ ) {
const tokens = lines[ i ].split( ' ' );
const cmd = tokens[ 0 ].toUpperCase();
//Argumments
const args = {};
tokens.splice( 1 ).forEach( function ( token ) {
if ( token[ 0 ] !== undefined ) {
const key = token[ 0 ].toLowerCase();
const value = parseFloat( token.substring( 1 ) );
args[ key ] = value;
}
} );
//Process commands
//G0/G1 Linear Movement
if ( cmd === 'G0' || cmd === 'G1' ) {
const line = {
x: args.x !== undefined ? absolute( state.x, args.x ) : state.x,
y: args.y !== undefined ? absolute( state.y, args.y ) : state.y,
z: args.z !== undefined ? absolute( state.z, args.z ) : state.z,
e: args.e !== undefined ? absolute( state.e, args.e ) : state.e,
f: args.f !== undefined ? absolute( state.f, args.f ) : state.f,
};
//Layer change detection is or made by watching Z, it's made by watching when we extrude at a new Z position
if ( delta( state.e, line.e ) > 0 ) {
state.extruding = delta( state.e, line.e ) > 0;
if ( currentLayer == undefined || line.z != currentLayer.z ) {
newLayer( line );
}
}
addSegment( state, line );
state = line;
} else if ( cmd === 'G2' || cmd === 'G3' ) {
//G2/G3 - Arc Movement ( G2 clock wise and G3 counter clock wise )
//console.warn( 'THREE.GCodeLoader: Arc command not supported' );
} else if ( cmd === 'G90' ) {
//G90: Set to Absolute Positioning
state.relative = false;
} else if ( cmd === 'G91' ) {
//G91: Set to state.relative Positioning
state.relative = true;
} else if ( cmd === 'G92' ) {
//G92: Set Position
const line = state;
line.x = args.x !== undefined ? args.x : line.x;
line.y = args.y !== undefined ? args.y : line.y;
line.z = args.z !== undefined ? args.z : line.z;
line.e = args.e !== undefined ? args.e : line.e;
} else {
//console.warn( 'THREE.GCodeLoader: Command not supported:' + cmd );
}
}
function addObject( vertex, extruding, i ) {
const geometry = new BufferGeometry();
geometry.setAttribute( 'position', new Float32BufferAttribute( vertex, 3 ) );
const segments = new LineSegments( geometry, extruding ? extrudingMaterial : pathMaterial );
segments.name = 'layer' + i;
object.add( segments );
}
const object = new Group();
object.name = 'gcode';
if ( this.splitLayer ) {
for ( let i = 0; i < layers.length; i ++ ) {
const layer = layers[ i ];
addObject( layer.vertex, true, i );
addObject( layer.pathVertex, false, i );
}
} else {
const vertex = [],
pathVertex = [];
for ( let i = 0; i < layers.length; i ++ ) {
const layer = layers[ i ];
const layerVertex = layer.vertex;
const layerPathVertex = layer.pathVertex;
for ( let j = 0; j < layerVertex.length; j ++ ) {
vertex.push( layerVertex[ j ] );
}
for ( let j = 0; j < layerPathVertex.length; j ++ ) {
pathVertex.push( layerPathVertex[ j ] );
}
}
addObject( vertex, true, layers.length );
addObject( pathVertex, false, layers.length );
}
object.rotation.set( - Math.PI / 2, 0, 0 );
return object;
}
}
export { GCodeLoader };

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,115 @@
import {
CubeTexture,
DataTexture,
FileLoader,
FloatType,
HalfFloatType,
LinearFilter,
LinearSRGBColorSpace,
Loader
} from 'three';
import { RGBELoader } from '../loaders/RGBELoader.js';
class HDRCubeTextureLoader extends Loader {
constructor( manager ) {
super( manager );
this.hdrLoader = new RGBELoader();
this.type = HalfFloatType;
}
load( urls, onLoad, onProgress, onError ) {
const texture = new CubeTexture();
texture.type = this.type;
switch ( texture.type ) {
case FloatType:
texture.colorSpace = LinearSRGBColorSpace;
texture.minFilter = LinearFilter;
texture.magFilter = LinearFilter;
texture.generateMipmaps = false;
break;
case HalfFloatType:
texture.colorSpace = LinearSRGBColorSpace;
texture.minFilter = LinearFilter;
texture.magFilter = LinearFilter;
texture.generateMipmaps = false;
break;
}
const scope = this;
let loaded = 0;
function loadHDRData( i, onLoad, onProgress, onError ) {
new FileLoader( scope.manager )
.setPath( scope.path )
.setResponseType( 'arraybuffer' )
.setWithCredentials( scope.withCredentials )
.load( urls[ i ], function ( buffer ) {
loaded ++;
const texData = scope.hdrLoader.parse( buffer );
if ( ! texData ) return;
if ( texData.data !== undefined ) {
const dataTexture = new DataTexture( texData.data, texData.width, texData.height );
dataTexture.type = texture.type;
dataTexture.colorSpace = texture.colorSpace;
dataTexture.format = texture.format;
dataTexture.minFilter = texture.minFilter;
dataTexture.magFilter = texture.magFilter;
dataTexture.generateMipmaps = texture.generateMipmaps;
texture.images[ i ] = dataTexture;
}
if ( loaded === 6 ) {
texture.needsUpdate = true;
if ( onLoad ) onLoad( texture );
}
}, onProgress, onError );
}
for ( let i = 0; i < urls.length; i ++ ) {
loadHDRData( i, onLoad, onProgress, onError );
}
return texture;
}
setDataType( value ) {
this.type = value;
this.hdrLoader.setDataType( value );
return this;
}
}
export { HDRCubeTextureLoader };

View File

@ -0,0 +1,337 @@
import {
DataTexture,
FileLoader,
FloatType,
RedFormat,
MathUtils,
Loader,
UnsignedByteType,
LinearFilter,
HalfFloatType,
DataUtils
} from 'three';
class IESLoader extends Loader {
constructor( manager ) {
super( manager );
this.type = HalfFloatType;
}
_getIESValues( iesLamp, type ) {
const width = 360;
const height = 180;
const size = width * height;
const data = new Array( size );
function interpolateCandelaValues( phi, theta ) {
let phiIndex = 0, thetaIndex = 0;
let startTheta = 0, endTheta = 0, startPhi = 0, endPhi = 0;
for ( let i = 0; i < iesLamp.numHorAngles - 1; ++ i ) { // numHorAngles = horAngles.length-1 because of extra padding, so this wont cause an out of bounds error
if ( theta < iesLamp.horAngles[ i + 1 ] || i == iesLamp.numHorAngles - 2 ) {
thetaIndex = i;
startTheta = iesLamp.horAngles[ i ];
endTheta = iesLamp.horAngles[ i + 1 ];
break;
}
}
for ( let i = 0; i < iesLamp.numVerAngles - 1; ++ i ) {
if ( phi < iesLamp.verAngles[ i + 1 ] || i == iesLamp.numVerAngles - 2 ) {
phiIndex = i;
startPhi = iesLamp.verAngles[ i ];
endPhi = iesLamp.verAngles[ i + 1 ];
break;
}
}
const deltaTheta = endTheta - startTheta;
const deltaPhi = endPhi - startPhi;
if ( deltaPhi === 0 ) // Outside range
return 0;
const t1 = deltaTheta === 0 ? 0 : ( theta - startTheta ) / deltaTheta;
const t2 = ( phi - startPhi ) / deltaPhi;
const nextThetaIndex = deltaTheta === 0 ? thetaIndex : thetaIndex + 1;
const v1 = MathUtils.lerp( iesLamp.candelaValues[ thetaIndex ][ phiIndex ], iesLamp.candelaValues[ nextThetaIndex ][ phiIndex ], t1 );
const v2 = MathUtils.lerp( iesLamp.candelaValues[ thetaIndex ][ phiIndex + 1 ], iesLamp.candelaValues[ nextThetaIndex ][ phiIndex + 1 ], t1 );
const v = MathUtils.lerp( v1, v2, t2 );
return v;
}
const startTheta = iesLamp.horAngles[ 0 ], endTheta = iesLamp.horAngles[ iesLamp.numHorAngles - 1 ];
for ( let i = 0; i < size; ++ i ) {
let theta = i % width;
const phi = Math.floor( i / width );
if ( endTheta - startTheta !== 0 && ( theta < startTheta || theta >= endTheta ) ) { // Handle symmetry for hor angles
theta %= endTheta * 2;
if ( theta > endTheta )
theta = endTheta * 2 - theta;
}
data[ phi + theta * height ] = interpolateCandelaValues( phi, theta );
}
let result = null;
if ( type === UnsignedByteType ) result = Uint8Array.from( data.map( v => Math.min( v * 0xFF, 0xFF ) ) );
else if ( type === HalfFloatType ) result = Uint16Array.from( data.map( v => DataUtils.toHalfFloat( v ) ) );
else if ( type === FloatType ) result = Float32Array.from( data );
else console.error( 'IESLoader: Unsupported type:', type );
return result;
}
load( url, onLoad, onProgress, onError ) {
const loader = new FileLoader( this.manager );
loader.setResponseType( 'text' );
loader.setCrossOrigin( this.crossOrigin );
loader.setWithCredentials( this.withCredentials );
loader.setPath( this.path );
loader.setRequestHeader( this.requestHeader );
loader.load( url, text => {
onLoad( this.parse( text ) );
}, onProgress, onError );
}
parse( text ) {
const type = this.type;
const iesLamp = new IESLamp( text );
const data = this._getIESValues( iesLamp, type );
const texture = new DataTexture( data, 180, 1, RedFormat, type );
texture.minFilter = LinearFilter;
texture.magFilter = LinearFilter;
texture.needsUpdate = true;
return texture;
}
}
function IESLamp( text ) {
const _self = this;
const textArray = text.split( '\n' );
let lineNumber = 0;
let line;
_self.verAngles = [ ];
_self.horAngles = [ ];
_self.candelaValues = [ ];
_self.tiltData = { };
_self.tiltData.angles = [ ];
_self.tiltData.mulFactors = [ ];
function textToArray( text ) {
text = text.replace( /^\s+|\s+$/g, '' ); // remove leading or trailing spaces
text = text.replace( /,/g, ' ' ); // replace commas with spaces
text = text.replace( /\s\s+/g, ' ' ); // replace white space/tabs etc by single whitespace
const array = text.split( ' ' );
return array;
}
function readArray( count, array ) {
while ( true ) {
const line = textArray[ lineNumber ++ ];
const lineData = textToArray( line );
for ( let i = 0; i < lineData.length; ++ i ) {
array.push( Number( lineData[ i ] ) );
}
if ( array.length === count )
break;
}
}
function readTilt() {
let line = textArray[ lineNumber ++ ];
let lineData = textToArray( line );
_self.tiltData.lampToLumGeometry = Number( lineData[ 0 ] );
line = textArray[ lineNumber ++ ];
lineData = textToArray( line );
_self.tiltData.numAngles = Number( lineData[ 0 ] );
readArray( _self.tiltData.numAngles, _self.tiltData.angles );
readArray( _self.tiltData.numAngles, _self.tiltData.mulFactors );
}
function readLampValues() {
const values = [ ];
readArray( 10, values );
_self.count = Number( values[ 0 ] );
_self.lumens = Number( values[ 1 ] );
_self.multiplier = Number( values[ 2 ] );
_self.numVerAngles = Number( values[ 3 ] );
_self.numHorAngles = Number( values[ 4 ] );
_self.gonioType = Number( values[ 5 ] );
_self.units = Number( values[ 6 ] );
_self.width = Number( values[ 7 ] );
_self.length = Number( values[ 8 ] );
_self.height = Number( values[ 9 ] );
}
function readLampFactors() {
const values = [ ];
readArray( 3, values );
_self.ballFactor = Number( values[ 0 ] );
_self.blpFactor = Number( values[ 1 ] );
_self.inputWatts = Number( values[ 2 ] );
}
while ( true ) {
line = textArray[ lineNumber ++ ];
if ( line.includes( 'TILT' ) ) {
break;
}
}
if ( ! line.includes( 'NONE' ) ) {
if ( line.includes( 'INCLUDE' ) ) {
readTilt();
} else {
// TODO:: Read tilt data from a file
}
}
readLampValues();
readLampFactors();
// Initialize candela value array
for ( let i = 0; i < _self.numHorAngles; ++ i ) {
_self.candelaValues.push( [ ] );
}
// Parse Angles
readArray( _self.numVerAngles, _self.verAngles );
readArray( _self.numHorAngles, _self.horAngles );
// Parse Candela values
for ( let i = 0; i < _self.numHorAngles; ++ i ) {
readArray( _self.numVerAngles, _self.candelaValues[ i ] );
}
// Calculate actual candela values, and normalize.
for ( let i = 0; i < _self.numHorAngles; ++ i ) {
for ( let j = 0; j < _self.numVerAngles; ++ j ) {
_self.candelaValues[ i ][ j ] *= _self.candelaValues[ i ][ j ] * _self.multiplier
* _self.ballFactor * _self.blpFactor;
}
}
let maxVal = - 1;
for ( let i = 0; i < _self.numHorAngles; ++ i ) {
for ( let j = 0; j < _self.numVerAngles; ++ j ) {
const value = _self.candelaValues[ i ][ j ];
maxVal = maxVal < value ? value : maxVal;
}
}
const bNormalize = true;
if ( bNormalize && maxVal > 0 ) {
for ( let i = 0; i < _self.numHorAngles; ++ i ) {
for ( let j = 0; j < _self.numVerAngles; ++ j ) {
_self.candelaValues[ i ][ j ] /= maxVal;
}
}
}
}
export { IESLoader };

View File

@ -0,0 +1,130 @@
import {
FileLoader,
Group,
Loader,
LoadingManager
} from 'three';
import { ColladaLoader } from '../loaders/ColladaLoader.js';
import * as fflate from '../libs/fflate.module.js';
class KMZLoader extends Loader {
constructor( manager ) {
super( manager );
}
load( url, onLoad, onProgress, onError ) {
const scope = this;
const loader = new FileLoader( scope.manager );
loader.setPath( scope.path );
loader.setResponseType( 'arraybuffer' );
loader.setRequestHeader( scope.requestHeader );
loader.setWithCredentials( scope.withCredentials );
loader.load( url, function ( text ) {
try {
onLoad( scope.parse( text ) );
} catch ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
scope.manager.itemError( url );
}
}, onProgress, onError );
}
parse( data ) {
function findFile( url ) {
for ( const path in zip ) {
if ( path.slice( - url.length ) === url ) {
return zip[ path ];
}
}
}
const manager = new LoadingManager();
manager.setURLModifier( function ( url ) {
const image = findFile( url );
if ( image ) {
console.log( 'Loading', url );
const blob = new Blob( [ image.buffer ], { type: 'application/octet-stream' } );
return URL.createObjectURL( blob );
}
return url;
} );
//
const zip = fflate.unzipSync( new Uint8Array( data ) );
if ( zip[ 'doc.kml' ] ) {
const xml = new DOMParser().parseFromString( fflate.strFromU8( zip[ 'doc.kml' ] ), 'application/xml' );
const model = xml.querySelector( 'Placemark Model Link href' );
if ( model ) {
const loader = new ColladaLoader( manager );
return loader.parse( fflate.strFromU8( zip[ model.textContent ] ) );
}
} else {
console.warn( 'KMZLoader: Missing doc.kml file.' );
for ( const path in zip ) {
const extension = path.split( '.' ).pop().toLowerCase();
if ( extension === 'dae' ) {
const loader = new ColladaLoader( manager );
return loader.parse( fflate.strFromU8( zip[ path ] ) );
}
}
}
console.error( 'KMZLoader: Couldn\'t find .dae file.' );
return { scene: new Group() };
}
}
export { KMZLoader };

View File

@ -0,0 +1,925 @@
/**
* Loader for KTX 2.0 GPU Texture containers.
*
* KTX 2.0 is a container format for various GPU texture formats. The loader
* supports Basis Universal GPU textures, which can be quickly transcoded to
* a wide variety of GPU texture compression formats, as well as some
* uncompressed DataTexture and Data3DTexture formats.
*
* References:
* - KTX: http://github.khronos.org/KTX-Specification/
* - DFD: https://www.khronos.org/registry/DataFormat/specs/1.3/dataformat.1.3.html#basicdescriptor
*/
import {
CompressedTexture,
CompressedArrayTexture,
CompressedCubeTexture,
Data3DTexture,
DataTexture,
DisplayP3ColorSpace,
FileLoader,
FloatType,
HalfFloatType,
NoColorSpace,
LinearFilter,
LinearMipmapLinearFilter,
LinearDisplayP3ColorSpace,
LinearSRGBColorSpace,
Loader,
RedFormat,
RGB_ETC1_Format,
RGB_ETC2_Format,
RGB_PVRTC_4BPPV1_Format,
RGBA_ASTC_4x4_Format,
RGBA_ASTC_6x6_Format,
RGBA_BPTC_Format,
RGBA_ETC2_EAC_Format,
RGBA_PVRTC_4BPPV1_Format,
RGBA_S3TC_DXT5_Format,
RGBA_S3TC_DXT1_Format,
RGBAFormat,
RGFormat,
SRGBColorSpace,
UnsignedByteType,
} from 'three';
import { WorkerPool } from '../utils/WorkerPool.js';
import {
read,
KHR_DF_FLAG_ALPHA_PREMULTIPLIED,
KHR_DF_TRANSFER_SRGB,
KHR_SUPERCOMPRESSION_NONE,
KHR_SUPERCOMPRESSION_ZSTD,
VK_FORMAT_UNDEFINED,
VK_FORMAT_R16_SFLOAT,
VK_FORMAT_R16G16_SFLOAT,
VK_FORMAT_R16G16B16A16_SFLOAT,
VK_FORMAT_R32_SFLOAT,
VK_FORMAT_R32G32_SFLOAT,
VK_FORMAT_R32G32B32A32_SFLOAT,
VK_FORMAT_R8_SRGB,
VK_FORMAT_R8_UNORM,
VK_FORMAT_R8G8_SRGB,
VK_FORMAT_R8G8_UNORM,
VK_FORMAT_R8G8B8A8_SRGB,
VK_FORMAT_R8G8B8A8_UNORM,
VK_FORMAT_ASTC_6x6_SRGB_BLOCK,
VK_FORMAT_ASTC_6x6_UNORM_BLOCK,
KHR_DF_PRIMARIES_UNSPECIFIED,
KHR_DF_PRIMARIES_BT709,
KHR_DF_PRIMARIES_DISPLAYP3
} from '../libs/ktx-parse.module.js';
import { ZSTDDecoder } from '../libs/zstddec.module.js';
const _taskCache = new WeakMap();
let _activeLoaders = 0;
let _zstd;
class KTX2Loader extends Loader {
constructor( manager ) {
super( manager );
this.transcoderPath = '';
this.transcoderBinary = null;
this.transcoderPending = null;
this.workerPool = new WorkerPool();
this.workerSourceURL = '';
this.workerConfig = null;
if ( typeof MSC_TRANSCODER !== 'undefined' ) {
console.warn(
'THREE.KTX2Loader: Please update to latest "basis_transcoder".'
+ ' "msc_basis_transcoder" is no longer supported in three.js r125+.'
);
}
}
setTranscoderPath( path ) {
this.transcoderPath = path;
return this;
}
setWorkerLimit( num ) {
this.workerPool.setWorkerLimit( num );
return this;
}
async detectSupportAsync( renderer ) {
this.workerConfig = {
astcSupported: await renderer.hasFeatureAsync( 'texture-compression-astc' ),
etc1Supported: await renderer.hasFeatureAsync( 'texture-compression-etc1' ),
etc2Supported: await renderer.hasFeatureAsync( 'texture-compression-etc2' ),
dxtSupported: await renderer.hasFeatureAsync( 'texture-compression-bc' ),
bptcSupported: await renderer.hasFeatureAsync( 'texture-compression-bptc' ),
pvrtcSupported: await renderer.hasFeatureAsync( 'texture-compression-pvrtc' )
};
return this;
}
detectSupport( renderer ) {
if ( renderer.isWebGPURenderer === true ) {
this.workerConfig = {
astcSupported: renderer.hasFeature( 'texture-compression-astc' ),
etc1Supported: renderer.hasFeature( 'texture-compression-etc1' ),
etc2Supported: renderer.hasFeature( 'texture-compression-etc2' ),
dxtSupported: renderer.hasFeature( 'texture-compression-bc' ),
bptcSupported: renderer.hasFeature( 'texture-compression-bptc' ),
pvrtcSupported: renderer.hasFeature( 'texture-compression-pvrtc' )
};
} else {
this.workerConfig = {
astcSupported: renderer.extensions.has( 'WEBGL_compressed_texture_astc' ),
etc1Supported: renderer.extensions.has( 'WEBGL_compressed_texture_etc1' ),
etc2Supported: renderer.extensions.has( 'WEBGL_compressed_texture_etc' ),
dxtSupported: renderer.extensions.has( 'WEBGL_compressed_texture_s3tc' ),
bptcSupported: renderer.extensions.has( 'EXT_texture_compression_bptc' ),
pvrtcSupported: renderer.extensions.has( 'WEBGL_compressed_texture_pvrtc' )
|| renderer.extensions.has( 'WEBKIT_WEBGL_compressed_texture_pvrtc' )
};
}
return this;
}
init() {
if ( ! this.transcoderPending ) {
// Load transcoder wrapper.
const jsLoader = new FileLoader( this.manager );
jsLoader.setPath( this.transcoderPath );
jsLoader.setWithCredentials( this.withCredentials );
const jsContent = jsLoader.loadAsync( 'basis_transcoder.js' );
// Load transcoder WASM binary.
const binaryLoader = new FileLoader( this.manager );
binaryLoader.setPath( this.transcoderPath );
binaryLoader.setResponseType( 'arraybuffer' );
binaryLoader.setWithCredentials( this.withCredentials );
const binaryContent = binaryLoader.loadAsync( 'basis_transcoder.wasm' );
this.transcoderPending = Promise.all( [ jsContent, binaryContent ] )
.then( ( [ jsContent, binaryContent ] ) => {
const fn = KTX2Loader.BasisWorker.toString();
const body = [
'/* constants */',
'let _EngineFormat = ' + JSON.stringify( KTX2Loader.EngineFormat ),
'let _TranscoderFormat = ' + JSON.stringify( KTX2Loader.TranscoderFormat ),
'let _BasisFormat = ' + JSON.stringify( KTX2Loader.BasisFormat ),
'/* basis_transcoder.js */',
jsContent,
'/* worker */',
fn.substring( fn.indexOf( '{' ) + 1, fn.lastIndexOf( '}' ) )
].join( '\n' );
this.workerSourceURL = URL.createObjectURL( new Blob( [ body ] ) );
this.transcoderBinary = binaryContent;
this.workerPool.setWorkerCreator( () => {
const worker = new Worker( this.workerSourceURL );
const transcoderBinary = this.transcoderBinary.slice( 0 );
worker.postMessage( { type: 'init', config: this.workerConfig, transcoderBinary }, [ transcoderBinary ] );
return worker;
} );
} );
if ( _activeLoaders > 0 ) {
// Each instance loads a transcoder and allocates workers, increasing network and memory cost.
console.warn(
'THREE.KTX2Loader: Multiple active KTX2 loaders may cause performance issues.'
+ ' Use a single KTX2Loader instance, or call .dispose() on old instances.'
);
}
_activeLoaders ++;
}
return this.transcoderPending;
}
load( url, onLoad, onProgress, onError ) {
if ( this.workerConfig === null ) {
throw new Error( 'THREE.KTX2Loader: Missing initialization with `.detectSupport( renderer )`.' );
}
const loader = new FileLoader( this.manager );
loader.setResponseType( 'arraybuffer' );
loader.setWithCredentials( this.withCredentials );
loader.load( url, ( buffer ) => {
// Check for an existing task using this buffer. A transferred buffer cannot be transferred
// again from this thread.
if ( _taskCache.has( buffer ) ) {
const cachedTask = _taskCache.get( buffer );
return cachedTask.promise.then( onLoad ).catch( onError );
}
this._createTexture( buffer )
.then( ( texture ) => onLoad ? onLoad( texture ) : null )
.catch( onError );
}, onProgress, onError );
}
_createTextureFrom( transcodeResult, container ) {
const { faces, width, height, format, type, error, dfdFlags } = transcodeResult;
if ( type === 'error' ) return Promise.reject( error );
let texture;
if ( container.faceCount === 6 ) {
texture = new CompressedCubeTexture( faces, format, UnsignedByteType );
} else {
const mipmaps = faces[ 0 ].mipmaps;
texture = container.layerCount > 1
? new CompressedArrayTexture( mipmaps, width, height, container.layerCount, format, UnsignedByteType )
: new CompressedTexture( mipmaps, width, height, format, UnsignedByteType );
}
texture.minFilter = faces[ 0 ].mipmaps.length === 1 ? LinearFilter : LinearMipmapLinearFilter;
texture.magFilter = LinearFilter;
texture.generateMipmaps = false;
texture.needsUpdate = true;
texture.colorSpace = parseColorSpace( container );
texture.premultiplyAlpha = !! ( dfdFlags & KHR_DF_FLAG_ALPHA_PREMULTIPLIED );
return texture;
}
/**
* @param {ArrayBuffer} buffer
* @param {object?} config
* @return {Promise<CompressedTexture|CompressedArrayTexture|DataTexture|Data3DTexture>}
*/
async _createTexture( buffer, config = {} ) {
const container = read( new Uint8Array( buffer ) );
if ( container.vkFormat !== VK_FORMAT_UNDEFINED ) {
return createRawTexture( container );
}
//
const taskConfig = config;
const texturePending = this.init().then( () => {
return this.workerPool.postMessage( { type: 'transcode', buffer, taskConfig: taskConfig }, [ buffer ] );
} ).then( ( e ) => this._createTextureFrom( e.data, container ) );
// Cache the task result.
_taskCache.set( buffer, { promise: texturePending } );
return texturePending;
}
dispose() {
this.workerPool.dispose();
if ( this.workerSourceURL ) URL.revokeObjectURL( this.workerSourceURL );
_activeLoaders --;
return this;
}
}
/* CONSTANTS */
KTX2Loader.BasisFormat = {
ETC1S: 0,
UASTC_4x4: 1,
};
KTX2Loader.TranscoderFormat = {
ETC1: 0,
ETC2: 1,
BC1: 2,
BC3: 3,
BC4: 4,
BC5: 5,
BC7_M6_OPAQUE_ONLY: 6,
BC7_M5: 7,
PVRTC1_4_RGB: 8,
PVRTC1_4_RGBA: 9,
ASTC_4x4: 10,
ATC_RGB: 11,
ATC_RGBA_INTERPOLATED_ALPHA: 12,
RGBA32: 13,
RGB565: 14,
BGR565: 15,
RGBA4444: 16,
};
KTX2Loader.EngineFormat = {
RGBAFormat: RGBAFormat,
RGBA_ASTC_4x4_Format: RGBA_ASTC_4x4_Format,
RGBA_BPTC_Format: RGBA_BPTC_Format,
RGBA_ETC2_EAC_Format: RGBA_ETC2_EAC_Format,
RGBA_PVRTC_4BPPV1_Format: RGBA_PVRTC_4BPPV1_Format,
RGBA_S3TC_DXT5_Format: RGBA_S3TC_DXT5_Format,
RGB_ETC1_Format: RGB_ETC1_Format,
RGB_ETC2_Format: RGB_ETC2_Format,
RGB_PVRTC_4BPPV1_Format: RGB_PVRTC_4BPPV1_Format,
RGBA_S3TC_DXT1_Format: RGBA_S3TC_DXT1_Format,
};
/* WEB WORKER */
KTX2Loader.BasisWorker = function () {
let config;
let transcoderPending;
let BasisModule;
const EngineFormat = _EngineFormat; // eslint-disable-line no-undef
const TranscoderFormat = _TranscoderFormat; // eslint-disable-line no-undef
const BasisFormat = _BasisFormat; // eslint-disable-line no-undef
self.addEventListener( 'message', function ( e ) {
const message = e.data;
switch ( message.type ) {
case 'init':
config = message.config;
init( message.transcoderBinary );
break;
case 'transcode':
transcoderPending.then( () => {
try {
const { faces, buffers, width, height, hasAlpha, format, dfdFlags } = transcode( message.buffer );
self.postMessage( { type: 'transcode', id: message.id, faces, width, height, hasAlpha, format, dfdFlags }, buffers );
} catch ( error ) {
console.error( error );
self.postMessage( { type: 'error', id: message.id, error: error.message } );
}
} );
break;
}
} );
function init( wasmBinary ) {
transcoderPending = new Promise( ( resolve ) => {
BasisModule = { wasmBinary, onRuntimeInitialized: resolve };
BASIS( BasisModule ); // eslint-disable-line no-undef
} ).then( () => {
BasisModule.initializeBasis();
if ( BasisModule.KTX2File === undefined ) {
console.warn( 'THREE.KTX2Loader: Please update Basis Universal transcoder.' );
}
} );
}
function transcode( buffer ) {
const ktx2File = new BasisModule.KTX2File( new Uint8Array( buffer ) );
function cleanup() {
ktx2File.close();
ktx2File.delete();
}
if ( ! ktx2File.isValid() ) {
cleanup();
throw new Error( 'THREE.KTX2Loader: Invalid or unsupported .ktx2 file' );
}
const basisFormat = ktx2File.isUASTC() ? BasisFormat.UASTC_4x4 : BasisFormat.ETC1S;
const width = ktx2File.getWidth();
const height = ktx2File.getHeight();
const layerCount = ktx2File.getLayers() || 1;
const levelCount = ktx2File.getLevels();
const faceCount = ktx2File.getFaces();
const hasAlpha = ktx2File.getHasAlpha();
const dfdFlags = ktx2File.getDFDFlags();
const { transcoderFormat, engineFormat } = getTranscoderFormat( basisFormat, width, height, hasAlpha );
if ( ! width || ! height || ! levelCount ) {
cleanup();
throw new Error( 'THREE.KTX2Loader: Invalid texture' );
}
if ( ! ktx2File.startTranscoding() ) {
cleanup();
throw new Error( 'THREE.KTX2Loader: .startTranscoding failed' );
}
const faces = [];
const buffers = [];
for ( let face = 0; face < faceCount; face ++ ) {
const mipmaps = [];
for ( let mip = 0; mip < levelCount; mip ++ ) {
const layerMips = [];
let mipWidth, mipHeight;
for ( let layer = 0; layer < layerCount; layer ++ ) {
const levelInfo = ktx2File.getImageLevelInfo( mip, layer, face );
if ( face === 0 && mip === 0 && layer === 0 && ( levelInfo.origWidth % 4 !== 0 || levelInfo.origHeight % 4 !== 0 ) ) {
console.warn( 'THREE.KTX2Loader: ETC1S and UASTC textures should use multiple-of-four dimensions.' );
}
if ( levelCount > 1 ) {
mipWidth = levelInfo.origWidth;
mipHeight = levelInfo.origHeight;
} else {
// Handles non-multiple-of-four dimensions in textures without mipmaps. Textures with
// mipmaps must use multiple-of-four dimensions, for some texture formats and APIs.
// See mrdoob/three.js#25908.
mipWidth = levelInfo.width;
mipHeight = levelInfo.height;
}
const dst = new Uint8Array( ktx2File.getImageTranscodedSizeInBytes( mip, layer, 0, transcoderFormat ) );
const status = ktx2File.transcodeImage( dst, mip, layer, face, transcoderFormat, 0, - 1, - 1 );
if ( ! status ) {
cleanup();
throw new Error( 'THREE.KTX2Loader: .transcodeImage failed.' );
}
layerMips.push( dst );
}
const mipData = concat( layerMips );
mipmaps.push( { data: mipData, width: mipWidth, height: mipHeight } );
buffers.push( mipData.buffer );
}
faces.push( { mipmaps, width, height, format: engineFormat } );
}
cleanup();
return { faces, buffers, width, height, hasAlpha, format: engineFormat, dfdFlags };
}
//
// Optimal choice of a transcoder target format depends on the Basis format (ETC1S or UASTC),
// device capabilities, and texture dimensions. The list below ranks the formats separately
// for ETC1S and UASTC.
//
// In some cases, transcoding UASTC to RGBA32 might be preferred for higher quality (at
// significant memory cost) compared to ETC1/2, BC1/3, and PVRTC. The transcoder currently
// chooses RGBA32 only as a last resort and does not expose that option to the caller.
const FORMAT_OPTIONS = [
{
if: 'astcSupported',
basisFormat: [ BasisFormat.UASTC_4x4 ],
transcoderFormat: [ TranscoderFormat.ASTC_4x4, TranscoderFormat.ASTC_4x4 ],
engineFormat: [ EngineFormat.RGBA_ASTC_4x4_Format, EngineFormat.RGBA_ASTC_4x4_Format ],
priorityETC1S: Infinity,
priorityUASTC: 1,
needsPowerOfTwo: false,
},
{
if: 'bptcSupported',
basisFormat: [ BasisFormat.ETC1S, BasisFormat.UASTC_4x4 ],
transcoderFormat: [ TranscoderFormat.BC7_M5, TranscoderFormat.BC7_M5 ],
engineFormat: [ EngineFormat.RGBA_BPTC_Format, EngineFormat.RGBA_BPTC_Format ],
priorityETC1S: 3,
priorityUASTC: 2,
needsPowerOfTwo: false,
},
{
if: 'dxtSupported',
basisFormat: [ BasisFormat.ETC1S, BasisFormat.UASTC_4x4 ],
transcoderFormat: [ TranscoderFormat.BC1, TranscoderFormat.BC3 ],
engineFormat: [ EngineFormat.RGBA_S3TC_DXT1_Format, EngineFormat.RGBA_S3TC_DXT5_Format ],
priorityETC1S: 4,
priorityUASTC: 5,
needsPowerOfTwo: false,
},
{
if: 'etc2Supported',
basisFormat: [ BasisFormat.ETC1S, BasisFormat.UASTC_4x4 ],
transcoderFormat: [ TranscoderFormat.ETC1, TranscoderFormat.ETC2 ],
engineFormat: [ EngineFormat.RGB_ETC2_Format, EngineFormat.RGBA_ETC2_EAC_Format ],
priorityETC1S: 1,
priorityUASTC: 3,
needsPowerOfTwo: false,
},
{
if: 'etc1Supported',
basisFormat: [ BasisFormat.ETC1S, BasisFormat.UASTC_4x4 ],
transcoderFormat: [ TranscoderFormat.ETC1 ],
engineFormat: [ EngineFormat.RGB_ETC1_Format ],
priorityETC1S: 2,
priorityUASTC: 4,
needsPowerOfTwo: false,
},
{
if: 'pvrtcSupported',
basisFormat: [ BasisFormat.ETC1S, BasisFormat.UASTC_4x4 ],
transcoderFormat: [ TranscoderFormat.PVRTC1_4_RGB, TranscoderFormat.PVRTC1_4_RGBA ],
engineFormat: [ EngineFormat.RGB_PVRTC_4BPPV1_Format, EngineFormat.RGBA_PVRTC_4BPPV1_Format ],
priorityETC1S: 5,
priorityUASTC: 6,
needsPowerOfTwo: true,
},
];
const ETC1S_OPTIONS = FORMAT_OPTIONS.sort( function ( a, b ) {
return a.priorityETC1S - b.priorityETC1S;
} );
const UASTC_OPTIONS = FORMAT_OPTIONS.sort( function ( a, b ) {
return a.priorityUASTC - b.priorityUASTC;
} );
function getTranscoderFormat( basisFormat, width, height, hasAlpha ) {
let transcoderFormat;
let engineFormat;
const options = basisFormat === BasisFormat.ETC1S ? ETC1S_OPTIONS : UASTC_OPTIONS;
for ( let i = 0; i < options.length; i ++ ) {
const opt = options[ i ];
if ( ! config[ opt.if ] ) continue;
if ( ! opt.basisFormat.includes( basisFormat ) ) continue;
if ( hasAlpha && opt.transcoderFormat.length < 2 ) continue;
if ( opt.needsPowerOfTwo && ! ( isPowerOfTwo( width ) && isPowerOfTwo( height ) ) ) continue;
transcoderFormat = opt.transcoderFormat[ hasAlpha ? 1 : 0 ];
engineFormat = opt.engineFormat[ hasAlpha ? 1 : 0 ];
return { transcoderFormat, engineFormat };
}
console.warn( 'THREE.KTX2Loader: No suitable compressed texture format found. Decoding to RGBA32.' );
transcoderFormat = TranscoderFormat.RGBA32;
engineFormat = EngineFormat.RGBAFormat;
return { transcoderFormat, engineFormat };
}
function isPowerOfTwo( value ) {
if ( value <= 2 ) return true;
return ( value & ( value - 1 ) ) === 0 && value !== 0;
}
/** Concatenates N byte arrays. */
function concat( arrays ) {
if ( arrays.length === 1 ) return arrays[ 0 ];
let totalByteLength = 0;
for ( let i = 0; i < arrays.length; i ++ ) {
const array = arrays[ i ];
totalByteLength += array.byteLength;
}
const result = new Uint8Array( totalByteLength );
let byteOffset = 0;
for ( let i = 0; i < arrays.length; i ++ ) {
const array = arrays[ i ];
result.set( array, byteOffset );
byteOffset += array.byteLength;
}
return result;
}
};
//
// Parsing for non-Basis textures. These textures are may have supercompression
// like Zstd, but they do not require transcoding.
const UNCOMPRESSED_FORMATS = new Set( [ RGBAFormat, RGFormat, RedFormat ] );
const FORMAT_MAP = {
[ VK_FORMAT_R32G32B32A32_SFLOAT ]: RGBAFormat,
[ VK_FORMAT_R16G16B16A16_SFLOAT ]: RGBAFormat,
[ VK_FORMAT_R8G8B8A8_UNORM ]: RGBAFormat,
[ VK_FORMAT_R8G8B8A8_SRGB ]: RGBAFormat,
[ VK_FORMAT_R32G32_SFLOAT ]: RGFormat,
[ VK_FORMAT_R16G16_SFLOAT ]: RGFormat,
[ VK_FORMAT_R8G8_UNORM ]: RGFormat,
[ VK_FORMAT_R8G8_SRGB ]: RGFormat,
[ VK_FORMAT_R32_SFLOAT ]: RedFormat,
[ VK_FORMAT_R16_SFLOAT ]: RedFormat,
[ VK_FORMAT_R8_SRGB ]: RedFormat,
[ VK_FORMAT_R8_UNORM ]: RedFormat,
[ VK_FORMAT_ASTC_6x6_SRGB_BLOCK ]: RGBA_ASTC_6x6_Format,
[ VK_FORMAT_ASTC_6x6_UNORM_BLOCK ]: RGBA_ASTC_6x6_Format,
};
const TYPE_MAP = {
[ VK_FORMAT_R32G32B32A32_SFLOAT ]: FloatType,
[ VK_FORMAT_R16G16B16A16_SFLOAT ]: HalfFloatType,
[ VK_FORMAT_R8G8B8A8_UNORM ]: UnsignedByteType,
[ VK_FORMAT_R8G8B8A8_SRGB ]: UnsignedByteType,
[ VK_FORMAT_R32G32_SFLOAT ]: FloatType,
[ VK_FORMAT_R16G16_SFLOAT ]: HalfFloatType,
[ VK_FORMAT_R8G8_UNORM ]: UnsignedByteType,
[ VK_FORMAT_R8G8_SRGB ]: UnsignedByteType,
[ VK_FORMAT_R32_SFLOAT ]: FloatType,
[ VK_FORMAT_R16_SFLOAT ]: HalfFloatType,
[ VK_FORMAT_R8_SRGB ]: UnsignedByteType,
[ VK_FORMAT_R8_UNORM ]: UnsignedByteType,
[ VK_FORMAT_ASTC_6x6_SRGB_BLOCK ]: UnsignedByteType,
[ VK_FORMAT_ASTC_6x6_UNORM_BLOCK ]: UnsignedByteType,
};
async function createRawTexture( container ) {
const { vkFormat } = container;
if ( FORMAT_MAP[ vkFormat ] === undefined ) {
throw new Error( 'THREE.KTX2Loader: Unsupported vkFormat.' );
}
//
let zstd;
if ( container.supercompressionScheme === KHR_SUPERCOMPRESSION_ZSTD ) {
if ( ! _zstd ) {
_zstd = new Promise( async ( resolve ) => {
const zstd = new ZSTDDecoder();
await zstd.init();
resolve( zstd );
} );
}
zstd = await _zstd;
}
//
const mipmaps = [];
for ( let levelIndex = 0; levelIndex < container.levels.length; levelIndex ++ ) {
const levelWidth = Math.max( 1, container.pixelWidth >> levelIndex );
const levelHeight = Math.max( 1, container.pixelHeight >> levelIndex );
const levelDepth = container.pixelDepth ? Math.max( 1, container.pixelDepth >> levelIndex ) : 0;
const level = container.levels[ levelIndex ];
let levelData;
if ( container.supercompressionScheme === KHR_SUPERCOMPRESSION_NONE ) {
levelData = level.levelData;
} else if ( container.supercompressionScheme === KHR_SUPERCOMPRESSION_ZSTD ) {
levelData = zstd.decode( level.levelData, level.uncompressedByteLength );
} else {
throw new Error( 'THREE.KTX2Loader: Unsupported supercompressionScheme.' );
}
let data;
if ( TYPE_MAP[ vkFormat ] === FloatType ) {
data = new Float32Array(
levelData.buffer,
levelData.byteOffset,
levelData.byteLength / Float32Array.BYTES_PER_ELEMENT
);
} else if ( TYPE_MAP[ vkFormat ] === HalfFloatType ) {
data = new Uint16Array(
levelData.buffer,
levelData.byteOffset,
levelData.byteLength / Uint16Array.BYTES_PER_ELEMENT
);
} else {
data = levelData;
}
mipmaps.push( {
data: data,
width: levelWidth,
height: levelHeight,
depth: levelDepth,
} );
}
let texture;
if ( UNCOMPRESSED_FORMATS.has( FORMAT_MAP[ vkFormat ] ) ) {
texture = container.pixelDepth === 0
? new DataTexture( mipmaps[ 0 ].data, container.pixelWidth, container.pixelHeight )
: new Data3DTexture( mipmaps[ 0 ].data, container.pixelWidth, container.pixelHeight, container.pixelDepth );
} else {
if ( container.pixelDepth > 0 ) throw new Error( 'THREE.KTX2Loader: Unsupported pixelDepth.' );
texture = new CompressedTexture( mipmaps, container.pixelWidth, container.pixelHeight );
}
texture.mipmaps = mipmaps;
texture.type = TYPE_MAP[ vkFormat ];
texture.format = FORMAT_MAP[ vkFormat ];
texture.colorSpace = parseColorSpace( container );
texture.needsUpdate = true;
//
return Promise.resolve( texture );
}
function parseColorSpace( container ) {
const dfd = container.dataFormatDescriptor[ 0 ];
if ( dfd.colorPrimaries === KHR_DF_PRIMARIES_BT709 ) {
return dfd.transferFunction === KHR_DF_TRANSFER_SRGB ? SRGBColorSpace : LinearSRGBColorSpace;
} else if ( dfd.colorPrimaries === KHR_DF_PRIMARIES_DISPLAYP3 ) {
return dfd.transferFunction === KHR_DF_TRANSFER_SRGB ? DisplayP3ColorSpace : LinearDisplayP3ColorSpace;
} else if ( dfd.colorPrimaries === KHR_DF_PRIMARIES_UNSPECIFIED ) {
return NoColorSpace;
} else {
console.warn( `THREE.KTX2Loader: Unsupported color primaries, "${ dfd.colorPrimaries }"` );
return NoColorSpace;
}
}
export { KTX2Loader };

View File

@ -0,0 +1,176 @@
import {
CompressedTextureLoader
} from 'three';
/**
* for description see https://www.khronos.org/opengles/sdk/tools/KTX/
* for file layout see https://www.khronos.org/opengles/sdk/tools/KTX/file_format_spec/
*
* ported from https://github.com/BabylonJS/Babylon.js/blob/master/src/Misc/khronosTextureContainer.ts
*/
class KTXLoader extends CompressedTextureLoader {
constructor( manager ) {
super( manager );
}
parse( buffer, loadMipmaps ) {
const ktx = new KhronosTextureContainer( buffer, 1 );
return {
mipmaps: ktx.mipmaps( loadMipmaps ),
width: ktx.pixelWidth,
height: ktx.pixelHeight,
format: ktx.glInternalFormat,
isCubemap: ktx.numberOfFaces === 6,
mipmapCount: ktx.numberOfMipmapLevels
};
}
}
const HEADER_LEN = 12 + ( 13 * 4 ); // identifier + header elements (not including key value meta-data pairs)
// load types
const COMPRESSED_2D = 0; // uses a gl.compressedTexImage2D()
//const COMPRESSED_3D = 1; // uses a gl.compressedTexImage3D()
//const TEX_2D = 2; // uses a gl.texImage2D()
//const TEX_3D = 3; // uses a gl.texImage3D()
class KhronosTextureContainer {
/**
* @param {ArrayBuffer} arrayBuffer- contents of the KTX container file
* @param {number} facesExpected- should be either 1 or 6, based whether a cube texture or or
* @param {boolean} threeDExpected- provision for indicating that data should be a 3D texture, not implemented
* @param {boolean} textureArrayExpected- provision for indicating that data should be a texture array, not implemented
*/
constructor( arrayBuffer, facesExpected /*, threeDExpected, textureArrayExpected */ ) {
this.arrayBuffer = arrayBuffer;
// Test that it is a ktx formatted file, based on the first 12 bytes, character representation is:
// '´', 'K', 'T', 'X', ' ', '1', '1', 'ª', '\r', '\n', '\x1A', '\n'
// 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A
const identifier = new Uint8Array( this.arrayBuffer, 0, 12 );
if ( identifier[ 0 ] !== 0xAB ||
identifier[ 1 ] !== 0x4B ||
identifier[ 2 ] !== 0x54 ||
identifier[ 3 ] !== 0x58 ||
identifier[ 4 ] !== 0x20 ||
identifier[ 5 ] !== 0x31 ||
identifier[ 6 ] !== 0x31 ||
identifier[ 7 ] !== 0xBB ||
identifier[ 8 ] !== 0x0D ||
identifier[ 9 ] !== 0x0A ||
identifier[ 10 ] !== 0x1A ||
identifier[ 11 ] !== 0x0A ) {
console.error( 'texture missing KTX identifier' );
return;
}
// load the reset of the header in native 32 bit uint
const dataSize = Uint32Array.BYTES_PER_ELEMENT;
const headerDataView = new DataView( this.arrayBuffer, 12, 13 * dataSize );
const endianness = headerDataView.getUint32( 0, true );
const littleEndian = endianness === 0x04030201;
this.glType = headerDataView.getUint32( 1 * dataSize, littleEndian ); // must be 0 for compressed textures
this.glTypeSize = headerDataView.getUint32( 2 * dataSize, littleEndian ); // must be 1 for compressed textures
this.glFormat = headerDataView.getUint32( 3 * dataSize, littleEndian ); // must be 0 for compressed textures
this.glInternalFormat = headerDataView.getUint32( 4 * dataSize, littleEndian ); // the value of arg passed to gl.compressedTexImage2D(,,x,,,,)
this.glBaseInternalFormat = headerDataView.getUint32( 5 * dataSize, littleEndian ); // specify GL_RGB, GL_RGBA, GL_ALPHA, etc (un-compressed only)
this.pixelWidth = headerDataView.getUint32( 6 * dataSize, littleEndian ); // level 0 value of arg passed to gl.compressedTexImage2D(,,,x,,,)
this.pixelHeight = headerDataView.getUint32( 7 * dataSize, littleEndian ); // level 0 value of arg passed to gl.compressedTexImage2D(,,,,x,,)
this.pixelDepth = headerDataView.getUint32( 8 * dataSize, littleEndian ); // level 0 value of arg passed to gl.compressedTexImage3D(,,,,,x,,)
this.numberOfArrayElements = headerDataView.getUint32( 9 * dataSize, littleEndian ); // used for texture arrays
this.numberOfFaces = headerDataView.getUint32( 10 * dataSize, littleEndian ); // used for cubemap textures, should either be 1 or 6
this.numberOfMipmapLevels = headerDataView.getUint32( 11 * dataSize, littleEndian ); // number of levels; disregard possibility of 0 for compressed textures
this.bytesOfKeyValueData = headerDataView.getUint32( 12 * dataSize, littleEndian ); // the amount of space after the header for meta-data
// Make sure we have a compressed type. Not only reduces work, but probably better to let dev know they are not compressing.
if ( this.glType !== 0 ) {
console.warn( 'only compressed formats currently supported' );
return;
} else {
// value of zero is an indication to generate mipmaps @ runtime. Not usually allowed for compressed, so disregard.
this.numberOfMipmapLevels = Math.max( 1, this.numberOfMipmapLevels );
}
if ( this.pixelHeight === 0 || this.pixelDepth !== 0 ) {
console.warn( 'only 2D textures currently supported' );
return;
}
if ( this.numberOfArrayElements !== 0 ) {
console.warn( 'texture arrays not currently supported' );
return;
}
if ( this.numberOfFaces !== facesExpected ) {
console.warn( 'number of faces expected' + facesExpected + ', but found ' + this.numberOfFaces );
return;
}
// we now have a completely validated file, so could use existence of loadType as success
// would need to make this more elaborate & adjust checks above to support more than one load type
this.loadType = COMPRESSED_2D;
}
mipmaps( loadMipmaps ) {
const mipmaps = [];
// initialize width & height for level 1
let dataOffset = HEADER_LEN + this.bytesOfKeyValueData;
let width = this.pixelWidth;
let height = this.pixelHeight;
const mipmapCount = loadMipmaps ? this.numberOfMipmapLevels : 1;
for ( let level = 0; level < mipmapCount; level ++ ) {
const imageSize = new Int32Array( this.arrayBuffer, dataOffset, 1 )[ 0 ]; // size per face, since not supporting array cubemaps
dataOffset += 4; // size of the image + 4 for the imageSize field
for ( let face = 0; face < this.numberOfFaces; face ++ ) {
const byteArray = new Uint8Array( this.arrayBuffer, dataOffset, imageSize );
mipmaps.push( { 'data': byteArray, 'width': width, 'height': height } );
dataOffset += imageSize;
dataOffset += 3 - ( ( imageSize + 3 ) % 4 ); // add padding for odd sized image
}
width = Math.max( 1.0, width * 0.5 );
height = Math.max( 1.0, height * 0.5 );
}
return mipmaps;
}
}
export { KTXLoader };

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,168 @@
// http://download.autodesk.com/us/systemdocs/help/2011/lustre/index.html?url=./files/WSc4e151a45a3b785a24c3d9a411df9298473-7ffd.htm,topicNumber=d0e9492
// https://community.foundry.com/discuss/topic/103636/format-spec-for-3dl?mode=Post&postID=895258
import {
ClampToEdgeWrapping,
Data3DTexture,
FileLoader,
FloatType,
LinearFilter,
Loader,
RGBAFormat,
UnsignedByteType,
} from 'three';
export class LUT3dlLoader extends Loader {
constructor( manager ) {
super( manager );
this.type = UnsignedByteType;
}
setType( type ) {
if ( type !== UnsignedByteType && type !== FloatType ) {
throw new Error( 'LUT3dlLoader: Unsupported type' );
}
this.type = type;
return this;
}
load( url, onLoad, onProgress, onError ) {
const loader = new FileLoader( this.manager );
loader.setPath( this.path );
loader.setResponseType( 'text' );
loader.load( url, text => {
try {
onLoad( this.parse( text ) );
} catch ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
this.manager.itemError( url );
}
}, onProgress, onError );
}
parse( input ) {
const regExpGridInfo = /^[\d ]+$/m;
const regExpDataPoints = /^([\d.e+-]+) +([\d.e+-]+) +([\d.e+-]+) *$/gm;
// The first line describes the positions of values on the LUT grid.
let result = regExpGridInfo.exec( input );
if ( result === null ) {
throw new Error( 'LUT3dlLoader: Missing grid information' );
}
const gridLines = result[ 0 ].trim().split( /\s+/g ).map( Number );
const gridStep = gridLines[ 1 ] - gridLines[ 0 ];
const size = gridLines.length;
const sizeSq = size ** 2;
for ( let i = 1, l = gridLines.length; i < l; ++ i ) {
if ( gridStep !== ( gridLines[ i ] - gridLines[ i - 1 ] ) ) {
throw new Error( 'LUT3dlLoader: Inconsistent grid size' );
}
}
const dataFloat = new Float32Array( size ** 3 * 4 );
let maxValue = 0.0;
let index = 0;
while ( ( result = regExpDataPoints.exec( input ) ) !== null ) {
const r = Number( result[ 1 ] );
const g = Number( result[ 2 ] );
const b = Number( result[ 3 ] );
maxValue = Math.max( maxValue, r, g, b );
const bLayer = index % size;
const gLayer = Math.floor( index / size ) % size;
const rLayer = Math.floor( index / ( sizeSq ) ) % size;
// b grows first, then g, then r.
const d4 = ( bLayer * sizeSq + gLayer * size + rLayer ) * 4;
dataFloat[ d4 + 0 ] = r;
dataFloat[ d4 + 1 ] = g;
dataFloat[ d4 + 2 ] = b;
++ index;
}
// Determine the bit depth to scale the values to [0.0, 1.0].
const bits = Math.ceil( Math.log2( maxValue ) );
const maxBitValue = Math.pow( 2, bits );
const data = this.type === UnsignedByteType ? new Uint8Array( dataFloat.length ) : dataFloat;
const scale = this.type === UnsignedByteType ? 255 : 1;
for ( let i = 0, l = data.length; i < l; i += 4 ) {
const i1 = i + 1;
const i2 = i + 2;
const i3 = i + 3;
// Note: data is dataFloat when type is FloatType.
data[ i ] = dataFloat[ i ] / maxBitValue * scale;
data[ i1 ] = dataFloat[ i1 ] / maxBitValue * scale;
data[ i2 ] = dataFloat[ i2 ] / maxBitValue * scale;
data[ i3 ] = scale;
}
const texture3D = new Data3DTexture();
texture3D.image.data = data;
texture3D.image.width = size;
texture3D.image.height = size;
texture3D.image.depth = size;
texture3D.format = RGBAFormat;
texture3D.type = this.type;
texture3D.magFilter = LinearFilter;
texture3D.minFilter = LinearFilter;
texture3D.wrapS = ClampToEdgeWrapping;
texture3D.wrapT = ClampToEdgeWrapping;
texture3D.wrapR = ClampToEdgeWrapping;
texture3D.generateMipmaps = false;
texture3D.needsUpdate = true;
return {
size,
texture3D,
};
}
}

View File

@ -0,0 +1,153 @@
// https://wwwimages2.adobe.com/content/dam/acom/en/products/speedgrade/cc/pdfs/cube-lut-specification-1.0.pdf
import {
ClampToEdgeWrapping,
Data3DTexture,
FileLoader,
FloatType,
LinearFilter,
Loader,
UnsignedByteType,
Vector3,
} from 'three';
export class LUTCubeLoader extends Loader {
constructor( manager ) {
super( manager );
this.type = UnsignedByteType;
}
setType( type ) {
if ( type !== UnsignedByteType && type !== FloatType ) {
throw new Error( 'LUTCubeLoader: Unsupported type' );
}
this.type = type;
return this;
}
load( url, onLoad, onProgress, onError ) {
const loader = new FileLoader( this.manager );
loader.setPath( this.path );
loader.setResponseType( 'text' );
loader.load( url, text => {
try {
onLoad( this.parse( text ) );
} catch ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
this.manager.itemError( url );
}
}, onProgress, onError );
}
parse( input ) {
const regExpTitle = /TITLE +"([^"]*)"/;
const regExpSize = /LUT_3D_SIZE +(\d+)/;
const regExpDomainMin = /DOMAIN_MIN +([\d.]+) +([\d.]+) +([\d.]+)/;
const regExpDomainMax = /DOMAIN_MAX +([\d.]+) +([\d.]+) +([\d.]+)/;
const regExpDataPoints = /^([\d.e+-]+) +([\d.e+-]+) +([\d.e+-]+) *$/gm;
let result = regExpTitle.exec( input );
const title = ( result !== null ) ? result[ 1 ] : null;
result = regExpSize.exec( input );
if ( result === null ) {
throw new Error( 'LUTCubeLoader: Missing LUT_3D_SIZE information' );
}
const size = Number( result[ 1 ] );
const length = size ** 3 * 4;
const data = this.type === UnsignedByteType ? new Uint8Array( length ) : new Float32Array( length );
const domainMin = new Vector3( 0, 0, 0 );
const domainMax = new Vector3( 1, 1, 1 );
result = regExpDomainMin.exec( input );
if ( result !== null ) {
domainMin.set( Number( result[ 1 ] ), Number( result[ 2 ] ), Number( result[ 3 ] ) );
}
result = regExpDomainMax.exec( input );
if ( result !== null ) {
domainMax.set( Number( result[ 1 ] ), Number( result[ 2 ] ), Number( result[ 3 ] ) );
}
if ( domainMin.x > domainMax.x || domainMin.y > domainMax.y || domainMin.z > domainMax.z ) {
throw new Error( 'LUTCubeLoader: Invalid input domain' );
}
const scale = this.type === UnsignedByteType ? 255 : 1;
let i = 0;
while ( ( result = regExpDataPoints.exec( input ) ) !== null ) {
data[ i ++ ] = Number( result[ 1 ] ) * scale;
data[ i ++ ] = Number( result[ 2 ] ) * scale;
data[ i ++ ] = Number( result[ 3 ] ) * scale;
data[ i ++ ] = scale;
}
const texture3D = new Data3DTexture();
texture3D.image.data = data;
texture3D.image.width = size;
texture3D.image.height = size;
texture3D.image.depth = size;
texture3D.type = this.type;
texture3D.magFilter = LinearFilter;
texture3D.minFilter = LinearFilter;
texture3D.wrapS = ClampToEdgeWrapping;
texture3D.wrapT = ClampToEdgeWrapping;
texture3D.wrapR = ClampToEdgeWrapping;
texture3D.generateMipmaps = false;
texture3D.needsUpdate = true;
return {
title,
size,
domainMin,
domainMax,
texture3D,
};
}
}

View File

@ -0,0 +1,149 @@
import {
Loader,
TextureLoader,
Data3DTexture,
RGBAFormat,
UnsignedByteType,
ClampToEdgeWrapping,
LinearFilter,
} from 'three';
export class LUTImageLoader extends Loader {
constructor( flipVertical = false ) {
//The NeutralLUT.png has green at the bottom for Unreal ang green at the top for Unity URP Color Lookup
//post-processing. If you're using lut image strips from a Unity pipeline then pass true to the constructor
super();
this.flip = flipVertical;
}
load( url, onLoad, onProgress, onError ) {
const loader = new TextureLoader( this.manager );
loader.setCrossOrigin( this.crossOrigin );
loader.setPath( this.path );
loader.load( url, texture => {
try {
let imageData;
if ( texture.image.width < texture.image.height ) {
imageData = this.getImageData( texture );
} else {
imageData = this.horz2Vert( texture );
}
onLoad( this.parse( imageData.data, Math.min( texture.image.width, texture.image.height ) ) );
} catch ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
this.manager.itemError( url );
}
}, onProgress, onError );
}
getImageData( texture ) {
const width = texture.image.width;
const height = texture.image.height;
const canvas = document.createElement( 'canvas' );
canvas.width = width;
canvas.height = height;
const context = canvas.getContext( '2d' );
if ( this.flip === true ) {
context.scale( 1, - 1 );
context.translate( 0, - height );
}
context.drawImage( texture.image, 0, 0 );
return context.getImageData( 0, 0, width, height );
}
horz2Vert( texture ) {
const width = texture.image.height;
const height = texture.image.width;
const canvas = document.createElement( 'canvas' );
canvas.width = width;
canvas.height = height;
const context = canvas.getContext( '2d' );
if ( this.flip === true ) {
context.scale( 1, - 1 );
context.translate( 0, - height );
}
for ( let i = 0; i < width; i ++ ) {
const sy = i * width;
const dy = ( this.flip ) ? height - i * width : i * width;
context.drawImage( texture.image, sy, 0, width, width, 0, dy, width, width );
}
return context.getImageData( 0, 0, width, height );
}
parse( dataArray, size ) {
const data = new Uint8Array( dataArray );
const texture3D = new Data3DTexture();
texture3D.image.data = data;
texture3D.image.width = size;
texture3D.image.height = size;
texture3D.image.depth = size;
texture3D.format = RGBAFormat;
texture3D.type = UnsignedByteType;
texture3D.magFilter = LinearFilter;
texture3D.minFilter = LinearFilter;
texture3D.wrapS = ClampToEdgeWrapping;
texture3D.wrapT = ClampToEdgeWrapping;
texture3D.wrapR = ClampToEdgeWrapping;
texture3D.generateMipmaps = false;
texture3D.needsUpdate = true;
return {
size,
texture3D,
};
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,606 @@
import {
DataUtils,
DataTextureLoader,
FloatType,
HalfFloatType,
RGBAFormat
} from 'three';
class LogLuvLoader extends DataTextureLoader {
constructor( manager ) {
super( manager );
this.type = HalfFloatType;
}
parse( buffer ) {
const ifds = UTIF.decode( buffer );
UTIF.decodeImage( buffer, ifds[ 0 ] );
const rgba = UTIF.toRGBA( ifds[ 0 ], this.type );
return {
width: ifds[ 0 ].width,
height: ifds[ 0 ].height,
data: rgba,
format: RGBAFormat,
type: this.type,
flipY: true
};
}
setDataType( value ) {
this.type = value;
return this;
}
}
// from https://github.com/photopea/UTIF.js (MIT License)
const UTIF = {};
UTIF.decode = function ( buff, prm ) {
if ( prm == null ) prm = { parseMN: true, debug: false }; // read MakerNote, debug
var data = new Uint8Array( buff ), offset = 0;
var id = UTIF._binBE.readASCII( data, offset, 2 ); offset += 2;
var bin = id == 'II' ? UTIF._binLE : UTIF._binBE;
bin.readUshort( data, offset ); offset += 2;
var ifdo = bin.readUint( data, offset );
var ifds = [];
while ( true ) {
var cnt = bin.readUshort( data, ifdo ), typ = bin.readUshort( data, ifdo + 4 ); if ( cnt != 0 ) if ( typ < 1 || 13 < typ ) {
console.log( 'error in TIFF' ); break;
}
UTIF._readIFD( bin, data, ifdo, ifds, 0, prm );
ifdo = bin.readUint( data, ifdo + 2 + cnt * 12 );
if ( ifdo == 0 ) break;
}
return ifds;
};
UTIF.decodeImage = function ( buff, img, ifds ) {
if ( img.data ) return;
var data = new Uint8Array( buff );
var id = UTIF._binBE.readASCII( data, 0, 2 );
if ( img[ 't256' ] == null ) return; // No width => probably not an image
img.isLE = id == 'II';
img.width = img[ 't256' ][ 0 ]; //delete img["t256"];
img.height = img[ 't257' ][ 0 ]; //delete img["t257"];
var cmpr = img[ 't259' ] ? img[ 't259' ][ 0 ] : 1; //delete img["t259"];
var fo = img[ 't266' ] ? img[ 't266' ][ 0 ] : 1; //delete img["t266"];
if ( img[ 't284' ] && img[ 't284' ][ 0 ] == 2 ) console.log( 'PlanarConfiguration 2 should not be used!' );
if ( cmpr == 7 && img[ 't258' ] && img[ 't258' ].length > 3 ) img[ 't258' ] = img[ 't258' ].slice( 0, 3 );
var bipp; // bits per pixel
if ( img[ 't258' ] ) bipp = Math.min( 32, img[ 't258' ][ 0 ] ) * img[ 't258' ].length;
else bipp = ( img[ 't277' ] ? img[ 't277' ][ 0 ] : 1 );
// Some .NEF files have t258==14, even though they use 16 bits per pixel
if ( cmpr == 1 && img[ 't279' ] != null && img[ 't278' ] && img[ 't262' ][ 0 ] == 32803 ) {
bipp = Math.round( ( img[ 't279' ][ 0 ] * 8 ) / ( img.width * img[ 't278' ][ 0 ] ) );
}
var bipl = Math.ceil( img.width * bipp / 8 ) * 8;
var soff = img[ 't273' ]; if ( soff == null ) soff = img[ 't324' ];
var bcnt = img[ 't279' ]; if ( cmpr == 1 && soff.length == 1 ) bcnt = [ img.height * ( bipl >>> 3 ) ]; if ( bcnt == null ) bcnt = img[ 't325' ];
//bcnt[0] = Math.min(bcnt[0], data.length); // Hasselblad, "RAW_HASSELBLAD_H3D39II.3FR"
var bytes = new Uint8Array( img.height * ( bipl >>> 3 ) ), bilen = 0;
if ( img[ 't322' ] != null ) {
var tw = img[ 't322' ][ 0 ], th = img[ 't323' ][ 0 ];
var tx = Math.floor( ( img.width + tw - 1 ) / tw );
var ty = Math.floor( ( img.height + th - 1 ) / th );
var tbuff = new Uint8Array( Math.ceil( tw * th * bipp / 8 ) | 0 );
for ( var y = 0; y < ty; y ++ )
for ( var x = 0; x < tx; x ++ ) {
var i = y * tx + x; for ( var j = 0; j < tbuff.length; j ++ ) tbuff[ j ] = 0;
UTIF.decode._decompress( img, ifds, data, soff[ i ], bcnt[ i ], cmpr, tbuff, 0, fo );
// Might be required for 7 too. Need to check
if ( cmpr == 6 ) bytes = tbuff;
else UTIF._copyTile( tbuff, Math.ceil( tw * bipp / 8 ) | 0, th, bytes, Math.ceil( img.width * bipp / 8 ) | 0, img.height, Math.ceil( x * tw * bipp / 8 ) | 0, y * th );
}
bilen = bytes.length * 8;
} else {
var rps = img[ 't278' ] ? img[ 't278' ][ 0 ] : img.height; rps = Math.min( rps, img.height );
for ( var i = 0; i < soff.length; i ++ ) {
UTIF.decode._decompress( img, ifds, data, soff[ i ], bcnt[ i ], cmpr, bytes, Math.ceil( bilen / 8 ) | 0, fo );
bilen += bipl * rps;
}
bilen = Math.min( bilen, bytes.length * 8 );
}
img.data = new Uint8Array( bytes.buffer, 0, Math.ceil( bilen / 8 ) | 0 );
};
UTIF.decode._decompress = function ( img, ifds, data, off, len, cmpr, tgt, toff ) {
//console.log("compression", cmpr);
//var time = Date.now();
if ( cmpr == 34676 ) UTIF.decode._decodeLogLuv32( img, data, off, len, tgt, toff );
else console.log( 'Unsupported compression', cmpr );
//console.log(Date.now()-time);
var bps = ( img[ 't258' ] ? Math.min( 32, img[ 't258' ][ 0 ] ) : 1 );
var noc = ( img[ 't277' ] ? img[ 't277' ][ 0 ] : 1 ), bpp = ( bps * noc ) >>> 3, h = ( img[ 't278' ] ? img[ 't278' ][ 0 ] : img.height ), bpl = Math.ceil( bps * noc * img.width / 8 );
// convert to Little Endian /*
if ( bps == 16 && ! img.isLE && img[ 't33422' ] == null ) // not DNG
for ( var y = 0; y < h; y ++ ) {
//console.log("fixing endianity");
var roff = toff + y * bpl;
for ( var x = 1; x < bpl; x += 2 ) {
var t = tgt[ roff + x ]; tgt[ roff + x ] = tgt[ roff + x - 1 ]; tgt[ roff + x - 1 ] = t;
}
} //*/
if ( img[ 't317' ] && img[ 't317' ][ 0 ] == 2 ) {
for ( var y = 0; y < h; y ++ ) {
var ntoff = toff + y * bpl;
if ( bps == 16 ) for ( var j = bpp; j < bpl; j += 2 ) {
var nv = ( ( tgt[ ntoff + j + 1 ] << 8 ) | tgt[ ntoff + j ] ) + ( ( tgt[ ntoff + j - bpp + 1 ] << 8 ) | tgt[ ntoff + j - bpp ] );
tgt[ ntoff + j ] = nv & 255; tgt[ ntoff + j + 1 ] = ( nv >>> 8 ) & 255;
}
else if ( noc == 3 ) for ( var j = 3; j < bpl; j += 3 ) {
tgt[ ntoff + j ] = ( tgt[ ntoff + j ] + tgt[ ntoff + j - 3 ] ) & 255;
tgt[ ntoff + j + 1 ] = ( tgt[ ntoff + j + 1 ] + tgt[ ntoff + j - 2 ] ) & 255;
tgt[ ntoff + j + 2 ] = ( tgt[ ntoff + j + 2 ] + tgt[ ntoff + j - 1 ] ) & 255;
}
else for ( var j = bpp; j < bpl; j ++ ) tgt[ ntoff + j ] = ( tgt[ ntoff + j ] + tgt[ ntoff + j - bpp ] ) & 255;
}
}
};
UTIF.decode._decodeLogLuv32 = function ( img, data, off, len, tgt, toff ) {
var w = img.width, qw = w * 4;
var io = 0, out = new Uint8Array( qw );
while ( io < len ) {
var oo = 0;
while ( oo < qw ) {
var c = data[ off + io ]; io ++;
if ( c < 128 ) {
for ( var j = 0; j < c; j ++ ) out[ oo + j ] = data[ off + io + j ]; oo += c; io += c;
} else {
c = c - 126; for ( var j = 0; j < c; j ++ ) out[ oo + j ] = data[ off + io ]; oo += c; io ++;
}
}
for ( var x = 0; x < w; x ++ ) {
tgt[ toff + 0 ] = out[ x ];
tgt[ toff + 1 ] = out[ x + w ];
tgt[ toff + 2 ] = out[ x + w * 2 ];
tgt[ toff + 4 ] = out[ x + w * 3 ];
toff += 6;
}
}
};
UTIF.tags = {};
//UTIF.ttypes = { 256:3,257:3,258:3, 259:3, 262:3, 273:4, 274:3, 277:3,278:4,279:4, 282:5, 283:5, 284:3, 286:5,287:5, 296:3, 305:2, 306:2, 338:3, 513:4, 514:4, 34665:4 };
// start at tag 250
UTIF._types = function () {
var main = new Array( 250 ); main.fill( 0 );
main = main.concat( [ 0, 0, 0, 0, 4, 3, 3, 3, 3, 3, 0, 0, 3, 0, 0, 0, 3, 0, 0, 2, 2, 2, 2, 4, 3, 0, 0, 3, 4, 4, 3, 3, 5, 5, 3, 2, 5, 5, 0, 0, 0, 0, 4, 4, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 3, 5, 5, 3, 0, 3, 3, 4, 4, 4, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] );
var rest = { 33432: 2, 33434: 5, 33437: 5, 34665: 4, 34850: 3, 34853: 4, 34855: 3, 34864: 3, 34866: 4, 36864: 7, 36867: 2, 36868: 2, 37121: 7, 37377: 10, 37378: 5, 37380: 10, 37381: 5, 37383: 3, 37384: 3, 37385: 3, 37386: 5, 37510: 7, 37520: 2, 37521: 2, 37522: 2, 40960: 7, 40961: 3, 40962: 4, 40963: 4, 40965: 4, 41486: 5, 41487: 5, 41488: 3, 41985: 3, 41986: 3, 41987: 3, 41988: 5, 41989: 3, 41990: 3, 41993: 3, 41994: 3, 41995: 7, 41996: 3, 42032: 2, 42033: 2, 42034: 5, 42036: 2, 42037: 2, 59932: 7 };
return {
basic: {
main: main,
rest: rest
},
gps: {
main: [ 1, 2, 5, 2, 5, 1, 5, 5, 0, 9 ],
rest: { 18: 2, 29: 2 }
}
};
}();
UTIF._readIFD = function ( bin, data, offset, ifds, depth, prm ) {
var cnt = bin.readUshort( data, offset ); offset += 2;
var ifd = {};
if ( prm.debug ) console.log( ' '.repeat( depth ), ifds.length - 1, '>>>----------------' );
for ( var i = 0; i < cnt; i ++ ) {
var tag = bin.readUshort( data, offset ); offset += 2;
var type = bin.readUshort( data, offset ); offset += 2;
var num = bin.readUint( data, offset ); offset += 4;
var voff = bin.readUint( data, offset ); offset += 4;
var arr = [];
//ifd["t"+tag+"-"+UTIF.tags[tag]] = arr;
if ( type == 1 || type == 7 ) {
arr = new Uint8Array( data.buffer, ( num < 5 ? offset - 4 : voff ), num );
}
if ( type == 2 ) {
var o0 = ( num < 5 ? offset - 4 : voff ), c = data[ o0 ], len = Math.max( 0, Math.min( num - 1, data.length - o0 ) );
if ( c < 128 || len == 0 ) arr.push( bin.readASCII( data, o0, len ) );
else arr = new Uint8Array( data.buffer, o0, len );
}
if ( type == 3 ) {
for ( var j = 0; j < num; j ++ ) arr.push( bin.readUshort( data, ( num < 3 ? offset - 4 : voff ) + 2 * j ) );
}
if ( type == 4
|| type == 13 ) {
for ( var j = 0; j < num; j ++ ) arr.push( bin.readUint( data, ( num < 2 ? offset - 4 : voff ) + 4 * j ) );
}
if ( type == 5 || type == 10 ) {
var ri = type == 5 ? bin.readUint : bin.readInt;
for ( var j = 0; j < num; j ++ ) arr.push( [ ri( data, voff + j * 8 ), ri( data, voff + j * 8 + 4 ) ] );
}
if ( type == 8 ) {
for ( var j = 0; j < num; j ++ ) arr.push( bin.readShort( data, ( num < 3 ? offset - 4 : voff ) + 2 * j ) );
}
if ( type == 9 ) {
for ( var j = 0; j < num; j ++ ) arr.push( bin.readInt( data, ( num < 2 ? offset - 4 : voff ) + 4 * j ) );
}
if ( type == 11 ) {
for ( var j = 0; j < num; j ++ ) arr.push( bin.readFloat( data, voff + j * 4 ) );
}
if ( type == 12 ) {
for ( var j = 0; j < num; j ++ ) arr.push( bin.readDouble( data, voff + j * 8 ) );
}
if ( num != 0 && arr.length == 0 ) {
console.log( tag, 'unknown TIFF tag type: ', type, 'num:', num ); if ( i == 0 ) return; continue;
}
if ( prm.debug ) console.log( ' '.repeat( depth ), tag, type, UTIF.tags[ tag ], arr );
ifd[ 't' + tag ] = arr;
if ( tag == 330 || tag == 34665 || tag == 34853 || ( tag == 50740 && bin.readUshort( data, bin.readUint( arr, 0 ) ) < 300 ) || tag == 61440 ) {
var oarr = tag == 50740 ? [ bin.readUint( arr, 0 ) ] : arr;
var subfd = [];
for ( var j = 0; j < oarr.length; j ++ ) UTIF._readIFD( bin, data, oarr[ j ], subfd, depth + 1, prm );
if ( tag == 330 ) ifd.subIFD = subfd;
if ( tag == 34665 ) ifd.exifIFD = subfd[ 0 ];
if ( tag == 34853 ) ifd.gpsiIFD = subfd[ 0 ]; //console.log("gps", subfd[0]); }
if ( tag == 50740 ) ifd.dngPrvt = subfd[ 0 ];
if ( tag == 61440 ) ifd.fujiIFD = subfd[ 0 ];
}
if ( tag == 37500 && prm.parseMN ) {
var mn = arr;
//console.log(bin.readASCII(mn,0,mn.length), mn);
if ( bin.readASCII( mn, 0, 5 ) == 'Nikon' ) ifd.makerNote = UTIF[ 'decode' ]( mn.slice( 10 ).buffer )[ 0 ];
else if ( bin.readUshort( data, voff ) < 300 && bin.readUshort( data, voff + 4 ) <= 12 ) {
var subsub = []; UTIF._readIFD( bin, data, voff, subsub, depth + 1, prm );
ifd.makerNote = subsub[ 0 ];
}
}
}
ifds.push( ifd );
if ( prm.debug ) console.log( ' '.repeat( depth ), '<<<---------------' );
return offset;
};
UTIF.toRGBA = function ( out, type ) {
const w = out.width, h = out.height, area = w * h, data = out.data;
let img;
switch ( type ) {
case HalfFloatType:
img = new Uint16Array( area * 4 );
break;
case FloatType:
img = new Float32Array( area * 4 );
break;
default:
throw new Error( 'THREE.LogLuvLoader: Unsupported texture data type: ' + type );
}
let intp = out[ 't262' ] ? out[ 't262' ][ 0 ] : 2;
const bps = out[ 't258' ] ? Math.min( 32, out[ 't258' ][ 0 ] ) : 1;
if ( out[ 't262' ] == null && bps == 1 ) intp = 0;
if ( intp == 32845 ) {
for ( let y = 0; y < h; y ++ ) {
for ( let x = 0; x < w; x ++ ) {
const si = ( y * w + x ) * 6, qi = ( y * w + x ) * 4;
let L = ( data[ si + 1 ] << 8 ) | data[ si ];
L = Math.pow( 2, ( L + 0.5 ) / 256 - 64 );
const u = ( data[ si + 3 ] + 0.5 ) / 410;
const v = ( data[ si + 5 ] + 0.5 ) / 410;
// Luv to xyY
const sX = ( 9 * u ) / ( 6 * u - 16 * v + 12 );
const sY = ( 4 * v ) / ( 6 * u - 16 * v + 12 );
const bY = L;
// xyY to XYZ
const X = ( sX * bY ) / sY, Y = bY, Z = ( 1 - sX - sY ) * bY / sY;
// XYZ to linear RGB
const r = 2.690 * X - 1.276 * Y - 0.414 * Z;
const g = - 1.022 * X + 1.978 * Y + 0.044 * Z;
const b = 0.061 * X - 0.224 * Y + 1.163 * Z;
if ( type === HalfFloatType ) {
img[ qi ] = DataUtils.toHalfFloat( Math.min( r, 65504 ) );
img[ qi + 1 ] = DataUtils.toHalfFloat( Math.min( g, 65504 ) );
img[ qi + 2 ] = DataUtils.toHalfFloat( Math.min( b, 65504 ) );
img[ qi + 3 ] = DataUtils.toHalfFloat( 1 );
} else {
img[ qi ] = r;
img[ qi + 1 ] = g;
img[ qi + 2 ] = b;
img[ qi + 3 ] = 1;
}
}
}
} else {
throw new Error( 'THREE.LogLuvLoader: Unsupported Photometric interpretation: ' + intp );
}
return img;
};
UTIF._binBE =
{
nextZero: function ( data, o ) {
while ( data[ o ] != 0 ) o ++; return o;
},
readUshort: function ( buff, p ) {
return ( buff[ p ] << 8 ) | buff[ p + 1 ];
},
readShort: function ( buff, p ) {
var a = UTIF._binBE.ui8; a[ 0 ] = buff[ p + 1 ]; a[ 1 ] = buff[ p + 0 ]; return UTIF._binBE.i16[ 0 ];
},
readInt: function ( buff, p ) {
var a = UTIF._binBE.ui8; a[ 0 ] = buff[ p + 3 ]; a[ 1 ] = buff[ p + 2 ]; a[ 2 ] = buff[ p + 1 ]; a[ 3 ] = buff[ p + 0 ]; return UTIF._binBE.i32[ 0 ];
},
readUint: function ( buff, p ) {
var a = UTIF._binBE.ui8; a[ 0 ] = buff[ p + 3 ]; a[ 1 ] = buff[ p + 2 ]; a[ 2 ] = buff[ p + 1 ]; a[ 3 ] = buff[ p + 0 ]; return UTIF._binBE.ui32[ 0 ];
},
readASCII: function ( buff, p, l ) {
var s = ''; for ( var i = 0; i < l; i ++ ) s += String.fromCharCode( buff[ p + i ] ); return s;
},
readFloat: function ( buff, p ) {
var a = UTIF._binBE.ui8; for ( var i = 0; i < 4; i ++ ) a[ i ] = buff[ p + 3 - i ]; return UTIF._binBE.fl32[ 0 ];
},
readDouble: function ( buff, p ) {
var a = UTIF._binBE.ui8; for ( var i = 0; i < 8; i ++ ) a[ i ] = buff[ p + 7 - i ]; return UTIF._binBE.fl64[ 0 ];
},
writeUshort: function ( buff, p, n ) {
buff[ p ] = ( n >> 8 ) & 255; buff[ p + 1 ] = n & 255;
},
writeInt: function ( buff, p, n ) {
var a = UTIF._binBE.ui8; UTIF._binBE.i32[ 0 ] = n; buff[ p + 3 ] = a[ 0 ]; buff[ p + 2 ] = a[ 1 ]; buff[ p + 1 ] = a[ 2 ]; buff[ p + 0 ] = a[ 3 ];
},
writeUint: function ( buff, p, n ) {
buff[ p ] = ( n >> 24 ) & 255; buff[ p + 1 ] = ( n >> 16 ) & 255; buff[ p + 2 ] = ( n >> 8 ) & 255; buff[ p + 3 ] = ( n >> 0 ) & 255;
},
writeASCII: function ( buff, p, s ) {
for ( var i = 0; i < s.length; i ++ ) buff[ p + i ] = s.charCodeAt( i );
},
writeDouble: function ( buff, p, n ) {
UTIF._binBE.fl64[ 0 ] = n;
for ( var i = 0; i < 8; i ++ ) buff[ p + i ] = UTIF._binBE.ui8[ 7 - i ];
}
};
UTIF._binBE.ui8 = new Uint8Array( 8 );
UTIF._binBE.i16 = new Int16Array( UTIF._binBE.ui8.buffer );
UTIF._binBE.i32 = new Int32Array( UTIF._binBE.ui8.buffer );
UTIF._binBE.ui32 = new Uint32Array( UTIF._binBE.ui8.buffer );
UTIF._binBE.fl32 = new Float32Array( UTIF._binBE.ui8.buffer );
UTIF._binBE.fl64 = new Float64Array( UTIF._binBE.ui8.buffer );
UTIF._binLE =
{
nextZero: UTIF._binBE.nextZero,
readUshort: function ( buff, p ) {
return ( buff[ p + 1 ] << 8 ) | buff[ p ];
},
readShort: function ( buff, p ) {
var a = UTIF._binBE.ui8; a[ 0 ] = buff[ p + 0 ]; a[ 1 ] = buff[ p + 1 ]; return UTIF._binBE.i16[ 0 ];
},
readInt: function ( buff, p ) {
var a = UTIF._binBE.ui8; a[ 0 ] = buff[ p + 0 ]; a[ 1 ] = buff[ p + 1 ]; a[ 2 ] = buff[ p + 2 ]; a[ 3 ] = buff[ p + 3 ]; return UTIF._binBE.i32[ 0 ];
},
readUint: function ( buff, p ) {
var a = UTIF._binBE.ui8; a[ 0 ] = buff[ p + 0 ]; a[ 1 ] = buff[ p + 1 ]; a[ 2 ] = buff[ p + 2 ]; a[ 3 ] = buff[ p + 3 ]; return UTIF._binBE.ui32[ 0 ];
},
readASCII: UTIF._binBE.readASCII,
readFloat: function ( buff, p ) {
var a = UTIF._binBE.ui8; for ( var i = 0; i < 4; i ++ ) a[ i ] = buff[ p + i ]; return UTIF._binBE.fl32[ 0 ];
},
readDouble: function ( buff, p ) {
var a = UTIF._binBE.ui8; for ( var i = 0; i < 8; i ++ ) a[ i ] = buff[ p + i ]; return UTIF._binBE.fl64[ 0 ];
},
writeUshort: function ( buff, p, n ) {
buff[ p ] = ( n ) & 255; buff[ p + 1 ] = ( n >> 8 ) & 255;
},
writeInt: function ( buff, p, n ) {
var a = UTIF._binBE.ui8; UTIF._binBE.i32[ 0 ] = n; buff[ p + 0 ] = a[ 0 ]; buff[ p + 1 ] = a[ 1 ]; buff[ p + 2 ] = a[ 2 ]; buff[ p + 3 ] = a[ 3 ];
},
writeUint: function ( buff, p, n ) {
buff[ p ] = ( n >>> 0 ) & 255; buff[ p + 1 ] = ( n >>> 8 ) & 255; buff[ p + 2 ] = ( n >>> 16 ) & 255; buff[ p + 3 ] = ( n >>> 24 ) & 255;
},
writeASCII: UTIF._binBE.writeASCII
};
UTIF._copyTile = function ( tb, tw, th, b, w, h, xoff, yoff ) {
//log("copyTile", tw, th, w, h, xoff, yoff);
var xlim = Math.min( tw, w - xoff );
var ylim = Math.min( th, h - yoff );
for ( var y = 0; y < ylim; y ++ ) {
var tof = ( yoff + y ) * w + xoff;
var sof = y * tw;
for ( var x = 0; x < xlim; x ++ ) b[ tof + x ] = tb[ sof + x ];
}
};
export { LogLuvLoader };

View File

@ -0,0 +1,77 @@
import {
FileLoader,
Loader,
CanvasTexture,
NearestFilter,
SRGBColorSpace
} from 'three';
import lottie from '../libs/lottie_canvas.module.js';
class LottieLoader extends Loader {
setQuality( value ) {
this._quality = value;
}
load( url, onLoad, onProgress, onError ) {
const quality = this._quality || 1;
const texture = new CanvasTexture();
texture.minFilter = NearestFilter;
texture.colorSpace = SRGBColorSpace;
const loader = new FileLoader( this.manager );
loader.setPath( this.path );
loader.setWithCredentials( this.withCredentials );
loader.load( url, function ( text ) {
const data = JSON.parse( text );
// lottie uses container.offetWidth and offsetHeight
// to define width/height
const container = document.createElement( 'div' );
container.style.width = data.w + 'px';
container.style.height = data.h + 'px';
document.body.appendChild( container );
const animation = lottie.loadAnimation( {
container: container,
animType: 'canvas',
loop: true,
autoplay: true,
animationData: data,
rendererSettings: { dpr: quality }
} );
texture.animation = animation;
texture.image = animation.container;
animation.addEventListener( 'enterFrame', function () {
texture.needsUpdate = true;
} );
container.style.display = 'none';
if ( onLoad !== undefined ) {
onLoad( texture );
}
}, onProgress, onError );
return texture;
}
}
export { LottieLoader };

View File

@ -0,0 +1,399 @@
import {
AnimationClip,
BufferGeometry,
FileLoader,
Float32BufferAttribute,
Loader,
Vector3
} from 'three';
const _normalData = [
[ - 0.525731, 0.000000, 0.850651 ], [ - 0.442863, 0.238856, 0.864188 ],
[ - 0.295242, 0.000000, 0.955423 ], [ - 0.309017, 0.500000, 0.809017 ],
[ - 0.162460, 0.262866, 0.951056 ], [ 0.000000, 0.000000, 1.000000 ],
[ 0.000000, 0.850651, 0.525731 ], [ - 0.147621, 0.716567, 0.681718 ],
[ 0.147621, 0.716567, 0.681718 ], [ 0.000000, 0.525731, 0.850651 ],
[ 0.309017, 0.500000, 0.809017 ], [ 0.525731, 0.000000, 0.850651 ],
[ 0.295242, 0.000000, 0.955423 ], [ 0.442863, 0.238856, 0.864188 ],
[ 0.162460, 0.262866, 0.951056 ], [ - 0.681718, 0.147621, 0.716567 ],
[ - 0.809017, 0.309017, 0.500000 ], [ - 0.587785, 0.425325, 0.688191 ],
[ - 0.850651, 0.525731, 0.000000 ], [ - 0.864188, 0.442863, 0.238856 ],
[ - 0.716567, 0.681718, 0.147621 ], [ - 0.688191, 0.587785, 0.425325 ],
[ - 0.500000, 0.809017, 0.309017 ], [ - 0.238856, 0.864188, 0.442863 ],
[ - 0.425325, 0.688191, 0.587785 ], [ - 0.716567, 0.681718, - 0.147621 ],
[ - 0.500000, 0.809017, - 0.309017 ], [ - 0.525731, 0.850651, 0.000000 ],
[ 0.000000, 0.850651, - 0.525731 ], [ - 0.238856, 0.864188, - 0.442863 ],
[ 0.000000, 0.955423, - 0.295242 ], [ - 0.262866, 0.951056, - 0.162460 ],
[ 0.000000, 1.000000, 0.000000 ], [ 0.000000, 0.955423, 0.295242 ],
[ - 0.262866, 0.951056, 0.162460 ], [ 0.238856, 0.864188, 0.442863 ],
[ 0.262866, 0.951056, 0.162460 ], [ 0.500000, 0.809017, 0.309017 ],
[ 0.238856, 0.864188, - 0.442863 ], [ 0.262866, 0.951056, - 0.162460 ],
[ 0.500000, 0.809017, - 0.309017 ], [ 0.850651, 0.525731, 0.000000 ],
[ 0.716567, 0.681718, 0.147621 ], [ 0.716567, 0.681718, - 0.147621 ],
[ 0.525731, 0.850651, 0.000000 ], [ 0.425325, 0.688191, 0.587785 ],
[ 0.864188, 0.442863, 0.238856 ], [ 0.688191, 0.587785, 0.425325 ],
[ 0.809017, 0.309017, 0.500000 ], [ 0.681718, 0.147621, 0.716567 ],
[ 0.587785, 0.425325, 0.688191 ], [ 0.955423, 0.295242, 0.000000 ],
[ 1.000000, 0.000000, 0.000000 ], [ 0.951056, 0.162460, 0.262866 ],
[ 0.850651, - 0.525731, 0.000000 ], [ 0.955423, - 0.295242, 0.000000 ],
[ 0.864188, - 0.442863, 0.238856 ], [ 0.951056, - 0.162460, 0.262866 ],
[ 0.809017, - 0.309017, 0.500000 ], [ 0.681718, - 0.147621, 0.716567 ],
[ 0.850651, 0.000000, 0.525731 ], [ 0.864188, 0.442863, - 0.238856 ],
[ 0.809017, 0.309017, - 0.500000 ], [ 0.951056, 0.162460, - 0.262866 ],
[ 0.525731, 0.000000, - 0.850651 ], [ 0.681718, 0.147621, - 0.716567 ],
[ 0.681718, - 0.147621, - 0.716567 ], [ 0.850651, 0.000000, - 0.525731 ],
[ 0.809017, - 0.309017, - 0.500000 ], [ 0.864188, - 0.442863, - 0.238856 ],
[ 0.951056, - 0.162460, - 0.262866 ], [ 0.147621, 0.716567, - 0.681718 ],
[ 0.309017, 0.500000, - 0.809017 ], [ 0.425325, 0.688191, - 0.587785 ],
[ 0.442863, 0.238856, - 0.864188 ], [ 0.587785, 0.425325, - 0.688191 ],
[ 0.688191, 0.587785, - 0.425325 ], [ - 0.147621, 0.716567, - 0.681718 ],
[ - 0.309017, 0.500000, - 0.809017 ], [ 0.000000, 0.525731, - 0.850651 ],
[ - 0.525731, 0.000000, - 0.850651 ], [ - 0.442863, 0.238856, - 0.864188 ],
[ - 0.295242, 0.000000, - 0.955423 ], [ - 0.162460, 0.262866, - 0.951056 ],
[ 0.000000, 0.000000, - 1.000000 ], [ 0.295242, 0.000000, - 0.955423 ],
[ 0.162460, 0.262866, - 0.951056 ], [ - 0.442863, - 0.238856, - 0.864188 ],
[ - 0.309017, - 0.500000, - 0.809017 ], [ - 0.162460, - 0.262866, - 0.951056 ],
[ 0.000000, - 0.850651, - 0.525731 ], [ - 0.147621, - 0.716567, - 0.681718 ],
[ 0.147621, - 0.716567, - 0.681718 ], [ 0.000000, - 0.525731, - 0.850651 ],
[ 0.309017, - 0.500000, - 0.809017 ], [ 0.442863, - 0.238856, - 0.864188 ],
[ 0.162460, - 0.262866, - 0.951056 ], [ 0.238856, - 0.864188, - 0.442863 ],
[ 0.500000, - 0.809017, - 0.309017 ], [ 0.425325, - 0.688191, - 0.587785 ],
[ 0.716567, - 0.681718, - 0.147621 ], [ 0.688191, - 0.587785, - 0.425325 ],
[ 0.587785, - 0.425325, - 0.688191 ], [ 0.000000, - 0.955423, - 0.295242 ],
[ 0.000000, - 1.000000, 0.000000 ], [ 0.262866, - 0.951056, - 0.162460 ],
[ 0.000000, - 0.850651, 0.525731 ], [ 0.000000, - 0.955423, 0.295242 ],
[ 0.238856, - 0.864188, 0.442863 ], [ 0.262866, - 0.951056, 0.162460 ],
[ 0.500000, - 0.809017, 0.309017 ], [ 0.716567, - 0.681718, 0.147621 ],
[ 0.525731, - 0.850651, 0.000000 ], [ - 0.238856, - 0.864188, - 0.442863 ],
[ - 0.500000, - 0.809017, - 0.309017 ], [ - 0.262866, - 0.951056, - 0.162460 ],
[ - 0.850651, - 0.525731, 0.000000 ], [ - 0.716567, - 0.681718, - 0.147621 ],
[ - 0.716567, - 0.681718, 0.147621 ], [ - 0.525731, - 0.850651, 0.000000 ],
[ - 0.500000, - 0.809017, 0.309017 ], [ - 0.238856, - 0.864188, 0.442863 ],
[ - 0.262866, - 0.951056, 0.162460 ], [ - 0.864188, - 0.442863, 0.238856 ],
[ - 0.809017, - 0.309017, 0.500000 ], [ - 0.688191, - 0.587785, 0.425325 ],
[ - 0.681718, - 0.147621, 0.716567 ], [ - 0.442863, - 0.238856, 0.864188 ],
[ - 0.587785, - 0.425325, 0.688191 ], [ - 0.309017, - 0.500000, 0.809017 ],
[ - 0.147621, - 0.716567, 0.681718 ], [ - 0.425325, - 0.688191, 0.587785 ],
[ - 0.162460, - 0.262866, 0.951056 ], [ 0.442863, - 0.238856, 0.864188 ],
[ 0.162460, - 0.262866, 0.951056 ], [ 0.309017, - 0.500000, 0.809017 ],
[ 0.147621, - 0.716567, 0.681718 ], [ 0.000000, - 0.525731, 0.850651 ],
[ 0.425325, - 0.688191, 0.587785 ], [ 0.587785, - 0.425325, 0.688191 ],
[ 0.688191, - 0.587785, 0.425325 ], [ - 0.955423, 0.295242, 0.000000 ],
[ - 0.951056, 0.162460, 0.262866 ], [ - 1.000000, 0.000000, 0.000000 ],
[ - 0.850651, 0.000000, 0.525731 ], [ - 0.955423, - 0.295242, 0.000000 ],
[ - 0.951056, - 0.162460, 0.262866 ], [ - 0.864188, 0.442863, - 0.238856 ],
[ - 0.951056, 0.162460, - 0.262866 ], [ - 0.809017, 0.309017, - 0.500000 ],
[ - 0.864188, - 0.442863, - 0.238856 ], [ - 0.951056, - 0.162460, - 0.262866 ],
[ - 0.809017, - 0.309017, - 0.500000 ], [ - 0.681718, 0.147621, - 0.716567 ],
[ - 0.681718, - 0.147621, - 0.716567 ], [ - 0.850651, 0.000000, - 0.525731 ],
[ - 0.688191, 0.587785, - 0.425325 ], [ - 0.587785, 0.425325, - 0.688191 ],
[ - 0.425325, 0.688191, - 0.587785 ], [ - 0.425325, - 0.688191, - 0.587785 ],
[ - 0.587785, - 0.425325, - 0.688191 ], [ - 0.688191, - 0.587785, - 0.425325 ]
];
class MD2Loader extends Loader {
constructor( manager ) {
super( manager );
}
load( url, onLoad, onProgress, onError ) {
const scope = this;
const loader = new FileLoader( scope.manager );
loader.setPath( scope.path );
loader.setResponseType( 'arraybuffer' );
loader.setRequestHeader( scope.requestHeader );
loader.setWithCredentials( scope.withCredentials );
loader.load( url, function ( buffer ) {
try {
onLoad( scope.parse( buffer ) );
} catch ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
scope.manager.itemError( url );
}
}, onProgress, onError );
}
parse( buffer ) {
const data = new DataView( buffer );
// http://tfc.duke.free.fr/coding/md2-specs-en.html
const header = {};
const headerNames = [
'ident', 'version',
'skinwidth', 'skinheight',
'framesize',
'num_skins', 'num_vertices', 'num_st', 'num_tris', 'num_glcmds', 'num_frames',
'offset_skins', 'offset_st', 'offset_tris', 'offset_frames', 'offset_glcmds', 'offset_end'
];
for ( let i = 0; i < headerNames.length; i ++ ) {
header[ headerNames[ i ] ] = data.getInt32( i * 4, true );
}
if ( header.ident !== 844121161 || header.version !== 8 ) {
console.error( 'Not a valid MD2 file' );
return;
}
if ( header.offset_end !== data.byteLength ) {
console.error( 'Corrupted MD2 file' );
return;
}
//
const geometry = new BufferGeometry();
// uvs
const uvsTemp = [];
let offset = header.offset_st;
for ( let i = 0, l = header.num_st; i < l; i ++ ) {
const u = data.getInt16( offset + 0, true );
const v = data.getInt16( offset + 2, true );
uvsTemp.push( u / header.skinwidth, 1 - ( v / header.skinheight ) );
offset += 4;
}
// triangles
offset = header.offset_tris;
const vertexIndices = [];
const uvIndices = [];
for ( let i = 0, l = header.num_tris; i < l; i ++ ) {
vertexIndices.push(
data.getUint16( offset + 0, true ),
data.getUint16( offset + 2, true ),
data.getUint16( offset + 4, true )
);
uvIndices.push(
data.getUint16( offset + 6, true ),
data.getUint16( offset + 8, true ),
data.getUint16( offset + 10, true )
);
offset += 12;
}
// frames
const translation = new Vector3();
const scale = new Vector3();
const frames = [];
offset = header.offset_frames;
for ( let i = 0, l = header.num_frames; i < l; i ++ ) {
scale.set(
data.getFloat32( offset + 0, true ),
data.getFloat32( offset + 4, true ),
data.getFloat32( offset + 8, true )
);
translation.set(
data.getFloat32( offset + 12, true ),
data.getFloat32( offset + 16, true ),
data.getFloat32( offset + 20, true )
);
offset += 24;
const string = [];
for ( let j = 0; j < 16; j ++ ) {
const character = data.getUint8( offset + j );
if ( character === 0 ) break;
string[ j ] = character;
}
const frame = {
name: String.fromCharCode.apply( null, string ),
vertices: [],
normals: []
};
offset += 16;
for ( let j = 0; j < header.num_vertices; j ++ ) {
let x = data.getUint8( offset ++ );
let y = data.getUint8( offset ++ );
let z = data.getUint8( offset ++ );
const n = _normalData[ data.getUint8( offset ++ ) ];
x = x * scale.x + translation.x;
y = y * scale.y + translation.y;
z = z * scale.z + translation.z;
frame.vertices.push( x, z, y ); // convert to Y-up
frame.normals.push( n[ 0 ], n[ 2 ], n[ 1 ] ); // convert to Y-up
}
frames.push( frame );
}
// static
const positions = [];
const normals = [];
const uvs = [];
const verticesTemp = frames[ 0 ].vertices;
const normalsTemp = frames[ 0 ].normals;
for ( let i = 0, l = vertexIndices.length; i < l; i ++ ) {
const vertexIndex = vertexIndices[ i ];
let stride = vertexIndex * 3;
//
const x = verticesTemp[ stride ];
const y = verticesTemp[ stride + 1 ];
const z = verticesTemp[ stride + 2 ];
positions.push( x, y, z );
//
const nx = normalsTemp[ stride ];
const ny = normalsTemp[ stride + 1 ];
const nz = normalsTemp[ stride + 2 ];
normals.push( nx, ny, nz );
//
const uvIndex = uvIndices[ i ];
stride = uvIndex * 2;
const u = uvsTemp[ stride ];
const v = uvsTemp[ stride + 1 ];
uvs.push( u, v );
}
geometry.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) );
geometry.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
geometry.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) );
// animation
const morphPositions = [];
const morphNormals = [];
for ( let i = 0, l = frames.length; i < l; i ++ ) {
const frame = frames[ i ];
const attributeName = frame.name;
if ( frame.vertices.length > 0 ) {
const positions = [];
for ( let j = 0, jl = vertexIndices.length; j < jl; j ++ ) {
const vertexIndex = vertexIndices[ j ];
const stride = vertexIndex * 3;
const x = frame.vertices[ stride ];
const y = frame.vertices[ stride + 1 ];
const z = frame.vertices[ stride + 2 ];
positions.push( x, y, z );
}
const positionAttribute = new Float32BufferAttribute( positions, 3 );
positionAttribute.name = attributeName;
morphPositions.push( positionAttribute );
}
if ( frame.normals.length > 0 ) {
const normals = [];
for ( let j = 0, jl = vertexIndices.length; j < jl; j ++ ) {
const vertexIndex = vertexIndices[ j ];
const stride = vertexIndex * 3;
const nx = frame.normals[ stride ];
const ny = frame.normals[ stride + 1 ];
const nz = frame.normals[ stride + 2 ];
normals.push( nx, ny, nz );
}
const normalAttribute = new Float32BufferAttribute( normals, 3 );
normalAttribute.name = attributeName;
morphNormals.push( normalAttribute );
}
}
geometry.morphAttributes.position = morphPositions;
geometry.morphAttributes.normal = morphNormals;
geometry.morphTargetsRelative = false;
geometry.animations = AnimationClip.CreateClipsFromMorphTargetSequences( frames, 10 );
return geometry;
}
}
export { MD2Loader };

View File

@ -0,0 +1,102 @@
/**
* MDD is a special format that stores a position for every vertex in a model for every frame in an animation.
* Similar to BVH, it can be used to transfer animation data between different 3D applications or engines.
*
* MDD stores its data in binary format (big endian) in the following way:
*
* number of frames (a single uint32)
* number of vertices (a single uint32)
* time values for each frame (sequence of float32)
* vertex data for each frame (sequence of float32)
*/
import {
AnimationClip,
BufferAttribute,
FileLoader,
Loader,
NumberKeyframeTrack
} from 'three';
class MDDLoader extends Loader {
constructor( manager ) {
super( manager );
}
load( url, onLoad, onProgress, onError ) {
const scope = this;
const loader = new FileLoader( this.manager );
loader.setPath( this.path );
loader.setResponseType( 'arraybuffer' );
loader.load( url, function ( data ) {
onLoad( scope.parse( data ) );
}, onProgress, onError );
}
parse( data ) {
const view = new DataView( data );
const totalFrames = view.getUint32( 0 );
const totalPoints = view.getUint32( 4 );
let offset = 8;
// animation clip
const times = new Float32Array( totalFrames );
const values = new Float32Array( totalFrames * totalFrames ).fill( 0 );
for ( let i = 0; i < totalFrames; i ++ ) {
times[ i ] = view.getFloat32( offset ); offset += 4;
values[ ( totalFrames * i ) + i ] = 1;
}
const track = new NumberKeyframeTrack( '.morphTargetInfluences', times, values );
const clip = new AnimationClip( 'default', times[ times.length - 1 ], [ track ] );
// morph targets
const morphTargets = [];
for ( let i = 0; i < totalFrames; i ++ ) {
const morphTarget = new Float32Array( totalPoints * 3 );
for ( let j = 0; j < totalPoints; j ++ ) {
const stride = ( j * 3 );
morphTarget[ stride + 0 ] = view.getFloat32( offset ); offset += 4; // x
morphTarget[ stride + 1 ] = view.getFloat32( offset ); offset += 4; // y
morphTarget[ stride + 2 ] = view.getFloat32( offset ); offset += 4; // z
}
const attribute = new BufferAttribute( morphTarget, 3 );
attribute.name = 'morph_' + i;
morphTargets.push( attribute );
}
return {
morphTargets: morphTargets,
clip: clip
};
}
}
export { MDDLoader };

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,567 @@
import {
Color,
DefaultLoadingManager,
FileLoader,
FrontSide,
Loader,
LoaderUtils,
MeshPhongMaterial,
RepeatWrapping,
TextureLoader,
Vector2,
SRGBColorSpace
} from 'three';
/**
* Loads a Wavefront .mtl file specifying materials
*/
class MTLLoader extends Loader {
constructor( manager ) {
super( manager );
}
/**
* Loads and parses a MTL asset from a URL.
*
* @param {String} url - URL to the MTL file.
* @param {Function} [onLoad] - Callback invoked with the loaded object.
* @param {Function} [onProgress] - Callback for download progress.
* @param {Function} [onError] - Callback for download errors.
*
* @see setPath setResourcePath
*
* @note In order for relative texture references to resolve correctly
* you must call setResourcePath() explicitly prior to load.
*/
load( url, onLoad, onProgress, onError ) {
const scope = this;
const path = ( this.path === '' ) ? LoaderUtils.extractUrlBase( url ) : this.path;
const loader = new FileLoader( this.manager );
loader.setPath( this.path );
loader.setRequestHeader( this.requestHeader );
loader.setWithCredentials( this.withCredentials );
loader.load( url, function ( text ) {
try {
onLoad( scope.parse( text, path ) );
} catch ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
scope.manager.itemError( url );
}
}, onProgress, onError );
}
setMaterialOptions( value ) {
this.materialOptions = value;
return this;
}
/**
* Parses a MTL file.
*
* @param {String} text - Content of MTL file
* @return {MaterialCreator}
*
* @see setPath setResourcePath
*
* @note In order for relative texture references to resolve correctly
* you must call setResourcePath() explicitly prior to parse.
*/
parse( text, path ) {
const lines = text.split( '\n' );
let info = {};
const delimiter_pattern = /\s+/;
const materialsInfo = {};
for ( let i = 0; i < lines.length; i ++ ) {
let line = lines[ i ];
line = line.trim();
if ( line.length === 0 || line.charAt( 0 ) === '#' ) {
// Blank line or comment ignore
continue;
}
const pos = line.indexOf( ' ' );
let key = ( pos >= 0 ) ? line.substring( 0, pos ) : line;
key = key.toLowerCase();
let value = ( pos >= 0 ) ? line.substring( pos + 1 ) : '';
value = value.trim();
if ( key === 'newmtl' ) {
// New material
info = { name: value };
materialsInfo[ value ] = info;
} else {
if ( key === 'ka' || key === 'kd' || key === 'ks' || key === 'ke' ) {
const ss = value.split( delimiter_pattern, 3 );
info[ key ] = [ parseFloat( ss[ 0 ] ), parseFloat( ss[ 1 ] ), parseFloat( ss[ 2 ] ) ];
} else {
info[ key ] = value;
}
}
}
const materialCreator = new MaterialCreator( this.resourcePath || path, this.materialOptions );
materialCreator.setCrossOrigin( this.crossOrigin );
materialCreator.setManager( this.manager );
materialCreator.setMaterials( materialsInfo );
return materialCreator;
}
}
/**
* Create a new MTLLoader.MaterialCreator
* @param baseUrl - Url relative to which textures are loaded
* @param options - Set of options on how to construct the materials
* side: Which side to apply the material
* FrontSide (default), THREE.BackSide, THREE.DoubleSide
* wrap: What type of wrapping to apply for textures
* RepeatWrapping (default), THREE.ClampToEdgeWrapping, THREE.MirroredRepeatWrapping
* normalizeRGB: RGBs need to be normalized to 0-1 from 0-255
* Default: false, assumed to be already normalized
* ignoreZeroRGBs: Ignore values of RGBs (Ka,Kd,Ks) that are all 0's
* Default: false
* @constructor
*/
class MaterialCreator {
constructor( baseUrl = '', options = {} ) {
this.baseUrl = baseUrl;
this.options = options;
this.materialsInfo = {};
this.materials = {};
this.materialsArray = [];
this.nameLookup = {};
this.crossOrigin = 'anonymous';
this.side = ( this.options.side !== undefined ) ? this.options.side : FrontSide;
this.wrap = ( this.options.wrap !== undefined ) ? this.options.wrap : RepeatWrapping;
}
setCrossOrigin( value ) {
this.crossOrigin = value;
return this;
}
setManager( value ) {
this.manager = value;
}
setMaterials( materialsInfo ) {
this.materialsInfo = this.convert( materialsInfo );
this.materials = {};
this.materialsArray = [];
this.nameLookup = {};
}
convert( materialsInfo ) {
if ( ! this.options ) return materialsInfo;
const converted = {};
for ( const mn in materialsInfo ) {
// Convert materials info into normalized form based on options
const mat = materialsInfo[ mn ];
const covmat = {};
converted[ mn ] = covmat;
for ( const prop in mat ) {
let save = true;
let value = mat[ prop ];
const lprop = prop.toLowerCase();
switch ( lprop ) {
case 'kd':
case 'ka':
case 'ks':
// Diffuse color (color under white light) using RGB values
if ( this.options && this.options.normalizeRGB ) {
value = [ value[ 0 ] / 255, value[ 1 ] / 255, value[ 2 ] / 255 ];
}
if ( this.options && this.options.ignoreZeroRGBs ) {
if ( value[ 0 ] === 0 && value[ 1 ] === 0 && value[ 2 ] === 0 ) {
// ignore
save = false;
}
}
break;
default:
break;
}
if ( save ) {
covmat[ lprop ] = value;
}
}
}
return converted;
}
preload() {
for ( const mn in this.materialsInfo ) {
this.create( mn );
}
}
getIndex( materialName ) {
return this.nameLookup[ materialName ];
}
getAsArray() {
let index = 0;
for ( const mn in this.materialsInfo ) {
this.materialsArray[ index ] = this.create( mn );
this.nameLookup[ mn ] = index;
index ++;
}
return this.materialsArray;
}
create( materialName ) {
if ( this.materials[ materialName ] === undefined ) {
this.createMaterial_( materialName );
}
return this.materials[ materialName ];
}
createMaterial_( materialName ) {
// Create material
const scope = this;
const mat = this.materialsInfo[ materialName ];
const params = {
name: materialName,
side: this.side
};
function resolveURL( baseUrl, url ) {
if ( typeof url !== 'string' || url === '' )
return '';
// Absolute URL
if ( /^https?:\/\//i.test( url ) ) return url;
return baseUrl + url;
}
function setMapForType( mapType, value ) {
if ( params[ mapType ] ) return; // Keep the first encountered texture
const texParams = scope.getTextureParams( value, params );
const map = scope.loadTexture( resolveURL( scope.baseUrl, texParams.url ) );
map.repeat.copy( texParams.scale );
map.offset.copy( texParams.offset );
map.wrapS = scope.wrap;
map.wrapT = scope.wrap;
if ( mapType === 'map' || mapType === 'emissiveMap' ) {
map.colorSpace = SRGBColorSpace;
}
params[ mapType ] = map;
}
for ( const prop in mat ) {
const value = mat[ prop ];
let n;
if ( value === '' ) continue;
switch ( prop.toLowerCase() ) {
// Ns is material specular exponent
case 'kd':
// Diffuse color (color under white light) using RGB values
params.color = new Color().fromArray( value ).convertSRGBToLinear();
break;
case 'ks':
// Specular color (color when light is reflected from shiny surface) using RGB values
params.specular = new Color().fromArray( value ).convertSRGBToLinear();
break;
case 'ke':
// Emissive using RGB values
params.emissive = new Color().fromArray( value ).convertSRGBToLinear();
break;
case 'map_kd':
// Diffuse texture map
setMapForType( 'map', value );
break;
case 'map_ks':
// Specular map
setMapForType( 'specularMap', value );
break;
case 'map_ke':
// Emissive map
setMapForType( 'emissiveMap', value );
break;
case 'norm':
setMapForType( 'normalMap', value );
break;
case 'map_bump':
case 'bump':
// Bump texture map
setMapForType( 'bumpMap', value );
break;
case 'map_d':
// Alpha map
setMapForType( 'alphaMap', value );
params.transparent = true;
break;
case 'ns':
// The specular exponent (defines the focus of the specular highlight)
// A high exponent results in a tight, concentrated highlight. Ns values normally range from 0 to 1000.
params.shininess = parseFloat( value );
break;
case 'd':
n = parseFloat( value );
if ( n < 1 ) {
params.opacity = n;
params.transparent = true;
}
break;
case 'tr':
n = parseFloat( value );
if ( this.options && this.options.invertTrProperty ) n = 1 - n;
if ( n > 0 ) {
params.opacity = 1 - n;
params.transparent = true;
}
break;
default:
break;
}
}
this.materials[ materialName ] = new MeshPhongMaterial( params );
return this.materials[ materialName ];
}
getTextureParams( value, matParams ) {
const texParams = {
scale: new Vector2( 1, 1 ),
offset: new Vector2( 0, 0 )
};
const items = value.split( /\s+/ );
let pos;
pos = items.indexOf( '-bm' );
if ( pos >= 0 ) {
matParams.bumpScale = parseFloat( items[ pos + 1 ] );
items.splice( pos, 2 );
}
pos = items.indexOf( '-s' );
if ( pos >= 0 ) {
texParams.scale.set( parseFloat( items[ pos + 1 ] ), parseFloat( items[ pos + 2 ] ) );
items.splice( pos, 4 ); // we expect 3 parameters here!
}
pos = items.indexOf( '-o' );
if ( pos >= 0 ) {
texParams.offset.set( parseFloat( items[ pos + 1 ] ), parseFloat( items[ pos + 2 ] ) );
items.splice( pos, 4 ); // we expect 3 parameters here!
}
texParams.url = items.join( ' ' ).trim();
return texParams;
}
loadTexture( url, mapping, onLoad, onProgress, onError ) {
const manager = ( this.manager !== undefined ) ? this.manager : DefaultLoadingManager;
let loader = manager.getHandler( url );
if ( loader === null ) {
loader = new TextureLoader( manager );
}
if ( loader.setCrossOrigin ) loader.setCrossOrigin( this.crossOrigin );
const texture = loader.load( url, onLoad, onProgress, onError );
if ( mapping !== undefined ) texture.mapping = mapping;
return texture;
}
}
export { MTLLoader };

View File

@ -0,0 +1,852 @@
import {
FileLoader,
Loader,
TextureLoader,
RepeatWrapping
} from 'three';
import {
MeshBasicNodeMaterial, MeshPhysicalNodeMaterial,
float, bool, int, vec2, vec3, vec4, color, texture,
positionLocal, positionWorld, uv, vertexColor,
normalLocal, normalWorld, tangentLocal, tangentWorld,
add, sub, mul, div, mod, abs, sign, floor, ceil, round, pow, sin, cos, tan,
asin, acos, atan2, sqrt, exp, clamp, min, max, normalize, length, dot, cross, normalMap,
remap, smoothstep, luminance, mx_rgbtohsv, mx_hsvtorgb,
mix,
mx_ramplr, mx_ramptb, mx_splitlr, mx_splittb,
mx_fractal_noise_float, mx_noise_float, mx_cell_noise_float, mx_worley_noise_float,
mx_transform_uv,
mx_safepower, mx_contrast,
mx_srgb_texture_to_lin_rec709,
saturation
} from '../nodes/Nodes.js';
const colorSpaceLib = {
mx_srgb_texture_to_lin_rec709
};
class MXElement {
constructor( name, nodeFunc, params = null ) {
this.name = name;
this.nodeFunc = nodeFunc;
this.params = params;
}
}
// Ref: https://github.com/mrdoob/three.js/issues/24674
const mx_add = ( in1, in2 = float( 0 ) ) => add( in1, in2 );
const mx_subtract = ( in1, in2 = float( 0 ) ) => sub( in1, in2 );
const mx_multiply = ( in1, in2 = float( 1 ) ) => mul( in1, in2 );
const mx_divide = ( in1, in2 = float( 1 ) ) => div( in1, in2 );
const mx_modulo = ( in1, in2 = float( 1 ) ) => mod( in1, in2 );
const mx_power = ( in1, in2 = float( 1 ) ) => pow( in1, in2 );
const mx_atan2 = ( in1 = float( 0 ), in2 = float( 1 ) ) => atan2( in1, in2 );
const MXElements = [
// << Math >>
new MXElement( 'add', mx_add, [ 'in1', 'in2' ] ),
new MXElement( 'subtract', mx_subtract, [ 'in1', 'in2' ] ),
new MXElement( 'multiply', mx_multiply, [ 'in1', 'in2' ] ),
new MXElement( 'divide', mx_divide, [ 'in1', 'in2' ] ),
new MXElement( 'modulo', mx_modulo, [ 'in1', 'in2' ] ),
new MXElement( 'absval', abs, [ 'in1', 'in2' ] ),
new MXElement( 'sign', sign, [ 'in1', 'in2' ] ),
new MXElement( 'floor', floor, [ 'in1', 'in2' ] ),
new MXElement( 'ceil', ceil, [ 'in1', 'in2' ] ),
new MXElement( 'round', round, [ 'in1', 'in2' ] ),
new MXElement( 'power', mx_power, [ 'in1', 'in2' ] ),
new MXElement( 'sin', sin, [ 'in' ] ),
new MXElement( 'cos', cos, [ 'in' ] ),
new MXElement( 'tan', tan, [ 'in' ] ),
new MXElement( 'asin', asin, [ 'in' ] ),
new MXElement( 'acos', acos, [ 'in' ] ),
new MXElement( 'atan2', mx_atan2, [ 'in1', 'in2' ] ),
new MXElement( 'sqrt', sqrt, [ 'in' ] ),
//new MtlXElement( 'ln', ... ),
new MXElement( 'exp', exp, [ 'in' ] ),
new MXElement( 'clamp', clamp, [ 'in', 'low', 'high' ] ),
new MXElement( 'min', min, [ 'in1', 'in2' ] ),
new MXElement( 'max', max, [ 'in1', 'in2' ] ),
new MXElement( 'normalize', normalize, [ 'in' ] ),
new MXElement( 'magnitude', length, [ 'in1', 'in2' ] ),
new MXElement( 'dotproduct', dot, [ 'in1', 'in2' ] ),
new MXElement( 'crossproduct', cross, [ 'in' ] ),
//new MtlXElement( 'transformpoint', ... ),
//new MtlXElement( 'transformvector', ... ),
//new MtlXElement( 'transformnormal', ... ),
//new MtlXElement( 'transformmatrix', ... ),
new MXElement( 'normalmap', normalMap, [ 'in', 'scale' ] ),
//new MtlXElement( 'transpose', ... ),
//new MtlXElement( 'determinant', ... ),
//new MtlXElement( 'invertmatrix', ... ),
//new MtlXElement( 'rotate2d', rotateUV, [ 'in', radians( 'amount' )** ] ),
//new MtlXElement( 'rotate3d', ... ),
//new MtlXElement( 'arrayappend', ... ),
//new MtlXElement( 'dot', ... ),
// << Adjustment >>
new MXElement( 'remap', remap, [ 'in', 'inlow', 'inhigh', 'outlow', 'outhigh' ] ),
new MXElement( 'smoothstep', smoothstep, [ 'in', 'low', 'high' ] ),
//new MtlXElement( 'curveadjust', ... ),
//new MtlXElement( 'curvelookup', ... ),
new MXElement( 'luminance', luminance, [ 'in', 'lumacoeffs' ] ),
new MXElement( 'rgbtohsv', mx_rgbtohsv, [ 'in' ] ),
new MXElement( 'hsvtorgb', mx_hsvtorgb, [ 'in' ] ),
// << Mix >>
new MXElement( 'mix', mix, [ 'bg', 'fg', 'mix' ] ),
// << Channel >>
new MXElement( 'combine2', vec2, [ 'in1', 'in2' ] ),
new MXElement( 'combine3', vec3, [ 'in1', 'in2', 'in3' ] ),
new MXElement( 'combine4', vec4, [ 'in1', 'in2', 'in3', 'in4' ] ),
// << Procedural >>
new MXElement( 'ramplr', mx_ramplr, [ 'valuel', 'valuer', 'texcoord' ] ),
new MXElement( 'ramptb', mx_ramptb, [ 'valuet', 'valueb', 'texcoord' ] ),
new MXElement( 'splitlr', mx_splitlr, [ 'valuel', 'valuer', 'texcoord' ] ),
new MXElement( 'splittb', mx_splittb, [ 'valuet', 'valueb', 'texcoord' ] ),
new MXElement( 'noise2d', mx_noise_float, [ 'texcoord', 'amplitude', 'pivot' ] ),
new MXElement( 'noise3d', mx_noise_float, [ 'texcoord', 'amplitude', 'pivot' ] ),
new MXElement( 'fractal3d', mx_fractal_noise_float, [ 'position', 'octaves', 'lacunarity', 'diminish', 'amplitude' ] ),
new MXElement( 'cellnoise2d', mx_cell_noise_float, [ 'texcoord' ] ),
new MXElement( 'cellnoise3d', mx_cell_noise_float, [ 'texcoord' ] ),
new MXElement( 'worleynoise2d', mx_worley_noise_float, [ 'texcoord', 'jitter' ] ),
new MXElement( 'worleynoise3d', mx_worley_noise_float, [ 'texcoord', 'jitter' ] ),
// << Supplemental >>
//new MtlXElement( 'tiledimage', ... ),
//new MtlXElement( 'triplanarprojection', triplanarTextures, [ 'filex', 'filey', 'filez' ] ),
//new MtlXElement( 'ramp4', ... ),
//new MtlXElement( 'place2d', mx_place2d, [ 'texcoord', 'pivot', 'scale', 'rotate', 'offset' ] ),
new MXElement( 'safepower', mx_safepower, [ 'in1', 'in2' ] ),
new MXElement( 'contrast', mx_contrast, [ 'in', 'amount', 'pivot' ] ),
//new MtlXElement( 'hsvadjust', ... ),
new MXElement( 'saturate', saturation, [ 'in', 'amount' ] ),
//new MtlXElement( 'extract', ... ),
//new MtlXElement( 'separate2', ... ),
//new MtlXElement( 'separate3', ... ),
//new MtlXElement( 'separate4', ... )
];
const MtlXLibrary = {};
MXElements.forEach( element => MtlXLibrary[ element.name ] = element );
class MaterialXLoader extends Loader {
constructor( manager ) {
super( manager );
}
load( url, onLoad, onProgress, onError ) {
const _onError = function ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
};
new FileLoader( this.manager )
.setPath( this.path )
.load( url, async ( text ) => {
try {
onLoad( this.parse( text ) );
} catch ( e ) {
_onError( e );
}
}, onProgress, _onError );
return this;
}
parse( text ) {
return new MaterialX( this.manager, this.path ).parse( text );
}
}
class MaterialXNode {
constructor( materialX, nodeXML, nodePath = '' ) {
this.materialX = materialX;
this.nodeXML = nodeXML;
this.nodePath = nodePath ? nodePath + '/' + this.name : this.name;
this.parent = null;
this.node = null;
this.children = [];
}
get element() {
return this.nodeXML.nodeName;
}
get nodeGraph() {
return this.getAttribute( 'nodegraph' );
}
get nodeName() {
return this.getAttribute( 'nodename' );
}
get interfaceName() {
return this.getAttribute( 'interfacename' );
}
get output() {
return this.getAttribute( 'output' );
}
get name() {
return this.getAttribute( 'name' );
}
get type() {
return this.getAttribute( 'type' );
}
get value() {
return this.getAttribute( 'value' );
}
getNodeGraph() {
let nodeX = this;
while ( nodeX !== null ) {
if ( nodeX.element === 'nodegraph' ) {
break;
}
nodeX = nodeX.parent;
}
return nodeX;
}
getRoot() {
let nodeX = this;
while ( nodeX.parent !== null ) {
nodeX = nodeX.parent;
}
return nodeX;
}
get referencePath() {
let referencePath = null;
if ( this.nodeGraph !== null && this.output !== null ) {
referencePath = this.nodeGraph + '/' + this.output;
} else if ( this.nodeName !== null || this.interfaceName !== null ) {
referencePath = this.getNodeGraph().nodePath + '/' + ( this.nodeName || this.interfaceName );
}
return referencePath;
}
get hasReference() {
return this.referencePath !== null;
}
get isConst() {
return this.element === 'input' && this.value !== null && this.type !== 'filename';
}
getColorSpaceNode() {
const csSource = this.getAttribute( 'colorspace' );
const csTarget = this.getRoot().getAttribute( 'colorspace' );
const nodeName = `mx_${ csSource }_to_${ csTarget }`;
return colorSpaceLib[ nodeName ];
}
getTexture() {
const filePrefix = this.getRecursiveAttribute( 'fileprefix' ) || '';
let loader = this.materialX.textureLoader;
const uri = filePrefix + this.value;
if ( uri ) {
const handler = this.materialX.manager.getHandler( uri );
if ( handler !== null ) loader = handler;
}
const texture = loader.load( uri );
texture.wrapS = texture.wrapT = RepeatWrapping;
texture.flipY = false;
return texture;
}
getClassFromType( type ) {
let nodeClass = null;
if ( type === 'integer' ) nodeClass = int;
else if ( type === 'float' ) nodeClass = float;
else if ( type === 'vector2' ) nodeClass = vec2;
else if ( type === 'vector3' ) nodeClass = vec3;
else if ( type === 'vector4' || type === 'color4' ) nodeClass = vec4;
else if ( type === 'color3' ) nodeClass = color;
else if ( type === 'boolean' ) nodeClass = bool;
return nodeClass;
}
getNode() {
let node = this.node;
if ( node !== null ) {
return node;
}
//
const type = this.type;
if ( this.isConst ) {
const nodeClass = this.getClassFromType( type );
node = nodeClass( ...this.getVector() );
} else if ( this.hasReference ) {
node = this.materialX.getMaterialXNode( this.referencePath ).getNode();
} else {
const element = this.element;
if ( element === 'convert' ) {
const nodeClass = this.getClassFromType( type );
node = nodeClass( this.getNodeByName( 'in' ) );
} else if ( element === 'constant' ) {
node = this.getNodeByName( 'value' );
} else if ( element === 'position' ) {
const space = this.getAttribute( 'space' );
node = space === 'world' ? positionWorld : positionLocal;
} else if ( element === 'normal' ) {
const space = this.getAttribute( 'space' );
node = space === 'world' ? normalWorld : normalLocal;
} else if ( element === 'tangent' ) {
const space = this.getAttribute( 'space' );
node = space === 'world' ? tangentWorld : tangentLocal;
} else if ( element === 'texcoord' ) {
const indexNode = this.getChildByName( 'index' );
const index = indexNode ? parseInt( indexNode.value ) : 0;
node = uv( index );
} else if ( element === 'geomcolor' ) {
const indexNode = this.getChildByName( 'index' );
const index = indexNode ? parseInt( indexNode.value ) : 0;
node = vertexColor( index );
} else if ( element === 'tiledimage' ) {
const file = this.getChildByName( 'file' );
const textureFile = file.getTexture();
const uvTiling = mx_transform_uv( ...this.getNodesByNames( [ 'uvtiling', 'uvoffset' ] ) );
node = texture( textureFile, uvTiling );
const colorSpaceNode = file.getColorSpaceNode();
if ( colorSpaceNode ) {
node = colorSpaceNode( node );
}
} else if ( element === 'image' ) {
const file = this.getChildByName( 'file' );
const uvNode = this.getNodeByName( 'texcoord' );
const textureFile = file.getTexture();
node = texture( textureFile, uvNode );
const colorSpaceNode = file.getColorSpaceNode();
if ( colorSpaceNode ) {
node = colorSpaceNode( node );
}
} else if ( MtlXLibrary[ element ] !== undefined ) {
const nodeElement = MtlXLibrary[ element ];
node = nodeElement.nodeFunc( ...this.getNodesByNames( ...nodeElement.params ) );
}
}
//
if ( node === null ) {
console.warn( `THREE.MaterialXLoader: Unexpected node ${ new XMLSerializer().serializeToString( this.nodeXML ) }.` );
node = float( 0 );
}
//
const nodeToTypeClass = this.getClassFromType( type );
if ( nodeToTypeClass !== null ) {
node = nodeToTypeClass( node );
}
node.name = this.name;
this.node = node;
return node;
}
getChildByName( name ) {
for ( const input of this.children ) {
if ( input.name === name ) {
return input;
}
}
}
getNodes() {
const nodes = {};
for ( const input of this.children ) {
const node = input.getNode();
nodes[ node.name ] = node;
}
return nodes;
}
getNodeByName( name ) {
const child = this.getChildByName( name );
return child ? child.getNode() : undefined;
}
getNodesByNames( ...names ) {
const nodes = [];
for ( const name of names ) {
const node = this.getNodeByName( name );
if ( node ) nodes.push( node );
}
return nodes;
}
getValue() {
return this.value.trim();
}
getVector() {
const vector = [];
for ( const val of this.getValue().split( /[,|\s]/ ) ) {
if ( val !== '' ) {
vector.push( Number( val.trim() ) );
}
}
return vector;
}
getAttribute( name ) {
return this.nodeXML.getAttribute( name );
}
getRecursiveAttribute( name ) {
let attribute = this.nodeXML.getAttribute( name );
if ( attribute === null && this.parent !== null ) {
attribute = this.parent.getRecursiveAttribute( name );
}
return attribute;
}
setStandardSurfaceToGltfPBR( material ) {
const inputs = this.getNodes();
//
let colorNode = null;
if ( inputs.base && inputs.base_color ) colorNode = mul( inputs.base, inputs.base_color );
else if ( inputs.base ) colorNode = inputs.base;
else if ( inputs.base_color ) colorNode = inputs.base_color;
//
let roughnessNode = null;
if ( inputs.specular_roughness ) roughnessNode = inputs.specular_roughness;
//
let metalnessNode = null;
if ( inputs.metalness ) metalnessNode = inputs.metalness;
//
let clearcoatNode = null;
let clearcoatRoughnessNode = null;
if ( inputs.coat ) clearcoatNode = inputs.coat;
if ( inputs.coat_roughness ) clearcoatRoughnessNode = inputs.coat_roughness;
if ( inputs.coat_color ) {
colorNode = colorNode ? mul( colorNode, inputs.coat_color ) : colorNode;
}
//
let normalNode = null;
if ( inputs.normal ) normalNode = inputs.normal;
//
let emissiveNode = null;
if ( inputs.emission ) emissiveNode = inputs.emission;
if ( inputs.emissionColor ) {
emissiveNode = emissiveNode ? mul( emissiveNode, inputs.emissionColor ) : emissiveNode;
}
//
material.colorNode = colorNode || color( 0.8, 0.8, 0.8 );
material.roughnessNode = roughnessNode || float( 0.2 );
material.metalnessNode = metalnessNode || float( 0 );
material.clearcoatNode = clearcoatNode || float( 0 );
material.clearcoatRoughnessNode = clearcoatRoughnessNode || float( 0 );
if ( normalNode ) material.normalNode = normalNode;
if ( emissiveNode ) material.emissiveNode = emissiveNode;
}
/*setGltfPBR( material ) {
const inputs = this.getNodes();
console.log( inputs );
}*/
setMaterial( material ) {
const element = this.element;
if ( element === 'gltf_pbr' ) {
//this.setGltfPBR( material );
} else if ( element === 'standard_surface' ) {
this.setStandardSurfaceToGltfPBR( material );
}
}
toBasicMaterial() {
const material = new MeshBasicNodeMaterial();
material.name = this.name;
for ( const nodeX of this.children.toReversed() ) {
if ( nodeX.name === 'out' ) {
material.colorNode = nodeX.getNode();
break;
}
}
return material;
}
toPhysicalMaterial() {
const material = new MeshPhysicalNodeMaterial();
material.name = this.name;
for ( const nodeX of this.children ) {
const shaderProperties = this.materialX.getMaterialXNode( nodeX.nodeName );
shaderProperties.setMaterial( material );
}
return material;
}
toMaterials() {
const materials = {};
let isUnlit = true;
for ( const nodeX of this.children ) {
if ( nodeX.element === 'surfacematerial' ) {
const material = nodeX.toPhysicalMaterial();
materials[ material.name ] = material;
isUnlit = false;
}
}
if ( isUnlit ) {
for ( const nodeX of this.children ) {
if ( nodeX.element === 'nodegraph' ) {
const material = nodeX.toBasicMaterial();
materials[ material.name ] = material;
}
}
}
return materials;
}
add( materialXNode ) {
materialXNode.parent = this;
this.children.push( materialXNode );
}
}
class MaterialX {
constructor( manager, path ) {
this.manager = manager;
this.path = path;
this.resourcePath = '';
this.nodesXLib = new Map();
//this.nodesXRefLib = new WeakMap();
this.textureLoader = new TextureLoader( manager );
}
addMaterialXNode( materialXNode ) {
this.nodesXLib.set( materialXNode.nodePath, materialXNode );
}
/*getMaterialXNodeFromXML( xmlNode ) {
return this.nodesXRefLib.get( xmlNode );
}*/
getMaterialXNode( ...names ) {
return this.nodesXLib.get( names.join( '/' ) );
}
parseNode( nodeXML, nodePath = '' ) {
const materialXNode = new MaterialXNode( this, nodeXML, nodePath );
if ( materialXNode.nodePath ) this.addMaterialXNode( materialXNode );
for ( const childNodeXML of nodeXML.children ) {
const childMXNode = this.parseNode( childNodeXML, materialXNode.nodePath );
materialXNode.add( childMXNode );
}
return materialXNode;
}
parse( text ) {
const rootXML = new DOMParser().parseFromString( text, 'application/xml' ).documentElement;
this.textureLoader.setPath( this.path );
//
const materials = this.parseNode( rootXML ).toMaterials();
return { materials };
}
}
export { MaterialXLoader };

View File

@ -0,0 +1,686 @@
import {
FileLoader,
Loader,
Matrix4,
Vector3
} from 'three';
import * as fflate from '../libs/fflate.module.js';
import { Volume } from '../misc/Volume.js';
class NRRDLoader extends Loader {
constructor( manager ) {
super( manager );
}
load( url, onLoad, onProgress, onError ) {
const scope = this;
const loader = new FileLoader( scope.manager );
loader.setPath( scope.path );
loader.setResponseType( 'arraybuffer' );
loader.setRequestHeader( scope.requestHeader );
loader.setWithCredentials( scope.withCredentials );
loader.load( url, function ( data ) {
try {
onLoad( scope.parse( data ) );
} catch ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
scope.manager.itemError( url );
}
}, onProgress, onError );
}
/**
*
* @param {boolean} segmentation is a option for user to choose
*/
setSegmentation( segmentation ) {
this.segmentation = segmentation;
}
parse( data ) {
// this parser is largely inspired from the XTK NRRD parser : https://github.com/xtk/X
let _data = data;
let _dataPointer = 0;
const _nativeLittleEndian = new Int8Array( new Int16Array( [ 1 ] ).buffer )[ 0 ] > 0;
const _littleEndian = true;
const headerObject = {};
function scan( type, chunks ) {
let _chunkSize = 1;
let _array_type = Uint8Array;
switch ( type ) {
// 1 byte data types
case 'uchar':
break;
case 'schar':
_array_type = Int8Array;
break;
// 2 byte data types
case 'ushort':
_array_type = Uint16Array;
_chunkSize = 2;
break;
case 'sshort':
_array_type = Int16Array;
_chunkSize = 2;
break;
// 4 byte data types
case 'uint':
_array_type = Uint32Array;
_chunkSize = 4;
break;
case 'sint':
_array_type = Int32Array;
_chunkSize = 4;
break;
case 'float':
_array_type = Float32Array;
_chunkSize = 4;
break;
case 'complex':
_array_type = Float64Array;
_chunkSize = 8;
break;
case 'double':
_array_type = Float64Array;
_chunkSize = 8;
break;
}
// increase the data pointer in-place
let _bytes = new _array_type( _data.slice( _dataPointer,
_dataPointer += chunks * _chunkSize ) );
// if required, flip the endianness of the bytes
if ( _nativeLittleEndian != _littleEndian ) {
// we need to flip here since the format doesn't match the native endianness
_bytes = flipEndianness( _bytes, _chunkSize );
}
// return the byte array
return _bytes;
}
//Flips typed array endianness in-place. Based on https://github.com/kig/DataStream.js/blob/master/DataStream.js.
function flipEndianness( array, chunkSize ) {
const u8 = new Uint8Array( array.buffer, array.byteOffset, array.byteLength );
for ( let i = 0; i < array.byteLength; i += chunkSize ) {
for ( let j = i + chunkSize - 1, k = i; j > k; j --, k ++ ) {
const tmp = u8[ k ];
u8[ k ] = u8[ j ];
u8[ j ] = tmp;
}
}
return array;
}
//parse the header
function parseHeader( header ) {
let data, field, fn, i, l, m, _i, _len;
const lines = header.split( /\r?\n/ );
for ( _i = 0, _len = lines.length; _i < _len; _i ++ ) {
l = lines[ _i ];
if ( l.match( /NRRD\d+/ ) ) {
headerObject.isNrrd = true;
} else if ( ! l.match( /^#/ ) && ( m = l.match( /(.*):(.*)/ ) ) ) {
field = m[ 1 ].trim();
data = m[ 2 ].trim();
fn = _fieldFunctions[ field ];
if ( fn ) {
fn.call( headerObject, data );
} else {
headerObject[ field ] = data;
}
}
}
if ( ! headerObject.isNrrd ) {
throw new Error( 'Not an NRRD file' );
}
if ( headerObject.encoding === 'bz2' || headerObject.encoding === 'bzip2' ) {
throw new Error( 'Bzip is not supported' );
}
if ( ! headerObject.vectors ) {
//if no space direction is set, let's use the identity
headerObject.vectors = [ ];
headerObject.vectors.push( [ 1, 0, 0 ] );
headerObject.vectors.push( [ 0, 1, 0 ] );
headerObject.vectors.push( [ 0, 0, 1 ] );
//apply spacing if defined
if ( headerObject.spacings ) {
for ( i = 0; i <= 2; i ++ ) {
if ( ! isNaN( headerObject.spacings[ i ] ) ) {
for ( let j = 0; j <= 2; j ++ ) {
headerObject.vectors[ i ][ j ] *= headerObject.spacings[ i ];
}
}
}
}
}
}
//parse the data when registred as one of this type : 'text', 'ascii', 'txt'
function parseDataAsText( data, start, end ) {
let number = '';
start = start || 0;
end = end || data.length;
let value;
//length of the result is the product of the sizes
const lengthOfTheResult = headerObject.sizes.reduce( function ( previous, current ) {
return previous * current;
}, 1 );
let base = 10;
if ( headerObject.encoding === 'hex' ) {
base = 16;
}
const result = new headerObject.__array( lengthOfTheResult );
let resultIndex = 0;
let parsingFunction = parseInt;
if ( headerObject.__array === Float32Array || headerObject.__array === Float64Array ) {
parsingFunction = parseFloat;
}
for ( let i = start; i < end; i ++ ) {
value = data[ i ];
//if value is not a space
if ( ( value < 9 || value > 13 ) && value !== 32 ) {
number += String.fromCharCode( value );
} else {
if ( number !== '' ) {
result[ resultIndex ] = parsingFunction( number, base );
resultIndex ++;
}
number = '';
}
}
if ( number !== '' ) {
result[ resultIndex ] = parsingFunction( number, base );
resultIndex ++;
}
return result;
}
const _bytes = scan( 'uchar', data.byteLength );
const _length = _bytes.length;
let _header = null;
let _data_start = 0;
let i;
for ( i = 1; i < _length; i ++ ) {
if ( _bytes[ i - 1 ] == 10 && _bytes[ i ] == 10 ) {
// we found two line breaks in a row
// now we know what the header is
_header = this.parseChars( _bytes, 0, i - 2 );
// this is were the data starts
_data_start = i + 1;
break;
}
}
// parse the header
parseHeader( _header );
_data = _bytes.subarray( _data_start ); // the data without header
if ( headerObject.encoding.substring( 0, 2 ) === 'gz' ) {
// we need to decompress the datastream
// here we start the unzipping and get a typed Uint8Array back
_data = fflate.gunzipSync( new Uint8Array( _data ) );
} else if ( headerObject.encoding === 'ascii' || headerObject.encoding === 'text' || headerObject.encoding === 'txt' || headerObject.encoding === 'hex' ) {
_data = parseDataAsText( _data );
} else if ( headerObject.encoding === 'raw' ) {
//we need to copy the array to create a new array buffer, else we retrieve the original arraybuffer with the header
const _copy = new Uint8Array( _data.length );
for ( let i = 0; i < _data.length; i ++ ) {
_copy[ i ] = _data[ i ];
}
_data = _copy;
}
// .. let's use the underlying array buffer
_data = _data.buffer;
const volume = new Volume();
volume.header = headerObject;
volume.segmentation = this.segmentation;
//
// parse the (unzipped) data to a datastream of the correct type
//
volume.data = new headerObject.__array( _data );
// get the min and max intensities
const min_max = volume.computeMinMax();
const min = min_max[ 0 ];
const max = min_max[ 1 ];
// attach the scalar range to the volume
volume.windowLow = min;
volume.windowHigh = max;
// get the image dimensions
volume.dimensions = [ headerObject.sizes[ 0 ], headerObject.sizes[ 1 ], headerObject.sizes[ 2 ] ];
volume.xLength = volume.dimensions[ 0 ];
volume.yLength = volume.dimensions[ 1 ];
volume.zLength = volume.dimensions[ 2 ];
// Identify axis order in the space-directions matrix from the header if possible.
if ( headerObject.vectors ) {
const xIndex = headerObject.vectors.findIndex( vector => vector[ 0 ] !== 0 );
const yIndex = headerObject.vectors.findIndex( vector => vector[ 1 ] !== 0 );
const zIndex = headerObject.vectors.findIndex( vector => vector[ 2 ] !== 0 );
const axisOrder = [];
if ( xIndex !== yIndex && xIndex !== zIndex && yIndex !== zIndex ) {
axisOrder[ xIndex ] = 'x';
axisOrder[ yIndex ] = 'y';
axisOrder[ zIndex ] = 'z';
} else {
axisOrder[ 0 ] = 'x';
axisOrder[ 1 ] = 'y';
axisOrder[ 2 ] = 'z';
}
volume.axisOrder = axisOrder;
} else {
volume.axisOrder = [ 'x', 'y', 'z' ];
}
// spacing
const spacingX = new Vector3().fromArray( headerObject.vectors[ 0 ] ).length();
const spacingY = new Vector3().fromArray( headerObject.vectors[ 1 ] ).length();
const spacingZ = new Vector3().fromArray( headerObject.vectors[ 2 ] ).length();
volume.spacing = [ spacingX, spacingY, spacingZ ];
// Create IJKtoRAS matrix
volume.matrix = new Matrix4();
const transitionMatrix = new Matrix4();
if ( headerObject.space === 'left-posterior-superior' ) {
transitionMatrix.set(
- 1, 0, 0, 0,
0, - 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
);
} else if ( headerObject.space === 'left-anterior-superior' ) {
transitionMatrix.set(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, - 1, 0,
0, 0, 0, 1
);
}
if ( ! headerObject.vectors ) {
volume.matrix.set(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1 );
} else {
const v = headerObject.vectors;
const ijk_to_transition = new Matrix4().set(
v[ 0 ][ 0 ], v[ 1 ][ 0 ], v[ 2 ][ 0 ], 0,
v[ 0 ][ 1 ], v[ 1 ][ 1 ], v[ 2 ][ 1 ], 0,
v[ 0 ][ 2 ], v[ 1 ][ 2 ], v[ 2 ][ 2 ], 0,
0, 0, 0, 1
);
const transition_to_ras = new Matrix4().multiplyMatrices( ijk_to_transition, transitionMatrix );
volume.matrix = transition_to_ras;
}
volume.inverseMatrix = new Matrix4();
volume.inverseMatrix.copy( volume.matrix ).invert();
volume.RASDimensions = [
Math.floor( volume.xLength * spacingX ),
Math.floor( volume.yLength * spacingY ),
Math.floor( volume.zLength * spacingZ )
];
// .. and set the default threshold
// only if the threshold was not already set
if ( volume.lowerThreshold === - Infinity ) {
volume.lowerThreshold = min;
}
if ( volume.upperThreshold === Infinity ) {
volume.upperThreshold = max;
}
return volume;
}
parseChars( array, start, end ) {
// without borders, use the whole array
if ( start === undefined ) {
start = 0;
}
if ( end === undefined ) {
end = array.length;
}
let output = '';
// create and append the chars
let i = 0;
for ( i = start; i < end; ++ i ) {
output += String.fromCharCode( array[ i ] );
}
return output;
}
}
const _fieldFunctions = {
type: function ( data ) {
switch ( data ) {
case 'uchar':
case 'unsigned char':
case 'uint8':
case 'uint8_t':
this.__array = Uint8Array;
break;
case 'signed char':
case 'int8':
case 'int8_t':
this.__array = Int8Array;
break;
case 'short':
case 'short int':
case 'signed short':
case 'signed short int':
case 'int16':
case 'int16_t':
this.__array = Int16Array;
break;
case 'ushort':
case 'unsigned short':
case 'unsigned short int':
case 'uint16':
case 'uint16_t':
this.__array = Uint16Array;
break;
case 'int':
case 'signed int':
case 'int32':
case 'int32_t':
this.__array = Int32Array;
break;
case 'uint':
case 'unsigned int':
case 'uint32':
case 'uint32_t':
this.__array = Uint32Array;
break;
case 'float':
this.__array = Float32Array;
break;
case 'double':
this.__array = Float64Array;
break;
default:
throw new Error( 'Unsupported NRRD data type: ' + data );
}
return this.type = data;
},
endian: function ( data ) {
return this.endian = data;
},
encoding: function ( data ) {
return this.encoding = data;
},
dimension: function ( data ) {
return this.dim = parseInt( data, 10 );
},
sizes: function ( data ) {
let i;
return this.sizes = ( function () {
const _ref = data.split( /\s+/ );
const _results = [];
for ( let _i = 0, _len = _ref.length; _i < _len; _i ++ ) {
i = _ref[ _i ];
_results.push( parseInt( i, 10 ) );
}
return _results;
} )();
},
space: function ( data ) {
return this.space = data;
},
'space origin': function ( data ) {
return this.space_origin = data.split( '(' )[ 1 ].split( ')' )[ 0 ].split( ',' );
},
'space directions': function ( data ) {
let f, v;
const parts = data.match( /\(.*?\)/g );
return this.vectors = ( function () {
const _results = [];
for ( let _i = 0, _len = parts.length; _i < _len; _i ++ ) {
v = parts[ _i ];
_results.push( ( function () {
const _ref = v.slice( 1, - 1 ).split( /,/ );
const _results2 = [];
for ( let _j = 0, _len2 = _ref.length; _j < _len2; _j ++ ) {
f = _ref[ _j ];
_results2.push( parseFloat( f ) );
}
return _results2;
} )() );
}
return _results;
} )();
},
spacings: function ( data ) {
let f;
const parts = data.split( /\s+/ );
return this.spacings = ( function () {
const _results = [];
for ( let _i = 0, _len = parts.length; _i < _len; _i ++ ) {
f = parts[ _i ];
_results.push( parseFloat( f ) );
}
return _results;
} )();
}
};
export { NRRDLoader };

View File

@ -0,0 +1,905 @@
import {
BufferGeometry,
FileLoader,
Float32BufferAttribute,
Group,
LineBasicMaterial,
LineSegments,
Loader,
Material,
Mesh,
MeshPhongMaterial,
Points,
PointsMaterial,
Vector3,
Color
} from 'three';
// o object_name | g group_name
const _object_pattern = /^[og]\s*(.+)?/;
// mtllib file_reference
const _material_library_pattern = /^mtllib /;
// usemtl material_name
const _material_use_pattern = /^usemtl /;
// usemap map_name
const _map_use_pattern = /^usemap /;
const _face_vertex_data_separator_pattern = /\s+/;
const _vA = new Vector3();
const _vB = new Vector3();
const _vC = new Vector3();
const _ab = new Vector3();
const _cb = new Vector3();
const _color = new Color();
function ParserState() {
const state = {
objects: [],
object: {},
vertices: [],
normals: [],
colors: [],
uvs: [],
materials: {},
materialLibraries: [],
startObject: function ( name, fromDeclaration ) {
// If the current object (initial from reset) is not from a g/o declaration in the parsed
// file. We need to use it for the first parsed g/o to keep things in sync.
if ( this.object && this.object.fromDeclaration === false ) {
this.object.name = name;
this.object.fromDeclaration = ( fromDeclaration !== false );
return;
}
const previousMaterial = ( this.object && typeof this.object.currentMaterial === 'function' ? this.object.currentMaterial() : undefined );
if ( this.object && typeof this.object._finalize === 'function' ) {
this.object._finalize( true );
}
this.object = {
name: name || '',
fromDeclaration: ( fromDeclaration !== false ),
geometry: {
vertices: [],
normals: [],
colors: [],
uvs: [],
hasUVIndices: false
},
materials: [],
smooth: true,
startMaterial: function ( name, libraries ) {
const previous = this._finalize( false );
// New usemtl declaration overwrites an inherited material, except if faces were declared
// after the material, then it must be preserved for proper MultiMaterial continuation.
if ( previous && ( previous.inherited || previous.groupCount <= 0 ) ) {
this.materials.splice( previous.index, 1 );
}
const material = {
index: this.materials.length,
name: name || '',
mtllib: ( Array.isArray( libraries ) && libraries.length > 0 ? libraries[ libraries.length - 1 ] : '' ),
smooth: ( previous !== undefined ? previous.smooth : this.smooth ),
groupStart: ( previous !== undefined ? previous.groupEnd : 0 ),
groupEnd: - 1,
groupCount: - 1,
inherited: false,
clone: function ( index ) {
const cloned = {
index: ( typeof index === 'number' ? index : this.index ),
name: this.name,
mtllib: this.mtllib,
smooth: this.smooth,
groupStart: 0,
groupEnd: - 1,
groupCount: - 1,
inherited: false
};
cloned.clone = this.clone.bind( cloned );
return cloned;
}
};
this.materials.push( material );
return material;
},
currentMaterial: function () {
if ( this.materials.length > 0 ) {
return this.materials[ this.materials.length - 1 ];
}
return undefined;
},
_finalize: function ( end ) {
const lastMultiMaterial = this.currentMaterial();
if ( lastMultiMaterial && lastMultiMaterial.groupEnd === - 1 ) {
lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3;
lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart;
lastMultiMaterial.inherited = false;
}
// Ignore objects tail materials if no face declarations followed them before a new o/g started.
if ( end && this.materials.length > 1 ) {
for ( let mi = this.materials.length - 1; mi >= 0; mi -- ) {
if ( this.materials[ mi ].groupCount <= 0 ) {
this.materials.splice( mi, 1 );
}
}
}
// Guarantee at least one empty material, this makes the creation later more straight forward.
if ( end && this.materials.length === 0 ) {
this.materials.push( {
name: '',
smooth: this.smooth
} );
}
return lastMultiMaterial;
}
};
// Inherit previous objects material.
// Spec tells us that a declared material must be set to all objects until a new material is declared.
// If a usemtl declaration is encountered while this new object is being parsed, it will
// overwrite the inherited material. Exception being that there was already face declarations
// to the inherited material, then it will be preserved for proper MultiMaterial continuation.
if ( previousMaterial && previousMaterial.name && typeof previousMaterial.clone === 'function' ) {
const declared = previousMaterial.clone( 0 );
declared.inherited = true;
this.object.materials.push( declared );
}
this.objects.push( this.object );
},
finalize: function () {
if ( this.object && typeof this.object._finalize === 'function' ) {
this.object._finalize( true );
}
},
parseVertexIndex: function ( value, len ) {
const index = parseInt( value, 10 );
return ( index >= 0 ? index - 1 : index + len / 3 ) * 3;
},
parseNormalIndex: function ( value, len ) {
const index = parseInt( value, 10 );
return ( index >= 0 ? index - 1 : index + len / 3 ) * 3;
},
parseUVIndex: function ( value, len ) {
const index = parseInt( value, 10 );
return ( index >= 0 ? index - 1 : index + len / 2 ) * 2;
},
addVertex: function ( a, b, c ) {
const src = this.vertices;
const dst = this.object.geometry.vertices;
dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] );
dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] );
},
addVertexPoint: function ( a ) {
const src = this.vertices;
const dst = this.object.geometry.vertices;
dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
},
addVertexLine: function ( a ) {
const src = this.vertices;
const dst = this.object.geometry.vertices;
dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
},
addNormal: function ( a, b, c ) {
const src = this.normals;
const dst = this.object.geometry.normals;
dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] );
dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] );
},
addFaceNormal: function ( a, b, c ) {
const src = this.vertices;
const dst = this.object.geometry.normals;
_vA.fromArray( src, a );
_vB.fromArray( src, b );
_vC.fromArray( src, c );
_cb.subVectors( _vC, _vB );
_ab.subVectors( _vA, _vB );
_cb.cross( _ab );
_cb.normalize();
dst.push( _cb.x, _cb.y, _cb.z );
dst.push( _cb.x, _cb.y, _cb.z );
dst.push( _cb.x, _cb.y, _cb.z );
},
addColor: function ( a, b, c ) {
const src = this.colors;
const dst = this.object.geometry.colors;
if ( src[ a ] !== undefined ) dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
if ( src[ b ] !== undefined ) dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] );
if ( src[ c ] !== undefined ) dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] );
},
addUV: function ( a, b, c ) {
const src = this.uvs;
const dst = this.object.geometry.uvs;
dst.push( src[ a + 0 ], src[ a + 1 ] );
dst.push( src[ b + 0 ], src[ b + 1 ] );
dst.push( src[ c + 0 ], src[ c + 1 ] );
},
addDefaultUV: function () {
const dst = this.object.geometry.uvs;
dst.push( 0, 0 );
dst.push( 0, 0 );
dst.push( 0, 0 );
},
addUVLine: function ( a ) {
const src = this.uvs;
const dst = this.object.geometry.uvs;
dst.push( src[ a + 0 ], src[ a + 1 ] );
},
addFace: function ( a, b, c, ua, ub, uc, na, nb, nc ) {
const vLen = this.vertices.length;
let ia = this.parseVertexIndex( a, vLen );
let ib = this.parseVertexIndex( b, vLen );
let ic = this.parseVertexIndex( c, vLen );
this.addVertex( ia, ib, ic );
this.addColor( ia, ib, ic );
// normals
if ( na !== undefined && na !== '' ) {
const nLen = this.normals.length;
ia = this.parseNormalIndex( na, nLen );
ib = this.parseNormalIndex( nb, nLen );
ic = this.parseNormalIndex( nc, nLen );
this.addNormal( ia, ib, ic );
} else {
this.addFaceNormal( ia, ib, ic );
}
// uvs
if ( ua !== undefined && ua !== '' ) {
const uvLen = this.uvs.length;
ia = this.parseUVIndex( ua, uvLen );
ib = this.parseUVIndex( ub, uvLen );
ic = this.parseUVIndex( uc, uvLen );
this.addUV( ia, ib, ic );
this.object.geometry.hasUVIndices = true;
} else {
// add placeholder values (for inconsistent face definitions)
this.addDefaultUV();
}
},
addPointGeometry: function ( vertices ) {
this.object.geometry.type = 'Points';
const vLen = this.vertices.length;
for ( let vi = 0, l = vertices.length; vi < l; vi ++ ) {
const index = this.parseVertexIndex( vertices[ vi ], vLen );
this.addVertexPoint( index );
this.addColor( index );
}
},
addLineGeometry: function ( vertices, uvs ) {
this.object.geometry.type = 'Line';
const vLen = this.vertices.length;
const uvLen = this.uvs.length;
for ( let vi = 0, l = vertices.length; vi < l; vi ++ ) {
this.addVertexLine( this.parseVertexIndex( vertices[ vi ], vLen ) );
}
for ( let uvi = 0, l = uvs.length; uvi < l; uvi ++ ) {
this.addUVLine( this.parseUVIndex( uvs[ uvi ], uvLen ) );
}
}
};
state.startObject( '', false );
return state;
}
//
class OBJLoader extends Loader {
constructor( manager ) {
super( manager );
this.materials = null;
}
load( url, onLoad, onProgress, onError ) {
const scope = this;
const loader = new FileLoader( this.manager );
loader.setPath( this.path );
loader.setRequestHeader( this.requestHeader );
loader.setWithCredentials( this.withCredentials );
loader.load( url, function ( text ) {
try {
onLoad( scope.parse( text ) );
} catch ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
scope.manager.itemError( url );
}
}, onProgress, onError );
}
setMaterials( materials ) {
this.materials = materials;
return this;
}
parse( text ) {
const state = new ParserState();
if ( text.indexOf( '\r\n' ) !== - 1 ) {
// This is faster than String.split with regex that splits on both
text = text.replace( /\r\n/g, '\n' );
}
if ( text.indexOf( '\\\n' ) !== - 1 ) {
// join lines separated by a line continuation character (\)
text = text.replace( /\\\n/g, '' );
}
const lines = text.split( '\n' );
let result = [];
for ( let i = 0, l = lines.length; i < l; i ++ ) {
const line = lines[ i ].trimStart();
if ( line.length === 0 ) continue;
const lineFirstChar = line.charAt( 0 );
// @todo invoke passed in handler if any
if ( lineFirstChar === '#' ) continue; // skip comments
if ( lineFirstChar === 'v' ) {
const data = line.split( _face_vertex_data_separator_pattern );
switch ( data[ 0 ] ) {
case 'v':
state.vertices.push(
parseFloat( data[ 1 ] ),
parseFloat( data[ 2 ] ),
parseFloat( data[ 3 ] )
);
if ( data.length >= 7 ) {
_color.setRGB(
parseFloat( data[ 4 ] ),
parseFloat( data[ 5 ] ),
parseFloat( data[ 6 ] )
).convertSRGBToLinear();
state.colors.push( _color.r, _color.g, _color.b );
} else {
// if no colors are defined, add placeholders so color and vertex indices match
state.colors.push( undefined, undefined, undefined );
}
break;
case 'vn':
state.normals.push(
parseFloat( data[ 1 ] ),
parseFloat( data[ 2 ] ),
parseFloat( data[ 3 ] )
);
break;
case 'vt':
state.uvs.push(
parseFloat( data[ 1 ] ),
parseFloat( data[ 2 ] )
);
break;
}
} else if ( lineFirstChar === 'f' ) {
const lineData = line.slice( 1 ).trim();
const vertexData = lineData.split( _face_vertex_data_separator_pattern );
const faceVertices = [];
// Parse the face vertex data into an easy to work with format
for ( let j = 0, jl = vertexData.length; j < jl; j ++ ) {
const vertex = vertexData[ j ];
if ( vertex.length > 0 ) {
const vertexParts = vertex.split( '/' );
faceVertices.push( vertexParts );
}
}
// Draw an edge between the first vertex and all subsequent vertices to form an n-gon
const v1 = faceVertices[ 0 ];
for ( let j = 1, jl = faceVertices.length - 1; j < jl; j ++ ) {
const v2 = faceVertices[ j ];
const v3 = faceVertices[ j + 1 ];
state.addFace(
v1[ 0 ], v2[ 0 ], v3[ 0 ],
v1[ 1 ], v2[ 1 ], v3[ 1 ],
v1[ 2 ], v2[ 2 ], v3[ 2 ]
);
}
} else if ( lineFirstChar === 'l' ) {
const lineParts = line.substring( 1 ).trim().split( ' ' );
let lineVertices = [];
const lineUVs = [];
if ( line.indexOf( '/' ) === - 1 ) {
lineVertices = lineParts;
} else {
for ( let li = 0, llen = lineParts.length; li < llen; li ++ ) {
const parts = lineParts[ li ].split( '/' );
if ( parts[ 0 ] !== '' ) lineVertices.push( parts[ 0 ] );
if ( parts[ 1 ] !== '' ) lineUVs.push( parts[ 1 ] );
}
}
state.addLineGeometry( lineVertices, lineUVs );
} else if ( lineFirstChar === 'p' ) {
const lineData = line.slice( 1 ).trim();
const pointData = lineData.split( ' ' );
state.addPointGeometry( pointData );
} else if ( ( result = _object_pattern.exec( line ) ) !== null ) {
// o object_name
// or
// g group_name
// WORKAROUND: https://bugs.chromium.org/p/v8/issues/detail?id=2869
// let name = result[ 0 ].slice( 1 ).trim();
const name = ( ' ' + result[ 0 ].slice( 1 ).trim() ).slice( 1 );
state.startObject( name );
} else if ( _material_use_pattern.test( line ) ) {
// material
state.object.startMaterial( line.substring( 7 ).trim(), state.materialLibraries );
} else if ( _material_library_pattern.test( line ) ) {
// mtl file
state.materialLibraries.push( line.substring( 7 ).trim() );
} else if ( _map_use_pattern.test( line ) ) {
// the line is parsed but ignored since the loader assumes textures are defined MTL files
// (according to https://www.okino.com/conv/imp_wave.htm, 'usemap' is the old-style Wavefront texture reference method)
console.warn( 'THREE.OBJLoader: Rendering identifier "usemap" not supported. Textures must be defined in MTL files.' );
} else if ( lineFirstChar === 's' ) {
result = line.split( ' ' );
// smooth shading
// @todo Handle files that have varying smooth values for a set of faces inside one geometry,
// but does not define a usemtl for each face set.
// This should be detected and a dummy material created (later MultiMaterial and geometry groups).
// This requires some care to not create extra material on each smooth value for "normal" obj files.
// where explicit usemtl defines geometry groups.
// Example asset: examples/models/obj/cerberus/Cerberus.obj
/*
* http://paulbourke.net/dataformats/obj/
*
* From chapter "Grouping" Syntax explanation "s group_number":
* "group_number is the smoothing group number. To turn off smoothing groups, use a value of 0 or off.
* Polygonal elements use group numbers to put elements in different smoothing groups. For free-form
* surfaces, smoothing groups are either turned on or off; there is no difference between values greater
* than 0."
*/
if ( result.length > 1 ) {
const value = result[ 1 ].trim().toLowerCase();
state.object.smooth = ( value !== '0' && value !== 'off' );
} else {
// ZBrush can produce "s" lines #11707
state.object.smooth = true;
}
const material = state.object.currentMaterial();
if ( material ) material.smooth = state.object.smooth;
} else {
// Handle null terminated files without exception
if ( line === '\0' ) continue;
console.warn( 'THREE.OBJLoader: Unexpected line: "' + line + '"' );
}
}
state.finalize();
const container = new Group();
container.materialLibraries = [].concat( state.materialLibraries );
const hasPrimitives = ! ( state.objects.length === 1 && state.objects[ 0 ].geometry.vertices.length === 0 );
if ( hasPrimitives === true ) {
for ( let i = 0, l = state.objects.length; i < l; i ++ ) {
const object = state.objects[ i ];
const geometry = object.geometry;
const materials = object.materials;
const isLine = ( geometry.type === 'Line' );
const isPoints = ( geometry.type === 'Points' );
let hasVertexColors = false;
// Skip o/g line declarations that did not follow with any faces
if ( geometry.vertices.length === 0 ) continue;
const buffergeometry = new BufferGeometry();
buffergeometry.setAttribute( 'position', new Float32BufferAttribute( geometry.vertices, 3 ) );
if ( geometry.normals.length > 0 ) {
buffergeometry.setAttribute( 'normal', new Float32BufferAttribute( geometry.normals, 3 ) );
}
if ( geometry.colors.length > 0 ) {
hasVertexColors = true;
buffergeometry.setAttribute( 'color', new Float32BufferAttribute( geometry.colors, 3 ) );
}
if ( geometry.hasUVIndices === true ) {
buffergeometry.setAttribute( 'uv', new Float32BufferAttribute( geometry.uvs, 2 ) );
}
// Create materials
const createdMaterials = [];
for ( let mi = 0, miLen = materials.length; mi < miLen; mi ++ ) {
const sourceMaterial = materials[ mi ];
const materialHash = sourceMaterial.name + '_' + sourceMaterial.smooth + '_' + hasVertexColors;
let material = state.materials[ materialHash ];
if ( this.materials !== null ) {
material = this.materials.create( sourceMaterial.name );
// mtl etc. loaders probably can't create line materials correctly, copy properties to a line material.
if ( isLine && material && ! ( material instanceof LineBasicMaterial ) ) {
const materialLine = new LineBasicMaterial();
Material.prototype.copy.call( materialLine, material );
materialLine.color.copy( material.color );
material = materialLine;
} else if ( isPoints && material && ! ( material instanceof PointsMaterial ) ) {
const materialPoints = new PointsMaterial( { size: 10, sizeAttenuation: false } );
Material.prototype.copy.call( materialPoints, material );
materialPoints.color.copy( material.color );
materialPoints.map = material.map;
material = materialPoints;
}
}
if ( material === undefined ) {
if ( isLine ) {
material = new LineBasicMaterial();
} else if ( isPoints ) {
material = new PointsMaterial( { size: 1, sizeAttenuation: false } );
} else {
material = new MeshPhongMaterial();
}
material.name = sourceMaterial.name;
material.flatShading = sourceMaterial.smooth ? false : true;
material.vertexColors = hasVertexColors;
state.materials[ materialHash ] = material;
}
createdMaterials.push( material );
}
// Create mesh
let mesh;
if ( createdMaterials.length > 1 ) {
for ( let mi = 0, miLen = materials.length; mi < miLen; mi ++ ) {
const sourceMaterial = materials[ mi ];
buffergeometry.addGroup( sourceMaterial.groupStart, sourceMaterial.groupCount, mi );
}
if ( isLine ) {
mesh = new LineSegments( buffergeometry, createdMaterials );
} else if ( isPoints ) {
mesh = new Points( buffergeometry, createdMaterials );
} else {
mesh = new Mesh( buffergeometry, createdMaterials );
}
} else {
if ( isLine ) {
mesh = new LineSegments( buffergeometry, createdMaterials[ 0 ] );
} else if ( isPoints ) {
mesh = new Points( buffergeometry, createdMaterials[ 0 ] );
} else {
mesh = new Mesh( buffergeometry, createdMaterials[ 0 ] );
}
}
mesh.name = object.name;
container.add( mesh );
}
} else {
// if there is only the default parser state object with no geometry data, interpret data as point cloud
if ( state.vertices.length > 0 ) {
const material = new PointsMaterial( { size: 1, sizeAttenuation: false } );
const buffergeometry = new BufferGeometry();
buffergeometry.setAttribute( 'position', new Float32BufferAttribute( state.vertices, 3 ) );
if ( state.colors.length > 0 && state.colors[ 0 ] !== undefined ) {
buffergeometry.setAttribute( 'color', new Float32BufferAttribute( state.colors, 3 ) );
material.vertexColors = true;
}
const points = new Points( buffergeometry, material );
container.add( points );
}
}
return container;
}
}
export { OBJLoader };

View File

@ -0,0 +1,467 @@
import {
BufferGeometry,
Color,
FileLoader,
Float32BufferAttribute,
Int32BufferAttribute,
Loader,
Points,
PointsMaterial
} from 'three';
class PCDLoader extends Loader {
constructor( manager ) {
super( manager );
this.littleEndian = true;
}
load( url, onLoad, onProgress, onError ) {
const scope = this;
const loader = new FileLoader( scope.manager );
loader.setPath( scope.path );
loader.setResponseType( 'arraybuffer' );
loader.setRequestHeader( scope.requestHeader );
loader.setWithCredentials( scope.withCredentials );
loader.load( url, function ( data ) {
try {
onLoad( scope.parse( data ) );
} catch ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
scope.manager.itemError( url );
}
}, onProgress, onError );
}
parse( data ) {
// from https://gitlab.com/taketwo/three-pcd-loader/blob/master/decompress-lzf.js
function decompressLZF( inData, outLength ) {
const inLength = inData.length;
const outData = new Uint8Array( outLength );
let inPtr = 0;
let outPtr = 0;
let ctrl;
let len;
let ref;
do {
ctrl = inData[ inPtr ++ ];
if ( ctrl < ( 1 << 5 ) ) {
ctrl ++;
if ( outPtr + ctrl > outLength ) throw new Error( 'Output buffer is not large enough' );
if ( inPtr + ctrl > inLength ) throw new Error( 'Invalid compressed data' );
do {
outData[ outPtr ++ ] = inData[ inPtr ++ ];
} while ( -- ctrl );
} else {
len = ctrl >> 5;
ref = outPtr - ( ( ctrl & 0x1f ) << 8 ) - 1;
if ( inPtr >= inLength ) throw new Error( 'Invalid compressed data' );
if ( len === 7 ) {
len += inData[ inPtr ++ ];
if ( inPtr >= inLength ) throw new Error( 'Invalid compressed data' );
}
ref -= inData[ inPtr ++ ];
if ( outPtr + len + 2 > outLength ) throw new Error( 'Output buffer is not large enough' );
if ( ref < 0 ) throw new Error( 'Invalid compressed data' );
if ( ref >= outPtr ) throw new Error( 'Invalid compressed data' );
do {
outData[ outPtr ++ ] = outData[ ref ++ ];
} while ( -- len + 2 );
}
} while ( inPtr < inLength );
return outData;
}
function parseHeader( data ) {
const PCDheader = {};
const result1 = data.search( /[\r\n]DATA\s(\S*)\s/i );
const result2 = /[\r\n]DATA\s(\S*)\s/i.exec( data.slice( result1 - 1 ) );
PCDheader.data = result2[ 1 ];
PCDheader.headerLen = result2[ 0 ].length + result1;
PCDheader.str = data.slice( 0, PCDheader.headerLen );
// remove comments
PCDheader.str = PCDheader.str.replace( /#.*/gi, '' );
// parse
PCDheader.version = /VERSION (.*)/i.exec( PCDheader.str );
PCDheader.fields = /FIELDS (.*)/i.exec( PCDheader.str );
PCDheader.size = /SIZE (.*)/i.exec( PCDheader.str );
PCDheader.type = /TYPE (.*)/i.exec( PCDheader.str );
PCDheader.count = /COUNT (.*)/i.exec( PCDheader.str );
PCDheader.width = /WIDTH (.*)/i.exec( PCDheader.str );
PCDheader.height = /HEIGHT (.*)/i.exec( PCDheader.str );
PCDheader.viewpoint = /VIEWPOINT (.*)/i.exec( PCDheader.str );
PCDheader.points = /POINTS (.*)/i.exec( PCDheader.str );
// evaluate
if ( PCDheader.version !== null )
PCDheader.version = parseFloat( PCDheader.version[ 1 ] );
PCDheader.fields = ( PCDheader.fields !== null ) ? PCDheader.fields[ 1 ].split( ' ' ) : [];
if ( PCDheader.type !== null )
PCDheader.type = PCDheader.type[ 1 ].split( ' ' );
if ( PCDheader.width !== null )
PCDheader.width = parseInt( PCDheader.width[ 1 ] );
if ( PCDheader.height !== null )
PCDheader.height = parseInt( PCDheader.height[ 1 ] );
if ( PCDheader.viewpoint !== null )
PCDheader.viewpoint = PCDheader.viewpoint[ 1 ];
if ( PCDheader.points !== null )
PCDheader.points = parseInt( PCDheader.points[ 1 ], 10 );
if ( PCDheader.points === null )
PCDheader.points = PCDheader.width * PCDheader.height;
if ( PCDheader.size !== null ) {
PCDheader.size = PCDheader.size[ 1 ].split( ' ' ).map( function ( x ) {
return parseInt( x, 10 );
} );
}
if ( PCDheader.count !== null ) {
PCDheader.count = PCDheader.count[ 1 ].split( ' ' ).map( function ( x ) {
return parseInt( x, 10 );
} );
} else {
PCDheader.count = [];
for ( let i = 0, l = PCDheader.fields.length; i < l; i ++ ) {
PCDheader.count.push( 1 );
}
}
PCDheader.offset = {};
let sizeSum = 0;
for ( let i = 0, l = PCDheader.fields.length; i < l; i ++ ) {
if ( PCDheader.data === 'ascii' ) {
PCDheader.offset[ PCDheader.fields[ i ] ] = i;
} else {
PCDheader.offset[ PCDheader.fields[ i ] ] = sizeSum;
sizeSum += PCDheader.size[ i ] * PCDheader.count[ i ];
}
}
// for binary only
PCDheader.rowSize = sizeSum;
return PCDheader;
}
const textData = new TextDecoder().decode( data );
// parse header (always ascii format)
const PCDheader = parseHeader( textData );
// parse data
const position = [];
const normal = [];
const color = [];
const intensity = [];
const label = [];
const c = new Color();
// ascii
if ( PCDheader.data === 'ascii' ) {
const offset = PCDheader.offset;
const pcdData = textData.slice( PCDheader.headerLen );
const lines = pcdData.split( '\n' );
for ( let i = 0, l = lines.length; i < l; i ++ ) {
if ( lines[ i ] === '' ) continue;
const line = lines[ i ].split( ' ' );
if ( offset.x !== undefined ) {
position.push( parseFloat( line[ offset.x ] ) );
position.push( parseFloat( line[ offset.y ] ) );
position.push( parseFloat( line[ offset.z ] ) );
}
if ( offset.rgb !== undefined ) {
const rgb_field_index = PCDheader.fields.findIndex( ( field ) => field === 'rgb' );
const rgb_type = PCDheader.type[ rgb_field_index ];
const float = parseFloat( line[ offset.rgb ] );
let rgb = float;
if ( rgb_type === 'F' ) {
// treat float values as int
// https://github.com/daavoo/pyntcloud/pull/204/commits/7b4205e64d5ed09abe708b2e91b615690c24d518
const farr = new Float32Array( 1 );
farr[ 0 ] = float;
rgb = new Int32Array( farr.buffer )[ 0 ];
}
const r = ( ( rgb >> 16 ) & 0x0000ff ) / 255;
const g = ( ( rgb >> 8 ) & 0x0000ff ) / 255;
const b = ( ( rgb >> 0 ) & 0x0000ff ) / 255;
c.set( r, g, b ).convertSRGBToLinear();
color.push( c.r, c.g, c.b );
}
if ( offset.normal_x !== undefined ) {
normal.push( parseFloat( line[ offset.normal_x ] ) );
normal.push( parseFloat( line[ offset.normal_y ] ) );
normal.push( parseFloat( line[ offset.normal_z ] ) );
}
if ( offset.intensity !== undefined ) {
intensity.push( parseFloat( line[ offset.intensity ] ) );
}
if ( offset.label !== undefined ) {
label.push( parseInt( line[ offset.label ] ) );
}
}
}
// binary-compressed
// normally data in PCD files are organized as array of structures: XYZRGBXYZRGB
// binary compressed PCD files organize their data as structure of arrays: XXYYZZRGBRGB
// that requires a totally different parsing approach compared to non-compressed data
if ( PCDheader.data === 'binary_compressed' ) {
const sizes = new Uint32Array( data.slice( PCDheader.headerLen, PCDheader.headerLen + 8 ) );
const compressedSize = sizes[ 0 ];
const decompressedSize = sizes[ 1 ];
const decompressed = decompressLZF( new Uint8Array( data, PCDheader.headerLen + 8, compressedSize ), decompressedSize );
const dataview = new DataView( decompressed.buffer );
const offset = PCDheader.offset;
for ( let i = 0; i < PCDheader.points; i ++ ) {
if ( offset.x !== undefined ) {
const xIndex = PCDheader.fields.indexOf( 'x' );
const yIndex = PCDheader.fields.indexOf( 'y' );
const zIndex = PCDheader.fields.indexOf( 'z' );
position.push( dataview.getFloat32( ( PCDheader.points * offset.x ) + PCDheader.size[ xIndex ] * i, this.littleEndian ) );
position.push( dataview.getFloat32( ( PCDheader.points * offset.y ) + PCDheader.size[ yIndex ] * i, this.littleEndian ) );
position.push( dataview.getFloat32( ( PCDheader.points * offset.z ) + PCDheader.size[ zIndex ] * i, this.littleEndian ) );
}
if ( offset.rgb !== undefined ) {
const rgbIndex = PCDheader.fields.indexOf( 'rgb' );
const r = dataview.getUint8( ( PCDheader.points * offset.rgb ) + PCDheader.size[ rgbIndex ] * i + 2 ) / 255.0;
const g = dataview.getUint8( ( PCDheader.points * offset.rgb ) + PCDheader.size[ rgbIndex ] * i + 1 ) / 255.0;
const b = dataview.getUint8( ( PCDheader.points * offset.rgb ) + PCDheader.size[ rgbIndex ] * i + 0 ) / 255.0;
c.set( r, g, b ).convertSRGBToLinear();
color.push( c.r, c.g, c.b );
}
if ( offset.normal_x !== undefined ) {
const xIndex = PCDheader.fields.indexOf( 'normal_x' );
const yIndex = PCDheader.fields.indexOf( 'normal_y' );
const zIndex = PCDheader.fields.indexOf( 'normal_z' );
normal.push( dataview.getFloat32( ( PCDheader.points * offset.normal_x ) + PCDheader.size[ xIndex ] * i, this.littleEndian ) );
normal.push( dataview.getFloat32( ( PCDheader.points * offset.normal_y ) + PCDheader.size[ yIndex ] * i, this.littleEndian ) );
normal.push( dataview.getFloat32( ( PCDheader.points * offset.normal_z ) + PCDheader.size[ zIndex ] * i, this.littleEndian ) );
}
if ( offset.intensity !== undefined ) {
const intensityIndex = PCDheader.fields.indexOf( 'intensity' );
intensity.push( dataview.getFloat32( ( PCDheader.points * offset.intensity ) + PCDheader.size[ intensityIndex ] * i, this.littleEndian ) );
}
if ( offset.label !== undefined ) {
const labelIndex = PCDheader.fields.indexOf( 'label' );
label.push( dataview.getInt32( ( PCDheader.points * offset.label ) + PCDheader.size[ labelIndex ] * i, this.littleEndian ) );
}
}
}
// binary
if ( PCDheader.data === 'binary' ) {
const dataview = new DataView( data, PCDheader.headerLen );
const offset = PCDheader.offset;
for ( let i = 0, row = 0; i < PCDheader.points; i ++, row += PCDheader.rowSize ) {
if ( offset.x !== undefined ) {
position.push( dataview.getFloat32( row + offset.x, this.littleEndian ) );
position.push( dataview.getFloat32( row + offset.y, this.littleEndian ) );
position.push( dataview.getFloat32( row + offset.z, this.littleEndian ) );
}
if ( offset.rgb !== undefined ) {
const r = dataview.getUint8( row + offset.rgb + 2 ) / 255.0;
const g = dataview.getUint8( row + offset.rgb + 1 ) / 255.0;
const b = dataview.getUint8( row + offset.rgb + 0 ) / 255.0;
c.set( r, g, b ).convertSRGBToLinear();
color.push( c.r, c.g, c.b );
}
if ( offset.normal_x !== undefined ) {
normal.push( dataview.getFloat32( row + offset.normal_x, this.littleEndian ) );
normal.push( dataview.getFloat32( row + offset.normal_y, this.littleEndian ) );
normal.push( dataview.getFloat32( row + offset.normal_z, this.littleEndian ) );
}
if ( offset.intensity !== undefined ) {
intensity.push( dataview.getFloat32( row + offset.intensity, this.littleEndian ) );
}
if ( offset.label !== undefined ) {
label.push( dataview.getInt32( row + offset.label, this.littleEndian ) );
}
}
}
// build geometry
const geometry = new BufferGeometry();
if ( position.length > 0 ) geometry.setAttribute( 'position', new Float32BufferAttribute( position, 3 ) );
if ( normal.length > 0 ) geometry.setAttribute( 'normal', new Float32BufferAttribute( normal, 3 ) );
if ( color.length > 0 ) geometry.setAttribute( 'color', new Float32BufferAttribute( color, 3 ) );
if ( intensity.length > 0 ) geometry.setAttribute( 'intensity', new Float32BufferAttribute( intensity, 1 ) );
if ( label.length > 0 ) geometry.setAttribute( 'label', new Int32BufferAttribute( label, 1 ) );
geometry.computeBoundingSphere();
// build material
const material = new PointsMaterial( { size: 0.005 } );
if ( color.length > 0 ) {
material.vertexColors = true;
}
// build point cloud
return new Points( geometry, material );
}
}
export { PCDLoader };

View File

@ -0,0 +1,232 @@
import {
BufferGeometry,
FileLoader,
Float32BufferAttribute,
Loader,
Color
} from 'three';
class PDBLoader extends Loader {
constructor( manager ) {
super( manager );
}
load( url, onLoad, onProgress, onError ) {
const scope = this;
const loader = new FileLoader( scope.manager );
loader.setPath( scope.path );
loader.setRequestHeader( scope.requestHeader );
loader.setWithCredentials( scope.withCredentials );
loader.load( url, function ( text ) {
try {
onLoad( scope.parse( text ) );
} catch ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
scope.manager.itemError( url );
}
}, onProgress, onError );
}
// Based on CanvasMol PDB parser
parse( text ) {
function trim( text ) {
return text.replace( /^\s\s*/, '' ).replace( /\s\s*$/, '' );
}
function capitalize( text ) {
return text.charAt( 0 ).toUpperCase() + text.slice( 1 ).toLowerCase();
}
function hash( s, e ) {
return 's' + Math.min( s, e ) + 'e' + Math.max( s, e );
}
function parseBond( start, length, satom, i ) {
const eatom = parseInt( lines[ i ].slice( start, start + length ) );
if ( eatom ) {
const h = hash( satom, eatom );
if ( _bhash[ h ] === undefined ) {
_bonds.push( [ satom - 1, eatom - 1, 1 ] );
_bhash[ h ] = _bonds.length - 1;
} else {
// doesn't really work as almost all PDBs
// have just normal bonds appearing multiple
// times instead of being double/triple bonds
// bonds[bhash[h]][2] += 1;
}
}
}
function buildGeometry() {
const build = {
geometryAtoms: new BufferGeometry(),
geometryBonds: new BufferGeometry(),
json: {
atoms: atoms
}
};
const geometryAtoms = build.geometryAtoms;
const geometryBonds = build.geometryBonds;
const verticesAtoms = [];
const colorsAtoms = [];
const verticesBonds = [];
// atoms
const c = new Color();
for ( let i = 0, l = atoms.length; i < l; i ++ ) {
const atom = atoms[ i ];
const x = atom[ 0 ];
const y = atom[ 1 ];
const z = atom[ 2 ];
verticesAtoms.push( x, y, z );
const r = atom[ 3 ][ 0 ] / 255;
const g = atom[ 3 ][ 1 ] / 255;
const b = atom[ 3 ][ 2 ] / 255;
c.set( r, g, b ).convertSRGBToLinear();
colorsAtoms.push( c.r, c.g, c.b );
}
// bonds
for ( let i = 0, l = _bonds.length; i < l; i ++ ) {
const bond = _bonds[ i ];
const start = bond[ 0 ];
const end = bond[ 1 ];
const startAtom = _atomMap[ start ];
const endAtom = _atomMap[ end ];
let x = startAtom[ 0 ];
let y = startAtom[ 1 ];
let z = startAtom[ 2 ];
verticesBonds.push( x, y, z );
x = endAtom[ 0 ];
y = endAtom[ 1 ];
z = endAtom[ 2 ];
verticesBonds.push( x, y, z );
}
// build geometry
geometryAtoms.setAttribute( 'position', new Float32BufferAttribute( verticesAtoms, 3 ) );
geometryAtoms.setAttribute( 'color', new Float32BufferAttribute( colorsAtoms, 3 ) );
geometryBonds.setAttribute( 'position', new Float32BufferAttribute( verticesBonds, 3 ) );
return build;
}
const CPK = { h: [ 255, 255, 255 ], he: [ 217, 255, 255 ], li: [ 204, 128, 255 ], be: [ 194, 255, 0 ], b: [ 255, 181, 181 ], c: [ 144, 144, 144 ], n: [ 48, 80, 248 ], o: [ 255, 13, 13 ], f: [ 144, 224, 80 ], ne: [ 179, 227, 245 ], na: [ 171, 92, 242 ], mg: [ 138, 255, 0 ], al: [ 191, 166, 166 ], si: [ 240, 200, 160 ], p: [ 255, 128, 0 ], s: [ 255, 255, 48 ], cl: [ 31, 240, 31 ], ar: [ 128, 209, 227 ], k: [ 143, 64, 212 ], ca: [ 61, 255, 0 ], sc: [ 230, 230, 230 ], ti: [ 191, 194, 199 ], v: [ 166, 166, 171 ], cr: [ 138, 153, 199 ], mn: [ 156, 122, 199 ], fe: [ 224, 102, 51 ], co: [ 240, 144, 160 ], ni: [ 80, 208, 80 ], cu: [ 200, 128, 51 ], zn: [ 125, 128, 176 ], ga: [ 194, 143, 143 ], ge: [ 102, 143, 143 ], as: [ 189, 128, 227 ], se: [ 255, 161, 0 ], br: [ 166, 41, 41 ], kr: [ 92, 184, 209 ], rb: [ 112, 46, 176 ], sr: [ 0, 255, 0 ], y: [ 148, 255, 255 ], zr: [ 148, 224, 224 ], nb: [ 115, 194, 201 ], mo: [ 84, 181, 181 ], tc: [ 59, 158, 158 ], ru: [ 36, 143, 143 ], rh: [ 10, 125, 140 ], pd: [ 0, 105, 133 ], ag: [ 192, 192, 192 ], cd: [ 255, 217, 143 ], in: [ 166, 117, 115 ], sn: [ 102, 128, 128 ], sb: [ 158, 99, 181 ], te: [ 212, 122, 0 ], i: [ 148, 0, 148 ], xe: [ 66, 158, 176 ], cs: [ 87, 23, 143 ], ba: [ 0, 201, 0 ], la: [ 112, 212, 255 ], ce: [ 255, 255, 199 ], pr: [ 217, 255, 199 ], nd: [ 199, 255, 199 ], pm: [ 163, 255, 199 ], sm: [ 143, 255, 199 ], eu: [ 97, 255, 199 ], gd: [ 69, 255, 199 ], tb: [ 48, 255, 199 ], dy: [ 31, 255, 199 ], ho: [ 0, 255, 156 ], er: [ 0, 230, 117 ], tm: [ 0, 212, 82 ], yb: [ 0, 191, 56 ], lu: [ 0, 171, 36 ], hf: [ 77, 194, 255 ], ta: [ 77, 166, 255 ], w: [ 33, 148, 214 ], re: [ 38, 125, 171 ], os: [ 38, 102, 150 ], ir: [ 23, 84, 135 ], pt: [ 208, 208, 224 ], au: [ 255, 209, 35 ], hg: [ 184, 184, 208 ], tl: [ 166, 84, 77 ], pb: [ 87, 89, 97 ], bi: [ 158, 79, 181 ], po: [ 171, 92, 0 ], at: [ 117, 79, 69 ], rn: [ 66, 130, 150 ], fr: [ 66, 0, 102 ], ra: [ 0, 125, 0 ], ac: [ 112, 171, 250 ], th: [ 0, 186, 255 ], pa: [ 0, 161, 255 ], u: [ 0, 143, 255 ], np: [ 0, 128, 255 ], pu: [ 0, 107, 255 ], am: [ 84, 92, 242 ], cm: [ 120, 92, 227 ], bk: [ 138, 79, 227 ], cf: [ 161, 54, 212 ], es: [ 179, 31, 212 ], fm: [ 179, 31, 186 ], md: [ 179, 13, 166 ], no: [ 189, 13, 135 ], lr: [ 199, 0, 102 ], rf: [ 204, 0, 89 ], db: [ 209, 0, 79 ], sg: [ 217, 0, 69 ], bh: [ 224, 0, 56 ], hs: [ 230, 0, 46 ], mt: [ 235, 0, 38 ], ds: [ 235, 0, 38 ], rg: [ 235, 0, 38 ], cn: [ 235, 0, 38 ], uut: [ 235, 0, 38 ], uuq: [ 235, 0, 38 ], uup: [ 235, 0, 38 ], uuh: [ 235, 0, 38 ], uus: [ 235, 0, 38 ], uuo: [ 235, 0, 38 ] };
const atoms = [];
const _bonds = [];
const _bhash = {};
const _atomMap = {};
// parse
const lines = text.split( '\n' );
for ( let i = 0, l = lines.length; i < l; i ++ ) {
if ( lines[ i ].slice( 0, 4 ) === 'ATOM' || lines[ i ].slice( 0, 6 ) === 'HETATM' ) {
const x = parseFloat( lines[ i ].slice( 30, 37 ) );
const y = parseFloat( lines[ i ].slice( 38, 45 ) );
const z = parseFloat( lines[ i ].slice( 46, 53 ) );
const index = parseInt( lines[ i ].slice( 6, 11 ) ) - 1;
let e = trim( lines[ i ].slice( 76, 78 ) ).toLowerCase();
if ( e === '' ) {
e = trim( lines[ i ].slice( 12, 14 ) ).toLowerCase();
}
const atomData = [ x, y, z, CPK[ e ], capitalize( e ) ];
atoms.push( atomData );
_atomMap[ index ] = atomData;
} else if ( lines[ i ].slice( 0, 6 ) === 'CONECT' ) {
const satom = parseInt( lines[ i ].slice( 6, 11 ) );
parseBond( 11, 5, satom, i );
parseBond( 16, 5, satom, i );
parseBond( 21, 5, satom, i );
parseBond( 26, 5, satom, i );
}
}
// build and return geometry
return buildGeometry();
}
}
export { PDBLoader };

View File

@ -0,0 +1,771 @@
import {
BufferGeometry,
FileLoader,
Float32BufferAttribute,
Loader,
Color
} from 'three';
/**
* Description: A THREE loader for PLY ASCII files (known as the Polygon
* File Format or the Stanford Triangle Format).
*
* Limitations: ASCII decoding assumes file is UTF-8.
*
* Usage:
* const loader = new PLYLoader();
* loader.load('./models/ply/ascii/dolphins.ply', function (geometry) {
*
* scene.add( new THREE.Mesh( geometry ) );
*
* } );
*
* If the PLY file uses non standard property names, they can be mapped while
* loading. For example, the following maps the properties
* “diffuse_(red|green|blue)” in the file to standard color names.
*
* loader.setPropertyNameMapping( {
* diffuse_red: 'red',
* diffuse_green: 'green',
* diffuse_blue: 'blue'
* } );
*
* Custom properties outside of the defaults for position, uv, normal
* and color attributes can be added using the setCustomPropertyNameMapping method.
* For example, the following maps the element properties “custom_property_a”
* and “custom_property_b” to an attribute “customAttribute” with an item size of 2.
* Attribute item sizes are set from the number of element properties in the property array.
*
* loader.setCustomPropertyNameMapping( {
* customAttribute: ['custom_property_a', 'custom_property_b'],
* } );
*
*/
const _color = new Color();
class PLYLoader extends Loader {
constructor( manager ) {
super( manager );
this.propertyNameMapping = {};
this.customPropertyMapping = {};
}
load( url, onLoad, onProgress, onError ) {
const scope = this;
const loader = new FileLoader( this.manager );
loader.setPath( this.path );
loader.setResponseType( 'arraybuffer' );
loader.setRequestHeader( this.requestHeader );
loader.setWithCredentials( this.withCredentials );
loader.load( url, function ( text ) {
try {
onLoad( scope.parse( text ) );
} catch ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
scope.manager.itemError( url );
}
}, onProgress, onError );
}
setPropertyNameMapping( mapping ) {
this.propertyNameMapping = mapping;
}
setCustomPropertyNameMapping( mapping ) {
this.customPropertyMapping = mapping;
}
parse( data ) {
function parseHeader( data, headerLength = 0 ) {
const patternHeader = /^ply([\s\S]*)end_header(\r\n|\r|\n)/;
let headerText = '';
const result = patternHeader.exec( data );
if ( result !== null ) {
headerText = result[ 1 ];
}
const header = {
comments: [],
elements: [],
headerLength: headerLength,
objInfo: ''
};
const lines = headerText.split( /\r\n|\r|\n/ );
let currentElement;
function make_ply_element_property( propertValues, propertyNameMapping ) {
const property = { type: propertValues[ 0 ] };
if ( property.type === 'list' ) {
property.name = propertValues[ 3 ];
property.countType = propertValues[ 1 ];
property.itemType = propertValues[ 2 ];
} else {
property.name = propertValues[ 1 ];
}
if ( property.name in propertyNameMapping ) {
property.name = propertyNameMapping[ property.name ];
}
return property;
}
for ( let i = 0; i < lines.length; i ++ ) {
let line = lines[ i ];
line = line.trim();
if ( line === '' ) continue;
const lineValues = line.split( /\s+/ );
const lineType = lineValues.shift();
line = lineValues.join( ' ' );
switch ( lineType ) {
case 'format':
header.format = lineValues[ 0 ];
header.version = lineValues[ 1 ];
break;
case 'comment':
header.comments.push( line );
break;
case 'element':
if ( currentElement !== undefined ) {
header.elements.push( currentElement );
}
currentElement = {};
currentElement.name = lineValues[ 0 ];
currentElement.count = parseInt( lineValues[ 1 ] );
currentElement.properties = [];
break;
case 'property':
currentElement.properties.push( make_ply_element_property( lineValues, scope.propertyNameMapping ) );
break;
case 'obj_info':
header.objInfo = line;
break;
default:
console.log( 'unhandled', lineType, lineValues );
}
}
if ( currentElement !== undefined ) {
header.elements.push( currentElement );
}
return header;
}
function parseASCIINumber( n, type ) {
switch ( type ) {
case 'char': case 'uchar': case 'short': case 'ushort': case 'int': case 'uint':
case 'int8': case 'uint8': case 'int16': case 'uint16': case 'int32': case 'uint32':
return parseInt( n );
case 'float': case 'double': case 'float32': case 'float64':
return parseFloat( n );
}
}
function parseASCIIElement( properties, tokens ) {
const element = {};
for ( let i = 0; i < properties.length; i ++ ) {
if ( tokens.empty() ) return null;
if ( properties[ i ].type === 'list' ) {
const list = [];
const n = parseASCIINumber( tokens.next(), properties[ i ].countType );
for ( let j = 0; j < n; j ++ ) {
if ( tokens.empty() ) return null;
list.push( parseASCIINumber( tokens.next(), properties[ i ].itemType ) );
}
element[ properties[ i ].name ] = list;
} else {
element[ properties[ i ].name ] = parseASCIINumber( tokens.next(), properties[ i ].type );
}
}
return element;
}
function createBuffer() {
const buffer = {
indices: [],
vertices: [],
normals: [],
uvs: [],
faceVertexUvs: [],
colors: [],
faceVertexColors: []
};
for ( const customProperty of Object.keys( scope.customPropertyMapping ) ) {
buffer[ customProperty ] = [];
}
return buffer;
}
function mapElementAttributes( properties ) {
const elementNames = properties.map( property => {
return property.name;
} );
function findAttrName( names ) {
for ( let i = 0, l = names.length; i < l; i ++ ) {
const name = names[ i ];
if ( elementNames.includes( name ) ) return name;
}
return null;
}
return {
attrX: findAttrName( [ 'x', 'px', 'posx' ] ) || 'x',
attrY: findAttrName( [ 'y', 'py', 'posy' ] ) || 'y',
attrZ: findAttrName( [ 'z', 'pz', 'posz' ] ) || 'z',
attrNX: findAttrName( [ 'nx', 'normalx' ] ),
attrNY: findAttrName( [ 'ny', 'normaly' ] ),
attrNZ: findAttrName( [ 'nz', 'normalz' ] ),
attrS: findAttrName( [ 's', 'u', 'texture_u', 'tx' ] ),
attrT: findAttrName( [ 't', 'v', 'texture_v', 'ty' ] ),
attrR: findAttrName( [ 'red', 'diffuse_red', 'r', 'diffuse_r' ] ),
attrG: findAttrName( [ 'green', 'diffuse_green', 'g', 'diffuse_g' ] ),
attrB: findAttrName( [ 'blue', 'diffuse_blue', 'b', 'diffuse_b' ] ),
};
}
function parseASCII( data, header ) {
// PLY ascii format specification, as per http://en.wikipedia.org/wiki/PLY_(file_format)
const buffer = createBuffer();
const patternBody = /end_header\s+(\S[\s\S]*\S|\S)\s*$/;
let body, matches;
if ( ( matches = patternBody.exec( data ) ) !== null ) {
body = matches[ 1 ].split( /\s+/ );
} else {
body = [ ];
}
const tokens = new ArrayStream( body );
loop: for ( let i = 0; i < header.elements.length; i ++ ) {
const elementDesc = header.elements[ i ];
const attributeMap = mapElementAttributes( elementDesc.properties );
for ( let j = 0; j < elementDesc.count; j ++ ) {
const element = parseASCIIElement( elementDesc.properties, tokens );
if ( ! element ) break loop;
handleElement( buffer, elementDesc.name, element, attributeMap );
}
}
return postProcess( buffer );
}
function postProcess( buffer ) {
let geometry = new BufferGeometry();
// mandatory buffer data
if ( buffer.indices.length > 0 ) {
geometry.setIndex( buffer.indices );
}
geometry.setAttribute( 'position', new Float32BufferAttribute( buffer.vertices, 3 ) );
// optional buffer data
if ( buffer.normals.length > 0 ) {
geometry.setAttribute( 'normal', new Float32BufferAttribute( buffer.normals, 3 ) );
}
if ( buffer.uvs.length > 0 ) {
geometry.setAttribute( 'uv', new Float32BufferAttribute( buffer.uvs, 2 ) );
}
if ( buffer.colors.length > 0 ) {
geometry.setAttribute( 'color', new Float32BufferAttribute( buffer.colors, 3 ) );
}
if ( buffer.faceVertexUvs.length > 0 || buffer.faceVertexColors.length > 0 ) {
geometry = geometry.toNonIndexed();
if ( buffer.faceVertexUvs.length > 0 ) geometry.setAttribute( 'uv', new Float32BufferAttribute( buffer.faceVertexUvs, 2 ) );
if ( buffer.faceVertexColors.length > 0 ) geometry.setAttribute( 'color', new Float32BufferAttribute( buffer.faceVertexColors, 3 ) );
}
// custom buffer data
for ( const customProperty of Object.keys( scope.customPropertyMapping ) ) {
if ( buffer[ customProperty ].length > 0 ) {
geometry.setAttribute(
customProperty,
new Float32BufferAttribute(
buffer[ customProperty ],
scope.customPropertyMapping[ customProperty ].length
)
);
}
}
geometry.computeBoundingSphere();
return geometry;
}
function handleElement( buffer, elementName, element, cacheEntry ) {
if ( elementName === 'vertex' ) {
buffer.vertices.push( element[ cacheEntry.attrX ], element[ cacheEntry.attrY ], element[ cacheEntry.attrZ ] );
if ( cacheEntry.attrNX !== null && cacheEntry.attrNY !== null && cacheEntry.attrNZ !== null ) {
buffer.normals.push( element[ cacheEntry.attrNX ], element[ cacheEntry.attrNY ], element[ cacheEntry.attrNZ ] );
}
if ( cacheEntry.attrS !== null && cacheEntry.attrT !== null ) {
buffer.uvs.push( element[ cacheEntry.attrS ], element[ cacheEntry.attrT ] );
}
if ( cacheEntry.attrR !== null && cacheEntry.attrG !== null && cacheEntry.attrB !== null ) {
_color.setRGB(
element[ cacheEntry.attrR ] / 255.0,
element[ cacheEntry.attrG ] / 255.0,
element[ cacheEntry.attrB ] / 255.0
).convertSRGBToLinear();
buffer.colors.push( _color.r, _color.g, _color.b );
}
for ( const customProperty of Object.keys( scope.customPropertyMapping ) ) {
for ( const elementProperty of scope.customPropertyMapping[ customProperty ] ) {
buffer[ customProperty ].push( element[ elementProperty ] );
}
}
} else if ( elementName === 'face' ) {
const vertex_indices = element.vertex_indices || element.vertex_index; // issue #9338
const texcoord = element.texcoord;
if ( vertex_indices.length === 3 ) {
buffer.indices.push( vertex_indices[ 0 ], vertex_indices[ 1 ], vertex_indices[ 2 ] );
if ( texcoord && texcoord.length === 6 ) {
buffer.faceVertexUvs.push( texcoord[ 0 ], texcoord[ 1 ] );
buffer.faceVertexUvs.push( texcoord[ 2 ], texcoord[ 3 ] );
buffer.faceVertexUvs.push( texcoord[ 4 ], texcoord[ 5 ] );
}
} else if ( vertex_indices.length === 4 ) {
buffer.indices.push( vertex_indices[ 0 ], vertex_indices[ 1 ], vertex_indices[ 3 ] );
buffer.indices.push( vertex_indices[ 1 ], vertex_indices[ 2 ], vertex_indices[ 3 ] );
}
// face colors
if ( cacheEntry.attrR !== null && cacheEntry.attrG !== null && cacheEntry.attrB !== null ) {
_color.setRGB(
element[ cacheEntry.attrR ] / 255.0,
element[ cacheEntry.attrG ] / 255.0,
element[ cacheEntry.attrB ] / 255.0
).convertSRGBToLinear();
buffer.faceVertexColors.push( _color.r, _color.g, _color.b );
buffer.faceVertexColors.push( _color.r, _color.g, _color.b );
buffer.faceVertexColors.push( _color.r, _color.g, _color.b );
}
}
}
function binaryReadElement( at, properties ) {
const element = {};
let read = 0;
for ( let i = 0; i < properties.length; i ++ ) {
const property = properties[ i ];
const valueReader = property.valueReader;
if ( property.type === 'list' ) {
const list = [];
const n = property.countReader.read( at + read );
read += property.countReader.size;
for ( let j = 0; j < n; j ++ ) {
list.push( valueReader.read( at + read ) );
read += valueReader.size;
}
element[ property.name ] = list;
} else {
element[ property.name ] = valueReader.read( at + read );
read += valueReader.size;
}
}
return [ element, read ];
}
function setPropertyBinaryReaders( properties, body, little_endian ) {
function getBinaryReader( dataview, type, little_endian ) {
switch ( type ) {
// corespondences for non-specific length types here match rply:
case 'int8': case 'char': return { read: ( at ) => {
return dataview.getInt8( at );
}, size: 1 };
case 'uint8': case 'uchar': return { read: ( at ) => {
return dataview.getUint8( at );
}, size: 1 };
case 'int16': case 'short': return { read: ( at ) => {
return dataview.getInt16( at, little_endian );
}, size: 2 };
case 'uint16': case 'ushort': return { read: ( at ) => {
return dataview.getUint16( at, little_endian );
}, size: 2 };
case 'int32': case 'int': return { read: ( at ) => {
return dataview.getInt32( at, little_endian );
}, size: 4 };
case 'uint32': case 'uint': return { read: ( at ) => {
return dataview.getUint32( at, little_endian );
}, size: 4 };
case 'float32': case 'float': return { read: ( at ) => {
return dataview.getFloat32( at, little_endian );
}, size: 4 };
case 'float64': case 'double': return { read: ( at ) => {
return dataview.getFloat64( at, little_endian );
}, size: 8 };
}
}
for ( let i = 0, l = properties.length; i < l; i ++ ) {
const property = properties[ i ];
if ( property.type === 'list' ) {
property.countReader = getBinaryReader( body, property.countType, little_endian );
property.valueReader = getBinaryReader( body, property.itemType, little_endian );
} else {
property.valueReader = getBinaryReader( body, property.type, little_endian );
}
}
}
function parseBinary( data, header ) {
const buffer = createBuffer();
const little_endian = ( header.format === 'binary_little_endian' );
const body = new DataView( data, header.headerLength );
let result, loc = 0;
for ( let currentElement = 0; currentElement < header.elements.length; currentElement ++ ) {
const elementDesc = header.elements[ currentElement ];
const properties = elementDesc.properties;
const attributeMap = mapElementAttributes( properties );
setPropertyBinaryReaders( properties, body, little_endian );
for ( let currentElementCount = 0; currentElementCount < elementDesc.count; currentElementCount ++ ) {
result = binaryReadElement( loc, properties );
loc += result[ 1 ];
const element = result[ 0 ];
handleElement( buffer, elementDesc.name, element, attributeMap );
}
}
return postProcess( buffer );
}
function extractHeaderText( bytes ) {
let i = 0;
let cont = true;
let line = '';
const lines = [];
const startLine = new TextDecoder().decode( bytes.subarray( 0, 5 ) );
const hasCRNL = /^ply\r\n/.test( startLine );
do {
const c = String.fromCharCode( bytes[ i ++ ] );
if ( c !== '\n' && c !== '\r' ) {
line += c;
} else {
if ( line === 'end_header' ) cont = false;
if ( line !== '' ) {
lines.push( line );
line = '';
}
}
} while ( cont && i < bytes.length );
// ascii section using \r\n as line endings
if ( hasCRNL === true ) i ++;
return { headerText: lines.join( '\r' ) + '\r', headerLength: i };
}
//
let geometry;
const scope = this;
if ( data instanceof ArrayBuffer ) {
const bytes = new Uint8Array( data );
const { headerText, headerLength } = extractHeaderText( bytes );
const header = parseHeader( headerText, headerLength );
if ( header.format === 'ascii' ) {
const text = new TextDecoder().decode( bytes );
geometry = parseASCII( text, header );
} else {
geometry = parseBinary( data, header );
}
} else {
geometry = parseASCII( data, parseHeader( data ) );
}
return geometry;
}
}
class ArrayStream {
constructor( arr ) {
this.arr = arr;
this.i = 0;
}
empty() {
return this.i >= this.arr.length;
}
next() {
return this.arr[ this.i ++ ];
}
}
export { PLYLoader };

View File

@ -0,0 +1,251 @@
import {
CompressedTextureLoader,
RGBA_PVRTC_2BPPV1_Format,
RGBA_PVRTC_4BPPV1_Format,
RGB_PVRTC_2BPPV1_Format,
RGB_PVRTC_4BPPV1_Format
} from 'three';
/*
* PVR v2 (legacy) parser
* TODO : Add Support for PVR v3 format
* TODO : implement loadMipmaps option
*/
class PVRLoader extends CompressedTextureLoader {
constructor( manager ) {
super( manager );
}
parse( buffer, loadMipmaps ) {
const headerLengthInt = 13;
const header = new Uint32Array( buffer, 0, headerLengthInt );
const pvrDatas = {
buffer: buffer,
header: header,
loadMipmaps: loadMipmaps
};
if ( header[ 0 ] === 0x03525650 ) {
// PVR v3
return _parseV3( pvrDatas );
} else if ( header[ 11 ] === 0x21525650 ) {
// PVR v2
return _parseV2( pvrDatas );
} else {
console.error( 'THREE.PVRLoader: Unknown PVR format.' );
}
}
}
function _parseV3( pvrDatas ) {
const header = pvrDatas.header;
let bpp, format;
const metaLen = header[ 12 ],
pixelFormat = header[ 2 ],
height = header[ 6 ],
width = header[ 7 ],
// numSurfs = header[ 9 ],
numFaces = header[ 10 ],
numMipmaps = header[ 11 ];
switch ( pixelFormat ) {
case 0 : // PVRTC 2bpp RGB
bpp = 2;
format = RGB_PVRTC_2BPPV1_Format;
break;
case 1 : // PVRTC 2bpp RGBA
bpp = 2;
format = RGBA_PVRTC_2BPPV1_Format;
break;
case 2 : // PVRTC 4bpp RGB
bpp = 4;
format = RGB_PVRTC_4BPPV1_Format;
break;
case 3 : // PVRTC 4bpp RGBA
bpp = 4;
format = RGBA_PVRTC_4BPPV1_Format;
break;
default :
console.error( 'THREE.PVRLoader: Unsupported PVR format:', pixelFormat );
}
pvrDatas.dataPtr = 52 + metaLen;
pvrDatas.bpp = bpp;
pvrDatas.format = format;
pvrDatas.width = width;
pvrDatas.height = height;
pvrDatas.numSurfaces = numFaces;
pvrDatas.numMipmaps = numMipmaps;
pvrDatas.isCubemap = ( numFaces === 6 );
return _extract( pvrDatas );
}
function _parseV2( pvrDatas ) {
const header = pvrDatas.header;
const headerLength = header[ 0 ],
height = header[ 1 ],
width = header[ 2 ],
numMipmaps = header[ 3 ],
flags = header[ 4 ],
// dataLength = header[ 5 ],
// bpp = header[ 6 ],
// bitmaskRed = header[ 7 ],
// bitmaskGreen = header[ 8 ],
// bitmaskBlue = header[ 9 ],
bitmaskAlpha = header[ 10 ],
// pvrTag = header[ 11 ],
numSurfs = header[ 12 ];
const TYPE_MASK = 0xff;
const PVRTC_2 = 24,
PVRTC_4 = 25;
const formatFlags = flags & TYPE_MASK;
let bpp, format;
const _hasAlpha = bitmaskAlpha > 0;
if ( formatFlags === PVRTC_4 ) {
format = _hasAlpha ? RGBA_PVRTC_4BPPV1_Format : RGB_PVRTC_4BPPV1_Format;
bpp = 4;
} else if ( formatFlags === PVRTC_2 ) {
format = _hasAlpha ? RGBA_PVRTC_2BPPV1_Format : RGB_PVRTC_2BPPV1_Format;
bpp = 2;
} else {
console.error( 'THREE.PVRLoader: Unknown PVR format:', formatFlags );
}
pvrDatas.dataPtr = headerLength;
pvrDatas.bpp = bpp;
pvrDatas.format = format;
pvrDatas.width = width;
pvrDatas.height = height;
pvrDatas.numSurfaces = numSurfs;
pvrDatas.numMipmaps = numMipmaps + 1;
// guess cubemap type seems tricky in v2
// it juste a pvr containing 6 surface (no explicit cubemap type)
pvrDatas.isCubemap = ( numSurfs === 6 );
return _extract( pvrDatas );
}
function _extract( pvrDatas ) {
const pvr = {
mipmaps: [],
width: pvrDatas.width,
height: pvrDatas.height,
format: pvrDatas.format,
mipmapCount: pvrDatas.numMipmaps,
isCubemap: pvrDatas.isCubemap
};
const buffer = pvrDatas.buffer;
let dataOffset = pvrDatas.dataPtr,
dataSize = 0,
blockSize = 0,
blockWidth = 0,
blockHeight = 0,
widthBlocks = 0,
heightBlocks = 0;
const bpp = pvrDatas.bpp,
numSurfs = pvrDatas.numSurfaces;
if ( bpp === 2 ) {
blockWidth = 8;
blockHeight = 4;
} else {
blockWidth = 4;
blockHeight = 4;
}
blockSize = ( blockWidth * blockHeight ) * bpp / 8;
pvr.mipmaps.length = pvrDatas.numMipmaps * numSurfs;
let mipLevel = 0;
while ( mipLevel < pvrDatas.numMipmaps ) {
const sWidth = pvrDatas.width >> mipLevel,
sHeight = pvrDatas.height >> mipLevel;
widthBlocks = sWidth / blockWidth;
heightBlocks = sHeight / blockHeight;
// Clamp to minimum number of blocks
if ( widthBlocks < 2 ) widthBlocks = 2;
if ( heightBlocks < 2 ) heightBlocks = 2;
dataSize = widthBlocks * heightBlocks * blockSize;
for ( let surfIndex = 0; surfIndex < numSurfs; surfIndex ++ ) {
const byteArray = new Uint8Array( buffer, dataOffset, dataSize );
const mipmap = {
data: byteArray,
width: sWidth,
height: sHeight
};
pvr.mipmaps[ surfIndex * pvrDatas.numMipmaps + mipLevel ] = mipmap;
dataOffset += dataSize;
}
mipLevel ++;
}
return pvr;
}
export { PVRLoader };

View File

@ -0,0 +1,450 @@
import {
DataTextureLoader,
DataUtils,
FloatType,
HalfFloatType,
LinearFilter,
LinearSRGBColorSpace
} from 'three';
// https://github.com/mrdoob/three.js/issues/5552
// http://en.wikipedia.org/wiki/RGBE_image_format
class RGBELoader extends DataTextureLoader {
constructor( manager ) {
super( manager );
this.type = HalfFloatType;
}
// adapted from http://www.graphics.cornell.edu/~bjw/rgbe.html
parse( buffer ) {
const
/* default error routine. change this to change error handling */
rgbe_read_error = 1,
rgbe_write_error = 2,
rgbe_format_error = 3,
rgbe_memory_error = 4,
rgbe_error = function ( rgbe_error_code, msg ) {
switch ( rgbe_error_code ) {
case rgbe_read_error: throw new Error( 'THREE.RGBELoader: Read Error: ' + ( msg || '' ) );
case rgbe_write_error: throw new Error( 'THREE.RGBELoader: Write Error: ' + ( msg || '' ) );
case rgbe_format_error: throw new Error( 'THREE.RGBELoader: Bad File Format: ' + ( msg || '' ) );
default:
case rgbe_memory_error: throw new Error( 'THREE.RGBELoader: Memory Error: ' + ( msg || '' ) );
}
},
/* offsets to red, green, and blue components in a data (float) pixel */
//RGBE_DATA_RED = 0,
//RGBE_DATA_GREEN = 1,
//RGBE_DATA_BLUE = 2,
/* number of floats per pixel, use 4 since stored in rgba image format */
//RGBE_DATA_SIZE = 4,
/* flags indicating which fields in an rgbe_header_info are valid */
RGBE_VALID_PROGRAMTYPE = 1,
RGBE_VALID_FORMAT = 2,
RGBE_VALID_DIMENSIONS = 4,
NEWLINE = '\n',
fgets = function ( buffer, lineLimit, consume ) {
const chunkSize = 128;
lineLimit = ! lineLimit ? 1024 : lineLimit;
let p = buffer.pos,
i = - 1, len = 0, s = '',
chunk = String.fromCharCode.apply( null, new Uint16Array( buffer.subarray( p, p + chunkSize ) ) );
while ( ( 0 > ( i = chunk.indexOf( NEWLINE ) ) ) && ( len < lineLimit ) && ( p < buffer.byteLength ) ) {
s += chunk; len += chunk.length;
p += chunkSize;
chunk += String.fromCharCode.apply( null, new Uint16Array( buffer.subarray( p, p + chunkSize ) ) );
}
if ( - 1 < i ) {
/*for (i=l-1; i>=0; i--) {
byteCode = m.charCodeAt(i);
if (byteCode > 0x7f && byteCode <= 0x7ff) byteLen++;
else if (byteCode > 0x7ff && byteCode <= 0xffff) byteLen += 2;
if (byteCode >= 0xDC00 && byteCode <= 0xDFFF) i--; //trail surrogate
}*/
if ( false !== consume ) buffer.pos += len + i + 1;
return s + chunk.slice( 0, i );
}
return false;
},
/* minimal header reading. modify if you want to parse more information */
RGBE_ReadHeader = function ( buffer ) {
// regexes to parse header info fields
const magic_token_re = /^#\?(\S+)/,
gamma_re = /^\s*GAMMA\s*=\s*(\d+(\.\d+)?)\s*$/,
exposure_re = /^\s*EXPOSURE\s*=\s*(\d+(\.\d+)?)\s*$/,
format_re = /^\s*FORMAT=(\S+)\s*$/,
dimensions_re = /^\s*\-Y\s+(\d+)\s+\+X\s+(\d+)\s*$/,
// RGBE format header struct
header = {
valid: 0, /* indicate which fields are valid */
string: '', /* the actual header string */
comments: '', /* comments found in header */
programtype: 'RGBE', /* listed at beginning of file to identify it after "#?". defaults to "RGBE" */
format: '', /* RGBE format, default 32-bit_rle_rgbe */
gamma: 1.0, /* image has already been gamma corrected with given gamma. defaults to 1.0 (no correction) */
exposure: 1.0, /* a value of 1.0 in an image corresponds to <exposure> watts/steradian/m^2. defaults to 1.0 */
width: 0, height: 0 /* image dimensions, width/height */
};
let line, match;
if ( buffer.pos >= buffer.byteLength || ! ( line = fgets( buffer ) ) ) {
rgbe_error( rgbe_read_error, 'no header found' );
}
/* if you want to require the magic token then uncomment the next line */
if ( ! ( match = line.match( magic_token_re ) ) ) {
rgbe_error( rgbe_format_error, 'bad initial token' );
}
header.valid |= RGBE_VALID_PROGRAMTYPE;
header.programtype = match[ 1 ];
header.string += line + '\n';
while ( true ) {
line = fgets( buffer );
if ( false === line ) break;
header.string += line + '\n';
if ( '#' === line.charAt( 0 ) ) {
header.comments += line + '\n';
continue; // comment line
}
if ( match = line.match( gamma_re ) ) {
header.gamma = parseFloat( match[ 1 ] );
}
if ( match = line.match( exposure_re ) ) {
header.exposure = parseFloat( match[ 1 ] );
}
if ( match = line.match( format_re ) ) {
header.valid |= RGBE_VALID_FORMAT;
header.format = match[ 1 ];//'32-bit_rle_rgbe';
}
if ( match = line.match( dimensions_re ) ) {
header.valid |= RGBE_VALID_DIMENSIONS;
header.height = parseInt( match[ 1 ], 10 );
header.width = parseInt( match[ 2 ], 10 );
}
if ( ( header.valid & RGBE_VALID_FORMAT ) && ( header.valid & RGBE_VALID_DIMENSIONS ) ) break;
}
if ( ! ( header.valid & RGBE_VALID_FORMAT ) ) {
rgbe_error( rgbe_format_error, 'missing format specifier' );
}
if ( ! ( header.valid & RGBE_VALID_DIMENSIONS ) ) {
rgbe_error( rgbe_format_error, 'missing image size specifier' );
}
return header;
},
RGBE_ReadPixels_RLE = function ( buffer, w, h ) {
const scanline_width = w;
if (
// run length encoding is not allowed so read flat
( ( scanline_width < 8 ) || ( scanline_width > 0x7fff ) ) ||
// this file is not run length encoded
( ( 2 !== buffer[ 0 ] ) || ( 2 !== buffer[ 1 ] ) || ( buffer[ 2 ] & 0x80 ) )
) {
// return the flat buffer
return new Uint8Array( buffer );
}
if ( scanline_width !== ( ( buffer[ 2 ] << 8 ) | buffer[ 3 ] ) ) {
rgbe_error( rgbe_format_error, 'wrong scanline width' );
}
const data_rgba = new Uint8Array( 4 * w * h );
if ( ! data_rgba.length ) {
rgbe_error( rgbe_memory_error, 'unable to allocate buffer space' );
}
let offset = 0, pos = 0;
const ptr_end = 4 * scanline_width;
const rgbeStart = new Uint8Array( 4 );
const scanline_buffer = new Uint8Array( ptr_end );
let num_scanlines = h;
// read in each successive scanline
while ( ( num_scanlines > 0 ) && ( pos < buffer.byteLength ) ) {
if ( pos + 4 > buffer.byteLength ) {
rgbe_error( rgbe_read_error );
}
rgbeStart[ 0 ] = buffer[ pos ++ ];
rgbeStart[ 1 ] = buffer[ pos ++ ];
rgbeStart[ 2 ] = buffer[ pos ++ ];
rgbeStart[ 3 ] = buffer[ pos ++ ];
if ( ( 2 != rgbeStart[ 0 ] ) || ( 2 != rgbeStart[ 1 ] ) || ( ( ( rgbeStart[ 2 ] << 8 ) | rgbeStart[ 3 ] ) != scanline_width ) ) {
rgbe_error( rgbe_format_error, 'bad rgbe scanline format' );
}
// read each of the four channels for the scanline into the buffer
// first red, then green, then blue, then exponent
let ptr = 0, count;
while ( ( ptr < ptr_end ) && ( pos < buffer.byteLength ) ) {
count = buffer[ pos ++ ];
const isEncodedRun = count > 128;
if ( isEncodedRun ) count -= 128;
if ( ( 0 === count ) || ( ptr + count > ptr_end ) ) {
rgbe_error( rgbe_format_error, 'bad scanline data' );
}
if ( isEncodedRun ) {
// a (encoded) run of the same value
const byteValue = buffer[ pos ++ ];
for ( let i = 0; i < count; i ++ ) {
scanline_buffer[ ptr ++ ] = byteValue;
}
//ptr += count;
} else {
// a literal-run
scanline_buffer.set( buffer.subarray( pos, pos + count ), ptr );
ptr += count; pos += count;
}
}
// now convert data from buffer into rgba
// first red, then green, then blue, then exponent (alpha)
const l = scanline_width; //scanline_buffer.byteLength;
for ( let i = 0; i < l; i ++ ) {
let off = 0;
data_rgba[ offset ] = scanline_buffer[ i + off ];
off += scanline_width; //1;
data_rgba[ offset + 1 ] = scanline_buffer[ i + off ];
off += scanline_width; //1;
data_rgba[ offset + 2 ] = scanline_buffer[ i + off ];
off += scanline_width; //1;
data_rgba[ offset + 3 ] = scanline_buffer[ i + off ];
offset += 4;
}
num_scanlines --;
}
return data_rgba;
};
const RGBEByteToRGBFloat = function ( sourceArray, sourceOffset, destArray, destOffset ) {
const e = sourceArray[ sourceOffset + 3 ];
const scale = Math.pow( 2.0, e - 128.0 ) / 255.0;
destArray[ destOffset + 0 ] = sourceArray[ sourceOffset + 0 ] * scale;
destArray[ destOffset + 1 ] = sourceArray[ sourceOffset + 1 ] * scale;
destArray[ destOffset + 2 ] = sourceArray[ sourceOffset + 2 ] * scale;
destArray[ destOffset + 3 ] = 1;
};
const RGBEByteToRGBHalf = function ( sourceArray, sourceOffset, destArray, destOffset ) {
const e = sourceArray[ sourceOffset + 3 ];
const scale = Math.pow( 2.0, e - 128.0 ) / 255.0;
// clamping to 65504, the maximum representable value in float16
destArray[ destOffset + 0 ] = DataUtils.toHalfFloat( Math.min( sourceArray[ sourceOffset + 0 ] * scale, 65504 ) );
destArray[ destOffset + 1 ] = DataUtils.toHalfFloat( Math.min( sourceArray[ sourceOffset + 1 ] * scale, 65504 ) );
destArray[ destOffset + 2 ] = DataUtils.toHalfFloat( Math.min( sourceArray[ sourceOffset + 2 ] * scale, 65504 ) );
destArray[ destOffset + 3 ] = DataUtils.toHalfFloat( 1 );
};
const byteArray = new Uint8Array( buffer );
byteArray.pos = 0;
const rgbe_header_info = RGBE_ReadHeader( byteArray );
const w = rgbe_header_info.width,
h = rgbe_header_info.height,
image_rgba_data = RGBE_ReadPixels_RLE( byteArray.subarray( byteArray.pos ), w, h );
let data, type;
let numElements;
switch ( this.type ) {
case FloatType:
numElements = image_rgba_data.length / 4;
const floatArray = new Float32Array( numElements * 4 );
for ( let j = 0; j < numElements; j ++ ) {
RGBEByteToRGBFloat( image_rgba_data, j * 4, floatArray, j * 4 );
}
data = floatArray;
type = FloatType;
break;
case HalfFloatType:
numElements = image_rgba_data.length / 4;
const halfArray = new Uint16Array( numElements * 4 );
for ( let j = 0; j < numElements; j ++ ) {
RGBEByteToRGBHalf( image_rgba_data, j * 4, halfArray, j * 4 );
}
data = halfArray;
type = HalfFloatType;
break;
default:
throw new Error( 'THREE.RGBELoader: Unsupported type: ' + this.type );
break;
}
return {
width: w, height: h,
data: data,
header: rgbe_header_info.string,
gamma: rgbe_header_info.gamma,
exposure: rgbe_header_info.exposure,
type: type
};
}
setDataType( value ) {
this.type = value;
return this;
}
load( url, onLoad, onProgress, onError ) {
function onLoadCallback( texture, texData ) {
switch ( texture.type ) {
case FloatType:
case HalfFloatType:
texture.colorSpace = LinearSRGBColorSpace;
texture.minFilter = LinearFilter;
texture.magFilter = LinearFilter;
texture.generateMipmaps = false;
texture.flipY = true;
break;
}
if ( onLoad ) onLoad( texture, texData );
}
return super.load( url, onLoadCallback, onProgress, onError );
}
}
export { RGBELoader };

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,410 @@
import {
BufferAttribute,
BufferGeometry,
Color,
FileLoader,
Float32BufferAttribute,
Loader,
Vector3
} from 'three';
/**
* Description: A THREE loader for STL ASCII files, as created by Solidworks and other CAD programs.
*
* Supports both binary and ASCII encoded files, with automatic detection of type.
*
* The loader returns a non-indexed buffer geometry.
*
* Limitations:
* Binary decoding supports "Magics" color format (http://en.wikipedia.org/wiki/STL_(file_format)#Color_in_binary_STL).
* There is perhaps some question as to how valid it is to always assume little-endian-ness.
* ASCII decoding assumes file is UTF-8.
*
* Usage:
* const loader = new STLLoader();
* loader.load( './models/stl/slotted_disk.stl', function ( geometry ) {
* scene.add( new THREE.Mesh( geometry ) );
* });
*
* For binary STLs geometry might contain colors for vertices. To use it:
* // use the same code to load STL as above
* if (geometry.hasColors) {
* material = new THREE.MeshPhongMaterial({ opacity: geometry.alpha, vertexColors: true });
* } else { .... }
* const mesh = new THREE.Mesh( geometry, material );
*
* For ASCII STLs containing multiple solids, each solid is assigned to a different group.
* Groups can be used to assign a different color by defining an array of materials with the same length of
* geometry.groups and passing it to the Mesh constructor:
*
* const mesh = new THREE.Mesh( geometry, material );
*
* For example:
*
* const materials = [];
* const nGeometryGroups = geometry.groups.length;
*
* const colorMap = ...; // Some logic to index colors.
*
* for (let i = 0; i < nGeometryGroups; i++) {
*
* const material = new THREE.MeshPhongMaterial({
* color: colorMap[i],
* wireframe: false
* });
*
* }
*
* materials.push(material);
* const mesh = new THREE.Mesh(geometry, materials);
*/
class STLLoader extends Loader {
constructor( manager ) {
super( manager );
}
load( url, onLoad, onProgress, onError ) {
const scope = this;
const loader = new FileLoader( this.manager );
loader.setPath( this.path );
loader.setResponseType( 'arraybuffer' );
loader.setRequestHeader( this.requestHeader );
loader.setWithCredentials( this.withCredentials );
loader.load( url, function ( text ) {
try {
onLoad( scope.parse( text ) );
} catch ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
scope.manager.itemError( url );
}
}, onProgress, onError );
}
parse( data ) {
function isBinary( data ) {
const reader = new DataView( data );
const face_size = ( 32 / 8 * 3 ) + ( ( 32 / 8 * 3 ) * 3 ) + ( 16 / 8 );
const n_faces = reader.getUint32( 80, true );
const expect = 80 + ( 32 / 8 ) + ( n_faces * face_size );
if ( expect === reader.byteLength ) {
return true;
}
// An ASCII STL data must begin with 'solid ' as the first six bytes.
// However, ASCII STLs lacking the SPACE after the 'd' are known to be
// plentiful. So, check the first 5 bytes for 'solid'.
// Several encodings, such as UTF-8, precede the text with up to 5 bytes:
// https://en.wikipedia.org/wiki/Byte_order_mark#Byte_order_marks_by_encoding
// Search for "solid" to start anywhere after those prefixes.
// US-ASCII ordinal values for 's', 'o', 'l', 'i', 'd'
const solid = [ 115, 111, 108, 105, 100 ];
for ( let off = 0; off < 5; off ++ ) {
// If "solid" text is matched to the current offset, declare it to be an ASCII STL.
if ( matchDataViewAt( solid, reader, off ) ) return false;
}
// Couldn't find "solid" text at the beginning; it is binary STL.
return true;
}
function matchDataViewAt( query, reader, offset ) {
// Check if each byte in query matches the corresponding byte from the current offset
for ( let i = 0, il = query.length; i < il; i ++ ) {
if ( query[ i ] !== reader.getUint8( offset + i ) ) return false;
}
return true;
}
function parseBinary( data ) {
const reader = new DataView( data );
const faces = reader.getUint32( 80, true );
let r, g, b, hasColors = false, colors;
let defaultR, defaultG, defaultB, alpha;
// process STL header
// check for default color in header ("COLOR=rgba" sequence).
for ( let index = 0; index < 80 - 10; index ++ ) {
if ( ( reader.getUint32( index, false ) == 0x434F4C4F /*COLO*/ ) &&
( reader.getUint8( index + 4 ) == 0x52 /*'R'*/ ) &&
( reader.getUint8( index + 5 ) == 0x3D /*'='*/ ) ) {
hasColors = true;
colors = new Float32Array( faces * 3 * 3 );
defaultR = reader.getUint8( index + 6 ) / 255;
defaultG = reader.getUint8( index + 7 ) / 255;
defaultB = reader.getUint8( index + 8 ) / 255;
alpha = reader.getUint8( index + 9 ) / 255;
}
}
const dataOffset = 84;
const faceLength = 12 * 4 + 2;
const geometry = new BufferGeometry();
const vertices = new Float32Array( faces * 3 * 3 );
const normals = new Float32Array( faces * 3 * 3 );
const color = new Color();
for ( let face = 0; face < faces; face ++ ) {
const start = dataOffset + face * faceLength;
const normalX = reader.getFloat32( start, true );
const normalY = reader.getFloat32( start + 4, true );
const normalZ = reader.getFloat32( start + 8, true );
if ( hasColors ) {
const packedColor = reader.getUint16( start + 48, true );
if ( ( packedColor & 0x8000 ) === 0 ) {
// facet has its own unique color
r = ( packedColor & 0x1F ) / 31;
g = ( ( packedColor >> 5 ) & 0x1F ) / 31;
b = ( ( packedColor >> 10 ) & 0x1F ) / 31;
} else {
r = defaultR;
g = defaultG;
b = defaultB;
}
}
for ( let i = 1; i <= 3; i ++ ) {
const vertexstart = start + i * 12;
const componentIdx = ( face * 3 * 3 ) + ( ( i - 1 ) * 3 );
vertices[ componentIdx ] = reader.getFloat32( vertexstart, true );
vertices[ componentIdx + 1 ] = reader.getFloat32( vertexstart + 4, true );
vertices[ componentIdx + 2 ] = reader.getFloat32( vertexstart + 8, true );
normals[ componentIdx ] = normalX;
normals[ componentIdx + 1 ] = normalY;
normals[ componentIdx + 2 ] = normalZ;
if ( hasColors ) {
color.set( r, g, b ).convertSRGBToLinear();
colors[ componentIdx ] = color.r;
colors[ componentIdx + 1 ] = color.g;
colors[ componentIdx + 2 ] = color.b;
}
}
}
geometry.setAttribute( 'position', new BufferAttribute( vertices, 3 ) );
geometry.setAttribute( 'normal', new BufferAttribute( normals, 3 ) );
if ( hasColors ) {
geometry.setAttribute( 'color', new BufferAttribute( colors, 3 ) );
geometry.hasColors = true;
geometry.alpha = alpha;
}
return geometry;
}
function parseASCII( data ) {
const geometry = new BufferGeometry();
const patternSolid = /solid([\s\S]*?)endsolid/g;
const patternFace = /facet([\s\S]*?)endfacet/g;
const patternName = /solid\s(.+)/;
let faceCounter = 0;
const patternFloat = /[\s]+([+-]?(?:\d*)(?:\.\d*)?(?:[eE][+-]?\d+)?)/.source;
const patternVertex = new RegExp( 'vertex' + patternFloat + patternFloat + patternFloat, 'g' );
const patternNormal = new RegExp( 'normal' + patternFloat + patternFloat + patternFloat, 'g' );
const vertices = [];
const normals = [];
const groupNames = [];
const normal = new Vector3();
let result;
let groupCount = 0;
let startVertex = 0;
let endVertex = 0;
while ( ( result = patternSolid.exec( data ) ) !== null ) {
startVertex = endVertex;
const solid = result[ 0 ];
const name = ( result = patternName.exec( solid ) ) !== null ? result[ 1 ] : '';
groupNames.push( name );
while ( ( result = patternFace.exec( solid ) ) !== null ) {
let vertexCountPerFace = 0;
let normalCountPerFace = 0;
const text = result[ 0 ];
while ( ( result = patternNormal.exec( text ) ) !== null ) {
normal.x = parseFloat( result[ 1 ] );
normal.y = parseFloat( result[ 2 ] );
normal.z = parseFloat( result[ 3 ] );
normalCountPerFace ++;
}
while ( ( result = patternVertex.exec( text ) ) !== null ) {
vertices.push( parseFloat( result[ 1 ] ), parseFloat( result[ 2 ] ), parseFloat( result[ 3 ] ) );
normals.push( normal.x, normal.y, normal.z );
vertexCountPerFace ++;
endVertex ++;
}
// every face have to own ONE valid normal
if ( normalCountPerFace !== 1 ) {
console.error( 'THREE.STLLoader: Something isn\'t right with the normal of face number ' + faceCounter );
}
// each face have to own THREE valid vertices
if ( vertexCountPerFace !== 3 ) {
console.error( 'THREE.STLLoader: Something isn\'t right with the vertices of face number ' + faceCounter );
}
faceCounter ++;
}
const start = startVertex;
const count = endVertex - startVertex;
geometry.userData.groupNames = groupNames;
geometry.addGroup( start, count, groupCount );
groupCount ++;
}
geometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
geometry.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) );
return geometry;
}
function ensureString( buffer ) {
if ( typeof buffer !== 'string' ) {
return new TextDecoder().decode( buffer );
}
return buffer;
}
function ensureBinary( buffer ) {
if ( typeof buffer === 'string' ) {
const array_buffer = new Uint8Array( buffer.length );
for ( let i = 0; i < buffer.length; i ++ ) {
array_buffer[ i ] = buffer.charCodeAt( i ) & 0xff; // implicitly assumes little-endian
}
return array_buffer.buffer || array_buffer;
} else {
return buffer;
}
}
// start
const binData = ensureBinary( data );
return isBinary( binData ) ? parseBinary( binData ) : parseASCII( ensureString( data ) );
}
}
export { STLLoader };

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,517 @@
import {
DataTextureLoader,
LinearMipmapLinearFilter
} from 'three';
class TGALoader extends DataTextureLoader {
constructor( manager ) {
super( manager );
}
parse( buffer ) {
// reference from vthibault, https://github.com/vthibault/roBrowser/blob/master/src/Loaders/Targa.js
function tgaCheckHeader( header ) {
switch ( header.image_type ) {
// check indexed type
case TGA_TYPE_INDEXED:
case TGA_TYPE_RLE_INDEXED:
if ( header.colormap_length > 256 || header.colormap_size !== 24 || header.colormap_type !== 1 ) {
throw new Error( 'THREE.TGALoader: Invalid type colormap data for indexed type.' );
}
break;
// check colormap type
case TGA_TYPE_RGB:
case TGA_TYPE_GREY:
case TGA_TYPE_RLE_RGB:
case TGA_TYPE_RLE_GREY:
if ( header.colormap_type ) {
throw new Error( 'THREE.TGALoader: Invalid type colormap data for colormap type.' );
}
break;
// What the need of a file without data ?
case TGA_TYPE_NO_DATA:
throw new Error( 'THREE.TGALoader: No data.' );
// Invalid type ?
default:
throw new Error( 'THREE.TGALoader: Invalid type ' + header.image_type );
}
// check image width and height
if ( header.width <= 0 || header.height <= 0 ) {
throw new Error( 'THREE.TGALoader: Invalid image size.' );
}
// check image pixel size
if ( header.pixel_size !== 8 && header.pixel_size !== 16 &&
header.pixel_size !== 24 && header.pixel_size !== 32 ) {
throw new Error( 'THREE.TGALoader: Invalid pixel size ' + header.pixel_size );
}
}
// parse tga image buffer
function tgaParse( use_rle, use_pal, header, offset, data ) {
let pixel_data,
palettes;
const pixel_size = header.pixel_size >> 3;
const pixel_total = header.width * header.height * pixel_size;
// read palettes
if ( use_pal ) {
palettes = data.subarray( offset, offset += header.colormap_length * ( header.colormap_size >> 3 ) );
}
// read RLE
if ( use_rle ) {
pixel_data = new Uint8Array( pixel_total );
let c, count, i;
let shift = 0;
const pixels = new Uint8Array( pixel_size );
while ( shift < pixel_total ) {
c = data[ offset ++ ];
count = ( c & 0x7f ) + 1;
// RLE pixels
if ( c & 0x80 ) {
// bind pixel tmp array
for ( i = 0; i < pixel_size; ++ i ) {
pixels[ i ] = data[ offset ++ ];
}
// copy pixel array
for ( i = 0; i < count; ++ i ) {
pixel_data.set( pixels, shift + i * pixel_size );
}
shift += pixel_size * count;
} else {
// raw pixels
count *= pixel_size;
for ( i = 0; i < count; ++ i ) {
pixel_data[ shift + i ] = data[ offset ++ ];
}
shift += count;
}
}
} else {
// raw pixels
pixel_data = data.subarray(
offset, offset += ( use_pal ? header.width * header.height : pixel_total )
);
}
return {
pixel_data: pixel_data,
palettes: palettes
};
}
function tgaGetImageData8bits( imageData, y_start, y_step, y_end, x_start, x_step, x_end, image, palettes ) {
const colormap = palettes;
let color, i = 0, x, y;
const width = header.width;
for ( y = y_start; y !== y_end; y += y_step ) {
for ( x = x_start; x !== x_end; x += x_step, i ++ ) {
color = image[ i ];
imageData[ ( x + width * y ) * 4 + 3 ] = 255;
imageData[ ( x + width * y ) * 4 + 2 ] = colormap[ ( color * 3 ) + 0 ];
imageData[ ( x + width * y ) * 4 + 1 ] = colormap[ ( color * 3 ) + 1 ];
imageData[ ( x + width * y ) * 4 + 0 ] = colormap[ ( color * 3 ) + 2 ];
}
}
return imageData;
}
function tgaGetImageData16bits( imageData, y_start, y_step, y_end, x_start, x_step, x_end, image ) {
let color, i = 0, x, y;
const width = header.width;
for ( y = y_start; y !== y_end; y += y_step ) {
for ( x = x_start; x !== x_end; x += x_step, i += 2 ) {
color = image[ i + 0 ] + ( image[ i + 1 ] << 8 );
imageData[ ( x + width * y ) * 4 + 0 ] = ( color & 0x7C00 ) >> 7;
imageData[ ( x + width * y ) * 4 + 1 ] = ( color & 0x03E0 ) >> 2;
imageData[ ( x + width * y ) * 4 + 2 ] = ( color & 0x001F ) << 3;
imageData[ ( x + width * y ) * 4 + 3 ] = ( color & 0x8000 ) ? 0 : 255;
}
}
return imageData;
}
function tgaGetImageData24bits( imageData, y_start, y_step, y_end, x_start, x_step, x_end, image ) {
let i = 0, x, y;
const width = header.width;
for ( y = y_start; y !== y_end; y += y_step ) {
for ( x = x_start; x !== x_end; x += x_step, i += 3 ) {
imageData[ ( x + width * y ) * 4 + 3 ] = 255;
imageData[ ( x + width * y ) * 4 + 2 ] = image[ i + 0 ];
imageData[ ( x + width * y ) * 4 + 1 ] = image[ i + 1 ];
imageData[ ( x + width * y ) * 4 + 0 ] = image[ i + 2 ];
}
}
return imageData;
}
function tgaGetImageData32bits( imageData, y_start, y_step, y_end, x_start, x_step, x_end, image ) {
let i = 0, x, y;
const width = header.width;
for ( y = y_start; y !== y_end; y += y_step ) {
for ( x = x_start; x !== x_end; x += x_step, i += 4 ) {
imageData[ ( x + width * y ) * 4 + 2 ] = image[ i + 0 ];
imageData[ ( x + width * y ) * 4 + 1 ] = image[ i + 1 ];
imageData[ ( x + width * y ) * 4 + 0 ] = image[ i + 2 ];
imageData[ ( x + width * y ) * 4 + 3 ] = image[ i + 3 ];
}
}
return imageData;
}
function tgaGetImageDataGrey8bits( imageData, y_start, y_step, y_end, x_start, x_step, x_end, image ) {
let color, i = 0, x, y;
const width = header.width;
for ( y = y_start; y !== y_end; y += y_step ) {
for ( x = x_start; x !== x_end; x += x_step, i ++ ) {
color = image[ i ];
imageData[ ( x + width * y ) * 4 + 0 ] = color;
imageData[ ( x + width * y ) * 4 + 1 ] = color;
imageData[ ( x + width * y ) * 4 + 2 ] = color;
imageData[ ( x + width * y ) * 4 + 3 ] = 255;
}
}
return imageData;
}
function tgaGetImageDataGrey16bits( imageData, y_start, y_step, y_end, x_start, x_step, x_end, image ) {
let i = 0, x, y;
const width = header.width;
for ( y = y_start; y !== y_end; y += y_step ) {
for ( x = x_start; x !== x_end; x += x_step, i += 2 ) {
imageData[ ( x + width * y ) * 4 + 0 ] = image[ i + 0 ];
imageData[ ( x + width * y ) * 4 + 1 ] = image[ i + 0 ];
imageData[ ( x + width * y ) * 4 + 2 ] = image[ i + 0 ];
imageData[ ( x + width * y ) * 4 + 3 ] = image[ i + 1 ];
}
}
return imageData;
}
function getTgaRGBA( data, width, height, image, palette ) {
let x_start,
y_start,
x_step,
y_step,
x_end,
y_end;
switch ( ( header.flags & TGA_ORIGIN_MASK ) >> TGA_ORIGIN_SHIFT ) {
default:
case TGA_ORIGIN_UL:
x_start = 0;
x_step = 1;
x_end = width;
y_start = 0;
y_step = 1;
y_end = height;
break;
case TGA_ORIGIN_BL:
x_start = 0;
x_step = 1;
x_end = width;
y_start = height - 1;
y_step = - 1;
y_end = - 1;
break;
case TGA_ORIGIN_UR:
x_start = width - 1;
x_step = - 1;
x_end = - 1;
y_start = 0;
y_step = 1;
y_end = height;
break;
case TGA_ORIGIN_BR:
x_start = width - 1;
x_step = - 1;
x_end = - 1;
y_start = height - 1;
y_step = - 1;
y_end = - 1;
break;
}
if ( use_grey ) {
switch ( header.pixel_size ) {
case 8:
tgaGetImageDataGrey8bits( data, y_start, y_step, y_end, x_start, x_step, x_end, image );
break;
case 16:
tgaGetImageDataGrey16bits( data, y_start, y_step, y_end, x_start, x_step, x_end, image );
break;
default:
throw new Error( 'THREE.TGALoader: Format not supported.' );
break;
}
} else {
switch ( header.pixel_size ) {
case 8:
tgaGetImageData8bits( data, y_start, y_step, y_end, x_start, x_step, x_end, image, palette );
break;
case 16:
tgaGetImageData16bits( data, y_start, y_step, y_end, x_start, x_step, x_end, image );
break;
case 24:
tgaGetImageData24bits( data, y_start, y_step, y_end, x_start, x_step, x_end, image );
break;
case 32:
tgaGetImageData32bits( data, y_start, y_step, y_end, x_start, x_step, x_end, image );
break;
default:
throw new Error( 'THREE.TGALoader: Format not supported.' );
break;
}
}
// Load image data according to specific method
// let func = 'tgaGetImageData' + (use_grey ? 'Grey' : '') + (header.pixel_size) + 'bits';
// func(data, y_start, y_step, y_end, x_start, x_step, x_end, width, image, palette );
return data;
}
// TGA constants
const TGA_TYPE_NO_DATA = 0,
TGA_TYPE_INDEXED = 1,
TGA_TYPE_RGB = 2,
TGA_TYPE_GREY = 3,
TGA_TYPE_RLE_INDEXED = 9,
TGA_TYPE_RLE_RGB = 10,
TGA_TYPE_RLE_GREY = 11,
TGA_ORIGIN_MASK = 0x30,
TGA_ORIGIN_SHIFT = 0x04,
TGA_ORIGIN_BL = 0x00,
TGA_ORIGIN_BR = 0x01,
TGA_ORIGIN_UL = 0x02,
TGA_ORIGIN_UR = 0x03;
if ( buffer.length < 19 ) throw new Error( 'THREE.TGALoader: Not enough data to contain header.' );
let offset = 0;
const content = new Uint8Array( buffer ),
header = {
id_length: content[ offset ++ ],
colormap_type: content[ offset ++ ],
image_type: content[ offset ++ ],
colormap_index: content[ offset ++ ] | content[ offset ++ ] << 8,
colormap_length: content[ offset ++ ] | content[ offset ++ ] << 8,
colormap_size: content[ offset ++ ],
origin: [
content[ offset ++ ] | content[ offset ++ ] << 8,
content[ offset ++ ] | content[ offset ++ ] << 8
],
width: content[ offset ++ ] | content[ offset ++ ] << 8,
height: content[ offset ++ ] | content[ offset ++ ] << 8,
pixel_size: content[ offset ++ ],
flags: content[ offset ++ ]
};
// check tga if it is valid format
tgaCheckHeader( header );
if ( header.id_length + offset > buffer.length ) {
throw new Error( 'THREE.TGALoader: No data.' );
}
// skip the needn't data
offset += header.id_length;
// get targa information about RLE compression and palette
let use_rle = false,
use_pal = false,
use_grey = false;
switch ( header.image_type ) {
case TGA_TYPE_RLE_INDEXED:
use_rle = true;
use_pal = true;
break;
case TGA_TYPE_INDEXED:
use_pal = true;
break;
case TGA_TYPE_RLE_RGB:
use_rle = true;
break;
case TGA_TYPE_RGB:
break;
case TGA_TYPE_RLE_GREY:
use_rle = true;
use_grey = true;
break;
case TGA_TYPE_GREY:
use_grey = true;
break;
}
//
const imageData = new Uint8Array( header.width * header.height * 4 );
const result = tgaParse( use_rle, use_pal, header, offset, content );
getTgaRGBA( imageData, header.width, header.height, result.pixel_data, result.palettes );
return {
data: imageData,
width: header.width,
height: header.height,
flipY: true,
generateMipmaps: true,
minFilter: LinearMipmapLinearFilter,
};
}
}
export { TGALoader };

View File

@ -0,0 +1,36 @@
import {
DataTextureLoader,
LinearFilter,
LinearMipmapLinearFilter
} from 'three';
import UTIF from '../libs/utif.module.js';
class TIFFLoader extends DataTextureLoader {
constructor( manager ) {
super( manager );
}
parse( buffer ) {
const ifds = UTIF.decode( buffer );
UTIF.decodeImage( buffer, ifds[ 0 ] );
const rgba = UTIF.toRGBA8( ifds[ 0 ] );
return {
width: ifds[ 0 ].width,
height: ifds[ 0 ].height,
data: rgba,
flipY: true,
magFilter: LinearFilter,
minFilter: LinearMipmapLinearFilter
};
}
}
export { TIFFLoader };

View File

@ -0,0 +1,214 @@
import {
FileLoader,
Loader
} from 'three';
import opentype from '../libs/opentype.module.js';
/**
* Requires opentype.js to be included in the project.
* Loads TTF files and converts them into typeface JSON that can be used directly
* to create THREE.Font objects.
*/
class TTFLoader extends Loader {
constructor( manager ) {
super( manager );
this.reversed = false;
}
load( url, onLoad, onProgress, onError ) {
const scope = this;
const loader = new FileLoader( this.manager );
loader.setPath( this.path );
loader.setResponseType( 'arraybuffer' );
loader.setRequestHeader( this.requestHeader );
loader.setWithCredentials( this.withCredentials );
loader.load( url, function ( buffer ) {
try {
onLoad( scope.parse( buffer ) );
} catch ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
scope.manager.itemError( url );
}
}, onProgress, onError );
}
parse( arraybuffer ) {
function convert( font, reversed ) {
const round = Math.round;
const glyphs = {};
const scale = ( 100000 ) / ( ( font.unitsPerEm || 2048 ) * 72 );
const glyphIndexMap = font.encoding.cmap.glyphIndexMap;
const unicodes = Object.keys( glyphIndexMap );
for ( let i = 0; i < unicodes.length; i ++ ) {
const unicode = unicodes[ i ];
const glyph = font.glyphs.glyphs[ glyphIndexMap[ unicode ] ];
if ( unicode !== undefined ) {
const token = {
ha: round( glyph.advanceWidth * scale ),
x_min: round( glyph.xMin * scale ),
x_max: round( glyph.xMax * scale ),
o: ''
};
if ( reversed ) {
glyph.path.commands = reverseCommands( glyph.path.commands );
}
glyph.path.commands.forEach( function ( command ) {
if ( command.type.toLowerCase() === 'c' ) {
command.type = 'b';
}
token.o += command.type.toLowerCase() + ' ';
if ( command.x !== undefined && command.y !== undefined ) {
token.o += round( command.x * scale ) + ' ' + round( command.y * scale ) + ' ';
}
if ( command.x1 !== undefined && command.y1 !== undefined ) {
token.o += round( command.x1 * scale ) + ' ' + round( command.y1 * scale ) + ' ';
}
if ( command.x2 !== undefined && command.y2 !== undefined ) {
token.o += round( command.x2 * scale ) + ' ' + round( command.y2 * scale ) + ' ';
}
} );
glyphs[ String.fromCodePoint( glyph.unicode ) ] = token;
}
}
return {
glyphs: glyphs,
familyName: font.getEnglishName( 'fullName' ),
ascender: round( font.ascender * scale ),
descender: round( font.descender * scale ),
underlinePosition: font.tables.post.underlinePosition,
underlineThickness: font.tables.post.underlineThickness,
boundingBox: {
xMin: font.tables.head.xMin,
xMax: font.tables.head.xMax,
yMin: font.tables.head.yMin,
yMax: font.tables.head.yMax
},
resolution: 1000,
original_font_information: font.tables.name
};
}
function reverseCommands( commands ) {
const paths = [];
let path;
commands.forEach( function ( c ) {
if ( c.type.toLowerCase() === 'm' ) {
path = [ c ];
paths.push( path );
} else if ( c.type.toLowerCase() !== 'z' ) {
path.push( c );
}
} );
const reversed = [];
paths.forEach( function ( p ) {
const result = {
type: 'm',
x: p[ p.length - 1 ].x,
y: p[ p.length - 1 ].y
};
reversed.push( result );
for ( let i = p.length - 1; i > 0; i -- ) {
const command = p[ i ];
const result = { type: command.type };
if ( command.x2 !== undefined && command.y2 !== undefined ) {
result.x1 = command.x2;
result.y1 = command.y2;
result.x2 = command.x1;
result.y2 = command.y1;
} else if ( command.x1 !== undefined && command.y1 !== undefined ) {
result.x1 = command.x1;
result.y1 = command.y1;
}
result.x = p[ i - 1 ].x;
result.y = p[ i - 1 ].y;
reversed.push( result );
}
} );
return reversed;
}
return convert( opentype.parse( arraybuffer ), this.reversed );
}
}
export { TTFLoader };

View File

@ -0,0 +1,520 @@
import {
BufferAttribute,
BufferGeometry,
Color,
DoubleSide,
FileLoader,
Group,
Loader,
Mesh,
MeshBasicMaterial,
RawShaderMaterial,
TextureLoader,
Quaternion,
Vector3
} from 'three';
import * as fflate from '../libs/fflate.module.js';
class TiltLoader extends Loader {
load( url, onLoad, onProgress, onError ) {
const scope = this;
const loader = new FileLoader( this.manager );
loader.setPath( this.path );
loader.setResponseType( 'arraybuffer' );
loader.setWithCredentials( this.withCredentials );
loader.load( url, function ( buffer ) {
try {
onLoad( scope.parse( buffer ) );
} catch ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
scope.manager.itemError( url );
}
}, onProgress, onError );
}
parse( buffer ) {
const group = new Group();
// https://docs.google.com/document/d/11ZsHozYn9FnWG7y3s3WAyKIACfbfwb4PbaS8cZ_xjvo/edit#
const zip = fflate.unzipSync( new Uint8Array( buffer.slice( 16 ) ) );
/*
const thumbnail = zip[ 'thumbnail.png' ].buffer;
const img = document.createElement( 'img' );
img.src = URL.createObjectURL( new Blob( [ thumbnail ] ) );
document.body.appendChild( img );
*/
const metadata = JSON.parse( fflate.strFromU8( zip[ 'metadata.json' ] ) );
/*
const blob = new Blob( [ zip[ 'data.sketch' ].buffer ], { type: 'application/octet-stream' } );
window.open( URL.createObjectURL( blob ) );
*/
const data = new DataView( zip[ 'data.sketch' ].buffer );
const num_strokes = data.getInt32( 16, true );
const brushes = {};
let offset = 20;
for ( let i = 0; i < num_strokes; i ++ ) {
const brush_index = data.getInt32( offset, true );
const brush_color = [
data.getFloat32( offset + 4, true ),
data.getFloat32( offset + 8, true ),
data.getFloat32( offset + 12, true ),
data.getFloat32( offset + 16, true )
];
const brush_size = data.getFloat32( offset + 20, true );
const stroke_mask = data.getUint32( offset + 24, true );
const controlpoint_mask = data.getUint32( offset + 28, true );
let offset_stroke_mask = 0;
let offset_controlpoint_mask = 0;
for ( let j = 0; j < 4; j ++ ) {
// TOFIX: I don't understand these masks yet
const byte = 1 << j;
if ( ( stroke_mask & byte ) > 0 ) offset_stroke_mask += 4;
if ( ( controlpoint_mask & byte ) > 0 ) offset_controlpoint_mask += 4;
}
// console.log( { brush_index, brush_color, brush_size, stroke_mask, controlpoint_mask } );
// console.log( offset_stroke_mask, offset_controlpoint_mask );
offset = offset + 28 + offset_stroke_mask + 4; // TOFIX: This is wrong
const num_control_points = data.getInt32( offset, true );
// console.log( { num_control_points } );
const positions = new Float32Array( num_control_points * 3 );
const quaternions = new Float32Array( num_control_points * 4 );
offset = offset + 4;
for ( let j = 0, k = 0; j < positions.length; j += 3, k += 4 ) {
positions[ j + 0 ] = data.getFloat32( offset + 0, true );
positions[ j + 1 ] = data.getFloat32( offset + 4, true );
positions[ j + 2 ] = data.getFloat32( offset + 8, true );
quaternions[ k + 0 ] = data.getFloat32( offset + 12, true );
quaternions[ k + 1 ] = data.getFloat32( offset + 16, true );
quaternions[ k + 2 ] = data.getFloat32( offset + 20, true );
quaternions[ k + 3 ] = data.getFloat32( offset + 24, true );
offset = offset + 28 + offset_controlpoint_mask; // TOFIX: This is wrong
}
if ( brush_index in brushes === false ) {
brushes[ brush_index ] = [];
}
brushes[ brush_index ].push( [ positions, quaternions, brush_size, brush_color ] );
}
for ( const brush_index in brushes ) {
const geometry = new StrokeGeometry( brushes[ brush_index ] );
const material = getMaterial( metadata.BrushIndex[ brush_index ] );
group.add( new Mesh( geometry, material ) );
}
return group;
}
}
class StrokeGeometry extends BufferGeometry {
constructor( strokes ) {
super();
const vertices = [];
const colors = [];
const uvs = [];
const position = new Vector3();
const prevPosition = new Vector3();
const quaternion = new Quaternion();
const prevQuaternion = new Quaternion();
const vector1 = new Vector3();
const vector2 = new Vector3();
const vector3 = new Vector3();
const vector4 = new Vector3();
const color = new Color();
// size = size / 2;
for ( const k in strokes ) {
const stroke = strokes[ k ];
const positions = stroke[ 0 ];
const quaternions = stroke[ 1 ];
const size = stroke[ 2 ];
const rgba = stroke[ 3 ];
const alpha = stroke[ 3 ][ 3 ];
color.fromArray( rgba ).convertSRGBToLinear();
prevPosition.fromArray( positions, 0 );
prevQuaternion.fromArray( quaternions, 0 );
for ( let i = 3, j = 4, l = positions.length; i < l; i += 3, j += 4 ) {
position.fromArray( positions, i );
quaternion.fromArray( quaternions, j );
vector1.set( - size, 0, 0 );
vector1.applyQuaternion( quaternion );
vector1.add( position );
vector2.set( size, 0, 0 );
vector2.applyQuaternion( quaternion );
vector2.add( position );
vector3.set( size, 0, 0 );
vector3.applyQuaternion( prevQuaternion );
vector3.add( prevPosition );
vector4.set( - size, 0, 0 );
vector4.applyQuaternion( prevQuaternion );
vector4.add( prevPosition );
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 );
prevPosition.copy( position );
prevQuaternion.copy( quaternion );
colors.push( ...color, alpha );
colors.push( ...color, alpha );
colors.push( ...color, alpha );
colors.push( ...color, alpha );
colors.push( ...color, alpha );
colors.push( ...color, alpha );
const p1 = i / l;
const p2 = ( i - 3 ) / l;
uvs.push( p1, 0 );
uvs.push( p1, 1 );
uvs.push( p2, 0 );
uvs.push( p1, 1 );
uvs.push( p2, 1 );
uvs.push( p2, 0 );
}
}
this.setAttribute( 'position', new BufferAttribute( new Float32Array( vertices ), 3 ) );
this.setAttribute( 'color', new BufferAttribute( new Float32Array( colors ), 4 ) );
this.setAttribute( 'uv', new BufferAttribute( new Float32Array( uvs ), 2 ) );
}
}
const BRUSH_LIST_ARRAY = {
'89d104cd-d012-426b-b5b3-bbaee63ac43c': 'Bubbles',
'700f3aa8-9a7c-2384-8b8a-ea028905dd8c': 'CelVinyl',
'0f0ff7b2-a677-45eb-a7d6-0cd7206f4816': 'ChromaticWave',
'1161af82-50cf-47db-9706-0c3576d43c43': 'CoarseBristles',
'79168f10-6961-464a-8be1-57ed364c5600': 'CoarseBristlesSingleSided',
'1caa6d7d-f015-3f54-3a4b-8b5354d39f81': 'Comet',
'c8313697-2563-47fc-832e-290f4c04b901': 'DiamondHull',
'4391aaaa-df73-4396-9e33-31e4e4930b27': 'Disco',
'd1d991f2-e7a0-4cf1-b328-f57e915e6260': 'DotMarker',
'6a1cf9f9-032c-45ec-9b1d-a6680bee30f7': 'Dots',
'0d3889f3-3ede-470c-8af4-f44813306126': 'DoubleTaperedFlat',
'0d3889f3-3ede-470c-8af4-de4813306126': 'DoubleTaperedMarker',
'd0262945-853c-4481-9cbd-88586bed93cb': 'DuctTape',
'3ca16e2f-bdcd-4da2-8631-dcef342f40f1': 'DuctTapeSingleSided',
'f6e85de3-6dcc-4e7f-87fd-cee8c3d25d51': 'Electricity',
'02ffb866-7fb2-4d15-b761-1012cefb1360': 'Embers',
'cb92b597-94ca-4255-b017-0e3f42f12f9e': 'Fire',
'2d35bcf0-e4d8-452c-97b1-3311be063130': 'Flat',
'55303bc4-c749-4a72-98d9-d23e68e76e18': 'FlatDeprecated',
'280c0a7a-aad8-416c-a7d2-df63d129ca70': 'FlatSingleSided',
'cf019139-d41c-4eb0-a1d0-5cf54b0a42f3': 'Highlighter',
'6a1cf9f9-032c-45ec-9b6e-a6680bee32e9': 'HyperGrid',
'dce872c2-7b49-4684-b59b-c45387949c5c': 'Hypercolor',
'e8ef32b1-baa8-460a-9c2c-9cf8506794f5': 'HypercolorSingleSided',
'2f212815-f4d3-c1a4-681a-feeaf9c6dc37': 'Icing',
'f5c336cf-5108-4b40-ade9-c687504385ab': 'Ink',
'c0012095-3ffd-4040-8ee1-fc180d346eaa': 'InkSingleSided',
'4a76a27a-44d8-4bfe-9a8c-713749a499b0': 'Leaves',
'ea19de07-d0c0-4484-9198-18489a3c1487': 'LeavesSingleSided',
'2241cd32-8ba2-48a5-9ee7-2caef7e9ed62': 'Light',
'4391aaaa-df81-4396-9e33-31e4e4930b27': 'LightWire',
'd381e0f5-3def-4a0d-8853-31e9200bcbda': 'Lofted',
'429ed64a-4e97-4466-84d3-145a861ef684': 'Marker',
'79348357-432d-4746-8e29-0e25c112e3aa': 'MatteHull',
'b2ffef01-eaaa-4ab5-aa64-95a2c4f5dbc6': 'NeonPulse',
'f72ec0e7-a844-4e38-82e3-140c44772699': 'OilPaint',
'c515dad7-4393-4681-81ad-162ef052241b': 'OilPaintSingleSided',
'f1114e2e-eb8d-4fde-915a-6e653b54e9f5': 'Paper',
'759f1ebd-20cd-4720-8d41-234e0da63716': 'PaperSingleSided',
'e0abbc80-0f80-e854-4970-8924a0863dcc': 'Petal',
'c33714d1-b2f9-412e-bd50-1884c9d46336': 'Plasma',
'ad1ad437-76e2-450d-a23a-e17f8310b960': 'Rainbow',
'faaa4d44-fcfb-4177-96be-753ac0421ba3': 'ShinyHull',
'70d79cca-b159-4f35-990c-f02193947fe8': 'Smoke',
'd902ed8b-d0d1-476c-a8de-878a79e3a34c': 'Snow',
'accb32f5-4509-454f-93f8-1df3fd31df1b': 'SoftHighlighter',
'cf7f0059-7aeb-53a4-2b67-c83d863a9ffa': 'Spikes',
'8dc4a70c-d558-4efd-a5ed-d4e860f40dc3': 'Splatter',
'7a1c8107-50c5-4b70-9a39-421576d6617e': 'SplatterSingleSided',
'0eb4db27-3f82-408d-b5a1-19ebd7d5b711': 'Stars',
'44bb800a-fbc3-4592-8426-94ecb05ddec3': 'Streamers',
'0077f88c-d93a-42f3-b59b-b31c50cdb414': 'Taffy',
'b468c1fb-f254-41ed-8ec9-57030bc5660c': 'TaperedFlat',
'c8ccb53d-ae13-45ef-8afb-b730d81394eb': 'TaperedFlatSingleSided',
'd90c6ad8-af0f-4b54-b422-e0f92abe1b3c': 'TaperedMarker',
'1a26b8c0-8a07-4f8a-9fac-d2ef36e0cad0': 'TaperedMarker_Flat',
'75b32cf0-fdd6-4d89-a64b-e2a00b247b0f': 'ThickPaint',
'fdf0326a-c0d1-4fed-b101-9db0ff6d071f': 'ThickPaintSingleSided',
'4391385a-df73-4396-9e33-31e4e4930b27': 'Toon',
'a8fea537-da7c-4d4b-817f-24f074725d6d': 'UnlitHull',
'd229d335-c334-495a-a801-660ac8a87360': 'VelvetInk',
'10201aa3-ebc2-42d8-84b7-2e63f6eeb8ab': 'Waveform',
'b67c0e81-ce6d-40a8-aeb0-ef036b081aa3': 'WetPaint',
'dea67637-cd1a-27e4-c9b1-52f4bbcb84e5': 'WetPaintSingleSided',
'5347acf0-a8e2-47b6-8346-30c70719d763': 'WigglyGraphite',
'e814fef1-97fd-7194-4a2f-50c2bb918be2': 'WigglyGraphiteSingleSided',
'4391385a-cf83-4396-9e33-31e4e4930b27': 'Wire'
};
const common = {
'colors': {
'BloomColor': `
vec3 BloomColor(vec3 color, float gain) {
// Guarantee that there's at least a little bit of all 3 channels.
// This makes fully-saturated strokes (which only have 2 non-zero
// color channels) eventually clip to white rather than to a secondary.
float cmin = length(color.rgb) * .05;
color.rgb = max(color.rgb, vec3(cmin, cmin, cmin));
// If we try to remove this pow() from .a, it brightens up
// pressure-sensitive strokes; looks better as-is.
color = pow(color, vec3(2.2));
color.rgb *= 2. * exp(gain * 10.);
return color;
}
`,
'LinearToSrgb': `
vec3 LinearToSrgb(vec3 color) {
// Approximation http://chilliant.blogspot.com/2012/08/srgb-approximations-for-hlsl.html
vec3 linearColor = color.rgb;
vec3 S1 = sqrt(linearColor);
vec3 S2 = sqrt(S1);
vec3 S3 = sqrt(S2);
color.rgb = 0.662002687 * S1 + 0.684122060 * S2 - 0.323583601 * S3 - 0.0225411470 * linearColor;
return color;
}
`,
'hsv': `
// uniform sampler2D lookupTex;
vec4 lookup(vec4 textureColor) {
return textureColor;
}
vec3 lookup(vec3 textureColor) {
return textureColor;
}
vec3 hsv2rgb( vec3 hsv ) {
vec3 rgb = clamp( abs(mod(hsv.x*6.0+vec3(0.0,4.0,2.0),6.0)-3.0)-1.0, 0.0, 1.0 );
return hsv.z * mix( vec3(1.0), rgb, hsv.y);
}
vec3 rgb2hsv( vec3 rgb ) {
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
vec4 p = mix(vec4(rgb.bg, K.wz), vec4(rgb.gb, K.xy), step(rgb.b, rgb.g));
vec4 q = mix(vec4(p.xyw, rgb.r), vec4(rgb.r, p.yzx), step(p.x, rgb.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}
`,
'SrgbToLinear': `
vec3 SrgbToLinear(vec3 color) {
// Approximation http://chilliant.blogspot.com/2012/08/srgb-approximations-for-hlsl.html
vec3 sRGB = color.rgb;
color.rgb = sRGB * (sRGB * (sRGB * 0.305306011 + 0.682171111) + 0.012522878);
return color;
}
`
}
};
let shaders = null;
function getShaders() {
if ( shaders === null ) {
const loader = new TextureLoader().setPath( './textures/tiltbrush/' );
shaders = {
'Light': {
uniforms: {
mainTex: { value: loader.load( 'Light.webp' ) },
alphaTest: { value: 0.067 },
emission_gain: { value: 0.45 },
alpha: { value: 1 },
},
vertexShader: `
precision highp float;
precision highp int;
attribute vec2 uv;
attribute vec4 color;
attribute vec3 position;
uniform mat4 modelMatrix;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat3 normalMatrix;
uniform vec3 cameraPosition;
varying vec2 vUv;
varying vec3 vColor;
${ common.colors.LinearToSrgb }
${ common.colors.hsv }
void main() {
vUv = uv;
vColor = lookup(color.rgb);
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
gl_Position = projectionMatrix * mvPosition;
}
`,
fragmentShader: `
precision highp float;
precision highp int;
uniform float emission_gain;
uniform sampler2D mainTex;
uniform float alphaTest;
varying vec2 vUv;
varying vec3 vColor;
${ common.colors.BloomColor }
${ common.colors.SrgbToLinear }
void main(){
vec4 col = texture2D(mainTex, vUv);
vec3 color = vColor;
color = BloomColor(color, emission_gain);
color = color * col.rgb;
color = color * col.a;
color = SrgbToLinear(color);
gl_FragColor = vec4(color, 1.0);
}
`,
side: 2,
transparent: true,
depthFunc: 2,
depthWrite: true,
depthTest: false,
blending: 5,
blendDst: 201,
blendDstAlpha: 201,
blendEquation: 100,
blendEquationAlpha: 100,
blendSrc: 201,
blendSrcAlpha: 201,
}
};
}
return shaders;
}
function getMaterial( GUID ) {
const name = BRUSH_LIST_ARRAY[ GUID ];
switch ( name ) {
case 'Light':
return new RawShaderMaterial( getShaders().Light );
default:
return new MeshBasicMaterial( { vertexColors: true, side: DoubleSide } );
}
}
export { TiltLoader };

View File

@ -0,0 +1,822 @@
import {
BufferAttribute,
BufferGeometry,
ClampToEdgeWrapping,
FileLoader,
Group,
NoColorSpace,
Loader,
Mesh,
MeshPhysicalMaterial,
MirroredRepeatWrapping,
RepeatWrapping,
SRGBColorSpace,
TextureLoader,
Object3D,
Vector2
} from 'three';
import * as fflate from '../libs/fflate.module.js';
class USDAParser {
parse( text ) {
const data = {};
const lines = text.split( '\n' );
let string = null;
let target = data;
const stack = [ data ];
// debugger;
for ( const line of lines ) {
// console.log( line );
if ( line.includes( '=' ) ) {
const assignment = line.split( '=' );
const lhs = assignment[ 0 ].trim();
const rhs = assignment[ 1 ].trim();
if ( rhs.endsWith( '{' ) ) {
const group = {};
stack.push( group );
target[ lhs ] = group;
target = group;
} else {
target[ lhs ] = rhs;
}
} else if ( line.endsWith( '{' ) ) {
const group = target[ string ] || {};
stack.push( group );
target[ string ] = group;
target = group;
} else if ( line.endsWith( '}' ) ) {
stack.pop();
if ( stack.length === 0 ) continue;
target = stack[ stack.length - 1 ];
} else if ( line.endsWith( '(' ) ) {
const meta = {};
stack.push( meta );
string = line.split( '(' )[ 0 ].trim() || string;
target[ string ] = meta;
target = meta;
} else if ( line.endsWith( ')' ) ) {
stack.pop();
target = stack[ stack.length - 1 ];
} else {
string = line.trim();
}
}
return data;
}
}
class USDZLoader extends Loader {
constructor( manager ) {
super( manager );
}
load( url, onLoad, onProgress, onError ) {
const scope = this;
const loader = new FileLoader( scope.manager );
loader.setPath( scope.path );
loader.setResponseType( 'arraybuffer' );
loader.setRequestHeader( scope.requestHeader );
loader.setWithCredentials( scope.withCredentials );
loader.load( url, function ( text ) {
try {
onLoad( scope.parse( text ) );
} catch ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
scope.manager.itemError( url );
}
}, onProgress, onError );
}
parse( buffer ) {
const parser = new USDAParser();
function parseAssets( zip ) {
const data = {};
const loader = new FileLoader();
loader.setResponseType( 'arraybuffer' );
for ( const filename in zip ) {
if ( filename.endsWith( 'png' ) ) {
const blob = new Blob( [ zip[ filename ] ], { type: { type: 'image/png' } } );
data[ filename ] = URL.createObjectURL( blob );
}
if ( filename.endsWith( 'usd' ) || filename.endsWith( 'usda' ) ) {
if ( isCrateFile( zip[ filename ] ) ) {
console.warn( 'THREE.USDZLoader: Crate files (.usdc or binary .usd) are not supported.' );
continue;
}
const text = fflate.strFromU8( zip[ filename ] );
data[ filename ] = parser.parse( text );
}
}
return data;
}
function isCrateFile( buffer ) {
// Check if this a crate file. First 7 bytes of a crate file are "PXR-USDC".
const fileHeader = buffer.slice( 0, 7 );
const crateHeader = new Uint8Array( [ 0x50, 0x58, 0x52, 0x2D, 0x55, 0x53, 0x44, 0x43 ] );
// If this is not a crate file, we assume it is a plain USDA file.
return fileHeader.every( ( value, index ) => value === crateHeader[ index ] );
}
function findUSD( zip ) {
if ( zip.length < 1 ) return undefined;
const firstFileName = Object.keys( zip )[ 0 ];
let isCrate = false;
// As per the USD specification, the first entry in the zip archive is used as the main file ("UsdStage").
// ASCII files can end in either .usda or .usd.
// See https://openusd.org/release/spec_usdz.html#layout
if ( firstFileName.endsWith( 'usda' ) ) return zip[ firstFileName ];
if ( firstFileName.endsWith( 'usdc' ) ) {
isCrate = true;
} else if ( firstFileName.endsWith( 'usd' ) ) {
// If this is not a crate file, we assume it is a plain USDA file.
if ( ! isCrateFile( zip[ firstFileName ] ) ) {
return zip[ firstFileName ];
} else {
isCrate = true;
}
}
if ( isCrate ) {
console.warn( 'THREE.USDZLoader: Crate files (.usdc or binary .usd) are not supported.' );
}
return undefined;
}
const zip = fflate.unzipSync( new Uint8Array( buffer ) );
// console.log( zip );
const assets = parseAssets( zip );
// console.log( assets )
const file = findUSD( zip );
if ( file === undefined ) {
console.warn( 'THREE.USDZLoader: No usda file found.' );
return new Group();
}
// Parse file
const text = fflate.strFromU8( file );
const root = parser.parse( text );
// Build scene
function findMeshGeometry( data ) {
if ( ! data ) return undefined;
if ( 'prepend references' in data ) {
const reference = data[ 'prepend references' ];
const parts = reference.split( '@' );
const path = parts[ 1 ].replace( /^.\//, '' );
const id = parts[ 2 ].replace( /^<\//, '' ).replace( />$/, '' );
return findGeometry( assets[ path ], id );
}
return findGeometry( data );
}
function findGeometry( data, id ) {
if ( ! data ) return undefined;
if ( id !== undefined ) {
const def = `def Mesh "${id}"`;
if ( def in data ) {
return data[ def ];
}
}
for ( const name in data ) {
const object = data[ name ];
if ( name.startsWith( 'def Mesh' ) ) {
// Move points to Mesh
if ( 'point3f[] points' in data ) {
object[ 'point3f[] points' ] = data[ 'point3f[] points' ];
}
// Move st to Mesh
if ( 'texCoord2f[] primvars:st' in data ) {
object[ 'texCoord2f[] primvars:st' ] = data[ 'texCoord2f[] primvars:st' ];
}
// Move st indices to Mesh
if ( 'int[] primvars:st:indices' in data ) {
object[ 'int[] primvars:st:indices' ] = data[ 'int[] primvars:st:indices' ];
}
return object;
}
if ( typeof object === 'object' ) {
const geometry = findGeometry( object );
if ( geometry ) return geometry;
}
}
}
function buildGeometry( data ) {
if ( ! data ) return undefined;
let geometry = new BufferGeometry();
if ( 'int[] faceVertexIndices' in data ) {
const indices = JSON.parse( data[ 'int[] faceVertexIndices' ] );
geometry.setIndex( indices );
}
if ( 'point3f[] points' in data ) {
const positions = JSON.parse( data[ 'point3f[] points' ].replace( /[()]*/g, '' ) );
const attribute = new BufferAttribute( new Float32Array( positions ), 3 );
geometry.setAttribute( 'position', attribute );
}
if ( 'normal3f[] normals' in data ) {
const normals = JSON.parse( data[ 'normal3f[] normals' ].replace( /[()]*/g, '' ) );
const attribute = new BufferAttribute( new Float32Array( normals ), 3 );
geometry.setAttribute( 'normal', attribute );
} else {
geometry.computeVertexNormals();
}
if ( 'float2[] primvars:st' in data ) {
data[ 'texCoord2f[] primvars:st' ] = data[ 'float2[] primvars:st' ];
}
if ( 'texCoord2f[] primvars:st' in data ) {
const uvs = JSON.parse( data[ 'texCoord2f[] primvars:st' ].replace( /[()]*/g, '' ) );
const attribute = new BufferAttribute( new Float32Array( uvs ), 2 );
if ( 'int[] primvars:st:indices' in data ) {
geometry = geometry.toNonIndexed();
const indices = JSON.parse( data[ 'int[] primvars:st:indices' ] );
geometry.setAttribute( 'uv', toFlatBufferAttribute( attribute, indices ) );
} else {
geometry.setAttribute( 'uv', attribute );
}
}
return geometry;
}
function toFlatBufferAttribute( attribute, indices ) {
const array = attribute.array;
const itemSize = attribute.itemSize;
const array2 = new array.constructor( indices.length * itemSize );
let index = 0, index2 = 0;
for ( let i = 0, l = indices.length; i < l; i ++ ) {
index = indices[ i ] * itemSize;
for ( let j = 0; j < itemSize; j ++ ) {
array2[ index2 ++ ] = array[ index ++ ];
}
}
return new BufferAttribute( array2, itemSize );
}
function findMeshMaterial( data ) {
if ( ! data ) return undefined;
if ( 'rel material:binding' in data ) {
const reference = data[ 'rel material:binding' ];
const id = reference.replace( /^<\//, '' ).replace( />$/, '' );
const parts = id.split( '/' );
return findMaterial( root, ` "${ parts[ 1 ] }"` );
}
return findMaterial( data );
}
function findMaterial( data, id = '' ) {
for ( const name in data ) {
const object = data[ name ];
if ( name.startsWith( 'def Material' + id ) ) {
return object;
}
if ( typeof object === 'object' ) {
const material = findMaterial( object, id );
if ( material ) return material;
}
}
}
function setTextureParams( map, data_value ) {
// rotation, scale and translation
if ( data_value[ 'float inputs:rotation' ] ) {
map.rotation = parseFloat( data_value[ 'float inputs:rotation' ] );
}
if ( data_value[ 'float2 inputs:scale' ] ) {
map.repeat = new Vector2().fromArray( JSON.parse( '[' + data_value[ 'float2 inputs:scale' ].replace( /[()]*/g, '' ) + ']' ) );
}
if ( data_value[ 'float2 inputs:translation' ] ) {
map.offset = new Vector2().fromArray( JSON.parse( '[' + data_value[ 'float2 inputs:translation' ].replace( /[()]*/g, '' ) + ']' ) );
}
}
function buildMaterial( data ) {
const material = new MeshPhysicalMaterial();
if ( data !== undefined ) {
if ( 'def Shader "PreviewSurface"' in data ) {
const surface = data[ 'def Shader "PreviewSurface"' ];
if ( 'color3f inputs:diffuseColor.connect' in surface ) {
const path = surface[ 'color3f inputs:diffuseColor.connect' ];
const sampler = findTexture( root, /(\w+).output/.exec( path )[ 1 ] );
material.map = buildTexture( sampler );
material.map.colorSpace = SRGBColorSpace;
if ( 'def Shader "Transform2d_diffuse"' in data ) {
setTextureParams( material.map, data[ 'def Shader "Transform2d_diffuse"' ] );
}
} else if ( 'color3f inputs:diffuseColor' in surface ) {
const color = surface[ 'color3f inputs:diffuseColor' ].replace( /[()]*/g, '' );
material.color.fromArray( JSON.parse( '[' + color + ']' ) );
}
if ( 'color3f inputs:emissiveColor.connect' in surface ) {
const path = surface[ 'color3f inputs:emissiveColor.connect' ];
const sampler = findTexture( root, /(\w+).output/.exec( path )[ 1 ] );
material.emissiveMap = buildTexture( sampler );
material.emissiveMap.colorSpace = SRGBColorSpace;
material.emissive.set( 0xffffff );
if ( 'def Shader "Transform2d_emissive"' in data ) {
setTextureParams( material.emissiveMap, data[ 'def Shader "Transform2d_emissive"' ] );
}
} else if ( 'color3f inputs:emissiveColor' in surface ) {
const color = surface[ 'color3f inputs:emissiveColor' ].replace( /[()]*/g, '' );
material.emissive.fromArray( JSON.parse( '[' + color + ']' ) );
}
if ( 'normal3f inputs:normal.connect' in surface ) {
const path = surface[ 'normal3f inputs:normal.connect' ];
const sampler = findTexture( root, /(\w+).output/.exec( path )[ 1 ] );
material.normalMap = buildTexture( sampler );
material.normalMap.colorSpace = NoColorSpace;
if ( 'def Shader "Transform2d_normal"' in data ) {
setTextureParams( material.normalMap, data[ 'def Shader "Transform2d_normal"' ] );
}
}
if ( 'float inputs:roughness.connect' in surface ) {
const path = surface[ 'float inputs:roughness.connect' ];
const sampler = findTexture( root, /(\w+).output/.exec( path )[ 1 ] );
material.roughness = 1.0;
material.roughnessMap = buildTexture( sampler );
material.roughnessMap.colorSpace = NoColorSpace;
if ( 'def Shader "Transform2d_roughness"' in data ) {
setTextureParams( material.roughnessMap, data[ 'def Shader "Transform2d_roughness"' ] );
}
} else if ( 'float inputs:roughness' in surface ) {
material.roughness = parseFloat( surface[ 'float inputs:roughness' ] );
}
if ( 'float inputs:metallic.connect' in surface ) {
const path = surface[ 'float inputs:metallic.connect' ];
const sampler = findTexture( root, /(\w+).output/.exec( path )[ 1 ] );
material.metalness = 1.0;
material.metalnessMap = buildTexture( sampler );
material.metalnessMap.colorSpace = NoColorSpace;
if ( 'def Shader "Transform2d_metallic"' in data ) {
setTextureParams( material.metalnessMap, data[ 'def Shader "Transform2d_metallic"' ] );
}
} else if ( 'float inputs:metallic' in surface ) {
material.metalness = parseFloat( surface[ 'float inputs:metallic' ] );
}
if ( 'float inputs:clearcoat.connect' in surface ) {
const path = surface[ 'float inputs:clearcoat.connect' ];
const sampler = findTexture( root, /(\w+).output/.exec( path )[ 1 ] );
material.clearcoat = 1.0;
material.clearcoatMap = buildTexture( sampler );
material.clearcoatMap.colorSpace = NoColorSpace;
if ( 'def Shader "Transform2d_clearcoat"' in data ) {
setTextureParams( material.clearcoatMap, data[ 'def Shader "Transform2d_clearcoat"' ] );
}
} else if ( 'float inputs:clearcoat' in surface ) {
material.clearcoat = parseFloat( surface[ 'float inputs:clearcoat' ] );
}
if ( 'float inputs:clearcoatRoughness.connect' in surface ) {
const path = surface[ 'float inputs:clearcoatRoughness.connect' ];
const sampler = findTexture( root, /(\w+).output/.exec( path )[ 1 ] );
material.clearcoatRoughness = 1.0;
material.clearcoatRoughnessMap = buildTexture( sampler );
material.clearcoatRoughnessMap.colorSpace = NoColorSpace;
if ( 'def Shader "Transform2d_clearcoatRoughness"' in data ) {
setTextureParams( material.clearcoatRoughnessMap, data[ 'def Shader "Transform2d_clearcoatRoughness"' ] );
}
} else if ( 'float inputs:clearcoatRoughness' in surface ) {
material.clearcoatRoughness = parseFloat( surface[ 'float inputs:clearcoatRoughness' ] );
}
if ( 'float inputs:ior' in surface ) {
material.ior = parseFloat( surface[ 'float inputs:ior' ] );
}
if ( 'float inputs:occlusion.connect' in surface ) {
const path = surface[ 'float inputs:occlusion.connect' ];
const sampler = findTexture( root, /(\w+).output/.exec( path )[ 1 ] );
material.aoMap = buildTexture( sampler );
material.aoMap.colorSpace = NoColorSpace;
if ( 'def Shader "Transform2d_occlusion"' in data ) {
setTextureParams( material.aoMap, data[ 'def Shader "Transform2d_occlusion"' ] );
}
}
}
if ( 'def Shader "diffuseColor_texture"' in data ) {
const sampler = data[ 'def Shader "diffuseColor_texture"' ];
material.map = buildTexture( sampler );
material.map.colorSpace = SRGBColorSpace;
}
if ( 'def Shader "normal_texture"' in data ) {
const sampler = data[ 'def Shader "normal_texture"' ];
material.normalMap = buildTexture( sampler );
material.normalMap.colorSpace = NoColorSpace;
}
}
return material;
}
function findTexture( data, id ) {
for ( const name in data ) {
const object = data[ name ];
if ( name.startsWith( `def Shader "${ id }"` ) ) {
return object;
}
if ( typeof object === 'object' ) {
const texture = findTexture( object, id );
if ( texture ) return texture;
}
}
}
function buildTexture( data ) {
if ( 'asset inputs:file' in data ) {
const path = data[ 'asset inputs:file' ].replace( /@*/g, '' );
const loader = new TextureLoader();
const texture = loader.load( assets[ path ] );
const map = {
'"clamp"': ClampToEdgeWrapping,
'"mirror"': MirroredRepeatWrapping,
'"repeat"': RepeatWrapping
};
if ( 'token inputs:wrapS' in data ) {
texture.wrapS = map[ data[ 'token inputs:wrapS' ] ];
}
if ( 'token inputs:wrapT' in data ) {
texture.wrapT = map[ data[ 'token inputs:wrapT' ] ];
}
return texture;
}
return null;
}
function buildObject( data ) {
const geometry = buildGeometry( findMeshGeometry( data ) );
const material = buildMaterial( findMeshMaterial( data ) );
const mesh = geometry ? new Mesh( geometry, material ) : new Object3D();
if ( 'matrix4d xformOp:transform' in data ) {
const array = JSON.parse( '[' + data[ 'matrix4d xformOp:transform' ].replace( /[()]*/g, '' ) + ']' );
mesh.matrix.fromArray( array );
mesh.matrix.decompose( mesh.position, mesh.quaternion, mesh.scale );
}
return mesh;
}
function buildHierarchy( data, group ) {
for ( const name in data ) {
if ( name.startsWith( 'def Scope' ) ) {
buildHierarchy( data[ name ], group );
} else if ( name.startsWith( 'def Xform' ) ) {
const mesh = buildObject( data[ name ] );
if ( /def Xform "(\w+)"/.test( name ) ) {
mesh.name = /def Xform "(\w+)"/.exec( name )[ 1 ];
}
group.add( mesh );
buildHierarchy( data[ name ], mesh );
}
}
}
const group = new Group();
buildHierarchy( root, group );
return group;
}
}
export { USDZLoader };

View File

@ -0,0 +1,318 @@
import {
BufferGeometry,
Color,
Data3DTexture,
FileLoader,
Float32BufferAttribute,
Loader,
LinearFilter,
Mesh,
MeshStandardMaterial,
NearestFilter,
RedFormat,
SRGBColorSpace
} from 'three';
class VOXLoader extends Loader {
load( url, onLoad, onProgress, onError ) {
const scope = this;
const loader = new FileLoader( scope.manager );
loader.setPath( scope.path );
loader.setResponseType( 'arraybuffer' );
loader.setRequestHeader( scope.requestHeader );
loader.load( url, function ( buffer ) {
try {
onLoad( scope.parse( buffer ) );
} catch ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
scope.manager.itemError( url );
}
}, onProgress, onError );
}
parse( buffer ) {
const data = new DataView( buffer );
const id = data.getUint32( 0, true );
const version = data.getUint32( 4, true );
if ( id !== 542658390 ) {
console.error( 'THREE.VOXLoader: Invalid VOX file.' );
return;
}
if ( version !== 150 ) {
console.error( 'THREE.VOXLoader: Invalid VOX file. Unsupported version:', version );
return;
}
const DEFAULT_PALETTE = [
0x00000000, 0xffffffff, 0xffccffff, 0xff99ffff, 0xff66ffff, 0xff33ffff, 0xff00ffff, 0xffffccff,
0xffccccff, 0xff99ccff, 0xff66ccff, 0xff33ccff, 0xff00ccff, 0xffff99ff, 0xffcc99ff, 0xff9999ff,
0xff6699ff, 0xff3399ff, 0xff0099ff, 0xffff66ff, 0xffcc66ff, 0xff9966ff, 0xff6666ff, 0xff3366ff,
0xff0066ff, 0xffff33ff, 0xffcc33ff, 0xff9933ff, 0xff6633ff, 0xff3333ff, 0xff0033ff, 0xffff00ff,
0xffcc00ff, 0xff9900ff, 0xff6600ff, 0xff3300ff, 0xff0000ff, 0xffffffcc, 0xffccffcc, 0xff99ffcc,
0xff66ffcc, 0xff33ffcc, 0xff00ffcc, 0xffffcccc, 0xffcccccc, 0xff99cccc, 0xff66cccc, 0xff33cccc,
0xff00cccc, 0xffff99cc, 0xffcc99cc, 0xff9999cc, 0xff6699cc, 0xff3399cc, 0xff0099cc, 0xffff66cc,
0xffcc66cc, 0xff9966cc, 0xff6666cc, 0xff3366cc, 0xff0066cc, 0xffff33cc, 0xffcc33cc, 0xff9933cc,
0xff6633cc, 0xff3333cc, 0xff0033cc, 0xffff00cc, 0xffcc00cc, 0xff9900cc, 0xff6600cc, 0xff3300cc,
0xff0000cc, 0xffffff99, 0xffccff99, 0xff99ff99, 0xff66ff99, 0xff33ff99, 0xff00ff99, 0xffffcc99,
0xffcccc99, 0xff99cc99, 0xff66cc99, 0xff33cc99, 0xff00cc99, 0xffff9999, 0xffcc9999, 0xff999999,
0xff669999, 0xff339999, 0xff009999, 0xffff6699, 0xffcc6699, 0xff996699, 0xff666699, 0xff336699,
0xff006699, 0xffff3399, 0xffcc3399, 0xff993399, 0xff663399, 0xff333399, 0xff003399, 0xffff0099,
0xffcc0099, 0xff990099, 0xff660099, 0xff330099, 0xff000099, 0xffffff66, 0xffccff66, 0xff99ff66,
0xff66ff66, 0xff33ff66, 0xff00ff66, 0xffffcc66, 0xffcccc66, 0xff99cc66, 0xff66cc66, 0xff33cc66,
0xff00cc66, 0xffff9966, 0xffcc9966, 0xff999966, 0xff669966, 0xff339966, 0xff009966, 0xffff6666,
0xffcc6666, 0xff996666, 0xff666666, 0xff336666, 0xff006666, 0xffff3366, 0xffcc3366, 0xff993366,
0xff663366, 0xff333366, 0xff003366, 0xffff0066, 0xffcc0066, 0xff990066, 0xff660066, 0xff330066,
0xff000066, 0xffffff33, 0xffccff33, 0xff99ff33, 0xff66ff33, 0xff33ff33, 0xff00ff33, 0xffffcc33,
0xffcccc33, 0xff99cc33, 0xff66cc33, 0xff33cc33, 0xff00cc33, 0xffff9933, 0xffcc9933, 0xff999933,
0xff669933, 0xff339933, 0xff009933, 0xffff6633, 0xffcc6633, 0xff996633, 0xff666633, 0xff336633,
0xff006633, 0xffff3333, 0xffcc3333, 0xff993333, 0xff663333, 0xff333333, 0xff003333, 0xffff0033,
0xffcc0033, 0xff990033, 0xff660033, 0xff330033, 0xff000033, 0xffffff00, 0xffccff00, 0xff99ff00,
0xff66ff00, 0xff33ff00, 0xff00ff00, 0xffffcc00, 0xffcccc00, 0xff99cc00, 0xff66cc00, 0xff33cc00,
0xff00cc00, 0xffff9900, 0xffcc9900, 0xff999900, 0xff669900, 0xff339900, 0xff009900, 0xffff6600,
0xffcc6600, 0xff996600, 0xff666600, 0xff336600, 0xff006600, 0xffff3300, 0xffcc3300, 0xff993300,
0xff663300, 0xff333300, 0xff003300, 0xffff0000, 0xffcc0000, 0xff990000, 0xff660000, 0xff330000,
0xff0000ee, 0xff0000dd, 0xff0000bb, 0xff0000aa, 0xff000088, 0xff000077, 0xff000055, 0xff000044,
0xff000022, 0xff000011, 0xff00ee00, 0xff00dd00, 0xff00bb00, 0xff00aa00, 0xff008800, 0xff007700,
0xff005500, 0xff004400, 0xff002200, 0xff001100, 0xffee0000, 0xffdd0000, 0xffbb0000, 0xffaa0000,
0xff880000, 0xff770000, 0xff550000, 0xff440000, 0xff220000, 0xff110000, 0xffeeeeee, 0xffdddddd,
0xffbbbbbb, 0xffaaaaaa, 0xff888888, 0xff777777, 0xff555555, 0xff444444, 0xff222222, 0xff111111
];
let i = 8;
let chunk;
const chunks = [];
while ( i < data.byteLength ) {
let id = '';
for ( let j = 0; j < 4; j ++ ) {
id += String.fromCharCode( data.getUint8( i ++ ) );
}
const chunkSize = data.getUint32( i, true ); i += 4;
i += 4; // childChunks
if ( id === 'SIZE' ) {
const x = data.getUint32( i, true ); i += 4;
const y = data.getUint32( i, true ); i += 4;
const z = data.getUint32( i, true ); i += 4;
chunk = {
palette: DEFAULT_PALETTE,
size: { x: x, y: y, z: z },
};
chunks.push( chunk );
i += chunkSize - ( 3 * 4 );
} else if ( id === 'XYZI' ) {
const numVoxels = data.getUint32( i, true ); i += 4;
chunk.data = new Uint8Array( buffer, i, numVoxels * 4 );
i += numVoxels * 4;
} else if ( id === 'RGBA' ) {
const palette = [ 0 ];
for ( let j = 0; j < 256; j ++ ) {
palette[ j + 1 ] = data.getUint32( i, true ); i += 4;
}
chunk.palette = palette;
} else {
// console.log( id, chunkSize, childChunks );
i += chunkSize;
}
}
return chunks;
}
}
class VOXMesh extends Mesh {
constructor( chunk ) {
const data = chunk.data;
const size = chunk.size;
const palette = chunk.palette;
//
const vertices = [];
const colors = [];
const nx = [ 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1 ];
const px = [ 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0 ];
const py = [ 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1 ];
const ny = [ 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0 ];
const nz = [ 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0 ];
const pz = [ 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1 ];
const _color = new Color();
function add( tile, x, y, z, r, g, b ) {
x -= size.x / 2;
y -= size.z / 2;
z += size.y / 2;
for ( let i = 0; i < 18; i += 3 ) {
_color.setRGB( r, g, b, SRGBColorSpace );
vertices.push( tile[ i + 0 ] + x, tile[ i + 1 ] + y, tile[ i + 2 ] + z );
colors.push( _color.r, _color.g, _color.b );
}
}
// Store data in a volume for sampling
const offsety = size.x;
const offsetz = size.x * size.y;
const array = new Uint8Array( size.x * size.y * size.z );
for ( let j = 0; j < data.length; j += 4 ) {
const x = data[ j + 0 ];
const y = data[ j + 1 ];
const z = data[ j + 2 ];
const index = x + ( y * offsety ) + ( z * offsetz );
array[ index ] = 255;
}
// Construct geometry
let hasColors = false;
for ( let j = 0; j < data.length; j += 4 ) {
const x = data[ j + 0 ];
const y = data[ j + 1 ];
const z = data[ j + 2 ];
const c = data[ j + 3 ];
const hex = palette[ c ];
const r = ( hex >> 0 & 0xff ) / 0xff;
const g = ( hex >> 8 & 0xff ) / 0xff;
const b = ( hex >> 16 & 0xff ) / 0xff;
if ( r > 0 || g > 0 || b > 0 ) hasColors = true;
const index = x + ( y * offsety ) + ( z * offsetz );
if ( array[ index + 1 ] === 0 || x === size.x - 1 ) add( px, x, z, - y, r, g, b );
if ( array[ index - 1 ] === 0 || x === 0 ) add( nx, x, z, - y, r, g, b );
if ( array[ index + offsety ] === 0 || y === size.y - 1 ) add( ny, x, z, - y, r, g, b );
if ( array[ index - offsety ] === 0 || y === 0 ) add( py, x, z, - y, r, g, b );
if ( array[ index + offsetz ] === 0 || z === size.z - 1 ) add( pz, x, z, - y, r, g, b );
if ( array[ index - offsetz ] === 0 || z === 0 ) add( nz, x, z, - y, r, g, b );
}
const geometry = new BufferGeometry();
geometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
geometry.computeVertexNormals();
const material = new MeshStandardMaterial();
if ( hasColors ) {
geometry.setAttribute( 'color', new Float32BufferAttribute( colors, 3 ) );
material.vertexColors = true;
}
super( geometry, material );
}
}
class VOXData3DTexture extends Data3DTexture {
constructor( chunk ) {
const data = chunk.data;
const size = chunk.size;
const offsety = size.x;
const offsetz = size.x * size.y;
const array = new Uint8Array( size.x * size.y * size.z );
for ( let j = 0; j < data.length; j += 4 ) {
const x = data[ j + 0 ];
const y = data[ j + 1 ];
const z = data[ j + 2 ];
const index = x + ( y * offsety ) + ( z * offsetz );
array[ index ] = 255;
}
super( array, size.x, size.y, size.z );
this.format = RedFormat;
this.minFilter = NearestFilter;
this.magFilter = LinearFilter;
this.unpackAlignment = 1;
this.needsUpdate = true;
}
}
export { VOXLoader, VOXMesh, VOXData3DTexture };

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,106 @@
import {
BufferGeometry,
Color,
FileLoader,
Float32BufferAttribute,
Loader
} from 'three';
class XYZLoader extends Loader {
load( url, onLoad, onProgress, onError ) {
const scope = this;
const loader = new FileLoader( this.manager );
loader.setPath( this.path );
loader.setRequestHeader( this.requestHeader );
loader.setWithCredentials( this.withCredentials );
loader.load( url, function ( text ) {
try {
onLoad( scope.parse( text ) );
} catch ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
scope.manager.itemError( url );
}
}, onProgress, onError );
}
parse( text ) {
const lines = text.split( '\n' );
const vertices = [];
const colors = [];
const color = new Color();
for ( let line of lines ) {
line = line.trim();
if ( line.charAt( 0 ) === '#' ) continue; // skip comments
const lineValues = line.split( /\s+/ );
if ( lineValues.length === 3 ) {
// XYZ
vertices.push( parseFloat( lineValues[ 0 ] ) );
vertices.push( parseFloat( lineValues[ 1 ] ) );
vertices.push( parseFloat( lineValues[ 2 ] ) );
}
if ( lineValues.length === 6 ) {
// XYZRGB
vertices.push( parseFloat( lineValues[ 0 ] ) );
vertices.push( parseFloat( lineValues[ 1 ] ) );
vertices.push( parseFloat( lineValues[ 2 ] ) );
const r = parseFloat( lineValues[ 3 ] ) / 255;
const g = parseFloat( lineValues[ 4 ] ) / 255;
const b = parseFloat( lineValues[ 5 ] ) / 255;
color.set( r, g, b ).convertSRGBToLinear();
colors.push( color.r, color.g, color.b );
}
}
const geometry = new BufferGeometry();
geometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
if ( colors.length > 0 ) {
geometry.setAttribute( 'color', new Float32BufferAttribute( colors, 3 ) );
}
return geometry;
}
}
export { XYZLoader };

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,414 @@
class LWO2Parser {
constructor( IFFParser ) {
this.IFF = IFFParser;
}
parseBlock() {
this.IFF.debugger.offset = this.IFF.reader.offset;
this.IFF.debugger.closeForms();
const blockID = this.IFF.reader.getIDTag();
let length = this.IFF.reader.getUint32(); // size of data in bytes
if ( length > this.IFF.reader.dv.byteLength - this.IFF.reader.offset ) {
this.IFF.reader.offset -= 4;
length = this.IFF.reader.getUint16();
}
this.IFF.debugger.dataOffset = this.IFF.reader.offset;
this.IFF.debugger.length = length;
// Data types may be found in either LWO2 OR LWO3 spec
switch ( blockID ) {
case 'FORM': // form blocks may consist of sub -chunks or sub-forms
this.IFF.parseForm( length );
break;
// SKIPPED CHUNKS
// if break; is called directly, the position in the lwoTree is not created
// any sub chunks and forms are added to the parent form instead
// MISC skipped
case 'ICON': // Thumbnail Icon Image
case 'VMPA': // Vertex Map Parameter
case 'BBOX': // bounding box
// case 'VMMD':
// case 'VTYP':
// normal maps can be specified, normally on models imported from other applications. Currently ignored
case 'NORM':
// ENVL FORM skipped
case 'PRE ':
case 'POST':
case 'KEY ':
case 'SPAN':
// CLIP FORM skipped
case 'TIME':
case 'CLRS':
case 'CLRA':
case 'FILT':
case 'DITH':
case 'CONT':
case 'BRIT':
case 'SATR':
case 'HUE ':
case 'GAMM':
case 'NEGA':
case 'IFLT':
case 'PFLT':
// Image Map Layer skipped
case 'PROJ':
case 'AXIS':
case 'AAST':
case 'PIXB':
case 'AUVO':
case 'STCK':
// Procedural Textures skipped
case 'PROC':
case 'VALU':
case 'FUNC':
// Gradient Textures skipped
case 'PNAM':
case 'INAM':
case 'GRST':
case 'GREN':
case 'GRPT':
case 'FKEY':
case 'IKEY':
// Texture Mapping Form skipped
case 'CSYS':
// Surface CHUNKs skipped
case 'OPAQ': // top level 'opacity' checkbox
case 'CMAP': // clip map
// Surface node CHUNKS skipped
// These mainly specify the node editor setup in LW
case 'NLOC':
case 'NZOM':
case 'NVER':
case 'NSRV':
case 'NVSK': // unknown
case 'NCRD':
case 'WRPW': // image wrap w ( for cylindrical and spherical projections)
case 'WRPH': // image wrap h
case 'NMOD':
case 'NSEL':
case 'NPRW':
case 'NPLA':
case 'NODS':
case 'VERS':
case 'ENUM':
case 'TAG ':
case 'OPAC':
// Car Material CHUNKS
case 'CGMD':
case 'CGTY':
case 'CGST':
case 'CGEN':
case 'CGTS':
case 'CGTE':
case 'OSMP':
case 'OMDE':
case 'OUTR':
case 'FLAG':
case 'TRNL':
case 'GLOW':
case 'GVAL': // glow intensity
case 'SHRP':
case 'RFOP':
case 'RSAN':
case 'TROP':
case 'RBLR':
case 'TBLR':
case 'CLRH':
case 'CLRF':
case 'ADTR':
case 'LINE':
case 'ALPH':
case 'VCOL':
case 'ENAB':
this.IFF.debugger.skipped = true;
this.IFF.reader.skip( length );
break;
case 'SURF':
this.IFF.parseSurfaceLwo2( length );
break;
case 'CLIP':
this.IFF.parseClipLwo2( length );
break;
// Texture node chunks (not in spec)
case 'IPIX': // usePixelBlending
case 'IMIP': // useMipMaps
case 'IMOD': // imageBlendingMode
case 'AMOD': // unknown
case 'IINV': // imageInvertAlpha
case 'INCR': // imageInvertColor
case 'IAXS': // imageAxis ( for non-UV maps)
case 'IFOT': // imageFallofType
case 'ITIM': // timing for animated textures
case 'IWRL':
case 'IUTI':
case 'IINX':
case 'IINY':
case 'IINZ':
case 'IREF': // possibly a VX for reused texture nodes
if ( length === 4 ) this.IFF.currentNode[ blockID ] = this.IFF.reader.getInt32();
else this.IFF.reader.skip( length );
break;
case 'OTAG':
this.IFF.parseObjectTag();
break;
case 'LAYR':
this.IFF.parseLayer( length );
break;
case 'PNTS':
this.IFF.parsePoints( length );
break;
case 'VMAP':
this.IFF.parseVertexMapping( length );
break;
case 'AUVU':
case 'AUVN':
this.IFF.reader.skip( length - 1 );
this.IFF.reader.getVariableLengthIndex(); // VX
break;
case 'POLS':
this.IFF.parsePolygonList( length );
break;
case 'TAGS':
this.IFF.parseTagStrings( length );
break;
case 'PTAG':
this.IFF.parsePolygonTagMapping( length );
break;
case 'VMAD':
this.IFF.parseVertexMapping( length, true );
break;
// Misc CHUNKS
case 'DESC': // Description Line
this.IFF.currentForm.description = this.IFF.reader.getString();
break;
case 'TEXT':
case 'CMNT':
case 'NCOM':
this.IFF.currentForm.comment = this.IFF.reader.getString();
break;
// Envelope Form
case 'NAME':
this.IFF.currentForm.channelName = this.IFF.reader.getString();
break;
// Image Map Layer
case 'WRAP':
this.IFF.currentForm.wrap = { w: this.IFF.reader.getUint16(), h: this.IFF.reader.getUint16() };
break;
case 'IMAG':
const index = this.IFF.reader.getVariableLengthIndex();
this.IFF.currentForm.imageIndex = index;
break;
// Texture Mapping Form
case 'OREF':
this.IFF.currentForm.referenceObject = this.IFF.reader.getString();
break;
case 'ROID':
this.IFF.currentForm.referenceObjectID = this.IFF.reader.getUint32();
break;
// Surface Blocks
case 'SSHN':
this.IFF.currentSurface.surfaceShaderName = this.IFF.reader.getString();
break;
case 'AOVN':
this.IFF.currentSurface.surfaceCustomAOVName = this.IFF.reader.getString();
break;
// Nodal Blocks
case 'NSTA':
this.IFF.currentForm.disabled = this.IFF.reader.getUint16();
break;
case 'NRNM':
this.IFF.currentForm.realName = this.IFF.reader.getString();
break;
case 'NNME':
this.IFF.currentForm.refName = this.IFF.reader.getString();
this.IFF.currentSurface.nodes[ this.IFF.currentForm.refName ] = this.IFF.currentForm;
break;
// Nodal Blocks : connections
case 'INME':
if ( ! this.IFF.currentForm.nodeName ) this.IFF.currentForm.nodeName = [];
this.IFF.currentForm.nodeName.push( this.IFF.reader.getString() );
break;
case 'IINN':
if ( ! this.IFF.currentForm.inputNodeName ) this.IFF.currentForm.inputNodeName = [];
this.IFF.currentForm.inputNodeName.push( this.IFF.reader.getString() );
break;
case 'IINM':
if ( ! this.IFF.currentForm.inputName ) this.IFF.currentForm.inputName = [];
this.IFF.currentForm.inputName.push( this.IFF.reader.getString() );
break;
case 'IONM':
if ( ! this.IFF.currentForm.inputOutputName ) this.IFF.currentForm.inputOutputName = [];
this.IFF.currentForm.inputOutputName.push( this.IFF.reader.getString() );
break;
case 'FNAM':
this.IFF.currentForm.fileName = this.IFF.reader.getString();
break;
case 'CHAN': // NOTE: ENVL Forms may also have CHAN chunk, however ENVL is currently ignored
if ( length === 4 ) this.IFF.currentForm.textureChannel = this.IFF.reader.getIDTag();
else this.IFF.reader.skip( length );
break;
// LWO2 Spec chunks: these are needed since the SURF FORMs are often in LWO2 format
case 'SMAN':
const maxSmoothingAngle = this.IFF.reader.getFloat32();
this.IFF.currentSurface.attributes.smooth = ( maxSmoothingAngle < 0 ) ? false : true;
break;
// LWO2: Basic Surface Parameters
case 'COLR':
this.IFF.currentSurface.attributes.Color = { value: this.IFF.reader.getFloat32Array( 3 ) };
this.IFF.reader.skip( 2 ); // VX: envelope
break;
case 'LUMI':
this.IFF.currentSurface.attributes.Luminosity = { value: this.IFF.reader.getFloat32() };
this.IFF.reader.skip( 2 );
break;
case 'SPEC':
this.IFF.currentSurface.attributes.Specular = { value: this.IFF.reader.getFloat32() };
this.IFF.reader.skip( 2 );
break;
case 'DIFF':
this.IFF.currentSurface.attributes.Diffuse = { value: this.IFF.reader.getFloat32() };
this.IFF.reader.skip( 2 );
break;
case 'REFL':
this.IFF.currentSurface.attributes.Reflection = { value: this.IFF.reader.getFloat32() };
this.IFF.reader.skip( 2 );
break;
case 'GLOS':
this.IFF.currentSurface.attributes.Glossiness = { value: this.IFF.reader.getFloat32() };
this.IFF.reader.skip( 2 );
break;
case 'TRAN':
this.IFF.currentSurface.attributes.opacity = this.IFF.reader.getFloat32();
this.IFF.reader.skip( 2 );
break;
case 'BUMP':
this.IFF.currentSurface.attributes.bumpStrength = this.IFF.reader.getFloat32();
this.IFF.reader.skip( 2 );
break;
case 'SIDE':
this.IFF.currentSurface.attributes.side = this.IFF.reader.getUint16();
break;
case 'RIMG':
this.IFF.currentSurface.attributes.reflectionMap = this.IFF.reader.getVariableLengthIndex();
break;
case 'RIND':
this.IFF.currentSurface.attributes.refractiveIndex = this.IFF.reader.getFloat32();
this.IFF.reader.skip( 2 );
break;
case 'TIMG':
this.IFF.currentSurface.attributes.refractionMap = this.IFF.reader.getVariableLengthIndex();
break;
case 'IMAP':
this.IFF.reader.skip( 2 );
break;
case 'TMAP':
this.IFF.debugger.skipped = true;
this.IFF.reader.skip( length ); // needs implementing
break;
case 'IUVI': // uv channel name
this.IFF.currentNode.UVChannel = this.IFF.reader.getString( length );
break;
case 'IUTL': // widthWrappingMode: 0 = Reset, 1 = Repeat, 2 = Mirror, 3 = Edge
this.IFF.currentNode.widthWrappingMode = this.IFF.reader.getUint32();
break;
case 'IVTL': // heightWrappingMode
this.IFF.currentNode.heightWrappingMode = this.IFF.reader.getUint32();
break;
// LWO2 USE
case 'BLOK':
// skip
break;
default:
this.IFF.parseUnknownCHUNK( blockID, length );
}
if ( blockID != 'FORM' ) {
this.IFF.debugger.node = 1;
this.IFF.debugger.nodeID = blockID;
this.IFF.debugger.log();
}
if ( this.IFF.reader.offset >= this.IFF.currentFormEnd ) {
this.IFF.currentForm = this.IFF.parentForm;
}
}
}
export { LWO2Parser };

View File

@ -0,0 +1,373 @@
class LWO3Parser {
constructor( IFFParser ) {
this.IFF = IFFParser;
}
parseBlock() {
this.IFF.debugger.offset = this.IFF.reader.offset;
this.IFF.debugger.closeForms();
const blockID = this.IFF.reader.getIDTag();
const length = this.IFF.reader.getUint32(); // size of data in bytes
this.IFF.debugger.dataOffset = this.IFF.reader.offset;
this.IFF.debugger.length = length;
// Data types may be found in either LWO2 OR LWO3 spec
switch ( blockID ) {
case 'FORM': // form blocks may consist of sub -chunks or sub-forms
this.IFF.parseForm( length );
break;
// SKIPPED CHUNKS
// MISC skipped
case 'ICON': // Thumbnail Icon Image
case 'VMPA': // Vertex Map Parameter
case 'BBOX': // bounding box
// case 'VMMD':
// case 'VTYP':
// normal maps can be specified, normally on models imported from other applications. Currently ignored
case 'NORM':
// ENVL FORM skipped
case 'PRE ': // Pre-loop behavior for the keyframe
case 'POST': // Post-loop behavior for the keyframe
case 'KEY ':
case 'SPAN':
// CLIP FORM skipped
case 'TIME':
case 'CLRS':
case 'CLRA':
case 'FILT':
case 'DITH':
case 'CONT':
case 'BRIT':
case 'SATR':
case 'HUE ':
case 'GAMM':
case 'NEGA':
case 'IFLT':
case 'PFLT':
// Image Map Layer skipped
case 'PROJ':
case 'AXIS':
case 'AAST':
case 'PIXB':
case 'STCK':
// Procedural Textures skipped
case 'VALU':
// Gradient Textures skipped
case 'PNAM':
case 'INAM':
case 'GRST':
case 'GREN':
case 'GRPT':
case 'FKEY':
case 'IKEY':
// Texture Mapping Form skipped
case 'CSYS':
// Surface CHUNKs skipped
case 'OPAQ': // top level 'opacity' checkbox
case 'CMAP': // clip map
// Surface node CHUNKS skipped
// These mainly specify the node editor setup in LW
case 'NLOC':
case 'NZOM':
case 'NVER':
case 'NSRV':
case 'NCRD':
case 'NMOD':
case 'NSEL':
case 'NPRW':
case 'NPLA':
case 'VERS':
case 'ENUM':
case 'TAG ':
// Car Material CHUNKS
case 'CGMD':
case 'CGTY':
case 'CGST':
case 'CGEN':
case 'CGTS':
case 'CGTE':
case 'OSMP':
case 'OMDE':
case 'OUTR':
case 'FLAG':
case 'TRNL':
case 'SHRP':
case 'RFOP':
case 'RSAN':
case 'TROP':
case 'RBLR':
case 'TBLR':
case 'CLRH':
case 'CLRF':
case 'ADTR':
case 'GLOW':
case 'LINE':
case 'ALPH':
case 'VCOL':
case 'ENAB':
this.IFF.debugger.skipped = true;
this.IFF.reader.skip( length );
break;
// Texture node chunks (not in spec)
case 'IPIX': // usePixelBlending
case 'IMIP': // useMipMaps
case 'IMOD': // imageBlendingMode
case 'AMOD': // unknown
case 'IINV': // imageInvertAlpha
case 'INCR': // imageInvertColor
case 'IAXS': // imageAxis ( for non-UV maps)
case 'IFOT': // imageFallofType
case 'ITIM': // timing for animated textures
case 'IWRL':
case 'IUTI':
case 'IINX':
case 'IINY':
case 'IINZ':
case 'IREF': // possibly a VX for reused texture nodes
if ( length === 4 ) this.IFF.currentNode[ blockID ] = this.IFF.reader.getInt32();
else this.IFF.reader.skip( length );
break;
case 'OTAG':
this.IFF.parseObjectTag();
break;
case 'LAYR':
this.IFF.parseLayer( length );
break;
case 'PNTS':
this.IFF.parsePoints( length );
break;
case 'VMAP':
this.IFF.parseVertexMapping( length );
break;
case 'POLS':
this.IFF.parsePolygonList( length );
break;
case 'TAGS':
this.IFF.parseTagStrings( length );
break;
case 'PTAG':
this.IFF.parsePolygonTagMapping( length );
break;
case 'VMAD':
this.IFF.parseVertexMapping( length, true );
break;
// Misc CHUNKS
case 'DESC': // Description Line
this.IFF.currentForm.description = this.IFF.reader.getString();
break;
case 'TEXT':
case 'CMNT':
case 'NCOM':
this.IFF.currentForm.comment = this.IFF.reader.getString();
break;
// Envelope Form
case 'NAME':
this.IFF.currentForm.channelName = this.IFF.reader.getString();
break;
// Image Map Layer
case 'WRAP':
this.IFF.currentForm.wrap = { w: this.IFF.reader.getUint16(), h: this.IFF.reader.getUint16() };
break;
case 'IMAG':
const index = this.IFF.reader.getVariableLengthIndex();
this.IFF.currentForm.imageIndex = index;
break;
// Texture Mapping Form
case 'OREF':
this.IFF.currentForm.referenceObject = this.IFF.reader.getString();
break;
case 'ROID':
this.IFF.currentForm.referenceObjectID = this.IFF.reader.getUint32();
break;
// Surface Blocks
case 'SSHN':
this.IFF.currentSurface.surfaceShaderName = this.IFF.reader.getString();
break;
case 'AOVN':
this.IFF.currentSurface.surfaceCustomAOVName = this.IFF.reader.getString();
break;
// Nodal Blocks
case 'NSTA':
this.IFF.currentForm.disabled = this.IFF.reader.getUint16();
break;
case 'NRNM':
this.IFF.currentForm.realName = this.IFF.reader.getString();
break;
case 'NNME':
this.IFF.currentForm.refName = this.IFF.reader.getString();
this.IFF.currentSurface.nodes[ this.IFF.currentForm.refName ] = this.IFF.currentForm;
break;
// Nodal Blocks : connections
case 'INME':
if ( ! this.IFF.currentForm.nodeName ) this.IFF.currentForm.nodeName = [];
this.IFF.currentForm.nodeName.push( this.IFF.reader.getString() );
break;
case 'IINN':
if ( ! this.IFF.currentForm.inputNodeName ) this.IFF.currentForm.inputNodeName = [];
this.IFF.currentForm.inputNodeName.push( this.IFF.reader.getString() );
break;
case 'IINM':
if ( ! this.IFF.currentForm.inputName ) this.IFF.currentForm.inputName = [];
this.IFF.currentForm.inputName.push( this.IFF.reader.getString() );
break;
case 'IONM':
if ( ! this.IFF.currentForm.inputOutputName ) this.IFF.currentForm.inputOutputName = [];
this.IFF.currentForm.inputOutputName.push( this.IFF.reader.getString() );
break;
case 'FNAM':
this.IFF.currentForm.fileName = this.IFF.reader.getString();
break;
case 'CHAN': // NOTE: ENVL Forms may also have CHAN chunk, however ENVL is currently ignored
if ( length === 4 ) this.IFF.currentForm.textureChannel = this.IFF.reader.getIDTag();
else this.IFF.reader.skip( length );
break;
// LWO2 Spec chunks: these are needed since the SURF FORMs are often in LWO2 format
case 'SMAN':
const maxSmoothingAngle = this.IFF.reader.getFloat32();
this.IFF.currentSurface.attributes.smooth = ( maxSmoothingAngle < 0 ) ? false : true;
break;
// LWO2: Basic Surface Parameters
case 'COLR':
this.IFF.currentSurface.attributes.Color = { value: this.IFF.reader.getFloat32Array( 3 ) };
this.IFF.reader.skip( 2 ); // VX: envelope
break;
case 'LUMI':
this.IFF.currentSurface.attributes.Luminosity = { value: this.IFF.reader.getFloat32() };
this.IFF.reader.skip( 2 );
break;
case 'SPEC':
this.IFF.currentSurface.attributes.Specular = { value: this.IFF.reader.getFloat32() };
this.IFF.reader.skip( 2 );
break;
case 'DIFF':
this.IFF.currentSurface.attributes.Diffuse = { value: this.IFF.reader.getFloat32() };
this.IFF.reader.skip( 2 );
break;
case 'REFL':
this.IFF.currentSurface.attributes.Reflection = { value: this.IFF.reader.getFloat32() };
this.IFF.reader.skip( 2 );
break;
case 'GLOS':
this.IFF.currentSurface.attributes.Glossiness = { value: this.IFF.reader.getFloat32() };
this.IFF.reader.skip( 2 );
break;
case 'TRAN':
this.IFF.currentSurface.attributes.opacity = this.IFF.reader.getFloat32();
this.IFF.reader.skip( 2 );
break;
case 'BUMP':
this.IFF.currentSurface.attributes.bumpStrength = this.IFF.reader.getFloat32();
this.IFF.reader.skip( 2 );
break;
case 'SIDE':
this.IFF.currentSurface.attributes.side = this.IFF.reader.getUint16();
break;
case 'RIMG':
this.IFF.currentSurface.attributes.reflectionMap = this.IFF.reader.getVariableLengthIndex();
break;
case 'RIND':
this.IFF.currentSurface.attributes.refractiveIndex = this.IFF.reader.getFloat32();
this.IFF.reader.skip( 2 );
break;
case 'TIMG':
this.IFF.currentSurface.attributes.refractionMap = this.IFF.reader.getVariableLengthIndex();
break;
case 'IMAP':
this.IFF.currentSurface.attributes.imageMapIndex = this.IFF.reader.getUint32();
break;
case 'IUVI': // uv channel name
this.IFF.currentNode.UVChannel = this.IFF.reader.getString( length );
break;
case 'IUTL': // widthWrappingMode: 0 = Reset, 1 = Repeat, 2 = Mirror, 3 = Edge
this.IFF.currentNode.widthWrappingMode = this.IFF.reader.getUint32();
break;
case 'IVTL': // heightWrappingMode
this.IFF.currentNode.heightWrappingMode = this.IFF.reader.getUint32();
break;
default:
this.IFF.parseUnknownCHUNK( blockID, length );
}
if ( blockID != 'FORM' ) {
this.IFF.debugger.node = 1;
this.IFF.debugger.nodeID = blockID;
this.IFF.debugger.log();
}
if ( this.IFF.reader.offset >= this.IFF.currentFormEnd ) {
this.IFF.currentForm = this.IFF.parentForm;
}
}
}
export { LWO3Parser };