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

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 );
}