1
0

threejs-textures.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  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. }