canvas-textures.html 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. <!DOCTYPE html><html lang="en"><head>
  2. <meta charset="utf-8">
  3. <title>Canvas Textures</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 – Canvas Textures">
  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>Canvas Textures</h1>
  25. </div>
  26. <div class="lesson">
  27. <div class="lesson-main">
  28. <p>This article continues from <a href="textures.html">the article on textures</a>.
  29. If you haven't read that yet you should probably start there.</p>
  30. <p>In <a href="textures.html">the previous article on textures</a> we mostly used
  31. image files for textures. Sometimes though we want to generate a texture
  32. at runtime. One way to do this is to use a <a href="/docs/#api/en/textures/CanvasTexture"><code class="notranslate" translate="no">CanvasTexture</code></a>.</p>
  33. <p>A canvas texture takes a <code class="notranslate" translate="no">&lt;canvas&gt;</code> as its input. If you don't know how to
  34. draw with the 2D canvas API on a canvas <a href="https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial">there's a good tutorial on MDN</a>.</p>
  35. <p>Let's make a simple canvas program. Here's one that draws dots at random places in random colors.</p>
  36. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const ctx = document.createElement('canvas').getContext('2d');
  37. document.body.appendChild(ctx.canvas);
  38. ctx.canvas.width = 256;
  39. ctx.canvas.height = 256;
  40. ctx.fillStyle = '#FFF';
  41. ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  42. function randInt(min, max) {
  43. if (max === undefined) {
  44. max = min;
  45. min = 0;
  46. }
  47. return Math.random() * (max - min) + min | 0;
  48. }
  49. function drawRandomDot() {
  50. ctx.fillStyle = `#${randInt(0x1000000).toString(16).padStart(6, '0')}`;
  51. ctx.beginPath();
  52. const x = randInt(256);
  53. const y = randInt(256);
  54. const radius = randInt(10, 64);
  55. ctx.arc(x, y, radius, 0, Math.PI * 2);
  56. ctx.fill();
  57. }
  58. function render() {
  59. drawRandomDot();
  60. requestAnimationFrame(render);
  61. }
  62. requestAnimationFrame(render);
  63. </pre>
  64. <p>it's pretty straight forward.</p>
  65. <p></p><div translate="no" class="threejs_example_container notranslate">
  66. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/canvas-random-dots.html"></iframe></div>
  67. <a class="threejs_center" href="/manual/examples/canvas-random-dots.html" target="_blank">click here to open in a separate window</a>
  68. </div>
  69. <p></p>
  70. <p>Now let's use it to texture something. We'll start with the example of texturing
  71. a cube from <a href="textures.html">the previous article</a>.
  72. We'll remove the code that loads an image and instead use
  73. our canvas by creating a <a href="/docs/#api/en/textures/CanvasTexture"><code class="notranslate" translate="no">CanvasTexture</code></a> and passing it the canvas we created.</p>
  74. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const cubes = []; // just an array we can use to rotate the cubes
  75. -const loader = new THREE.TextureLoader();
  76. -
  77. +const ctx = document.createElement('canvas').getContext('2d');
  78. +ctx.canvas.width = 256;
  79. +ctx.canvas.height = 256;
  80. +ctx.fillStyle = '#FFF';
  81. +ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  82. +const texture = new THREE.CanvasTexture(ctx.canvas);
  83. const material = new THREE.MeshBasicMaterial({
  84. - map: loader.load('resources/images/wall.jpg'),
  85. + map: texture,
  86. });
  87. const cube = new THREE.Mesh(geometry, material);
  88. scene.add(cube);
  89. cubes.push(cube); // add to our list of cubes to rotate
  90. </pre>
  91. <p>And then call the code to draw a random dot in our render loop</p>
  92. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
  93. time *= 0.001;
  94. if (resizeRendererToDisplaySize(renderer)) {
  95. const canvas = renderer.domElement;
  96. camera.aspect = canvas.clientWidth / canvas.clientHeight;
  97. camera.updateProjectionMatrix();
  98. }
  99. + drawRandomDot();
  100. + texture.needsUpdate = true;
  101. cubes.forEach((cube, ndx) =&gt; {
  102. const speed = .2 + ndx * .1;
  103. const rot = time * speed;
  104. cube.rotation.x = rot;
  105. cube.rotation.y = rot;
  106. });
  107. renderer.render(scene, camera);
  108. requestAnimationFrame(render);
  109. }
  110. </pre>
  111. <p>The only extra thing we need to do is set the <code class="notranslate" translate="no">needsUpdate</code> property
  112. of the <a href="/docs/#api/en/textures/CanvasTexture"><code class="notranslate" translate="no">CanvasTexture</code></a> to tell three.js to update the texture with
  113. the latest contents of the canvas.</p>
  114. <p>And with that we have a canvas textured cube</p>
  115. <p></p><div translate="no" class="threejs_example_container notranslate">
  116. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/canvas-textured-cube.html"></iframe></div>
  117. <a class="threejs_center" href="/manual/examples/canvas-textured-cube.html" target="_blank">click here to open in a separate window</a>
  118. </div>
  119. <p></p>
  120. <p>Note that if you want to use three.js to draw into the canvas you're
  121. better off using a <code class="notranslate" translate="no">RenderTarget</code> which is covered in <a href="rendertargets.html">this article</a>.</p>
  122. <p>A common use case for canvas textures is to provide text in a scene.
  123. For example if you wanted to put a person's name on their character's
  124. badge you might use a canvas texture to texture the badge.</p>
  125. <p>Let's make a scene with 3 people and give each person a badge
  126. or label.</p>
  127. <p>Let's take the example above and remove all the cube related
  128. stuff. Then let's set the background to white and add two <a href="lights.html">lights</a>.</p>
  129. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const scene = new THREE.Scene();
  130. +scene.background = new THREE.Color('white');
  131. +
  132. +function addLight(position) {
  133. + const color = 0xFFFFFF;
  134. + const intensity = 1;
  135. + const light = new THREE.DirectionalLight(color, intensity);
  136. + light.position.set(...position);
  137. + scene.add(light);
  138. + scene.add(light.target);
  139. +}
  140. +addLight([-3, 1, 1]);
  141. +addLight([ 2, 1, .5]);
  142. </pre>
  143. <p>Let's make some code to make a label using canvas 2D</p>
  144. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+function makeLabelCanvas(size, name) {
  145. + const borderSize = 2;
  146. + const ctx = document.createElement('canvas').getContext('2d');
  147. + const font = `${size}px bold sans-serif`;
  148. + ctx.font = font;
  149. + // measure how long the name will be
  150. + const doubleBorderSize = borderSize * 2;
  151. + const width = ctx.measureText(name).width + doubleBorderSize;
  152. + const height = size + doubleBorderSize;
  153. + ctx.canvas.width = width;
  154. + ctx.canvas.height = height;
  155. +
  156. + // need to set font again after resizing canvas
  157. + ctx.font = font;
  158. + ctx.textBaseline = 'top';
  159. +
  160. + ctx.fillStyle = 'blue';
  161. + ctx.fillRect(0, 0, width, height);
  162. + ctx.fillStyle = 'white';
  163. + ctx.fillText(name, borderSize, borderSize);
  164. +
  165. + return ctx.canvas;
  166. +}
  167. </pre>
  168. <p>Then we'll make simple people from a cylinder for the body, a sphere
  169. for the head, and a plane for the label.</p>
  170. <p>First let's make the shared geometry.</p>
  171. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const bodyRadiusTop = .4;
  172. +const bodyRadiusBottom = .2;
  173. +const bodyHeight = 2;
  174. +const bodyRadialSegments = 6;
  175. +const bodyGeometry = new THREE.CylinderGeometry(
  176. + bodyRadiusTop, bodyRadiusBottom, bodyHeight, bodyRadialSegments);
  177. +
  178. +const headRadius = bodyRadiusTop * 0.8;
  179. +const headLonSegments = 12;
  180. +const headLatSegments = 5;
  181. +const headGeometry = new THREE.SphereGeometry(
  182. + headRadius, headLonSegments, headLatSegments);
  183. +
  184. +const labelGeometry = new THREE.PlaneGeometry(1, 1);
  185. </pre>
  186. <p>Then let's make a function to build a person from these
  187. parts.</p>
  188. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+function makePerson(x, size, name, color) {
  189. + const canvas = makeLabelCanvas(size, name);
  190. + const texture = new THREE.CanvasTexture(canvas);
  191. + // because our canvas is likely not a power of 2
  192. + // in both dimensions set the filtering appropriately.
  193. + texture.minFilter = THREE.LinearFilter;
  194. + texture.wrapS = THREE.ClampToEdgeWrapping;
  195. + texture.wrapT = THREE.ClampToEdgeWrapping;
  196. +
  197. + const labelMaterial = new THREE.MeshBasicMaterial({
  198. + map: texture,
  199. + side: THREE.DoubleSide,
  200. + transparent: true,
  201. + });
  202. + const bodyMaterial = new THREE.MeshPhongMaterial({
  203. + color,
  204. + flatShading: true,
  205. + });
  206. +
  207. + const root = new THREE.Object3D();
  208. + root.position.x = x;
  209. +
  210. + const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
  211. + root.add(body);
  212. + body.position.y = bodyHeight / 2;
  213. +
  214. + const head = new THREE.Mesh(headGeometry, bodyMaterial);
  215. + root.add(head);
  216. + head.position.y = bodyHeight + headRadius * 1.1;
  217. +
  218. + const label = new THREE.Mesh(labelGeometry, labelMaterial);
  219. + root.add(label);
  220. + label.position.y = bodyHeight * 4 / 5;
  221. + label.position.z = bodyRadiusTop * 1.01;
  222. +
  223. + // if units are meters then 0.01 here makes size
  224. + // of the label into centimeters.
  225. + const labelBaseScale = 0.01;
  226. + label.scale.x = canvas.width * labelBaseScale;
  227. + label.scale.y = canvas.height * labelBaseScale;
  228. +
  229. + scene.add(root);
  230. + return root;
  231. +}
  232. </pre>
  233. <p>You can see above we put the body, head, and label on a root
  234. <a href="/docs/#api/en/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a> and adjust their positions. This would let us move the
  235. root object if we wanted to move the people. The body is 2 units
  236. high. If 1 unit equals 1 meter then the code above tries to
  237. make the label in centimeters so they will be size centimeters
  238. tall and however wide is needed to fit the text.</p>
  239. <p>We can then make people with labels</p>
  240. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+makePerson(-3, 32, 'Purple People Eater', 'purple');
  241. +makePerson(-0, 32, 'Green Machine', 'green');
  242. +makePerson(+3, 32, 'Red Menace', 'red');
  243. </pre>
  244. <p>What's left is to add some <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> so we can move
  245. the camera.</p>
  246. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from 'three';
  247. +import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
  248. </pre>
  249. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const fov = 75;
  250. const aspect = 2; // the canvas default
  251. const near = 0.1;
  252. -const far = 5;
  253. +const far = 50;
  254. const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
  255. -camera.position.z = 2;
  256. +camera.position.set(0, 2, 5);
  257. +const controls = new OrbitControls(camera, canvas);
  258. +controls.target.set(0, 2, 0);
  259. +controls.update();
  260. </pre>
  261. <p>and we get simple labels.</p>
  262. <p></p><div translate="no" class="threejs_example_container notranslate">
  263. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/canvas-textured-labels.html"></iframe></div>
  264. <a class="threejs_center" href="/manual/examples/canvas-textured-labels.html" target="_blank">click here to open in a separate window</a>
  265. </div>
  266. <p></p>
  267. <p>Some things to notice.</p>
  268. <ul>
  269. <li>If you zoom in the labels get pretty low-res.</li>
  270. </ul>
  271. <p>There is no easy solution. There are more complex font
  272. rendering techniques but I know of no plugin solutions.
  273. Plus they will require the user download font data which
  274. would be slow.</p>
  275. <p>One solution is to increase the resolution of the labels.
  276. Try setting the size passed into to double what it is now
  277. and setting <code class="notranslate" translate="no">labelBaseScale</code> to half what it currently is.</p>
  278. <ul>
  279. <li>The labels get longer the longer the name.</li>
  280. </ul>
  281. <p>If you wanted to fix this you'd instead choose a fixed sized
  282. label and then squish the text.</p>
  283. <p>This is pretty easy. Pass in a base width and scale the text to fit that
  284. width like this</p>
  285. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function makeLabelCanvas(size, name) {
  286. +function makeLabelCanvas(baseWidth, size, name) {
  287. const borderSize = 2;
  288. const ctx = document.createElement('canvas').getContext('2d');
  289. const font = `${size}px bold sans-serif`;
  290. ctx.font = font;
  291. // measure how long the name will be
  292. + const textWidth = ctx.measureText(name).width;
  293. const doubleBorderSize = borderSize * 2;
  294. - const width = ctx.measureText(name).width + doubleBorderSize;
  295. + const width = baseWidth + doubleBorderSize;
  296. const height = size + doubleBorderSize;
  297. ctx.canvas.width = width;
  298. ctx.canvas.height = height;
  299. // need to set font again after resizing canvas
  300. ctx.font = font;
  301. - ctx.textBaseline = 'top';
  302. + ctx.textBaseline = 'middle';
  303. + ctx.textAlign = 'center';
  304. ctx.fillStyle = 'blue';
  305. ctx.fillRect(0, 0, width, height);
  306. + // scale to fit but don't stretch
  307. + const scaleFactor = Math.min(1, baseWidth / textWidth);
  308. + ctx.translate(width / 2, height / 2);
  309. + ctx.scale(scaleFactor, 1);
  310. ctx.fillStyle = 'white';
  311. ctx.fillText(name, borderSize, borderSize);
  312. return ctx.canvas;
  313. }
  314. </pre>
  315. <p>Then we can pass in a width for the labels</p>
  316. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function makePerson(x, size, name, color) {
  317. - const canvas = makeLabelCanvas(size, name);
  318. +function makePerson(x, labelWidth, size, name, color) {
  319. + const canvas = makeLabelCanvas(labelWidth, size, name);
  320. ...
  321. }
  322. -makePerson(-3, 32, 'Purple People Eater', 'purple');
  323. -makePerson(-0, 32, 'Green Machine', 'green');
  324. -makePerson(+3, 32, 'Red Menace', 'red');
  325. +makePerson(-3, 150, 32, 'Purple People Eater', 'purple');
  326. +makePerson(-0, 150, 32, 'Green Machine', 'green');
  327. +makePerson(+3, 150, 32, 'Red Menace', 'red');
  328. </pre>
  329. <p>and we get labels where the text is centered and scaled to fit</p>
  330. <p></p><div translate="no" class="threejs_example_container notranslate">
  331. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/canvas-textured-labels-scale-to-fit.html"></iframe></div>
  332. <a class="threejs_center" href="/manual/examples/canvas-textured-labels-scale-to-fit.html" target="_blank">click here to open in a separate window</a>
  333. </div>
  334. <p></p>
  335. <p>Above we used a new canvas for each texture. Whether or not to use a
  336. canvas per texture is up to you. If you need to update them often then
  337. having one canvas per texture is probably the best option. If they are
  338. rarely or never updated then you can choose to use a single canvas
  339. for multiple textures by forcing three.js to use the texture.
  340. Let's change the code above to do just that.</p>
  341. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const ctx = document.createElement('canvas').getContext('2d');
  342. function makeLabelCanvas(baseWidth, size, name) {
  343. const borderSize = 2;
  344. - const ctx = document.createElement('canvas').getContext('2d');
  345. const font = `${size}px bold sans-serif`;
  346. ...
  347. }
  348. +const forceTextureInitialization = function() {
  349. + const material = new THREE.MeshBasicMaterial();
  350. + const geometry = new THREE.PlaneGeometry();
  351. + const scene = new THREE.Scene();
  352. + scene.add(new THREE.Mesh(geometry, material));
  353. + const camera = new THREE.Camera();
  354. +
  355. + return function forceTextureInitialization(texture) {
  356. + material.map = texture;
  357. + renderer.render(scene, camera);
  358. + };
  359. +}();
  360. function makePerson(x, labelWidth, size, name, color) {
  361. const canvas = makeLabelCanvas(labelWidth, size, name);
  362. const texture = new THREE.CanvasTexture(canvas);
  363. // because our canvas is likely not a power of 2
  364. // in both dimensions set the filtering appropriately.
  365. texture.minFilter = THREE.LinearFilter;
  366. texture.wrapS = THREE.ClampToEdgeWrapping;
  367. texture.wrapT = THREE.ClampToEdgeWrapping;
  368. + forceTextureInitialization(texture);
  369. ...
  370. </pre>
  371. <p></p><div translate="no" class="threejs_example_container notranslate">
  372. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/canvas-textured-labels-one-canvas.html"></iframe></div>
  373. <a class="threejs_center" href="/manual/examples/canvas-textured-labels-one-canvas.html" target="_blank">click here to open in a separate window</a>
  374. </div>
  375. <p></p>
  376. <p>Another issue is that the labels don't always face the camera. If you're using
  377. labels as badges that's probably a good thing. If you're using labels to put
  378. names over players in a 3D game maybe you want the labels to always face the camera.
  379. We'll cover how to do that in <a href="billboards.html">an article on billboards</a>.</p>
  380. <p>For labels in particular, <a href="align-html-elements-to-3d.html">another solution is to use HTML</a>.
  381. The labels in this article are <em>inside the 3D world</em> which is good if you want them
  382. to be hidden by other objects where as <a href="align-html-elements-to-3d.html">HTML labels</a> are always on top.</p>
  383. </div>
  384. </div>
  385. </div>
  386. <script src="../resources/prettify.js"></script>
  387. <script src="../resources/lesson.js"></script>
  388. </body></html>