最新代码

This commit is contained in:
Teo
2025-07-29 11:22:30 +08:00
parent d503e64098
commit b01d143ea6
1490 changed files with 680232 additions and 28 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 };