threejs-textures.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. import * as THREE from '../../resources/threejs/r127/build/three.module.js';
  2. import {threejsLessonUtils} from './threejs-lesson-utils.js';
  3. {
  4. const loader = new THREE.TextureLoader();
  5. function loadTextureAndPromise(url) {
  6. let textureResolve;
  7. const promise = new Promise((resolve) => {
  8. textureResolve = resolve;
  9. });
  10. const texture = loader.load(url, (texture) => {
  11. textureResolve(texture);
  12. });
  13. return {
  14. texture,
  15. promise,
  16. };
  17. }
  18. const filterTextureInfo = loadTextureAndPromise('/threejs/lessons/resources/images/mip-example.png');
  19. const filterTexture = filterTextureInfo.texture;
  20. const filterTexturePromise = filterTextureInfo.promise;
  21. function filterCube(scale, texture) {
  22. const size = 8;
  23. const geometry = new THREE.BoxGeometry(size, size, size);
  24. const material = new THREE.MeshBasicMaterial({
  25. map: texture || filterTexture,
  26. });
  27. const mesh = new THREE.Mesh(geometry, material);
  28. mesh.scale.set(scale, scale, scale);
  29. return mesh;
  30. }
  31. function lowResCube(scale, pixelSize = 16) {
  32. const mesh = filterCube(scale);
  33. const renderTarget = new THREE.WebGLRenderTarget(1, 1, {
  34. magFilter: THREE.NearestFilter,
  35. minFilter: THREE.NearestFilter,
  36. });
  37. const planeScene = new THREE.Scene();
  38. const plane = new THREE.PlaneGeometry(1, 1);
  39. const planeMaterial = new THREE.MeshBasicMaterial({
  40. map: renderTarget.texture,
  41. });
  42. const planeMesh = new THREE.Mesh(plane, planeMaterial);
  43. planeScene.add(planeMesh);
  44. const planeCamera = new THREE.OrthographicCamera(0, 1, 0, 1, -1, 1);
  45. planeCamera.position.z = 1;
  46. return {
  47. obj3D: mesh,
  48. update(time, renderInfo) {
  49. const { width, height, scene, camera, renderer, pixelRatio } = renderInfo;
  50. const rtWidth = Math.ceil(width / pixelRatio / pixelSize);
  51. const rtHeight = Math.ceil(height / pixelRatio / pixelSize);
  52. renderTarget.setSize(rtWidth, rtHeight);
  53. camera.aspect = rtWidth / rtHeight;
  54. camera.updateProjectionMatrix();
  55. renderer.setRenderTarget(renderTarget);
  56. renderer.render(scene, camera);
  57. renderer.setRenderTarget(null);
  58. },
  59. render(renderInfo) {
  60. const { width, height, renderer, pixelRatio } = renderInfo;
  61. const viewWidth = width / pixelRatio / pixelSize;
  62. const viewHeight = height / pixelRatio / pixelSize;
  63. planeCamera.left = -viewWidth / 2;
  64. planeCamera.right = viewWidth / 2;
  65. planeCamera.top = viewHeight / 2;
  66. planeCamera.bottom = -viewHeight / 2;
  67. planeCamera.updateProjectionMatrix();
  68. // compute the difference between our renderTarget size
  69. // and the view size. The renderTarget is a multiple pixels magnified pixels
  70. // so for example if the view is 15 pixels wide and the magnified pixel size is 10
  71. // the renderTarget will be 20 pixels wide. We only want to display 15 of those 20
  72. // pixels so
  73. planeMesh.scale.set(renderTarget.width, renderTarget.height, 1);
  74. renderer.render(planeScene, planeCamera);
  75. },
  76. };
  77. }
  78. function createMip(level, numLevels, scale) {
  79. const u = level / numLevels;
  80. const size = 2 ** (numLevels - level - 1);
  81. const halfSize = Math.ceil(size / 2);
  82. const ctx = document.createElement('canvas').getContext('2d');
  83. ctx.canvas.width = size * scale;
  84. ctx.canvas.height = size * scale;
  85. ctx.scale(scale, scale);
  86. ctx.fillStyle = level & 1 ? '#DDD' : '#000';
  87. ctx.fillStyle = `hsl(${180 + u * 360 | 0},100%,20%)`;
  88. ctx.fillRect(0, 0, size, size);
  89. ctx.fillStyle = `hsl(${u * 360 | 0},100%,50%)`;
  90. ctx.fillRect(0, 0, halfSize, halfSize);
  91. ctx.fillRect(halfSize, halfSize, halfSize, halfSize);
  92. return ctx.canvas;
  93. }
  94. threejsLessonUtils.addDiagrams({
  95. filterCube: {
  96. create() {
  97. return filterCube(1);
  98. },
  99. },
  100. filterCubeSmall: {
  101. create(info) {
  102. return lowResCube(.1, info.renderInfo.pixelRatio);
  103. },
  104. },
  105. filterCubeSmallLowRes: {
  106. create() {
  107. return lowResCube(1);
  108. },
  109. },
  110. filterCubeMagNearest: {
  111. async create() {
  112. const texture = await filterTexturePromise;
  113. const newTexture = texture.clone();
  114. newTexture.magFilter = THREE.NearestFilter;
  115. newTexture.needsUpdate = true;
  116. return filterCube(1, newTexture);
  117. },
  118. },
  119. filterCubeMagLinear: {
  120. async create() {
  121. const texture = await filterTexturePromise;
  122. const newTexture = texture.clone();
  123. newTexture.magFilter = THREE.LinearFilter;
  124. newTexture.needsUpdate = true;
  125. return filterCube(1, newTexture);
  126. },
  127. },
  128. filterModes: {
  129. async create(props) {
  130. const { scene, camera, renderInfo } = props;
  131. scene.background = new THREE.Color('black');
  132. camera.far = 150;
  133. const texture = await filterTexturePromise;
  134. const root = new THREE.Object3D();
  135. const depth = 50;
  136. const plane = new THREE.PlaneGeometry(1, depth);
  137. const mipmap = [];
  138. const numMips = 7;
  139. for (let i = 0; i < numMips; ++i) {
  140. mipmap.push(createMip(i, numMips, 1));
  141. }
  142. // Is this a design flaw in three.js?
  143. // AFAIK there's no way to clone a texture really
  144. // Textures can share an image and I guess deep down
  145. // if the image is the same they might share a WebGLTexture
  146. // but no checks for mipmaps I'm guessing. It seems like
  147. // they shouldn't be checking for same image, the should be
  148. // checking for same WebGLTexture. Given there is more than
  149. // WebGL to support maybe they need to abtract WebGLTexture to
  150. // PlatformTexture or something?
  151. const meshInfos = [
  152. { x: -1, y: 1, minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter },
  153. { x: 0, y: 1, minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter },
  154. { x: 1, y: 1, minFilter: THREE.NearestMipmapNearestFilter, magFilter: THREE.LinearFilter },
  155. { x: -1, y: -1, minFilter: THREE.NearestMipmapLinearFilter, magFilter: THREE.LinearFilter },
  156. { x: 0, y: -1, minFilter: THREE.LinearMipmapNearestFilter, magFilter: THREE.LinearFilter },
  157. { x: 1, y: -1, minFilter: THREE.LinearMipmapLinearFilter, magFilter: THREE.LinearFilter },
  158. ].map((info) => {
  159. const copyTexture = texture.clone();
  160. copyTexture.minFilter = info.minFilter;
  161. copyTexture.magFilter = info.magFilter;
  162. copyTexture.wrapT = THREE.RepeatWrapping;
  163. copyTexture.repeat.y = depth;
  164. copyTexture.needsUpdate = true;
  165. const mipTexture = new THREE.CanvasTexture(mipmap[0]);
  166. mipTexture.mipmaps = mipmap;
  167. mipTexture.minFilter = info.minFilter;
  168. mipTexture.magFilter = info.magFilter;
  169. mipTexture.wrapT = THREE.RepeatWrapping;
  170. mipTexture.repeat.y = depth;
  171. const material = new THREE.MeshBasicMaterial({
  172. map: copyTexture,
  173. });
  174. const mesh = new THREE.Mesh(plane, material);
  175. mesh.rotation.x = Math.PI * .5 * info.y;
  176. mesh.position.x = info.x * 1.5;
  177. mesh.position.y = info.y;
  178. root.add(mesh);
  179. return {
  180. material,
  181. copyTexture,
  182. mipTexture,
  183. };
  184. });
  185. scene.add(root);
  186. renderInfo.elem.addEventListener('click', () => {
  187. for (const meshInfo of meshInfos) {
  188. const { material, copyTexture, mipTexture } = meshInfo;
  189. material.map = material.map === copyTexture ? mipTexture : copyTexture;
  190. }
  191. });
  192. return {
  193. update(time, renderInfo) {
  194. const {camera} = renderInfo;
  195. camera.position.y = Math.sin(time * .2) * .5;
  196. },
  197. trackball: false,
  198. };
  199. },
  200. },
  201. });
  202. const textureDiagrams = {
  203. differentColoredMips(parent) {
  204. const numMips = 7;
  205. for (let i = 0; i < numMips; ++i) {
  206. const elem = createMip(i, numMips, 4);
  207. elem.className = 'border';
  208. elem.style.margin = '1px';
  209. parent.appendChild(elem);
  210. }
  211. },
  212. };
  213. function createTextureDiagram(elem) {
  214. const name = elem.dataset.textureDiagram;
  215. const info = textureDiagrams[name];
  216. info(elem);
  217. }
  218. [...document.querySelectorAll('[data-texture-diagram]')].forEach(createTextureDiagram);
  219. }