canvas-textures.html 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. <!DOCTYPE html><html lang="zh"><head>
  2. <meta charset="utf-8">
  3. <title>Canvas 纹理</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. <!-- Import maps polyfill -->
  14. <!-- Remove this when import maps will be widely supported -->
  15. <script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>
  16. <script type="importmap">
  17. {
  18. "imports": {
  19. "three": "../../build/three.module.js"
  20. }
  21. }
  22. </script>
  23. <link rel="stylesheet" href="/manual/zh/lang.css">
  24. </head>
  25. <body>
  26. <div class="container">
  27. <div class="lesson-title">
  28. <h1>Canvas 纹理</h1>
  29. </div>
  30. <div class="lesson">
  31. <div class="lesson-main">
  32. <p>这篇文章是此篇 <a href="textures.html">关于纹理</a> 文章的延续,如果你还没有读过,你或许应当从那篇开始。</p>
  33. <p>在<a href="textures.html">上一篇讲解纹理的文章中</a>,我们主要使用图像文件来生成动态纹理,有时候我们想在运行时生成一个纹理。一种可行的方式是使用 <a href="/docs/#api/en/textures/CanvasTexture"><code class="notranslate" translate="no">CanvasTexture</code></a>。</p>
  34. <p>Canvas纹理 使用一个<code class="notranslate" translate="no">&lt;canvas&gt;</code> 作为它的输入, 如果你还不知道如何使用2D Canvas API来在画布上绘制内容,<a href="https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial">MDN上有一篇很好的文章</a>。</p>
  35. <p>我们来写一段简单的Canvas代码,这是一个在随机位置上绘制随机颜色的点的程序。</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);</pre>
  63. <p>这实在太简单了。</p>
  64. <p></p><div translate="no" class="threejs_example_container notranslate">
  65. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/canvas-random-dots.html"></iframe></div>
  66. <a class="threejs_center" href="/manual/examples/canvas-random-dots.html" target="_blank">点击在新窗口打开</a>
  67. </div>
  68. <p></p>
  69. <p>现在让我们用它来绘制纹理。我们会用从 <a href="textures.html">上一篇文章</a> 中绘制立方体纹理的例子开始。
  70. 我们将删除加载图像的代码,取而代之的是使用我们的Canvas,通过创建一个<a href="/docs/#api/en/textures/CanvasTexture"><code class="notranslate" translate="no">CanvasTexture</code></a>,然后把我们创建好的Canvas对象传入。</p>
  71. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const cubes = []; // 我们使用这个数组来旋转这些立方体
  72. -const loader = new THREE.TextureLoader();
  73. -
  74. +const ctx = document.createElement('canvas').getContext('2d');
  75. +ctx.canvas.width = 256;
  76. +ctx.canvas.height = 256;
  77. +ctx.fillStyle = '#FFF';
  78. +ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  79. +const texture = new THREE.CanvasTexture(ctx.canvas);
  80. const material = new THREE.MeshBasicMaterial({
  81. - map: loader.load('resources/images/wall.jpg'),
  82. + map: texture,
  83. });
  84. const cube = new THREE.Mesh(geometry, material);
  85. scene.add(cube);
  86. cubes.push(cube); // 添加到cube list中方便旋转</pre>
  87. <p>然后调用代码,在我们的渲染循环中绘制一个随机点。</p>
  88. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">function render(time) {
  89. time *= 0.001;
  90. if (resizeRendererToDisplaySize(renderer)) {
  91. const canvas = renderer.domElement;
  92. camera.aspect = canvas.clientWidth / canvas.clientHeight;
  93. camera.updateProjectionMatrix();
  94. }
  95. + drawRandomDot();
  96. + texture.needsUpdate = true;
  97. cubes.forEach((cube, ndx) =&gt; {
  98. const speed = .2 + ndx * .1;
  99. const rot = time * speed;
  100. cube.rotation.x = rot;
  101. cube.rotation.y = rot;
  102. });
  103. renderer.render(scene, camera);
  104. requestAnimationFrame(render);
  105. }</pre>
  106. <p>我们只需要做额外的一件事,设置了 <a href="/docs/#api/en/textures/CanvasTexture"><code class="notranslate" translate="no">CanvasTexture</code></a> 的 <code class="notranslate" translate="no">needsUpdate</code>属性来告诉THREE.js来更新纹理画布的最新内容。</p>
  107. <p>这样,我们就有了一个用Canvas绘制纹理的立方体。</p>
  108. <p></p><div translate="no" class="threejs_example_container notranslate">
  109. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/canvas-textured-cube.html"></iframe></div>
  110. <a class="threejs_center" href="/manual/examples/canvas-textured-cube.html" target="_blank">点击在新窗口打开</a>
  111. </div>
  112. <p></p>
  113. <p>请注意,如果你想使用THREE.js绘制到Canvas中,你最好用 <code class="notranslate" translate="no">RenderTarget</code>,在 <a href="rendertargets.html">这篇文章</a> 中有提到。</p>
  114. <p>纹理画布的一个常见用法是在场景中绘制文本。例如,你想把一个人的名字放在他们角色上面作为一个徽标(Badge),你也许需要使用Canvas来绘制徽标纹理。</p>
  115. <p>让我们创建一个有3个人的场景,并给每个人绘制一个徽标或者标签(Label)。</p>
  116. <p>让我们用上面的例子,移除所有相关的立方体。然后设置背景为白色,然后添加两个<a href="lights.html">灯光</a>。</p>
  117. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const scene = new THREE.Scene();
  118. +scene.background = new THREE.Color('white');
  119. +
  120. +function addLight(position) {
  121. + const color = 0xFFFFFF;
  122. + const intensity = 1;
  123. + const light = new THREE.DirectionalLight(color, intensity);
  124. + light.position.set(...position);
  125. + scene.add(light);
  126. + scene.add(light.target);
  127. +}
  128. +addLight([-3, 1, 1]);
  129. +addLight([ 2, 1, .5]);</pre>
  130. <p>让我们写一些代码以使用2D Canvas绘制标签</p>
  131. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+function makeLabelCanvas(size, name) {
  132. + const borderSize = 2;
  133. + const ctx = document.createElement('canvas').getContext('2d');
  134. + const font = `${size}px bold sans-serif`;
  135. + ctx.font = font;
  136. + // 测量一下name有多长
  137. + const doubleBorderSize = borderSize * 2;
  138. + const width = ctx.measureText(name).width + doubleBorderSize;
  139. + const height = size + doubleBorderSize;
  140. + ctx.canvas.width = width;
  141. + ctx.canvas.height = height;
  142. +
  143. + // 注意,调整画布后需要重新修改字体
  144. + ctx.font = font;
  145. + ctx.textBaseline = 'top';
  146. +
  147. + ctx.fillStyle = 'blue';
  148. + ctx.fillRect(0, 0, width, height);
  149. + ctx.fillStyle = 'white';
  150. + ctx.fillText(name, borderSize, borderSize);
  151. +
  152. + return ctx.canvas;
  153. +}</pre>
  154. <p>然后我们将用一个圆柱体作为身体,一个球体作为头部,一个平面作为标签来制作一个简单的人。</p>
  155. <p>首先我们开始制作共享几何体。</p>
  156. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const bodyRadiusTop = .4;
  157. +const bodyRadiusBottom = .2;
  158. +const bodyHeight = 2;
  159. +const bodyRadialSegments = 6;
  160. +const bodyGeometry = new THREE.CylinderGeometry(
  161. + bodyRadiusTop, bodyRadiusBottom, bodyHeight, bodyRadialSegments);
  162. +
  163. +const headRadius = bodyRadiusTop * 0.8;
  164. +const headLonSegments = 12;
  165. +const headLatSegments = 5;
  166. +const headGeometry = new THREE.SphereGeometry(
  167. + headRadius, headLonSegments, headLatSegments);
  168. +
  169. +const labelGeometry = new THREE.PlaneGeometry(1, 1);</pre>
  170. <p>然后我们写一个函数把这些部分组合成一个人。</p>
  171. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+function makePerson(x, size, name, color) {
  172. + const canvas = makeLabelCanvas(size, name);
  173. + const texture = new THREE.CanvasTexture(canvas);
  174. + // 因为我们的Canvas长宽都不太可能是2的倍数,所以将filtering设置合理一些
  175. + texture.minFilter = THREE.LinearFilter;
  176. + texture.wrapS = THREE.ClampToEdgeWrapping;
  177. + texture.wrapT = THREE.ClampToEdgeWrapping;
  178. +
  179. + const labelMaterial = new THREE.MeshBasicMaterial({
  180. + map: texture,
  181. + side: THREE.DoubleSide,
  182. + transparent: true,
  183. + });
  184. + const bodyMaterial = new THREE.MeshPhongMaterial({
  185. + color,
  186. + flatShading: true,
  187. + });
  188. +
  189. + const root = new THREE.Object3D();
  190. + root.position.x = x;
  191. +
  192. + const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
  193. + root.add(body);
  194. + body.position.y = bodyHeight / 2;
  195. +
  196. + const head = new THREE.Mesh(headGeometry, bodyMaterial);
  197. + root.add(head);
  198. + head.position.y = bodyHeight + headRadius * 1.1;
  199. +
  200. + const label = new THREE.Mesh(labelGeometry, labelMaterial);
  201. + root.add(label);
  202. + label.position.y = bodyHeight * 4 / 5;
  203. + label.position.z = bodyRadiusTop * 1.01;
  204. +
  205. + // 如果单位是米, 那这里0.01就是将标签的尺寸转化为厘米
  206. + const labelBaseScale = 0.01;
  207. + label.scale.x = canvas.width * labelBaseScale;
  208. + label.scale.y = canvas.height * labelBaseScale;
  209. +
  210. + scene.add(root);
  211. + return root;
  212. +}</pre>
  213. <p>在上面你可以看到,我们把身体、头部、标签放在了一个根<a href="/docs/#api/en/core/Object3D"><code class="notranslate" translate="no">Object3D</code></a> 上并且调整了他们的位置。这样如果我们想移动人的话直接移动根对象就可以了。身体是2个单位的高度,如果1个单位等于1米,那么上面的代码会尝试用厘米为单位制作标签,它们使用厘米作为宽高,以更好的适合文本。</p>
  214. <p>然后我们可以制作带标签的人</p>
  215. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+makePerson(-3, 32, 'Purple People Eater', 'purple');
  216. +makePerson(-0, 32, 'Green Machine', 'green');
  217. +makePerson(+3, 32, 'Red Menace', 'red');</pre>
  218. <p>剩下的就是添加 <a href="/docs/#examples/controls/OrbitControls"><code class="notranslate" translate="no">OrbitControls</code></a> 这样我们就可以移动相机了。</p>
  219. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">import * as THREE from 'three';
  220. +import {OrbitControls} from 'three/addons/controls/OrbitControls.js';</pre>
  221. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">const fov = 75;
  222. const aspect = 2; // Canvas默认值
  223. const near = 0.1;
  224. -const far = 5;
  225. +const far = 50;
  226. const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
  227. -camera.position.z = 2;
  228. +camera.position.set(0, 2, 5);
  229. +const controls = new OrbitControls(camera, canvas);
  230. +controls.target.set(0, 2, 0);
  231. +controls.update();</pre>
  232. <p>然后我们得到了一些简单的标签。</p>
  233. <p></p><div translate="no" class="threejs_example_container notranslate">
  234. <div><iframe class="threejs_example notranslate" translate="no" style=" " src="/manual/examples/resources/editor.html?url=/manual/examples/canvas-textured-labels.html"></iframe></div>
  235. <a class="threejs_center" href="/manual/examples/canvas-textured-labels.html" target="_blank">点击在新窗口打开</a>
  236. </div>
  237. <p></p>
  238. <p>注意事项:</p>
  239. <ul>
  240. <li>如果你过度放大,标签的分辨率会降低。</li>
  241. </ul>
  242. <p>没有简单的解决方案,还有更复杂的字体渲染技术,据我所知没有插件可以解决这个问题。另外,还需要用户下载字体数据文件,这会变得很慢。</p>
  243. <p>一种方案是增加标签的分辨率,尝试让尺寸变成现在的2倍,然后设置 <code class="notranslate" translate="no">labelBaseScale</code> 是现在的一半。</p>
  244. <ul>
  245. <li>名字越长,标签越长。</li>
  246. </ul>
  247. <p>如果你想解决这个问题,你需要指定标签的固定大小,然后挤压文本。</p>
  248. <p>这很容易做到。传入一个基本宽度并缩放文本以适应。</p>
  249. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function makeLabelCanvas(size, name) {
  250. +function makeLabelCanvas(baseWidth, size, name) {
  251. const borderSize = 2;
  252. const ctx = document.createElement('canvas').getContext('2d');
  253. const font = `${size}px bold sans-serif`;
  254. ctx.font = font;
  255. // 测量一下name有多长
  256. + const textWidth = ctx.measureText(name).width;
  257. const doubleBorderSize = borderSize * 2;
  258. - const width = ctx.measureText(name).width + doubleBorderSize;
  259. + const width = baseWidth + doubleBorderSize;
  260. const height = size + doubleBorderSize;
  261. ctx.canvas.width = width;
  262. ctx.canvas.height = height;
  263. // 注意,调整画布后需要重新修改字体
  264. ctx.font = font;
  265. - ctx.textBaseline = 'top';
  266. + ctx.textBaseline = 'middle';
  267. + ctx.textAlign = 'center';
  268. ctx.fillStyle = 'blue';
  269. ctx.fillRect(0, 0, width, height);
  270. + // 缩放以适应,但是不要拉伸
  271. + const scaleFactor = Math.min(1, baseWidth / textWidth);
  272. + ctx.translate(width / 2, height / 2);
  273. + ctx.scale(scaleFactor, 1);
  274. ctx.fillStyle = 'white';
  275. ctx.fillText(name, borderSize, borderSize);
  276. return ctx.canvas;
  277. }</pre>
  278. <p>然后我们可以传入预期标签的长度</p>
  279. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">-function makePerson(x, size, name, color) {
  280. - const canvas = makeLabelCanvas(size, name);
  281. +function makePerson(x, labelWidth, size, name, color) {
  282. + const canvas = makeLabelCanvas(labelWidth, size, name);
  283. ...
  284. }
  285. -makePerson(-3, 32, 'Purple People Eater', 'purple');
  286. -makePerson(-0, 32, 'Green Machine', 'green');
  287. -makePerson(+3, 32, 'Red Menace', 'red');
  288. +makePerson(-3, 150, 32, 'Purple People Eater', 'purple');
  289. +makePerson(-0, 150, 32, 'Green Machine', 'green');
  290. +makePerson(+3, 150, 32, 'Red Menace', 'red');</pre>
  291. <p>我们将文本居中并缩放以适应标签的尺寸。</p>
  292. <p></p><div translate="no" class="threejs_example_container notranslate">
  293. <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>
  294. <a class="threejs_center" href="/manual/examples/canvas-textured-labels-scale-to-fit.html" target="_blank">点击在新窗口打开</a>
  295. </div>
  296. <p></p>
  297. <p>上面我们为每一个纹理使用了单独的Canvas,是否为每个纹理使用单独的Canvas取决于你。如果你需要经常单独更新它们,每个纹理一个Canvas是一个比较好的选择。如果它们很少或者从不更新,那么你可以用一个Canvas,通过THREE.js来生成多个纹理。让我们更改上面的代码来完成这一点。</p>
  298. <pre class="prettyprint showlinemods notranslate lang-js" translate="no">+const ctx = document.createElement('canvas').getContext('2d');
  299. function makeLabelCanvas(baseWidth, size, name) {
  300. const borderSize = 2;
  301. - const ctx = document.createElement('canvas').getContext('2d');
  302. const font = `${size}px bold sans-serif`;
  303. ...
  304. }
  305. +const forceTextureInitialization = function() {
  306. + const material = new THREE.MeshBasicMaterial();
  307. + const geometry = new THREE.PlaneGeometry();
  308. + const scene = new THREE.Scene();
  309. + scene.add(new THREE.Mesh(geometry, material));
  310. + const camera = new THREE.Camera();
  311. +
  312. + return function forceTextureInitialization(texture) {
  313. + material.map = texture;
  314. + renderer.render(scene, camera);
  315. + };
  316. +}();
  317. function makePerson(x, labelWidth, size, name, color) {
  318. const canvas = makeLabelCanvas(labelWidth, size, name);
  319. const texture = new THREE.CanvasTexture(canvas);
  320. // 因为我们的Canvas长宽都不太可能是2的倍数,所以将filtering设置合理一些
  321. texture.minFilter = THREE.LinearFilter;
  322. texture.wrapS = THREE.ClampToEdgeWrapping;
  323. texture.wrapT = THREE.ClampToEdgeWrapping;
  324. + forceTextureInitialization(texture);
  325. ...</pre>
  326. <p></p><div translate="no" class="threejs_example_container notranslate">
  327. <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>
  328. <a class="threejs_center" href="/manual/examples/canvas-textured-labels-one-canvas.html" target="_blank">点击在新窗口打开</a>
  329. </div>
  330. <p></p>
  331. <p>另一个问题是标签并不总是面向相机,如果你使用标签作为徽标,这可能是一件好事。
  332. 如果你使用标签来放置3D游戏中玩家的名字,也许你希望标签总是面对相机。
  333. 具体内容在 <a href="billboards.html">广告牌(Billboards)文章</a> 有覆盖到。</p>
  334. <p>特别是对于标签,<a href="align-html-elements-to-3d.html">另一种解决方案是使用HTML</a>,
  335. 本文中的标签是 <em>位于3D场景中</em> ,如果你想要他们被其他对象遮挡是很好的,因为 <a href="align-html-elements-to-3d.html">HTML 标签</a> 总是在最上层。
  336. </p>
  337. </div>
  338. </div>
  339. </div>
  340. <script src="../resources/prettify.js"></script>
  341. <script src="../resources/lesson.js"></script>
  342. </body></html>