Three.js 无限地面网格(WebGPU)
Three.js 无限地面网格基于WebGPUTSL实现的无限地面网格0. 先看最终效果关键特征你走一步网格也跟着走一步 → 永远在你脚下近处的线很清晰远处的线慢慢淡出有粗线和细线两层像坐标纸一样1. 先搞清楚网格到底是个什么东西别想复杂了。网格本质上就是一块跟着你走的大地板。用生活来类比想象你在一个巨大的操场上手里拿着一张A4 大小的坐标纸。你把纸放在脚下低头看——纸上画满了格子。你往前走 10 米纸也跟着你往前挪 10 米。你再低头看——纸上还是画满格子跟刚才一模一样。这就是无限网格的全部秘密不是操场上有画满格子的地面而是你手里那张纸一直跟着你在代码里那张纸 一个 200×200 的大平面PlaneGeometry纸上的格子 用数学公式在 GPU 里画出来的线跟着你走 每帧把平面的位置设成相机的位置2. 搭一个最简框架先让平面出现在场景里我们先不画线先把那张纸铺在地上。import*asTHREEfromthree/webgpu;// 步骤1做一个 200×200 的大平面constgeometrynewTHREE.PlaneGeometry(200,200);// 步骤2旋转一下让它平躺在地上默认是竖着的geometry.rotateX(-Math.PI/2);// 为什么要转PlaneGeometry 默认是 XY 平面竖着的墙// 转 -90° 就变成 XZ 平面平躺的地板// 步骤3给平面一个材质先随便给个颜色看得见就行constmaterialnewTHREE.MeshBasicNodeMaterial();// 步骤4组合成网格对象constgridMeshnewTHREE.Mesh(geometry,material);// 步骤5扔进场景scene.add(gridMesh);现在你的场景里多了一个巨大的平面。但它还只是个纯色的板子没有格子。3. 在平面上画线——认识两个神奇的数学工具画格子的代码写在着色器Shader里。着色器是在GPU上运行的小程序专门负责计算每个像素的颜色。我们用的不是传统的 GLSL 写法而是 Three.js 的TSL节点式写法看起来更像普通 JavaScript。但在写代码之前先认识两个数学工具工具一fract(x)—— 把数字切成无限重复的片段fract的意思是取小数部分。fract(0.0) 0.0 fract(0.3) 0.3 fract(0.9) 0.9 fract(1.0) 0.0 ← 回到 0 fract(1.2) 0.2 fract(2.7) 0.7 fract(3.0) 0.0 ← 又回到 0它的作用把任意大的数字折叠回 0 到 1 之间。就像钟表不管现在是 1 点、13 点还是 25 点时针都指在同一个位置。fract就是给数字做取模运算。这对画网格有什么用网格线每隔 1 米就要重复一次。fract(世界坐标)正好把你离原点多少米折叠成你离最近的那条网格线多少米。工具二fwidth(x)—— 自动计算线条该多粗这个更难理解但你可以先把它当成魔法。fwidth(x)回答的问题是“在屏幕上相邻两个像素之间x 的值差了多少”屏幕上的两个相邻像素 ┌──┐ ┌──┐ x 的值: 0.1 0.3 fwidth(x) |0.3 - 0.1| 0.2用它来做什么让线条无论屏幕分辨率高低始终只占 1 个像素宽。你不需要完全理解它。先记住某个值 / fwidth(那个值) 自动适配线宽 自动抗锯齿。后面会看到怎么用。4. 画第一条线——只画 X 方向的细线我们来一步步推导。先只画一个方向的线比如 X 方向让你看清每一行代码在做什么。第一步把世界坐标按间距缩放constr1xwp.x.div(uSize1);// 世界坐标 X ÷ 1 X 本身假设uSize1 1每 1 米一条线相机站在 (3.4, 0, 5.2)wp.x 3.4 → r1x 3.4 wp.x 4.0 → r1x 4.0 ← 在网格线上 wp.x 4.7 → r1x 4.7第二步用 fract 找离最近网格线有多近fract(r1x-0.5)-0.5来算几个例子r1x 3.4: fract(3.4 - 0.5) - 0.5 fract(2.9) - 0.5 0.9 - 0.5 0.4 r1x 4.0: fract(4.0 - 0.5) - 0.5 fract(3.5) - 0.5 0.5 - 0.5 0.0 ← 正好在线上 r1x 4.7: fract(4.7 - 0.5) - 0.5 fract(4.2) - 0.5 0.2 - 0.5 -0.3发现了什么当坐标正好在整数位置即网格线位置时结果 0。第三步取绝对值abs(fract(r1x-0.5)-0.5)r1x 3.4: | 0.4| 0.4 r1x 4.0: | 0.0| 0.0 ← 线上 0 r1x 4.7: |-0.3| 0.3第四步除以 fwidth魔法时刻abs(fract(r1x-0.5)-0.5)/fwidth(r1x)这个除法的效果在网格线上值 ≈ 00 ÷ fwidth 很小 → 黑色像素 → 线条 在两线中间值 ≈ 0.50.5 ÷ fwidth ≈ 很大 → 白色像素 → 空白这就是画线的全部秘密不是真的画一条线而是让每个像素自己计算——“我离最近的线有多近越近越黑。”5. 画完整的细网格X Z 两个方向上面只画了 X 方向。加上 Z 方向就得到完整的格子// X 方向constr1xwp.x.div(uSize1);// Z 方向constr1zwp.z.div(uSize1);// 合并成二维向量constr1vec2(r1x,r1z);// 两个方向同时计算constgr1abs(fract(r1.sub(0.5)).sub(0.5)).div(fwidth(r1));// gr1.x X 方向的离线条距离// gr1.z Z 方向的离线条距离// 取两个方向中离线条更近的那个constg1float(1).sub(min(min(gr1.x,gr1.y),1));用大白话解释最后一行g1表示这个像素在不在一根细线上。如果在线上不管 X 方向还是 Z 方向→g1 ≈ 1不透明如果不在线上 →g1 ≈ 0完全透明6. 加入粗网格——复制粘贴再改个间距粗网格和细网格的代码一模一样只改一个数字——间距从 1 改成 10// 细网格间距 1constr1vec2(wp.x.div(uSize1),wp.z.div(uSize1));// ...// 粗网格间距 10 ← 只有这个数字不一样constr2vec2(wp.x.div(uSize2),wp.z.div(uSize2));constgr2abs(fract(r2.sub(0.5)).sub(0.5)).div(fwidth(r2));constg2float(1).sub(min(min(gr2.x,gr2.y),1));效果细线每 1 米一条粗线每 10 米一条。粗线刚好和某些细线重合10 的倍数位置在那些位置上粗线覆盖细线。7. 让远处淡出——距离衰减现在网格延伸到无限远不太真实。我们要让远处慢慢变透明// 计算当前像素到相机的距离只算水平方向constdxwp.x.sub(cp.x);// 水平 X 距离constdzwp.z.sub(cp.z);// 水平 Z 距离constdistdx.mul(dx).add(dz.mul(dz)).sqrt();// 勾股定理√(dx² dz²)// 归一化距离 ÷ 最大可见距离constdistNormdist.div(uDistance);// 0 脚下, 1 边界, 1 超出// 反转1 - 距离近处1远处0constdfloat(1).sub(min(distNorm,1));// 三次方让近处更清晰远处快速衰减constfadepow(d,3);为什么要三次方对比一下线性d¹: 10m处90%可见, 50m处50%可见, 90m处10%可见 ← 过渡太慢 三次方d³: 10m处73%可见, 50m处12%可见, 90m处0.1%可见 ← 近处清晰远处快消失8. 合成所有效果——拼图时刻现在我们有g1细网格蒙版在细线上1不在0g2粗网格蒙版在粗线上1不在0fade距离衰减系数近处1远处0把它们拼在一起// 步骤1细线优先。有细线g1≈1就用它没有就用粗线letalphamix(g2,g1,g1).mul(fade);// ↑ ↑ ↑ ↑// 粗线 细线 权重 距离衰减// 解读g11 → 选 g1细线g10 → 选 g2粗线// 步骤2粗线处翻倍透明度让它更显眼alphamix(alpha.mul(0.5),alpha,g2);// 普通区域半透明 粗线处全不透明 只在粗线处生效// 步骤3颜色也混合——粗线处更白constcolmix(uColorFine.rgb,uColorCoarse.rgb,g2);// 灰色 #888888 白色 #ffffff 粗线处混入白色// 步骤4输出最终颜色returnvec4(col,alpha);9. 配置材质——最后一步设置constmaterialnewTHREE.MeshBasicNodeMaterial();material.colorNodecolorNode;// 把我们写的着色器挂上去material.transparenttrue;// 开启透明不然看不到线之间的空隙material.depthWritefalse;// 不写深度网格后面的东西不会被挡住还有几个重要设置this.frustumCulledfalse;// 不做视锥裁剪防止平面被意外切掉this.renderOrder-99999;// 最先渲染让其他物体画在网格上面10. 让网格跟着相机走在渲染循环里每帧执行update(camera){// 对齐到整数坐标防止线条抖动this.position.xMath.round(camera.position.x);this.position.zMath.round(camera.position.z);// 稍微低于地面避免和其他物体重叠闪烁this.position.y-0.01;}为什么用Math.round不用 round直接跟相机 用了 round对齐到整数 相机在 (3.4, 0, 5.2) 相机在 (3.4, 0, 5.2) 网格在 (3.4, 0, 5.2) 网格在 (3.0, 0, 5.0) → 线条在 3.4 位置 → 线条在 3.0 位置 → 下一帧相机动了线条又变了 → 线条位置始终稳定 → 看起来在抖因为着色器用fract(世界坐标)生成线条平面移动了 0.1 米线条也移动 0.1 米。对齐到整数后线条始终落在整数位置不会抖。11. 完整代码可以直接复制使用import*asTHREEfromthree/webgpu;import{Fn,uniform,vec2,vec4,float,positionWorld,cameraPosition,fract,fwidth,abs,min,mix,pow,}fromthree/tsl;exportclassCustomGridextendsTHREE.Mesh{constructor(size11,// 细线间距默认 1 米size210,// 粗线间距默认 10 米colorFine#888888,// 细线颜色colorCoarse#ffffff,// 粗线颜色distance100,// 最远可见距离){// 1 创建大平面躺在地上constgeometrynewTHREE.PlaneGeometry(distance*2,distance*2);geometry.rotateX(-Math.PI/2);// 2 准备参数传给 GPU 的变量constuSize1uniform(size1);constuSize2uniform(size2);constuColorFineuniform(newTHREE.Color(colorFine));constuColorCoarseuniform(newTHREE.Color(colorCoarse));constuDistanceuniform(distance);// 3 着色器计算每个像素的颜色constcolorNodeFn((){constwppositionWorld;// 当前像素在世界中的位置constcpcameraPosition;// 相机位置// --- 距离衰减 ---constdxwp.x.sub(cp.x);constdzwp.z.sub(cp.z);constdistdx.mul(dx).add(dz.mul(dz)).sqrt().div(uDistance);constdfloat(1).sub(min(dist,1));constfadepow(d,3);// --- 细网格 ---constr1vec2(wp.x.div(uSize1),wp.z.div(uSize1));constgr1abs(fract(r1.sub(0.5)).sub(0.5)).div(fwidth(r1));constg1float(1).sub(min(min(gr1.x,gr1.y),1));// --- 粗网格 ---constr2vec2(wp.x.div(uSize2),wp.z.div(uSize2));constgr2abs(fract(r2.sub(0.5)).sub(0.5)).div(fwidth(r2));constg2float(1).sub(min(min(gr2.x,gr2.y),1));// --- 合成 ---letalphamix(g2,g1,g1).mul(fade);alphamix(alpha.mul(0.5),alpha,g2);constcolmix(uColorFine.rgb,uColorCoarse.rgb,g2);returnvec4(col,alpha);})();// 4 材质配置constmaterialnewTHREE.MeshBasicNodeMaterial();material.colorNodecolorNode;material.transparenttrue;material.depthWritefalse;super(geometry,material);this.frustumCulledfalse;this.renderOrder-99999;}// 5 每帧更新跟着相机走update(camera){this.position.xMath.round(camera.position.x);this.position.zMath.round(camera.position.z);this.position.y-0.01;}dispose(){this.material.dispose();this.geometry.dispose();if(this.parent)this.removeFromParent();}}使用方式// 创建网格constgridnewCustomGrid();scene.add(grid);// 在渲染循环中functionanimate(){requestAnimationFrame(animate);grid.update(camera);// ← 每帧调用让网格跟着相机renderer.render(scene,camera);}animate();12 答疑问题答案为什么不用真的画线段性能。画几万条线段 GPU 扛不住着色器算一个公式就能搞定所有像素fract 和 fwidth 一定要懂原理吗刚开始不需要。先用起来看到效果后再慢慢理解能在 WebGL 上用吗可以把 TSL 写法换成 GLSL 字符串数学原理完全一样怎么改线的颜色改colorFine和colorCoarse参数怎么改线间距改size1细线间距和size2粗线间距参考https://github.com/Fyrestar/THREE.InfiniteGridHelper

相关新闻

“算能 BM1684X 嵌入式主板 EC-A1684XJD4 FD 版:32TOPS 满血配置,42W 满负载“

“算能 BM1684X 嵌入式主板 EC-A1684XJD4 FD 版:32TOPS 满血配置,42W 满负载“

算能 BM1684X 嵌入式主板 EC-A1684XJD4 FD 版 EC-A1684XJD4 的 FD(Full-featured,功能完整)版本。满血 32 TOPS(INT8)算力 八核 A53 2.3GHz,32 路 1080P25fps 视频全流程处理(解码 AI 分析同…

2026/10/18 2:33:45 阅读更多 →
Beagle vs 传统开发:Server-Driven UI如何提升开发效率?

Beagle vs 传统开发:Server-Driven UI如何提升开发效率?

Beagle vs 传统开发:Server-Driven UI如何提升开发效率? 【免费下载链接】beagle A framework to help implement Server-Driven UI in your apps natively. 项目地址: https://gitcode.com/gh_mirrors/be/beagle Beagle是一款基于Server-Driven …

2026/10/20 12:05:49 阅读更多 →
告别Google Photos!Fx Fotos让你轻松掌控照片存储的完整指南

告别Google Photos!Fx Fotos让你轻松掌控照片存储的完整指南

告别Google Photos!Fx Fotos让你轻松掌控照片存储的完整指南 【免费下载链接】fx-fotos "Fx Fotos" is an opensource gallery app in react native with the same smoothness and features of Google Photos and Apple Photos. It is backend gnostic an…

2026/10/19 20:12:46 阅读更多 →

最新新闻

用Wireshark嗅探ESP32的Wi-Fi通信:读懂802.11帧实战解析

用Wireshark嗅探ESP32的Wi-Fi通信:读懂802.11帧实战解析

我一直觉得,搞 Wi-Fi 通信的开发者,手里不能只有串口日志和 AT 指令回显。很多时候设备报“连接失败”或者“数据发不出去”,从日志里看一切正常,但真正的问题出在空气里——电磁波上承载的那些 802.11 帧说了什么,你根…

2026/10/20 15:33:25 阅读更多 →
libnids源码深度注释:TCP流重组与IP分片实战解析

libnids源码深度注释:TCP流重组与IP分片实战解析

简介:这份资源是 libnids 1.20 源码的深度解读版本,作者在原始代码基础上补充了大量中文注释,面向网络安全方向的学习者、入侵检测系统开发者以及需要理解 TCP/IP 协议栈实现细节的工程师。libnids 作为经典的开源网络入侵检测库,…

2026/10/20 15:33:25 阅读更多 →
技能提升的底层逻辑:体系搭建、刻意练习与能力沉淀

技能提升的底层逻辑:体系搭建、刻意练习与能力沉淀

1. 内容概览:为什么"skills"值得单独拎出来聊标题就一个词,skills,但越简单的词往往越容易糊弄过去。不少人在简历里写"熟练掌握XX技能",问到底掌握到什么程度,答不上来;也有人天天刷教…

2026/10/20 15:33:25 阅读更多 →
敏捷开发中的需求审批与变更管理:从失控到轻量协同

敏捷开发中的需求审批与变更管理:从失控到轻量协同

1. 先搞清楚:敏捷里的“需求审批”到底在反对什么1.1 传统审批的机械流程为什么让人讨厌我最早接触敏捷的时候,带我的老前辈说过一句特别狠的话:“审批一多,敏捷就死了。”当时我深以为然,因为那时候团队刚从一个重型流…

2026/10/20 15:33:25 阅读更多 →
AI声音克隆配音接单实战:从零搭建生产线,成本压到零

AI声音克隆配音接单实战:从零搭建生产线,成本压到零

1. 配音接单这门生意,成本结构到底长什么样先算一笔账。传统配音接单,如果你自己上,设备投入是麦克风加声卡加监听耳机,入门级一套下来一千到三千不等,稍微像样点的电容麦加专业声卡直接奔五千。这还只是硬件&#xff…

2026/10/20 15:33:25 阅读更多 →
Maltego双平台配置实战:从Java环境到关联分析图谱

Maltego双平台配置实战:从Java环境到关联分析图谱

做安全分析、做资产梳理、做事件调查的人,迟早会和Maltego这个工具打交道。它是一款开源信息收集与关联分析工具,擅长把域名、IP、邮箱、账号、证书这类零散信息,整理成一张可以交互操作的关系图谱。我第一次在别人的演示里看到它把一个IP展开…

2026/10/20 15:32:23 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →