Dies ist das Repository meines kleinen Portfolios.
Im Hintergrund läuft eine Planetensimulation, geschrieben in JavaScript und Three.js.
Die zu sehenden Texturen stammen von:
https://www.solarsystemscope.com/textures/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
587 lines
13 KiB
587 lines
13 KiB
import { |
|
BufferAttribute, |
|
BufferGeometry, |
|
FileLoader, |
|
Loader |
|
} 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 ) => { |
|
|
|
const taskConfig = { |
|
attributeIDs: this.defaultAttributeIDs, |
|
attributeTypes: this.defaultAttributeTypes, |
|
useUniqueIDs: false |
|
}; |
|
|
|
this.decodeGeometry( buffer, taskConfig ) |
|
.then( onLoad ) |
|
.catch( onError ); |
|
|
|
}, onProgress, onError ); |
|
|
|
} |
|
|
|
/** @deprecated Kept for backward-compatibility with previous DRACOLoader versions. */ |
|
decodeDracoFile( buffer, callback, attributeIDs, attributeTypes ) { |
|
|
|
const taskConfig = { |
|
attributeIDs: attributeIDs || this.defaultAttributeIDs, |
|
attributeTypes: attributeTypes || this.defaultAttributeTypes, |
|
useUniqueIDs: !! attributeIDs |
|
}; |
|
|
|
this.decodeGeometry( buffer, taskConfig ).then( callback ); |
|
|
|
} |
|
|
|
decodeGeometry( buffer, taskConfig ) { |
|
|
|
// TODO: For backward-compatibility, support 'attributeTypes' objects containing |
|
// references (rather than names) to typed array constructors. These must be |
|
// serialized before sending them to the worker. |
|
for ( const attribute in taskConfig.attributeTypes ) { |
|
|
|
const type = taskConfig.attributeTypes[ attribute ]; |
|
|
|
if ( type.BYTES_PER_ELEMENT !== undefined ) { |
|
|
|
taskConfig.attributeTypes[ attribute ] = type.name; |
|
|
|
} |
|
|
|
} |
|
|
|
// |
|
|
|
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 attribute = geometryData.attributes[ i ]; |
|
const name = attribute.name; |
|
const array = attribute.array; |
|
const itemSize = attribute.itemSize; |
|
|
|
geometry.setAttribute( name, new BufferAttribute( array, itemSize ) ); |
|
|
|
} |
|
|
|
return geometry; |
|
|
|
} |
|
|
|
_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; |
|
|
|
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(); |
|
const decoderBuffer = new draco.DecoderBuffer(); |
|
decoderBuffer.Init( new Int8Array( buffer ), buffer.byteLength ); |
|
|
|
try { |
|
|
|
const geometry = decodeGeometry( draco, decoder, decoderBuffer, 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( decoderBuffer ); |
|
draco.destroy( decoder ); |
|
|
|
} |
|
|
|
} ); |
|
break; |
|
|
|
} |
|
|
|
}; |
|
|
|
function decodeGeometry( draco, decoder, decoderBuffer, taskConfig ) { |
|
|
|
const attributeIDs = taskConfig.attributeIDs; |
|
const attributeTypes = taskConfig.attributeTypes; |
|
|
|
let dracoGeometry; |
|
let decodingStatus; |
|
|
|
const geometryType = decoder.GetEncodedGeometryType( decoderBuffer ); |
|
|
|
if ( geometryType === draco.TRIANGULAR_MESH ) { |
|
|
|
dracoGeometry = new draco.Mesh(); |
|
decodingStatus = decoder.DecodeBufferToMesh( decoderBuffer, dracoGeometry ); |
|
|
|
} else if ( geometryType === draco.POINT_CLOUD ) { |
|
|
|
dracoGeometry = new draco.PointCloud(); |
|
decodingStatus = decoder.DecodeBufferToPointCloud( decoderBuffer, 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 ); |
|
|
|
} |
|
|
|
geometry.attributes.push( decodeAttribute( draco, decoder, dracoGeometry, attributeName, attributeType, attribute ) ); |
|
|
|
} |
|
|
|
// 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 };
|
|
|