threejs-textures.js 8.4 KB

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