transparency.html 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. <!DOCTYPE html><html lang="en"><head>
  2. <meta charset="utf-8">
  3. <title>Transparency</title>
  4. <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  5. <meta name="twitter:card" content="summary_large_image">
  6. <meta name="twitter:site" content="@threejs">
  7. <meta name="twitter:title" content="Three.js – Transparency">
  8. <meta property="og:image" content="https://threejs.org/files/share.png">
  9. <link rel="shortcut icon" href="../../files/favicon_white.ico" media="(prefers-color-scheme: dark)">
  10. <link rel="shortcut icon" href="../../files/favicon.ico" media="(prefers-color-scheme: light)">
  11. <link rel="stylesheet" href="../resources/lesson.css">
  12. <link rel="stylesheet" href="../resources/lang.css">
  13. <script type="importmap">
  14. {
  15. "imports": {
  16. "three": "../../build/three.module.js"
  17. }
  18. }
  19. </script>
  20. </head>
  21. <body>
  22. <div class="container">
  23. <div class="lesson-title">
  24. <h1>Transparency</h1>
  25. </div>
  26. <div class="lesson">
  27. <div class="lesson-main">
  28. <p>Transparency in three.js is both easy and hard.</p>
  29. <p>First we'll go over the easy part. Let's make a
  30. scene with 8 cubes placed in a 2x2x2 grid.</p>
  31. <p>We'll start with the example from
  32. <a href="rendering-on-demand.html">the article on rendering on demand</a>
  33. which had 3 cubes and modify it to have 8. First
  34. let's change our <code class="notranslate" translate="no">makeInstance</code> function to take
  35. an x, y, and z</p>
  36. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function makeInstance(geometry, color) {
  37. +function makeInstance(geometry, color, x, y, z) {
  38. const material = new THREE.MeshPhongMaterial({color});
  39. const cube = new THREE.Mesh(geometry, material);
  40. scene.add(cube);
  41. - cube.position.x = x;
  42. + cube.position.set(x, y, z);
  43. return cube;
  44. }
  45. </pre>
  46. <p>Then we can create 8 cubes</p>
  47. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+function hsl(h, s, l) {
  48. + return (new THREE.Color()).setHSL(h, s, l);
  49. +}
  50. -makeInstance(geometry, 0x44aa88, 0);
  51. -makeInstance(geometry, 0x8844aa, -2);
  52. -makeInstance(geometry, 0xaa8844, 2);
  53. +{
  54. + const d = 0.8;
  55. + makeInstance(geometry, hsl(0 / 8, 1, .5), -d, -d, -d);
  56. + makeInstance(geometry, hsl(1 / 8, 1, .5), d, -d, -d);
  57. + makeInstance(geometry, hsl(2 / 8, 1, .5), -d, d, -d);
  58. + makeInstance(geometry, hsl(3 / 8, 1, .5), d, d, -d);
  59. + makeInstance(geometry, hsl(4 / 8, 1, .5), -d, -d, d);
  60. + makeInstance(geometry, hsl(5 / 8, 1, .5), d, -d, d);
  61. + makeInstance(geometry, hsl(6 / 8, 1, .5), -d, d, d);
  62. + makeInstance(geometry, hsl(7 / 8, 1, .5), d, d, d);
  63. +}
  64. </pre>
  65. <p>I also adjusted the camera</p>
  66. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const fov = 75;
  67. const aspect = 2; // the canvas default
  68. const near = 0.1;
  69. -const far = 5;
  70. +const far = 25;
  71. const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
  72. -camera.position.z = 4;
  73. +camera.position.z = 2;
  74. </pre>
  75. <p>Set the background to white</p>
  76. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const scene = new THREE.Scene();
  77. +scene.background = new THREE.Color('white');
  78. </pre>
  79. <p>And added a second light so all sides of the cubes get some lighting.</p>
  80. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-{
  81. +function addLight(...pos) {
  82. const color = 0xFFFFFF;
  83. const intensity = 1;
  84. const light = new THREE.DirectionalLight(color, intensity);
  85. - light.position.set(-1, 2, 4);
  86. + light.position.set(...pos);
  87. scene.add(light);
  88. }
  89. +addLight(-1, 2, 4);
  90. +addLight( 1, -1, -2);
  91. </pre>
  92. <p>To make the cubes transparent we just need to set the
  93. <a href="/docs/#api/en/materials/Material#transparent"><code class="notranslate" translate="no">transparent</code></a> flag and to set an
  94. <a href="/docs/#api/en/materials/Material#opacity"><code class="notranslate" translate="no">opacity</code></a> level with 1 being completely opaque
  95. and 0 being completely transparent.</p>
  96. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function makeInstance(geometry, color, x, y, z) {
  97. - const material = new THREE.MeshPhongMaterial({color});
  98. + const material = new THREE.MeshPhongMaterial({
  99. + color,
  100. + opacity: 0.5,
  101. + transparent: true,
  102. + });
  103. const cube = new THREE.Mesh(geometry, material);
  104. scene.add(cube);
  105. cube.position.set(x, y, z);
  106. return cube;
  107. }
  108. </pre>
  109. <p>and with that we get 8 transparent cubes</p>
  110. <p></p><div translate="no" class="threejs_example_container notranslate">
  111. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/transparency.html"></iframe></div>
  112. <a class="threejs_center" href="/manual/examples/transparency.html" target="_blank">click here to open in a separate window</a>
  113. </div>
  114. <p></p>
  115. <p>Drag on the example to rotate the view. </p>
  116. <p>So it seems easy but ... look closer. The cubes are
  117. missing their backs.</p>
  118. <div class="threejs_center"><img src="../resources/images/transparency-cubes-no-backs.png" style="width: 416px;"></div>
  119. <div class="threejs_center">no backs</div>
  120. <p>We learned about the <a href="/docs/#api/en/materials/Material#side"><code class="notranslate" translate="no">side</code></a> material property in
  121. <a href="materials.html">the article on materials</a>.
  122. So, let's set it to <code class="notranslate" translate="no">THREE.DoubleSide</code> to get both sides of each cube to be drawn.</p>
  123. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const material = new THREE.MeshPhongMaterial({
  124. color,
  125. map: loader.load(url),
  126. opacity: 0.5,
  127. transparent: true,
  128. + side: THREE.DoubleSide,
  129. });
  130. </pre>
  131. <p>And we get</p>
  132. <p></p><div translate="no" class="threejs_example_container notranslate">
  133. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/transparency-doubleside.html"></iframe></div>
  134. <a class="threejs_center" href="/manual/examples/transparency-doubleside.html" target="_blank">click here to open in a separate window</a>
  135. </div>
  136. <p></p>
  137. <p>Give it a spin. It kind of looks like it's working as we can see backs
  138. except on closer inspection sometimes we can't.</p>
  139. <div class="threejs_center"><img src="../resources/images/transparency-cubes-some-backs.png" style="width: 368px;"></div>
  140. <div class="threejs_center">the left back face of each cube is missing</div>
  141. <p>This happens because of the way 3D objects are generally drawn. For each geometry
  142. each triangle is drawn one at a time. When each pixel of the triangle is drawn
  143. 2 things are recorded. One, the color for that pixel and two, the depth of that pixel.
  144. When the next triangle is drawn, for each pixel if the depth is deeper than the
  145. previously recorded depth no pixel is drawn.</p>
  146. <p>This works great for opaque things but it fails for transparent things.</p>
  147. <p>The solution is to sort transparent things and draw the stuff in back before
  148. drawing the stuff in front. THREE.js does this for objects like <a href="/docs/#api/en/objects/Mesh"><code class="notranslate" translate="no">Mesh</code></a> otherwise
  149. the very first example would have failed between cubes with some cubes blocking
  150. out others. Unfortunately for individual triangles shorting would be extremely slow. </p>
  151. <p>The cube has 12 triangles, 2 for each face, and the order they are drawn is
  152. <a href="custom-buffergeometry.html">the same order they are built in the geometry</a>
  153. so depending on which direction we are looking the triangles closer to the camera
  154. might get drawn first. In that case the triangles in the back aren't drawn.
  155. This is why sometimes we don't see the backs.</p>
  156. <p>For a convex object like a sphere or a cube one kind of solution is to add
  157. every cube to the scene twice. Once with a material that draws
  158. only the back facing triangles and another with a material that only
  159. draws the front facing triangles.</p>
  160. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function makeInstance(geometry, color, x, y, z) {
  161. + [THREE.BackSide, THREE.FrontSide].forEach((side) =&gt; {
  162. const material = new THREE.MeshPhongMaterial({
  163. color,
  164. opacity: 0.5,
  165. transparent: true,
  166. + side,
  167. });
  168. const cube = new THREE.Mesh(geometry, material);
  169. scene.add(cube);
  170. cube.position.set(x, y, z);
  171. + });
  172. }
  173. </pre>
  174. <p>Any with that it <em>seems</em> to work.</p>
  175. <p></p><div translate="no" class="threejs_example_container notranslate">
  176. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/transparency-doubleside-hack.html"></iframe></div>
  177. <a class="threejs_center" href="/manual/examples/transparency-doubleside-hack.html" target="_blank">click here to open in a separate window</a>
  178. </div>
  179. <p></p>
  180. <p>It assumes that the three.js's sorting is stable. Meaning that because we
  181. added the <code class="notranslate" translate="no">side: THREE.BackSide</code> mesh first and because it's at the exact same
  182. position that it will be drawn before the <code class="notranslate" translate="no">side: THREE.FrontSide</code> mesh.</p>
  183. <p>Let's make 2 intersecting planes (after deleting all the code related to cubes).
  184. We'll <a href="textures.html">add a texture</a> to each plane.</p>
  185. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const planeWidth = 1;
  186. const planeHeight = 1;
  187. const geometry = new THREE.PlaneGeometry(planeWidth, planeHeight);
  188. const loader = new THREE.TextureLoader();
  189. function makeInstance(geometry, color, rotY, url) {
  190. const texture = loader.load(url, render);
  191. const material = new THREE.MeshPhongMaterial({
  192. color,
  193. map: texture,
  194. opacity: 0.5,
  195. transparent: true,
  196. side: THREE.DoubleSide,
  197. });
  198. const mesh = new THREE.Mesh(geometry, material);
  199. scene.add(mesh);
  200. mesh.rotation.y = rotY;
  201. }
  202. makeInstance(geometry, 'pink', 0, 'resources/images/happyface.png');
  203. makeInstance(geometry, 'lightblue', Math.PI * 0.5, 'resources/images/hmmmface.png');
  204. </pre>
  205. <p>This time we can use <code class="notranslate" translate="no">side: THREE.DoubleSide</code> since we can only ever see one
  206. side of a plane at a time. Also note we pass our <code class="notranslate" translate="no">render</code> function to the texture
  207. loading function so that when the texture finishes loading we re-render the scene.
  208. This is because this sample is <a href="rendering-on-demand.html">rendering on demand</a>
  209. instead of rendering continuously.</p>
  210. <p></p><div translate="no" class="threejs_example_container notranslate">
  211. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/transparency-intersecting-planes.html"></iframe></div>
  212. <a class="threejs_center" href="/manual/examples/transparency-intersecting-planes.html" target="_blank">click here to open in a separate window</a>
  213. </div>
  214. <p></p>
  215. <p>And again we see a similar issue.</p>
  216. <div class="threejs_center"><img src="../resources/images/transparency-planes.png" style="width: 408px;"></div>
  217. <div class="threejs_center">half a face is missing</div>
  218. <p>The solution here is to manually split the each pane into 2 panes
  219. so that there really is no intersection.</p>
  220. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function makeInstance(geometry, color, rotY, url) {
  221. + const base = new THREE.Object3D();
  222. + scene.add(base);
  223. + base.rotation.y = rotY;
  224. + [-1, 1].forEach((x) =&gt; {
  225. const texture = loader.load(url, render);
  226. + texture.offset.x = x &lt; 0 ? 0 : 0.5;
  227. + texture.repeat.x = .5;
  228. const material = new THREE.MeshPhongMaterial({
  229. color,
  230. map: texture,
  231. opacity: 0.5,
  232. transparent: true,
  233. side: THREE.DoubleSide,
  234. });
  235. const mesh = new THREE.Mesh(geometry, material);
  236. - scene.add(mesh);
  237. + base.add(mesh);
  238. - mesh.rotation.y = rotY;
  239. + mesh.position.x = x * .25;
  240. });
  241. }
  242. </pre>
  243. <p>How you accomplish that is up to you. If I was using modeling package like
  244. <a href="https://blender.org">Blender</a> I'd probably do this manually by adjusting
  245. texture coordinates. Here though we're using <a href="/docs/#api/en/geometries/PlaneGeometry"><code class="notranslate" translate="no">PlaneGeometry</code></a> which by
  246. default stretches the texture across the plane. Like we <a href="textures.html">covered
  247. before</a> By setting the <a href="/docs/#api/en/textures/Texture#repeat"><code class="notranslate" translate="no">texture.repeat</code></a>
  248. and <a href="/docs/#api/en/textures/Texture#offset"><code class="notranslate" translate="no">texture.offset</code></a> we can scale and move the texture to get
  249. the correct half of the face texture on each plane.</p>
  250. <p>The code above also makes a <a href="/docs/#api/en/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a> and parents the 2 planes to it.
  251. It seemed easier to rotate a parent <a href="/docs/#api/en/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a> than to do the math
  252. required do it without. </p>
  253. <p></p><div translate="no" class="threejs_example_container notranslate">
  254. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/transparency-intersecting-planes-fixed.html"></iframe></div>
  255. <a class="threejs_center" href="/manual/examples/transparency-intersecting-planes-fixed.html" target="_blank">click here to open in a separate window</a>
  256. </div>
  257. <p></p>
  258. <p>This solution really only works for simple things like 2 planes that
  259. are not changing their intersection position.</p>
  260. <p>For textured objects one more solution is to set an alpha test.</p>
  261. <p>An alpha test is a level of <em>alpha</em> below which three.js will not
  262. draw the pixel. If we don't draw a pixel at all then the depth
  263. issues mentioned above disappear. For relatively sharp edged textures
  264. this works pretty well. Examples include leaf textures on a plant or tree
  265. or often a patch of grass.</p>
  266. <p>Let's try on the 2 planes. First let's use different textures.
  267. The textures above were 100% opaque. These 2 use transparency.</p>
  268. <div class="spread">
  269. <div><img class="checkerboard" src="../examples/resources/images/tree-01.png"></div>
  270. <div><img class="checkerboard" src="../examples/resources/images/tree-02.png"></div>
  271. </div>
  272. <p>Going back to the 2 planes that intersect (before we split them) let's
  273. use these textures and set an <a href="/docs/#api/en/materials/Material#alphaTest"><code class="notranslate" translate="no">alphaTest</code></a>.</p>
  274. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function makeInstance(geometry, color, rotY, url) {
  275. const texture = loader.load(url, render);
  276. const material = new THREE.MeshPhongMaterial({
  277. color,
  278. map: texture,
  279. - opacity: 0.5,
  280. transparent: true,
  281. + alphaTest: 0.5,
  282. side: THREE.DoubleSide,
  283. });
  284. const mesh = new THREE.Mesh(geometry, material);
  285. scene.add(mesh);
  286. mesh.rotation.y = rotY;
  287. }
  288. -makeInstance(geometry, 'pink', 0, 'resources/images/happyface.png');
  289. -makeInstance(geometry, 'lightblue', Math.PI * 0.5, 'resources/images/hmmmface.png');
  290. +makeInstance(geometry, 'white', 0, 'resources/images/tree-01.png');
  291. +makeInstance(geometry, 'white', Math.PI * 0.5, 'resources/images/tree-02.png');
  292. </pre>
  293. <p>Before we run this let's add a small UI so we can more easily play with the <code class="notranslate" translate="no">alphaTest</code>
  294. and <code class="notranslate" translate="no">transparent</code> settings. We'll use lil-gui like we introduced
  295. in the <a href="scenegraph.html">article on three.js's scenegraph</a>.</p>
  296. <p>First we'll make a helper for lil-gui that sets every material in the scene
  297. to a value</p>
  298. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">class AllMaterialPropertyGUIHelper {
  299. constructor(prop, scene) {
  300. this.prop = prop;
  301. this.scene = scene;
  302. }
  303. get value() {
  304. const {scene, prop} = this;
  305. let v;
  306. scene.traverse((obj) =&gt; {
  307. if (obj.material &amp;&amp; obj.material[prop] !== undefined) {
  308. v = obj.material[prop];
  309. }
  310. });
  311. return v;
  312. }
  313. set value(v) {
  314. const {scene, prop} = this;
  315. scene.traverse((obj) =&gt; {
  316. if (obj.material &amp;&amp; obj.material[prop] !== undefined) {
  317. obj.material[prop] = v;
  318. obj.material.needsUpdate = true;
  319. }
  320. });
  321. }
  322. }
  323. </pre>
  324. <p>Then we'll add the gui</p>
  325. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const gui = new GUI();
  326. gui.add(new AllMaterialPropertyGUIHelper('alphaTest', scene), 'value', 0, 1)
  327. .name('alphaTest')
  328. .onChange(requestRenderIfNotRequested);
  329. gui.add(new AllMaterialPropertyGUIHelper('transparent', scene), 'value')
  330. .name('transparent')
  331. .onChange(requestRenderIfNotRequested);
  332. </pre>
  333. <p>and of course we need to include lil-gui</p>
  334. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from 'three';
  335. import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
  336. +import {GUI} from 'three/addons/libs/lil-gui.module.min.js';
  337. </pre>
  338. <p>and here's the results</p>
  339. <p></p><div translate="no" class="threejs_example_container notranslate">
  340. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/transparency-intersecting-planes-alphatest.html"></iframe></div>
  341. <a class="threejs_center" href="/manual/examples/transparency-intersecting-planes-alphatest.html" target="_blank">click here to open in a separate window</a>
  342. </div>
  343. <p></p>
  344. <p>You can see it works but zoom in and you'll see one plane has white lines.</p>
  345. <div class="threejs_center"><img src="../resources/images/transparency-alphatest-issues.png" style="width: 532px;"></div>
  346. <p>This is the same depth issue from before. That plane was drawn first
  347. so the plane behind is not drawn. There is no perfect solution.
  348. Adjust the <code class="notranslate" translate="no">alphaTest</code> and/or turn off <code class="notranslate" translate="no">transparent</code> to find a solution
  349. that fits your use case.</p>
  350. <p>The take way from this article is perfect transparency is hard.
  351. There are issues and trade offs and workarounds.</p>
  352. <p>For example say you have a car.
  353. Cars usually have windshields on all 4 sides. If you want to avoid the sorting issues
  354. above you'd have to make each window its own object so that three.js can
  355. sort the windows and draw them in the correct order.</p>
  356. <p>If you are making some plants or grass the alpha test solution is common.</p>
  357. <p>Which solution you pick depends on your needs. </p>
  358. </div>
  359. </div>
  360. </div>
  361. <script src="../resources/prettify.js"></script>
  362. <script src="../resources/lesson.js"></script>
  363. </body></html>