You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
296 lines
7.8 KiB
JavaScript
296 lines
7.8 KiB
JavaScript
2 months ago
|
import * as THREE from 'three';
|
||
|
import { threejsLessonUtils } from './threejs-lesson-utils.js';
|
||
|
|
||
|
{
|
||
|
|
||
|
const loader = new THREE.TextureLoader();
|
||
|
|
||
|
function loadTextureAndPromise( url ) {
|
||
|
|
||
|
let textureResolve;
|
||
|
const promise = new Promise( ( resolve ) => {
|
||
|
|
||
|
textureResolve = resolve;
|
||
|
|
||
|
} );
|
||
|
const texture = loader.load( url, ( texture ) => {
|
||
|
|
||
|
textureResolve( texture );
|
||
|
|
||
|
} );
|
||
|
return {
|
||
|
texture,
|
||
|
promise,
|
||
|
};
|
||
|
|
||
|
}
|
||
|
|
||
|
const filterTextureInfo = loadTextureAndPromise( '/manual/resources/images/mip-example.png' );
|
||
|
const filterTexture = filterTextureInfo.texture;
|
||
|
const filterTexturePromise = filterTextureInfo.promise;
|
||
|
|
||
|
function filterCube( scale, texture ) {
|
||
|
|
||
|
const size = 8;
|
||
|
const geometry = new THREE.BoxGeometry( size, size, size );
|
||
|
const material = new THREE.MeshBasicMaterial( {
|
||
|
map: texture || filterTexture,
|
||
|
} );
|
||
|
const mesh = new THREE.Mesh( geometry, material );
|
||
|
mesh.scale.set( scale, scale, scale );
|
||
|
return mesh;
|
||
|
|
||
|
}
|
||
|
|
||
|
function lowResCube( scale, pixelSize = 16 ) {
|
||
|
|
||
|
const mesh = filterCube( scale );
|
||
|
const renderTarget = new THREE.WebGLRenderTarget( 1, 1, {
|
||
|
magFilter: THREE.NearestFilter,
|
||
|
minFilter: THREE.NearestFilter,
|
||
|
} );
|
||
|
|
||
|
const planeScene = new THREE.Scene();
|
||
|
|
||
|
const plane = new THREE.PlaneGeometry( 1, 1 );
|
||
|
const planeMaterial = new THREE.MeshBasicMaterial( {
|
||
|
map: renderTarget.texture,
|
||
|
} );
|
||
|
const planeMesh = new THREE.Mesh( plane, planeMaterial );
|
||
|
planeScene.add( planeMesh );
|
||
|
|
||
|
const planeCamera = new THREE.OrthographicCamera( 0, 1, 0, 1, - 1, 1 );
|
||
|
planeCamera.position.z = 1;
|
||
|
|
||
|
return {
|
||
|
obj3D: mesh,
|
||
|
update( time, renderInfo ) {
|
||
|
|
||
|
const { width, height, scene, camera, renderer, pixelRatio } = renderInfo;
|
||
|
const rtWidth = Math.ceil( width / pixelRatio / pixelSize );
|
||
|
const rtHeight = Math.ceil( height / pixelRatio / pixelSize );
|
||
|
renderTarget.setSize( rtWidth, rtHeight );
|
||
|
|
||
|
camera.aspect = rtWidth / rtHeight;
|
||
|
camera.updateProjectionMatrix();
|
||
|
|
||
|
renderer.setRenderTarget( renderTarget );
|
||
|
|
||
|
renderer.render( scene, camera );
|
||
|
renderer.setRenderTarget( null );
|
||
|
|
||
|
},
|
||
|
render( renderInfo ) {
|
||
|
|
||
|
const { width, height, renderer, pixelRatio } = renderInfo;
|
||
|
const viewWidth = width / pixelRatio / pixelSize;
|
||
|
const viewHeight = height / pixelRatio / pixelSize;
|
||
|
planeCamera.left = - viewWidth / 2;
|
||
|
planeCamera.right = viewWidth / 2;
|
||
|
planeCamera.top = viewHeight / 2;
|
||
|
planeCamera.bottom = - viewHeight / 2;
|
||
|
planeCamera.updateProjectionMatrix();
|
||
|
|
||
|
// compute the difference between our renderTarget size
|
||
|
// and the view size. The renderTarget is a multiple pixels magnified pixels
|
||
|
// so for example if the view is 15 pixels wide and the magnified pixel size is 10
|
||
|
// the renderTarget will be 20 pixels wide. We only want to display 15 of those 20
|
||
|
// pixels so
|
||
|
|
||
|
planeMesh.scale.set( renderTarget.width, renderTarget.height, 1 );
|
||
|
|
||
|
renderer.render( planeScene, planeCamera );
|
||
|
|
||
|
},
|
||
|
};
|
||
|
|
||
|
}
|
||
|
|
||
|
function createMip( level, numLevels, scale ) {
|
||
|
|
||
|
const u = level / numLevels;
|
||
|
const size = 2 ** ( numLevels - level - 1 );
|
||
|
const halfSize = Math.ceil( size / 2 );
|
||
|
const ctx = document.createElement( 'canvas' ).getContext( '2d' );
|
||
|
ctx.canvas.width = size * scale;
|
||
|
ctx.canvas.height = size * scale;
|
||
|
ctx.scale( scale, scale );
|
||
|
ctx.fillStyle = `hsl(${180 + u * 360 | 0},100%,20%)`;
|
||
|
ctx.fillRect( 0, 0, size, size );
|
||
|
ctx.fillStyle = `hsl(${u * 360 | 0},100%,50%)`;
|
||
|
ctx.fillRect( 0, 0, halfSize, halfSize );
|
||
|
ctx.fillRect( halfSize, halfSize, halfSize, halfSize );
|
||
|
return ctx.canvas;
|
||
|
|
||
|
}
|
||
|
|
||
|
threejsLessonUtils.init( {
|
||
|
threejsOptions: { antialias: false },
|
||
|
} );
|
||
|
threejsLessonUtils.addDiagrams( {
|
||
|
filterCube: {
|
||
|
create() {
|
||
|
|
||
|
return filterCube( 1 );
|
||
|
|
||
|
},
|
||
|
},
|
||
|
filterCubeSmall: {
|
||
|
create( info ) {
|
||
|
|
||
|
return lowResCube( .1, info.renderInfo.pixelRatio );
|
||
|
|
||
|
},
|
||
|
},
|
||
|
filterCubeSmallLowRes: {
|
||
|
create() {
|
||
|
|
||
|
return lowResCube( 1 );
|
||
|
|
||
|
},
|
||
|
},
|
||
|
filterCubeMagNearest: {
|
||
|
async create() {
|
||
|
|
||
|
const texture = await filterTexturePromise;
|
||
|
const newTexture = texture.clone();
|
||
|
newTexture.magFilter = THREE.NearestFilter;
|
||
|
newTexture.needsUpdate = true;
|
||
|
return filterCube( 1, newTexture );
|
||
|
|
||
|
},
|
||
|
},
|
||
|
filterCubeMagLinear: {
|
||
|
async create() {
|
||
|
|
||
|
const texture = await filterTexturePromise;
|
||
|
const newTexture = texture.clone();
|
||
|
newTexture.magFilter = THREE.LinearFilter;
|
||
|
newTexture.needsUpdate = true;
|
||
|
return filterCube( 1, newTexture );
|
||
|
|
||
|
},
|
||
|
},
|
||
|
filterModes: {
|
||
|
async create( props ) {
|
||
|
|
||
|
const { scene, camera, renderInfo } = props;
|
||
|
scene.background = new THREE.Color( 'black' );
|
||
|
camera.far = 150;
|
||
|
const texture = await filterTexturePromise;
|
||
|
const root = new THREE.Object3D();
|
||
|
const depth = 50;
|
||
|
const plane = new THREE.PlaneGeometry( 1, depth );
|
||
|
const mipmap = [];
|
||
|
const numMips = 7;
|
||
|
for ( let i = 0; i < numMips; ++ i ) {
|
||
|
|
||
|
mipmap.push( createMip( i, numMips, 1 ) );
|
||
|
|
||
|
}
|
||
|
|
||
|
// Is this a design flaw in three.js?
|
||
|
// AFAIK there's no way to clone a texture really
|
||
|
// Textures can share an image and I guess deep down
|
||
|
// if the image is the same they might share a WebGLTexture
|
||
|
// but no checks for mipmaps I'm guessing. It seems like
|
||
|
// they shouldn't be checking for same image, the should be
|
||
|
// checking for same WebGLTexture. Given there is more than
|
||
|
// WebGL to support maybe they need to abstract WebGLTexture to
|
||
|
// PlatformTexture or something?
|
||
|
|
||
|
const meshInfos = [
|
||
|
{ x: - 1, y: 1, minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter },
|
||
|
{ x: 0, y: 1, minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter },
|
||
|
{ x: 1, y: 1, minFilter: THREE.NearestMipmapNearestFilter, magFilter: THREE.LinearFilter },
|
||
|
{ x: - 1, y: - 1, minFilter: THREE.NearestMipmapLinearFilter, magFilter: THREE.LinearFilter },
|
||
|
{ x: 0, y: - 1, minFilter: THREE.LinearMipmapNearestFilter, magFilter: THREE.LinearFilter },
|
||
|
{ x: 1, y: - 1, minFilter: THREE.LinearMipmapLinearFilter, magFilter: THREE.LinearFilter },
|
||
|
].map( ( info ) => {
|
||
|
|
||
|
const copyTexture = texture.clone();
|
||
|
copyTexture.minFilter = info.minFilter;
|
||
|
copyTexture.magFilter = info.magFilter;
|
||
|
copyTexture.wrapT = THREE.RepeatWrapping;
|
||
|
copyTexture.repeat.y = depth;
|
||
|
copyTexture.needsUpdate = true;
|
||
|
|
||
|
const mipTexture = new THREE.CanvasTexture( mipmap[ 0 ] );
|
||
|
mipTexture.mipmaps = mipmap;
|
||
|
mipTexture.minFilter = info.minFilter;
|
||
|
mipTexture.magFilter = info.magFilter;
|
||
|
mipTexture.wrapT = THREE.RepeatWrapping;
|
||
|
mipTexture.repeat.y = depth;
|
||
|
|
||
|
const material = new THREE.MeshBasicMaterial( {
|
||
|
map: copyTexture,
|
||
|
} );
|
||
|
|
||
|
const mesh = new THREE.Mesh( plane, material );
|
||
|
mesh.rotation.x = Math.PI * .5 * info.y;
|
||
|
mesh.position.x = info.x * 1.5;
|
||
|
mesh.position.y = info.y;
|
||
|
root.add( mesh );
|
||
|
return {
|
||
|
material,
|
||
|
copyTexture,
|
||
|
mipTexture,
|
||
|
};
|
||
|
|
||
|
} );
|
||
|
scene.add( root );
|
||
|
|
||
|
renderInfo.elem.addEventListener( 'click', () => {
|
||
|
|
||
|
for ( const meshInfo of meshInfos ) {
|
||
|
|
||
|
const { material, copyTexture, mipTexture } = meshInfo;
|
||
|
material.map = material.map === copyTexture ? mipTexture : copyTexture;
|
||
|
|
||
|
}
|
||
|
|
||
|
} );
|
||
|
|
||
|
return {
|
||
|
update( time, renderInfo ) {
|
||
|
|
||
|
const { camera } = renderInfo;
|
||
|
camera.position.y = Math.sin( time * .2 ) * .5;
|
||
|
|
||
|
},
|
||
|
trackball: false,
|
||
|
};
|
||
|
|
||
|
},
|
||
|
},
|
||
|
} );
|
||
|
|
||
|
const textureDiagrams = {
|
||
|
differentColoredMips( parent ) {
|
||
|
|
||
|
const numMips = 7;
|
||
|
for ( let i = 0; i < numMips; ++ i ) {
|
||
|
|
||
|
const elem = createMip( i, numMips, 4 );
|
||
|
elem.className = 'border';
|
||
|
elem.style.margin = '1px';
|
||
|
parent.appendChild( elem );
|
||
|
|
||
|
}
|
||
|
|
||
|
},
|
||
|
};
|
||
|
|
||
|
function createTextureDiagram( elem ) {
|
||
|
|
||
|
const name = elem.dataset.textureDiagram;
|
||
|
const info = textureDiagrams[ name ];
|
||
|
info( elem );
|
||
|
|
||
|
}
|
||
|
|
||
|
[ ...document.querySelectorAll( '[data-texture-diagram]' ) ].forEach( createTextureDiagram );
|
||
|
|
||
|
}
|
||
|
|