threejs-textures.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. import * as THREE from 'three';
  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('/manual/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 = `hsl(${180 + u * 360 | 0},100%,20%)`;
  87. ctx.fillRect(0, 0, size, size);
  88. ctx.fillStyle = `hsl(${u * 360 | 0},100%,50%)`;
  89. ctx.fillRect(0, 0, halfSize, halfSize);
  90. ctx.fillRect(halfSize, halfSize, halfSize, halfSize);
  91. return ctx.canvas;
  92. }
  93. threejsLessonUtils.init({
  94. threejsOptions: {antialias: false},
  95. });
  96. threejsLessonUtils.addDiagrams({
  97. filterCube: {
  98. create() {
  99. return filterCube(1);
  100. },
  101. },
  102. filterCubeSmall: {
  103. create(info) {
  104. return lowResCube(.1, info.renderInfo.pixelRatio);
  105. },
  106. },
  107. filterCubeSmallLowRes: {
  108. create() {
  109. return lowResCube(1);
  110. },
  111. },
  112. filterCubeMagNearest: {
  113. async create() {
  114. const texture = await filterTexturePromise;
  115. const newTexture = texture.clone();
  116. newTexture.magFilter = THREE.NearestFilter;
  117. newTexture.needsUpdate = true;
  118. return filterCube(1, newTexture);
  119. },
  120. },
  121. filterCubeMagLinear: {
  122. async create() {
  123. const texture = await filterTexturePromise;
  124. const newTexture = texture.clone();
  125. newTexture.magFilter = THREE.LinearFilter;
  126. newTexture.needsUpdate = true;
  127. return filterCube(1, newTexture);
  128. },
  129. },
  130. filterModes: {
  131. async create(props) {
  132. const { scene, camera, renderInfo } = props;
  133. scene.background = new THREE.Color('black');
  134. camera.far = 150;
  135. const texture = await filterTexturePromise;
  136. const root = new THREE.Object3D();
  137. const depth = 50;
  138. const plane = new THREE.PlaneGeometry(1, depth);
  139. const mipmap = [];
  140. const numMips = 7;
  141. for (let i = 0; i < numMips; ++i) {
  142. mipmap.push(createMip(i, numMips, 1));
  143. }
  144. // Is this a design flaw in three.js?
  145. // AFAIK there's no way to clone a texture really
  146. // Textures can share an image and I guess deep down
  147. // if the image is the same they might share a WebGLTexture
  148. // but no checks for mipmaps I'm guessing. It seems like
  149. // they shouldn't be checking for same image, the should be
  150. // checking for same WebGLTexture. Given there is more than
  151. // WebGL to support maybe they need to abtract WebGLTexture to
  152. // PlatformTexture or something?
  153. const meshInfos = [
  154. { x: -1, y: 1, minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter },
  155. { x: 0, y: 1, minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter },
  156. { x: 1, y: 1, minFilter: THREE.NearestMipmapNearestFilter, magFilter: THREE.LinearFilter },
  157. { x: -1, y: -1, minFilter: THREE.NearestMipmapLinearFilter, magFilter: THREE.LinearFilter },
  158. { x: 0, y: -1, minFilter: THREE.LinearMipmapNearestFilter, magFilter: THREE.LinearFilter },
  159. { x: 1, y: -1, minFilter: THREE.LinearMipmapLinearFilter, magFilter: THREE.LinearFilter },
  160. ].map((info) => {
  161. const copyTexture = texture.clone();
  162. copyTexture.minFilter = info.minFilter;
  163. copyTexture.magFilter = info.magFilter;
  164. copyTexture.wrapT = THREE.RepeatWrapping;
  165. copyTexture.repeat.y = depth;
  166. copyTexture.needsUpdate = true;
  167. const mipTexture = new THREE.CanvasTexture(mipmap[0]);
  168. mipTexture.mipmaps = mipmap;
  169. mipTexture.minFilter = info.minFilter;
  170. mipTexture.magFilter = info.magFilter;
  171. mipTexture.wrapT = THREE.RepeatWrapping;
  172. mipTexture.repeat.y = depth;
  173. const material = new THREE.MeshBasicMaterial({
  174. map: copyTexture,
  175. });
  176. const mesh = new THREE.Mesh(plane, material);
  177. mesh.rotation.x = Math.PI * .5 * info.y;
  178. mesh.position.x = info.x * 1.5;
  179. mesh.position.y = info.y;
  180. root.add(mesh);
  181. return {
  182. material,
  183. copyTexture,
  184. mipTexture,
  185. };
  186. });
  187. scene.add(root);
  188. renderInfo.elem.addEventListener('click', () => {
  189. for (const meshInfo of meshInfos) {
  190. const { material, copyTexture, mipTexture } = meshInfo;
  191. material.map = material.map === copyTexture ? mipTexture : copyTexture;
  192. }
  193. });
  194. return {
  195. update(time, renderInfo) {
  196. const {camera} = renderInfo;
  197. camera.position.y = Math.sin(time * .2) * .5;
  198. },
  199. trackball: false,
  200. };
  201. },
  202. },
  203. });
  204. const textureDiagrams = {
  205. differentColoredMips(parent) {
  206. const numMips = 7;
  207. for (let i = 0; i < numMips; ++i) {
  208. const elem = createMip(i, numMips, 4);
  209. elem.className = 'border';
  210. elem.style.margin = '1px';
  211. parent.appendChild(elem);
  212. }
  213. },
  214. };
  215. function createTextureDiagram(elem) {
  216. const name = elem.dataset.textureDiagram;
  217. const info = textureDiagrams[name];
  218. info(elem);
  219. }
  220. [...document.querySelectorAll('[data-texture-diagram]')].forEach(createTextureDiagram);
  221. }