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