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.
 
 
 
 
 

249 lines
8.3 KiB

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