Add 3d xbox

This commit is contained in:
Matt Borgerson 2024-06-13 23:44:27 -07:00
parent 6ea4a9d700
commit c32a3f777b
9 changed files with 63599 additions and 13 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

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, onError ).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 one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

BIN
resources/xbox.glb Normal file

Binary file not shown.

View File

@ -13,6 +13,17 @@
margin: -5rem 0 0; /* negative fixed header height */
}
#xbox-scene {
position: relative;
padding-bottom: 100%;
border: none;
}
#xbox-scene canvas {
position: absolute;
max-width: 100%;
height: auto;
}
#logo-canvas {
display: block;
@ -146,9 +157,9 @@
<div class="container">
<div class="row mt-5">
<div class="col-md-6 mt-4 pb-5 pb-md-0 text-center">
<img src="xbox_logo.png" id="xbox-logo" class="img-fluid" width=450 />
<div id="xbox-scene"></div>
</div>
<div class="col-md-6">
<div class="col-md-6 my-auto">
<h1 class="display-3">
<canvas id="logo-canvas" class="gl-logo"></canvas>
<img id="logo-fallback" alt="xemu logo" src="/logo-green-jumbotron.svg" style="height: 1.03em;">
@ -321,20 +332,41 @@ if (platform != undefined) {
}
</script>
<script type="text/javascript" src="three.min.js"></script>
<script type="text/javascript">
<script type="importmap">
{
"imports": {
"three": "./three/three.module.js",
"three/addons/": "./three/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import { ArcballControls } from 'three/addons/controls/ArcballControls.js';
const gltf_loader = new GLTFLoader();
const draco_loader = new DRACOLoader();
draco_loader.setDecoderPath( 'three/loaders/' );
draco_loader.setDecoderConfig( { type: 'js' } );
gltf_loader.setDRACOLoader( draco_loader );
var canvas = document.querySelector('#bg-scene');
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera( 74.5, canvas.offsetWidth/canvas.offsetHeight, 0.1, 1000 );
scene.fog = new THREE.Fog('black', 0.2, 2.45);
scene.background = new THREE.Color('black');
var renderer = new THREE.WebGLRenderer();
var renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize( canvas.offsetWidth, canvas.offsetHeight );
renderer.setPixelRatio( window.devicePixelRatio );
canvas.appendChild( renderer.domElement );
const texture = new THREE.TextureLoader().load("mesh_pattern.svg");
texture.colorSpace = THREE.SRGBColorSpace;
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set( 50, -25 );
@ -343,6 +375,7 @@ const geometry = new THREE.SphereGeometry( 1, 50, 25 );
var material = new THREE.MeshBasicMaterial( { map: texture, side: THREE.BackSide } );
var sphere = new THREE.Mesh( geometry, material );
scene.add( sphere );
camera.position.z = 0.65;
camera.position.x = 0.12;
@ -350,7 +383,8 @@ var clock = new THREE.Clock();
var render = function () {
requestAnimationFrame( render );
var t = clock.getElapsedTime();
sphere.rotation.y = (Math.PI/10) * Math.sin(t * Math.PI / 50);
var s = Math.sin(t * Math.PI / 50);
sphere.rotation.y = (Math.PI/10) * s;
renderer.render(scene, camera);
};
render();
@ -359,12 +393,123 @@ window.addEventListener('resize', function(){
camera.updateProjectionMatrix();
renderer.setSize(canvas.offsetWidth, canvas.offsetHeight);
}, false);
var xbox_canvas = document.querySelector('#xbox-scene');
var xbox_scene = new THREE.Scene();
// xbox_scene.background = new THREE.Color('black');
var xbox_camera = new THREE.PerspectiveCamera( 42.5, xbox_canvas.offsetWidth/xbox_canvas.offsetHeight, 0.1, 1000 );
var xbox_renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
xbox_renderer.setSize( xbox_canvas.offsetWidth, xbox_canvas.offsetHeight );
xbox_renderer.setPixelRatio( window.devicePixelRatio );
xbox_canvas.appendChild( xbox_renderer.domElement );
var xbox = null;
var xbox_scale = 1;
var xbox_sphere_radius = 0.23 * xbox_scale;
var xbox_base_color = new THREE.Color(0xA0B020);
var xbox_bright_color = new THREE.Color(0xD4E830);
var xbox_material = new THREE.MeshStandardMaterial( {
color: xbox_base_color,
transparent: true,
opacity: 0.25,
depthTest: false,
roughness: 0,
});
gltf_loader.load('xbox.glb', function ( gltf ) {
xbox = gltf.scene;
xbox.renderOrder = 10;
xbox.scale.set(xbox_scale, xbox_scale, xbox_scale);
xbox.position.set(0,0.05,0);
xbox.rotation.x = 0.14 * Math.PI;
xbox.rotation.z = -0.02 * Math.PI;
var order = 0;
xbox.traverse((o) => {
if (o.isMesh) {
o.material = xbox_material;
o.renderOrder = order;
order += 1;
}
});
xbox_scene.add(xbox);
});
const xbox_orb_geometry = new THREE.SphereGeometry( xbox_sphere_radius, 50, 25 );
var xbox_orb_material = new THREE.ShaderMaterial( {
// https://jsfiddle.net/8n36c47p/4/
vertexShader: `
varying vec3 vPositionW;
varying vec3 vNormalW;
void main() {
vPositionW = vec3( vec4( position, 1.0 ) * modelMatrix);
vNormalW = normalize( vec3( vec4( normal, 0.0 ) * modelMatrix ) );
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader: `
varying vec3 vPositionW;
varying vec3 vNormalW;
void main() {
vec3 color = vec3(0.5, 1.0, 0.3);
vec3 base = vec3(0.1, 0.6, 0.05);
vec3 viewDirectionW = normalize(cameraPosition - vPositionW);
float f = clamp(pow(1.0 - dot(viewDirectionW, vNormalW), 1.3), 0.0, 1.);
gl_FragColor = vec4(mix(base, color, f), f * 0.5);
}`,
transparent: true,
});
var xbox_orb = new THREE.Mesh( xbox_orb_geometry, xbox_orb_material );
xbox_orb.renderOrder = 0;
xbox_scene.add( xbox_orb );
var light_color = new THREE.Color(1,1,1);
const ambient_light = new THREE.AmbientLight(light_color, 2);
xbox_scene.add(ambient_light);
var point_light = new THREE.PointLight(light_color, 5);
point_light.position.set(-1, -0.5, 0.25);
xbox_scene.add(point_light);
xbox_camera.position.z = 0.65;
var clock = new THREE.Clock();
var xbox_render = function () {
requestAnimationFrame( xbox_render );
var t = clock.getElapsedTime();
var s = Math.sin(t * Math.PI / 50);
if (xbox != null) {
xbox.rotation.y = Math.PI * (0.1 + s / 10);
var new_color = new THREE.Color(xbox_base_color);
new_color.lerp(xbox_bright_color, Math.abs(Math.sin(t * Math.PI / 4 )));
xbox_material.color.set(new_color);
}
xbox_renderer.render(xbox_scene, xbox_camera);
};
xbox_render();
const controls = new ArcballControls( xbox_camera, xbox_renderer.domElement, xbox_scene );
controls.addEventListener( 'change', function () {
xbox_renderer.render( xbox_scene, xbox_camera );
} );
controls.update();
window.addEventListener('resize', function(){
xbox_camera.aspect = xbox_canvas.offsetWidth/xbox_canvas.offsetHeight;
xbox_camera.updateProjectionMatrix();
xbox_renderer.setSize(xbox_canvas.offsetWidth, xbox_canvas.offsetHeight);
}, false);
</script>
<script type="text/javascript" src="gl_logo.js"></script>
<script type="text/javascript">
function updateNavbarTransparency() {
var nav = $(".navbar");
var t = $(document).scrollTop() < $("#xbox-logo").offset().top - nav.height();
var t = $(document).scrollTop() < $("#xbox-scene").offset().top - nav.height();
nav.toggleClass('navbar-transparent', t);
}
$(document).scroll(updateNavbarTransparency);