{"id":476,"date":"2025-12-05T10:30:03","date_gmt":"2025-12-05T02:30:03","guid":{"rendered":"https:\/\/www.52runoob.com\/?p=476"},"modified":"2025-12-05T10:30:03","modified_gmt":"2025-12-05T02:30:03","slug":"three-js%e4%b8%ad%e8%87%aa%e5%ae%9a%e4%b9%89uv%e5%9d%90%e6%a0%87%e8%b4%b4%e5%9b%be%e4%b8%be%e4%be%8b%e8%b6%85%e8%af%a6%e7%bb%86%e8%ae%b2%e8%a7%a3","status":"publish","type":"post","link":"https:\/\/www.52runoob.com\/index.php\/2025\/12\/05\/three-js%e4%b8%ad%e8%87%aa%e5%ae%9a%e4%b9%89uv%e5%9d%90%e6%a0%87%e8%b4%b4%e5%9b%be%e4%b8%be%e4%be%8b%e8%b6%85%e8%af%a6%e7%bb%86%e8%ae%b2%e8%a7%a3\/","title":{"rendered":"Three.js\u4e2d\u81ea\u5b9a\u4e49UV\u5750\u6807\u8d34\u56fe\u4e3e\u4f8b\u8d85\u8be6\u7ec6\u8bb2\u89e3"},"content":{"rendered":"\n<h1 class=\"wp-block-heading\">Three.js \u4e2d\u81ea\u5b9a\u4e49 UV \u5750\u6807\u8d34\u56fe \u2014 \u8d85\u8be6\u7ec6\u4e3e\u4f8b\u4e0e\u89e3\u6790<\/h1>\n\n\n\n<p>\u4e0b\u9762\u662f\u4e00\u4efd\u9762\u5411\u5de5\u7a0b\u5b9e\u8df5\u7684 <strong>Three.js \u81ea\u5b9a\u4e49 UV\uff08\u7eb9\u7406\u5750\u6807\uff09<\/strong> \u6559\u7a0b\uff0c\u4ece\u57fa\u7840\u6982\u5ff5\u3001\u5e38\u89c1\u9677\u9631\u5230 <code>BufferGeometry<\/code> \u7ea7\u522b\u7684\u793a\u4f8b\uff08\u542b\u8d34\u56fe\u56fe\u96c6 atlas\u3001\u6bcf\u9762\u4e0d\u540c\u8d34\u56fe\u3001Lightmap\/uv2\u3001\u7403\u5f62\u5c55\u5f00\u7b49\uff09\u3002\u6bcf\u4e2a\u4ee3\u7801\u793a\u4f8b\u90fd\u53ef\u4ee5\u76f4\u63a5\u590d\u5236\u5230\u4f60\u7684\u5de5\u7a0b\u91cc\u8fd0\u884c\uff08Three.js rXXX \u5e38\u7528 API\uff09\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u6838\u5fc3\u6982\u5ff5\u56de\u987e\uff08\u5fc5\u987b\u5148\u61c2\uff09<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>UV \u662f 2D \u7eb9\u7406\u5750\u6807<\/strong>\uff1a\u901a\u5e38\u8bb0\u4e3a <code>(u, v)<\/code>\uff0c\u8303\u56f4\u901a\u5e38\u662f <code>[0,1]<\/code>\uff08\u53ef\u4ee5\u8d85\u51fa\uff0c\u7528\u4e8e\u5e73\u94fa\/\u91cd\u590d\uff09\u3002<code>u<\/code> \u2192 \u6c34\u5e73\u65b9\u5411\uff08\u7eb9\u7406 X\uff09\uff0c<code>v<\/code> \u2192 \u5782\u76f4\u65b9\u5411\uff08\u7eb9\u7406 Y\uff09\u3002<\/li>\n\n\n\n<li><strong>UV \u5bf9\u6bcf\u4e2a\u9876\u70b9<\/strong>\uff1a\u6bcf\u4e2a\u7f51\u683c\u9876\u70b9\u6709\u4e00\u4e2a UV \u5750\u6807\u3002\u4e0d\u540c\u7684\u4e09\u89d2\u5f62\u5373\u4f7f\u5171\u4eab\u9876\u70b9\uff0c\u4e5f\u53ef\u4ee5\u901a\u8fc7\u9876\u70b9\u62c6\u5206\uff08duplicate vertices\uff09\u6765\u62e5\u6709\u4e0d\u540c UV\uff0c\u4ece\u800c\u5f62\u6210\u201c\u7f1d\u201d\u3002<\/li>\n\n\n\n<li><strong>Three.js \u4e2d\u7528 <code>uv<\/code> \u5c5e\u6027\u5b58\u653e<\/strong>\uff1a<code>BufferGeometry<\/code> \u7684 <code>attributes.uv<\/code> \u662f <code>Float32BufferAttribute<\/code>\uff0c\u957f\u5ea6 = \u9876\u70b9\u6570 \u00d7 2\u3002<\/li>\n\n\n\n<li><strong>\u6ce8\u610f <code>flipY<\/code> \u4e0e\u7eb9\u7406\u65b9\u5411<\/strong>\uff1aThree.js \u52a0\u8f7d\u56fe\u7247\u65f6\u9ed8\u8ba4 <code>texture.flipY = true<\/code>\uff08\u57fa\u4e8e\u4f20\u7edf WebGL\u7eb9\u7406\u5750\u6807\uff09\u3002\u82e5\u4f60\u624b\u5de5\u8bbe\u7f6e UV\uff0c\u6216\u8d34\u56fe\u6765\u81ea\u5916\u90e8\u8f6f\u4ef6\uff0c\u53ef\u80fd\u9700\u8c03\u6574 <code>texture.flipY = false<\/code>\u3002<\/li>\n\n\n\n<li><strong>uv2 \u7528\u4e8e\u5149\u7167\u8d34\u56fe\uff08lightMap\uff09<\/strong>\uff1a\u5982\u679c\u8981\u4f7f\u7528 <code>MeshStandardMaterial.lightMap<\/code>\uff0c\u9700\u8981\u63d0\u4f9b\u7b2c\u4e8c\u7ec4 UV\uff1a<code>uv2<\/code> \u5c5e\u6027\u3002<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\u793a\u4f8b 1 \u2014 \u57fa\u672c\uff1a\u7ed9 <code>Plane<\/code> \u624b\u52a8\u8d4b UV\uff08\u81ea\u5b9a\u4e49\u56db\u89d2\u5750\u6807\uff09<\/h1>\n\n\n\n<p>\u6700\u7b80\u5355\u7684\u573a\u666f\uff1a\u521b\u5efa\u4e00\u4e2a\u5e73\u9762\u5e76\u624b\u52a8\u8bbe\u7f6e\u56db\u4e2a\u9876\u70b9\u7684 UV\uff08\u5de6\u4e0b(0,0) \u53f3\u4e0a(1,1) \u7b49\uff09\uff1a<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n\/\/ \u5047\u8bbe THREE \u5df2\u7ecf\u5bfc\u5165\u5e76\u521b\u5efa\u4e86 scene, camera, renderer\nconst geometry = new THREE.BufferGeometry();\n\n\/\/ \u56db\u4e2a\u9876\u70b9\uff08\u4e24\u4e2a\u4e09\u89d2\u5f62\uff09\nconst positions = new Float32Array(&#x5B;\n  -1, -1, 0, \/\/ 0: \u5de6\u4e0b\n   1, -1, 0, \/\/ 1: \u53f3\u4e0b\n   1,  1, 0, \/\/ 2: \u53f3\u4e0a\n  -1,  1, 0  \/\/ 3: \u5de6\u4e0a\n]);\n\n\/\/ \u4e09\u89d2\u5f62\u7d22\u5f15\nconst indices = new Uint16Array(&#x5B;0, 1, 2, 0, 2, 3]);\n\n\/\/ UV\uff1a\u6309\u9876\u70b9\u987a\u5e8f\u8bbe\u7f6e (u, v)\nconst uvs = new Float32Array(&#x5B;\n  0, 0,  \/\/ \u9876\u70b9 0 -&gt; \u5de6\u4e0b\n  1, 0,  \/\/ \u9876\u70b9 1 -&gt; \u53f3\u4e0b\n  1, 1,  \/\/ \u9876\u70b9 2 -&gt; \u53f3\u4e0a\n  0, 1   \/\/ \u9876\u70b9 3 -&gt; \u5de6\u4e0a\n]);\n\ngeometry.setIndex(new THREE.BufferAttribute(indices, 1));\ngeometry.setAttribute(&#039;position&#039;, new THREE.BufferAttribute(positions, 3));\ngeometry.setAttribute(&#039;uv&#039;, new THREE.BufferAttribute(uvs, 2));\n\nconst texture = new THREE.TextureLoader().load(&#039;texture.jpg&#039;);\nconst mat = new THREE.MeshBasicMaterial({ map: texture });\nconst mesh = new THREE.Mesh(geometry, mat);\nscene.add(mesh);\n\n<\/pre><\/div>\n\n\n<p><strong>\u5173\u952e\u70b9<\/strong>\uff1a<code>uv<\/code> \u7684\u957f\u5ea6\u8981\u7b49\u4e8e\u9876\u70b9\u6570 \u00d7 2\u3002\u82e5 geometry \u6709\u7d22\u5f15\uff08index\uff09\uff0cUV \u662f\u6309\u9876\u70b9\u7d22\u5f15\u5bf9\u5e94\u7684\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\u793a\u4f8b 2 \u2014 \u7f16\u8f91\u5df2\u6709\u51e0\u4f55\u4f53\u7684 UV\uff08\u4f8b\u5982 BoxGeometry \u6bcf\u4e2a\u9762\u5355\u72ec\u8d34\u4e0d\u540c\u533a\u57df\uff09<\/h1>\n\n\n\n<p><code>BoxGeometry<\/code> \u9ed8\u8ba4\u6bcf\u4e2a\u9762\u6709\u81ea\u5df1\u7684 UV\uff0c\u4f46\u5982\u679c\u4f60\u60f3\u81ea\u5b9a\u4e49\u6bcf\u4e2a\u9762\u7684 UV\uff08\u4f8b\u5982\u505a\u56fe\u96c6 atlas\uff0c\u628a\u4e0d\u540c\u9762\u6307\u5411\u8d34\u56fe\u7684\u4e0d\u540c\u533a\u57df\uff09\uff1a<\/p>\n\n\n\n<p>\u5047\u8bbe\u6211\u4eec\u4f7f\u7528\u4e00\u4e2a 2\u00d73 \u7684\u56fe\u96c6\uff086 \u4e2a\u5c0f\u56fe\uff09\uff0c\u6bcf\u4e2a\u9762\u53d6\u5176\u4e2d 1 \u4e2a\u5c0f\u56fe\uff1a<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nconst box = new THREE.BoxGeometry(1, 1, 1);\n\n\/\/ BoxGeometry \u9ed8\u8ba4\u751f\u6210 24 \u4e2a\u9876\u70b9\uff08\u6bcf\u4e2a\u97624\u4e2a\u5404\u81ea\u72ec\u7acb\uff09\u2014\u2014\u8fd9\u65b9\u4fbf\u6bcf\u4e2a\u9762\u6709\u4e0d\u540c UV\n\/\/ \u6211\u4eec\u5c06\u91cd\u5199 box.attributes.uv \u7684\u503c\nconst uvs = box.attributes.uv.array; \/\/ Float32Array\n\/\/ uvs.length = 24 * 2 = 48\n\n\/\/ \u5047\u8bbe\u6211\u4eec\u7684\u56fe\u96c6\u662f 2 \u5217\uff08cols\uff09 \u00d7 3 \u884c\uff08rows\uff09\nconst cols = 2, rows = 3;\nfunction setFaceUV(faceIndex, col, row) {\n  \/\/ faceIndex: 0..5\uff08box \u6709 6 \u4e2a\u9762\uff09\n  \/\/ \u6bcf\u97624\u4e2a\u9876\u70b9\uff0cuv \u5b58\u50a8\u987a\u5e8f\u4e0e BoxGeometry \u6784\u5efa\u65f6\u4e00\u81f4\n  const tileW = 1 \/ cols;\n  const tileH = 1 \/ rows;\n\n  \/\/ \u8ba1\u7b97 uv \u8d77\u59cb\u4e0b\u6807\uff1a\u6bcf\u9762 4 \u9876\u70b9 * 2 \u503c\n  const offset = faceIndex * 4 * 2;\n\n  \/\/ \u5c0f\u56fe\u5de6\u4e0b\u89d2\u7684 uv \u5750\u6807\n  const u0 = col * tileW;\n  const v0 = row * tileH;\n  const u1 = u0 + tileW;\n  const v1 = v0 + tileH;\n\n  \/\/ \u6ce8\u610f Three.js \u7684 UV \u9876\u70b9\u6392\u5217\uff0cBoxGeometry \u987a\u5e8f\u901a\u5e38\u4e3a\uff1a\n  \/\/ 0:(u0,v1), 1:(u1,v1), 2:(u1,v0), 3:(u0,v0)\n  uvs&#x5B;offset + 0] = u0; uvs&#x5B;offset + 1] = v1;\n  uvs&#x5B;offset + 2] = u1; uvs&#x5B;offset + 3] = v1;\n  uvs&#x5B;offset + 4] = u1; uvs&#x5B;offset + 5] = v0;\n  uvs&#x5B;offset + 6] = u0; uvs&#x5B;offset + 7] = v0;\n}\n\n\/\/ \u4e3a 6 \u4e2a\u9762\u5206\u914d\u56fe\u96c6\u683c\u5b50\nsetFaceUV(0, 0, 0);\nsetFaceUV(1, 1, 0);\nsetFaceUV(2, 0, 1);\nsetFaceUV(3, 1, 1);\nsetFaceUV(4, 0, 2);\nsetFaceUV(5, 1, 2);\n\nbox.attributes.uv.needsUpdate = true;\n\nconst texture = new THREE.TextureLoader().load(&#039;atlas.png&#039;);\ntexture.wrapS = THREE.RepeatWrapping;\ntexture.wrapT = THREE.RepeatWrapping;\nconst mat = new THREE.MeshBasicMaterial({ map: texture });\nconst mesh = new THREE.Mesh(box, mat);\nscene.add(mesh);\n\n<\/pre><\/div>\n\n\n<p><strong>\u8981\u70b9\u8bf4\u660e<\/strong>\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>BoxGeometry<\/code> \u9ed8\u8ba4\u5e76\u4e0d\u662f\u6240\u6709\u9762\u5171\u4eab\u9876\u70b9\uff08\u4e3a\u4e86\u4fbf\u4e8e\u6bcf\u9762\u72ec\u7acb\u8d34\u56fe\u5b83\u4f1a\u62c6\u5206\u9876\u70b9\uff09\uff0c\u56e0\u6b64\u76f4\u63a5\u64cd\u4f5c <code>attributes.uv<\/code> \u5f88\u65b9\u4fbf\u3002<\/li>\n\n\n\n<li>\u56fe\u96c6\u5750\u6807\u7684 <code>v<\/code> \u65b9\u5411\u662f\u5426\u9700\u8981\u7ffb\u8f6c\u53d6\u51b3\u4e8e\u56fe\u7247\u548c <code>texture.flipY<\/code>\u3002\u82e5\u4f60\u53d1\u73b0\u8d34\u53cd\u4e86\uff0c\u8bd5\u8bd5 <code>texture.flipY = false<\/code> \u6216\u8c03\u6574 <code>v<\/code> \u7684\u4f7f\u7528\uff08<code>v1<\/code> \u2194 <code>v0<\/code> \u4ea4\u6362\uff09\u3002<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\u793a\u4f8b 3 \u2014 \u4f7f\u7528\u56fe\u96c6 (Sprite Atlas) \u7684\u901a\u7528\u51fd\u6570<\/h1>\n\n\n\n<p>\u5e38\u89c1\u573a\u666f\uff1a\u4e00\u4e2a\u5927\u56fe\u7247\u91cc\u6709 <code>cols \u00d7 rows<\/code> \u4e2a\u5c0f\u56fe\uff0c\u4f60\u60f3\u628a\u67d0\u4e2a sprite \u7d22\u5f15\u6620\u5c04\u5230 UV\uff1a<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nfunction computeAtlasUV(colIndex, rowIndex, cols, rows) {\n  const tileW = 1 \/ cols;\n  const tileH = 1 \/ rows;\n  const u0 = colIndex * tileW;\n  const v0 = rowIndex * tileH;\n  const u1 = u0 + tileW;\n  const v1 = v0 + tileH;\n  return { u0, v0, u1, v1 };\n}\n\n\/\/ Example: \u5c06\u67d0\u4e2a\u5e73\u9762\u6307\u5411 atlas \u7b2c 3 \u4e2a\u683c\u5b50\uff08\u6a2a\u4f18\u5148\uff09\nconst idx = 2;\nconst cols = 4, rows = 4;\nconst col = idx % cols;\nconst row = Math.floor(idx \/ cols);\nconst { u0, v0, u1, v1 } = computeAtlasUV(col, row, cols, rows);\n\/\/ \u7136\u540e\u50cf\u4e0a\u9762\u90a3\u6837\u5199\u5165 uv buffer\uff08\u6ce8\u610f v \u534f\u8c03\u65b9\u5411\uff09\n\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\u793a\u4f8b 4 \u2014 \u4fee\u6539\u5df2\u6709\u51e0\u4f55\u4f53\uff08<code>geometry.attributes.uv<\/code>\uff09\u6ce8\u610f\u4e8b\u9879\u4e0e\u8c03\u8bd5\u65b9\u6cd5<\/h1>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u82e5 <code>geometry<\/code> \u6709\u7d22\u5f15\uff08<code>geometry.index<\/code>\uff09\uff0cUV \u6570\u7ec4\u5bf9\u5e94\u9876\u70b9\u6570\u7ec4\uff0c\u4e0d\u662f\u4e09\u89d2\u5f62\u7d22\u5f15\uff1b\u7d22\u5f15\u4ec5\u51b3\u5b9a\u7ed8\u5236\u987a\u5e8f\u3002<\/li>\n\n\n\n<li>\u4fee\u6539 <code>geometry.attributes.uv.array[...]<\/code> \u540e\u5fc5\u987b <code>geometry.attributes.uv.needsUpdate = true;<\/code><\/li>\n\n\n\n<li>\u4f7f\u7528 <code>console.log(geometry.attributes.uv.array)<\/code> \u53ef\u4ee5\u8c03\u8bd5 UV \u503c\u3002<\/li>\n\n\n\n<li>\u5982\u679c\u4f60\u770b\u5230\u7eb9\u7406\u62c9\u4f38\u6216\u7f1d\u9699\uff0c\u901a\u5e38\u539f\u56e0\u662f\uff1a\u9876\u70b9\u6ca1\u6709\u4e3a\u9762\u62c6\u5206\uff08\u5373\u76f8\u90bb\u9762\u5171\u4eab\u540c\u4e00\u4e2a\u9876\u70b9\u4f46\u9700\u8981\u4e0d\u540c UV\uff09\uff0c\u8fd9\u65f6\u9700\u8981\u62f7\u8d1d\u9876\u70b9\uff08duplicate vertices\uff09\u6216\u5bf9 <code>BufferGeometry<\/code> \u505a\u5c55\u5f00\u3002<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\u793a\u4f8b 5 \u2014 \u7ed9\u81ea\u5b9a\u4e49 <code>BufferGeometry<\/code> \u76f4\u63a5\u8bbe UV\uff08\u65e0\u7d22\u5f15\u793a\u4f8b\uff09<\/h1>\n\n\n\n<p>\u5f88\u591a\u6559\u7a0b\u4f7f\u7528\u201c\u975e\u7d22\u5f15\uff08non-indexed\uff09\u201d\u4e09\u89d2\u5f62\u6570\u7ec4\uff0c\u8fd9\u65f6\u6bcf\u4e2a\u4e09\u89d2\u5f62\u7684\u9876\u70b9\u90fd\u662f\u72ec\u7acb\u7684\uff0c\u8bbe UV \u66f4\u76f4\u89c2\uff1a<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nconst geometry = new THREE.BufferGeometry();\n\nconst positions = new Float32Array(&#x5B;\n  \/\/ \u4e09\u89d2\u5f62 A\n  -1, -1, 0,\n   1, -1, 0,\n   0,  1, 0,\n  \/\/ \u4e09\u89d2\u5f62 B (\u53e6\u4e00\u4e2a)\n  \/\/ ...\n]);\n\nconst uvs = new Float32Array(&#x5B;\n  \/\/ \u5bf9\u5e94 \u4e09\u89d2\u5f62 A \u7684\u4e09\u4e2a\u9876\u70b9\n  0, 0,\n  1, 0,\n  0.5, 1,\n  \/\/ \u4e09\u89d2\u5f62 B \u7684 uv...\n]);\n\ngeometry.setAttribute(&#039;position&#039;, new THREE.BufferAttribute(positions, 3));\ngeometry.setAttribute(&#039;uv&#039;, new THREE.BufferAttribute(uvs, 2));\n\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\u793a\u4f8b 6 \u2014 uv2\uff08\u7b2c\u4e8c\u7ec4 UV\uff09\u7528\u4e8e\u5149\u7167\u8d34\u56fe\uff08Lightmap\uff09<\/h1>\n\n\n\n<p>\u5f53\u4f60\u6709\u4e00\u4e2a\u9884\u70d8\u7119\u7684 lightmap\uff08\u5149\u7167\u8d34\u56fe\uff09\uff0c\u9700\u8981\u628a\u5b83\u7ed1\u5b9a\u5230\u6750\u8d28\u7684 <code>lightMap<\/code> \u4e0a\uff0c\u4e14 <code>MeshStandardMaterial<\/code> \u6216 <code>MeshLambertMaterial<\/code> \u7b49\u9700\u8981\u7b2c\u4e8c\u7ec4 UV (<code>uv2<\/code>)\uff1a<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n\/\/ geometry \u5df2\u7ecf\u6709 geometry.attributes.uv\n\/\/ \u5047\u8bbe\u4f60\u5df2\u7ecf\u7528\u5efa\u6a21\u5de5\u5177\u751f\u6210\u4e86 second UV set\uff0c\u5e76\u653e\u5230 geometry.userData \u6216\u6587\u4ef6\u4e2d\u3002\n\/\/ \u7b80\u5355\u65b9\u6cd5\uff1a\u628a uv \u590d\u5236\u4e00\u4efd\u6210 uv2\uff08\u6ce8\u610f\uff1a\u8fd9\u53ea\u662f\u590d\u5236 uv\uff0c\u4e0d\u662f\u6b63\u786e\u7684 lightmap UV \u5c55\u5f00\uff09\ngeometry.setAttribute(&#039;uv2&#039;, geometry.attributes.uv.clone());\n\nconst texture = new THREE.TextureLoader().load(&#039;albedo.jpg&#039;);\nconst lightMap = new THREE.TextureLoader().load(&#039;lightmap.jpg&#039;);\n\nconst mat = new THREE.MeshStandardMaterial({\n  map: texture,\n  lightMap: lightMap,\n  lightMapIntensity: 1.0\n});\n\nconst mesh = new THREE.Mesh(geometry, mat);\nscene.add(mesh);\n\n<\/pre><\/div>\n\n\n<p><strong>\u6ce8\u610f<\/strong>\uff1a\u6b63\u786e\u7684 <code>uv2<\/code> \u901a\u5e38\u8981\u5728 3D \u5efa\u6a21\u8f6f\u4ef6\u4e2d\u4e3a lightmap \u751f\u6210\u65e0\u91cd\u53e0\u3001\u6700\u5927\u5316\u7a7a\u95f4\u7684\u7b2c\u4e8c\u7ec4 UV\uff1b\u7b80\u5355\u590d\u5236 <code>uv<\/code> \u53ea\u662f\u793a\u8303\u7528\u9014\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\u793a\u4f8b 7 \u2014 \u7403\u9762\u6216\u5706\u67f1\u7684 UV \u7b97\u6cd5\uff08\u624b\u5199\u5c55\u5f00\uff09<\/h1>\n\n\n\n<p>\u5982\u679c\u4f60\u6709\u81ea\u5b9a\u4e49\u7403\u4f53\u9876\u70b9\uff0c\u4f60\u53ef\u4ee5\u624b\u5de5\u8ba1\u7b97 UV\uff08\u7403\u9762\u5c55\u5f00\u5e38\u7528\u516c\u5f0f\uff09\uff1a<\/p>\n\n\n\n<p>\u5bf9\u4e8e\u7403\u9762\u4e0a\u70b9 <code>(x, y, z)<\/code>\uff08\u5355\u4f4d\u7403\uff09\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>u = 0.5 + atan2(z, x) \/ (2\u03c0)<\/code><\/li>\n\n\n\n<li><code>v = 0.5 - asin(y) \/ \u03c0<\/code><\/li>\n<\/ul>\n\n\n\n<p>\u793a\u4f8b\uff08\u628a\u9876\u70b9\u6570\u7ec4\u53d8\u4e3a UV\uff09\uff1a<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nfunction computeSphericalUV(x, y, z) {\n  const u = 0.5 + Math.atan2(z, x) \/ (2 * Math.PI);\n  const v = 0.5 - Math.asin(y) \/ Math.PI;\n  return &#x5B;u, v];\n}\n\n\/\/ \u5047\u8bbe positions \u662f Float32Array\uff08\u6bcf 3 \u4e2a\u4e00\u7ec4\uff09\nconst pos = geometry.attributes.position.array;\nconst count = pos.length \/ 3;\nconst uvs = new Float32Array(count * 2);\nfor (let i = 0; i &amp;lt; count; i++) {\n  const x = pos&#x5B;i * 3 + 0];\n  const y = pos&#x5B;i * 3 + 1];\n  const z = pos&#x5B;i * 3 + 2];\n  const &#x5B;u, v] = computeSphericalUV(x, y, z);\n  uvs&#x5B;i * 2 + 0] = u;\n  uvs&#x5B;i * 2 + 1] = v;\n}\ngeometry.setAttribute(&#039;uv&#039;, new THREE.BufferAttribute(uvs, 2));\ngeometry.attributes.uv.needsUpdate = true;\n\n<\/pre><\/div>\n\n\n<p><strong>\u88c2\u7f1d\/\u7f1d\u5408\u95ee\u9898<\/strong>\uff1a\u7403\u9762\u7684 <code>atan2<\/code> \u4f1a\u5728\u7ecf\u5ea6\u8fb9\u754c\u4ea7\u751f <code>u<\/code> \u7684\u8df3\u8dc3\uff080 vs 1\uff09\uff0c\u82e5\u4e24\u8fb9\u5171\u4eab\u9876\u70b9\uff0c\u9700\u8981\u628a\u9876\u70b9\u62c6\u5206\uff08\u590d\u5236\u4e00\u7ec4\u9876\u70b9\uff0c\u5206\u522b\u7ed9 u\u22480 \u548c u\u22481\uff09\uff0c\u4ee5\u907f\u514d\u4e09\u89d2\u5f62\u8de8\u8d8a\u7ecf\u5ea6\u51fa\u73b0\u7eb9\u7406\u62c9\u4f38\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\u793a\u4f8b 8 \u2014 \u52a8\u6001\u8fd0\u884c\u65f6\u6539\u53d8 UV\uff08\u52a8\u753b\u3001\u6eda\u52a8\u7eb9\u7406\u4f46\u4fdd\u7559\u51e0\u4f55\u4e0d\u53d8\uff09<\/h1>\n\n\n\n<p>\u5e38\u7528\u4e8e\u505a\u6d41\u6c34\u3001\u8dd1\u9a6c\u706f\u3001\u79fb\u52a8\u56fe\u5c42\uff1a\u76f4\u63a5\u4fee\u6539 <code>uv<\/code> \u6216\u4f7f\u7528 <code>texture.offset<\/code>\/<code>texture.repeat<\/code>\u3002<\/p>\n\n\n\n<p><strong>\u65b9\u6cd5 A\uff1a\u4fee\u6539 texture.offset\uff08\u6700\u7b80\u5355\uff09<\/strong><\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\ntexture.wrapS = texture.wrapT = THREE.RepeatWrapping;\ntexture.repeat.set(1, 1);\n\nfunction animate() {\n  texture.offset.x += 0.01; \/\/ x \u65b9\u5411\u6eda\u52a8\n  renderer.render(scene, camera);\n  requestAnimationFrame(animate);\n}\nanimate();\n\n<\/pre><\/div>\n\n\n<p><strong>\u65b9\u6cd5 B\uff1a\u76f4\u63a5\u4fee\u6539\u9876\u70b9 UV\uff08\u66f4\u7075\u6d3b\uff0c\u6bd4\u5982\u5728 UV \u4e0a\u505a\u6ce2\u52a8\uff09<\/strong><\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nconst uvs = geometry.attributes.uv.array;\nfor (let i = 0; i &amp;lt; uvs.length; i += 2) {\n  uvs&#x5B;i + 1] += Math.sin(time + i) * 0.001; \/\/ \u5fae\u52a8 v\n}\ngeometry.attributes.uv.needsUpdate = true;\n\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\u5e38\u89c1\u5751\u4e0e\u8c03\u8bd5\u6e05\u5355\uff08\u52a1\u5fc5\u770b\uff09<\/h1>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>UV\u8303\u56f4\u4e0e Wrap<\/strong>\uff1a\u5982\u679c UV \u8d85\u51fa <code>[0,1]<\/code>\uff0c\u8bbe\u7f6e <code>texture.wrapS\/T = THREE.RepeatWrapping<\/code> \u624d\u4f1a\u5e73\u94fa\uff1b\u9ed8\u8ba4\u662f <code>ClampToEdge<\/code>\u3002<\/li>\n\n\n\n<li><strong>\u7eb9\u7406\u98a0\u5012\u95ee\u9898<\/strong>\uff1a\u56fe\u7247\u770b\u8d77\u6765\u4e0a\u4e0b\u98a0\u5012\uff1f\u8bd5 <code>texture.flipY = false<\/code> \u6216\u4ea4\u6362 <code>v<\/code> \u503c\u3002<\/li>\n\n\n\n<li><strong>\u5171\u4eab\u9876\u70b9\u5bfc\u81f4\u8d34\u56fe\u9519\u4f4d<\/strong>\uff1a\u82e5\u76f8\u90bb\u9762\u5171\u4eab\u9876\u70b9\u4f46\u9700\u8981\u4e0d\u540c UV\uff0c\u5fc5\u987b\u5728\u51e0\u4f55\u4f53\u7ea7\u522b\u590d\u5236\u9876\u70b9\uff08\u5373\u4f7f\u4f4d\u7f6e\u76f8\u540c\uff09\uff0c\u4ece\u800c UV \u53ef\u4ee5\u4e0d\u540c\u3002<\/li>\n\n\n\n<li><strong>\u7d22\u5f15 vs \u975e\u7d22\u5f15<\/strong>\uff1a\u82e5\u4f7f\u7528 <code>geometry.toNonIndexed()<\/code>\uff0c\u6bcf\u4e2a\u4e09\u89d2\u5f62\u4f1a\u6709\u72ec\u7acb\u9876\u70b9\uff0c\u4fbf\u4e8e\u76f4\u63a5\u6309\u4e09\u89d2\u5f62\u8bbe\u7f6e UV\uff08\u4e0d\u8fc7\u9876\u70b9\u91cd\u590d\u589e\u591a\uff09\u3002<\/li>\n\n\n\n<li><strong>lightMap \u9700\u8981 uv2<\/strong>\uff1a\u6ca1\u6709 UV2 \u4f1a\u5bfc\u81f4 lightMap \u65e0\u6548\u6216\u9519\u8bef\u3002<\/li>\n\n\n\n<li><strong>Precision\/\u8fb9\u7f18\u50cf\u7d20<\/strong>\uff1a\u56fe\u96c6tile\u8fb9\u7f18\u5982\u679c\u6ca1\u6709\u7559\u767d\uff0c\u7ebf\u6027\u6ee4\u6ce2\uff08LinearFilter\uff09\u4f1a\u6ea2\u51fa\u76f8\u90bb\u50cf\u7d20\u5bfc\u81f4\u201c\u7f1d\u201d\u3002\u89e3\u51b3\uff1a\u5728 atlas \u4e2d\u4e3a\u6bcf\u4e2a tile \u9884\u7559 padding \u6216\u4f7f\u7528 nearest filter\uff08\u4f46\u4f1a\u6a21\u7cca\uff09\u3002<\/li>\n\n\n\n<li><strong>\u8d34\u56fe Wrap \u4e0e Mipmaps<\/strong>\uff1a\u4f7f\u7528 Repeat + Mipmaps \u65f6\u6ce8\u610f\u8fb9\u754c\u50cf\u7d20\u91c7\u6837\uff0c\u4ecd\u53ef\u80fd\u53d6\u5230\u90bb\u8fd1 tile \u7684\u50cf\u7d20 \u2014\u2014 atlas \u8981\u6709 padding\u3002<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\u8fdb\u9636\uff1a\u4f7f\u7528\u7740\u8272\u5668\u5728 UV \u4e0a\u505a\u66f4\u591a\u64cd\u4f5c<\/h1>\n\n\n\n<p>\u5728 <code>ShaderMaterial<\/code> \u6216 <code>onBeforeCompile<\/code> \u4e2d\u76f4\u63a5\u53d8\u6362\u4f20\u5165\u7684 <code>vUv<\/code>\uff1a<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n\/\/ vertex shader\nvarying vec2 vUv;\nvoid main() {\n  vUv = uv;\n  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n}\n\n\/\/ fragment shader\nuniform sampler2D map;\nvarying vec2 vUv;\nvoid main() {\n  vec2 uv = vUv;\n  \/\/ \u4f8b\u5982\u65cb\u8f6c\u7eb9\u7406\u4e2d\u5fc3\n  float angle = 0.5;\n  vec2 center = vec2(0.5);\n  uv -= center;\n  uv = mat2(cos(angle), -sin(angle), sin(angle), cos(angle)) * uv;\n  uv += center;\n  gl_FragColor = texture2D(map, uv);\n}\n\n<\/pre><\/div>\n\n\n<p>\u8fd9\u6837\u4f60\u53ef\u4ee5\u5728 shader \u91cc\u4f7f\u7528 UV \u505a\u4efb\u610f\u6548\u679c\uff08\u65cb\u8f6c\u3001\u7f29\u653e\u3001\u6ce2\u52a8\u3001\u5207\u7247\u7b49\uff09\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\u5c0f\u7ed3\uff08\u6377\u5f84 &amp; \u63a8\u8350\u5b9e\u8df5\uff09<\/h1>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\u7b80\u5355\u8d34\u56fe\u53d8\u6362<\/strong>\uff1a\u4f18\u5148 <code>texture.offset<\/code> &amp; <code>texture.repeat<\/code>\uff08\u4e0d\u6539\u53d8\u51e0\u4f55\uff09\u3002<\/li>\n\n\n\n<li><strong>\u6bcf\u9762\u4e0d\u540c\u8d34\u56fe\/\u56fe\u96c6<\/strong>\uff1a\u4fee\u6539 <code>attributes.uv<\/code>\uff08BoxGeometry \u901a\u5e38\u5df2\u62c6\u5206\u9876\u70b9\uff0c\u76f4\u63a5\u4fee\u6539\u6570\u7ec4\uff09\u3002<\/li>\n\n\n\n<li><strong>\u590d\u6742 unwrap \/ lightmap<\/strong>\uff1a\u5728\u5efa\u6a21\u5de5\u5177\uff08Blender\u3001Maya\uff09\u4e2d\u5c55\u5f00 UV\uff0c\u5e76\u5bfc\u51fa\u5230\u6a21\u578b\u6587\u4ef6\uff08<code>.glb\/.gltf<\/code>\uff09\u4ee5\u4fdd\u7559 <code>uv2<\/code> \u7b49\u3002<\/li>\n\n\n\n<li><strong>\u907f\u514d atlas \u8fb9\u7f18\u6c61\u67d3<\/strong>\uff1a\u4e3a\u6bcf\u4e2a tile \u6dfb\u52a0 padding\uff0c\u6216\u5728 sampling \u65f6\u4f7f\u7528 clamp\/nearest\uff0c\u6839\u636e\u9700\u8981\u8c03\u6574\u3002<\/li>\n\n\n\n<li><strong>\u8c03\u8bd5\u6280\u5de7<\/strong>\uff1a\u5728 <code>fragment shader<\/code> \u4e2d\u8f93\u51fa <code>vUv<\/code> \u989c\u8272\uff08<code>gl_FragColor = vec4(vUv, 0.0,1.0);<\/code>\uff09\uff0c\u76f4\u63a5\u89c2\u5bdf UV \u5206\u5e03\u3002<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Three.js \u4e2d\u81ea\u5b9a\u4e49 UV \u5750\u6807\u8d34\u56fe \u2014 \u8d85\u8be6\u7ec6\u4e3e\u4f8b\u4e0e\u89e3\u6790 \u4e0b\u9762\u662f\u4e00\u4efd\u9762\u5411&#8230; <a class=\"more-link\" href=\"https:\/\/www.52runoob.com\/index.php\/2025\/12\/05\/three-js%e4%b8%ad%e8%87%aa%e5%ae%9a%e4%b9%89uv%e5%9d%90%e6%a0%87%e8%b4%b4%e5%9b%be%e4%b8%be%e4%be%8b%e8%b6%85%e8%af%a6%e7%bb%86%e8%ae%b2%e8%a7%a3\/\">Continue Reading &rarr;<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[49],"tags":[],"class_list":["post-476","post","type-post","status-publish","format-standard","hentry","category-javascript"],"amp_enabled":true,"_links":{"self":[{"href":"https:\/\/www.52runoob.com\/index.php\/wp-json\/wp\/v2\/posts\/476","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.52runoob.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.52runoob.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.52runoob.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.52runoob.com\/index.php\/wp-json\/wp\/v2\/comments?post=476"}],"version-history":[{"count":1,"href":"https:\/\/www.52runoob.com\/index.php\/wp-json\/wp\/v2\/posts\/476\/revisions"}],"predecessor-version":[{"id":477,"href":"https:\/\/www.52runoob.com\/index.php\/wp-json\/wp\/v2\/posts\/476\/revisions\/477"}],"wp:attachment":[{"href":"https:\/\/www.52runoob.com\/index.php\/wp-json\/wp\/v2\/media?parent=476"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.52runoob.com\/index.php\/wp-json\/wp\/v2\/categories?post=476"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.52runoob.com\/index.php\/wp-json\/wp\/v2\/tags?post=476"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}