WebGL圆形点绘制:gl_PointCoord与距离场实现技巧
1. 从方正的点讲起光栅化规则与两个关键内置变量做粒子效果的时候我第一次用gl.drawArrays(gl.POINTS, 0, 10000)往屏幕上撒了一万个点结果屏幕上全是密密麻麻的小方块。当时我以为是自己数据传错了检查半天发现纹理、缓冲区、着色器都没问题——查资料才知道WebGL里点图元默认就是方的想画圆形点得自己在片元着色器里“切”一刀。这个现象背后是图形管线的光栅化规则。你在顶点着色器里设置了gl_Position和gl_PointSizegl_Position告诉 GPU 这个点落在屏幕哪个位置gl_PointSize告诉 GPU 这个点占据多大区域。光栅化阶段GPU 把这个点当成一个以gl_Position为中心的方块来处理方块边长就是gl_PointSize对应的像素数。举个直观的例子gl_PointSize 10.0那光栅化器就会生成一个 10×10 的像素区域这个区域内的每个片元都会执行一次片元着色器。你没写任何特殊逻辑时片元着色器给什么颜色方块就显示什么颜色于是默认效果就是纯色方块。这里有两个内置变量必须理解透因为它们就是这次“修方为圆”的全部工具变量所在着色器含义gl_PointSize顶点着色器点的大小像素只能在这里赋值gl_PointCoord片元着色器当前片元在点区域内的归一化坐标范围 [0,0] 到 [1,1]gl_PointCoord是关键。GPU 在光栅化这个方块区域时会为每个片元计算一个落在 [0,0] 到 [1,1] 范围内的坐标可以把它理解为这个片元在“点区域”里的 UV 坐标左下角是 (0,0)右上角是 (1,1)正中心是 (0.5,0.5)。注意这个坐标是 GPU 自动算好传入片元着色器里的不需要你手动声明 attribute直接用就行。有了gl_PointCoord思路就通了计算出当前片元到中心 (0.5,0.5) 的距离如果距离大于圆的半径就把这个片元丢弃discard剩下的片元自然就是一个圆形区域。这套逻辑属于图形学里非常基础的程序化形状生成思路核心就一句话用距离做判断决定哪些片元保留、哪些片元丢弃。我还见过不少初学者在这步把gl_PointSize当成 uniform 传给片元着色器想在片元里自己算半径其实完全没必要。gl_PointCoord已经做了归一化圆心永远是 (0.5,0.5)边长永远是 1半径永远是 0.5不需要关心点实际占了多少像素。归一化坐标是这个方案最省心的地方。2. 距离场思想用gl_PointCoord把方点切成圆点先看完整代码再逐行拆。顶点着色器保持最简配置// 顶点着色器 attribute vec4 a_Position; uniform float u_PointSize; void main() { gl_Position a_Position; gl_PointSize u_PointSize; }片元着色器是这次的主角precision mediump float; void main() { // 计算当前片元到点中心的距离 float d distance(gl_PointCoord, vec2(0.5, 0.5)); // 距离超过半径丢弃片元 if (d 0.5) { discard; } gl_FragColor vec4(1.0, 0.5, 0.2, 1.0); }distance(gl_PointCoord, vec2(0.5, 0.5))算的是归一化坐标里的欧氏距离。因为整个点区域边长为 1圆心在 (0.5,0.5)所以圆的半径最大只能是 0.5正好可以内切于方块区域。当d 0.5时片元落在圆外直接discard掉GPU 不会把这个片元写入颜色缓冲区方块的四角就空出来了。渲染时JavaScript 侧只需要准备一个点的坐标设置好u_PointSize调用一次绘制const gl canvas.getContext(webgl); // ...编译链接 shader、绑定 buffer 等常规步骤略 gl.uniform1f(u_PointSize, 30.0); gl.drawArrays(gl.POINTS, 0, 1);drawArrays(gl.POINTS, 0, 1)表示从第 0 个顶点开始按点图元方式绘制 1 个顶点。WebGL 对点图元的处理是一个顶点对应一个点不是三个顶点一个三角形那种聚簇方式。想画 100 个圆点传 100 个顶点进去每个顶点的gl_PointSize也可以各自不同。实际项目里我更推荐写紧凑一点的等价形式省一次开方运算。GPU 虽然硬算distance也没问题但粒子数量上了万级以后这点开销能省则省precision mediump float; void main() { vec2 uv gl_PointCoord - vec2(0.5, 0.5); float d2 dot(uv, uv); // 距离平方 if (d2 0.25) { // 0.5 的平方是 0.25 discard; } gl_FragColor vec4(1.0, 0.5, 0.2, 1.0); }dot(uv, uv)算的是向量与自身的点积结果就是距离平方阈值从 0.5 变成 0.25。这在数学上完全等价删掉了sqrt计算。到这里你可能会想既然只是在片元里判断“圆内保留、圆外丢弃”那是不是可以推广到任意形状没错这就是距离场distance field的雏形。每个片元都拥有一个“到中心/到边界”的距离值配合不同的阈值、比较操作、数学函数就能程序化生成圆、环、方角矩形、光晕、条纹等各种形态。第 4 章我会展开三种最常见的变体这块先留个印象。3. 边缘处理discard直切与smoothstep抗锯齿哪个更适合你上一章的写法能跑但把点放大到 100 像素以上边缘会出现明显的锯齿圆轮廓线像狗啃过一样。原因很直接discard是硬切换片元要么完全保留、要么完全丢弃在圆的边界处一个像素可能恰好有一半在圆内、一半在圆外但最终结果只有两种状态没有中间过渡于是呈现锯齿。修锯齿的思路也很直白不搞非黑即白的discard改成“边缘处透明度渐变”。离圆心越近越不透明越远越透明让颜色在边界处平滑衰减。GLSL 里做这种渐变首选smoothstep。precision mediump float; void main() { vec2 uv gl_PointCoord - vec2(0.5, 0.5); float d length(uv); // 从 0.45 到 0.5 之间平滑过渡 float alpha 1.0 - smoothstep(0.45, 0.5, d); if (alpha 0.01) { discard; } gl_FragColor vec4(1.0, 0.5, 0.2, alpha); }smoothstep(0.45, 0.5, d)的意思是当d 0.45时返回 0d 0.5时返回 1中间区域做平滑插值。用1.0减去它相当于让边缘 0.05 的范围内透明度从 1 渐变到 0。这 0.05 的宽度是归一化值实际对应多少像素取决于点的大小。点大、过渡带就宽点小、过渡带就窄。想调边缘锐利程度改这两个数值即可比如smoothstep(0.48, 0.5, d)边缘更利落smoothstep(0.4, 0.5, d)边缘更柔和。注意光有 alpha 还不够JavaScript 侧必须开启混合否则透明部分会直接覆盖背景色看起来像加了黑边或花边gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);SRC_ALPHA, ONE_MINUS_SRC_ALPHA是标准透明混合源颜色的 alpha 作为权重混合当前片元和背景色。粒子系统里基本都会开这一行很多人漏掉结果圆形边缘出现黑色方块残留排查半天没想到是混合没开。两种方案的取舍我做了个对比表实际选型时可以直接参考方案边缘效果性能适用场景discard硬切锐利放大后有锯齿不产生混合开销但增加片元剔除小尺寸点、风格化像素效果smoothstep渐变平滑边缘自然需要开启混合多一次混合计算粒子系统、光晕、发光效果我个人的习惯是点尺寸小于 20 像素时直接discard看不出明显锯齿点尺寸大或数量多且重叠加时用smoothstep加混合。另外提一嘴WebGL 的 canvas 上下文本身有antialias属性但它主要影响几何图元的边缘对gl.POINTS的块状光栅化基本没有帮助别指望靠它解决圆形点锯齿。4. 圆环点、光晕点、圆角方点一套距离场的三种玩法学会了“距离判断 透明度控制”其实已经拿到了绘制任意程序化点样式的大门钥匙。这一章分享三种我在实际项目里用过的变体代码都基于同一个框架先算一个距离值再按需求做变换。4.1 圆环点内外双阈值圆环和实心圆唯一的区别是保留的不是d 0.5的圆盘而是0.4 d 0.5的环带。片元里用两个smoothstep做差即可precision mediump float; void main() { vec2 uv gl_PointCoord - vec2(0.5, 0.5); float d length(uv); // 外边缘 float outer smoothstep(0.42, 0.5, d); // 内边缘 float inner smoothstep(0.28, 0.35, d); float alpha outer - inner; if (alpha 0.01) { discard; } gl_FragColor vec4(0.3, 0.8, 1.0, alpha); }outer - inner的结果是在环带区域内 alpha 趋近 1在环带内外都趋近 0。也可以直接写成abs(d - 0.43) 0.035这种距离差形式但锐利边缘较多不如双 smoothstep 好看。这种点常用于游戏里的目标标记、技能指示圈、雷达小点。4.2 光晕点指数衰减代替阈值判断光晕和实心圆的最大区别是不需要discard来切边缘而是让透明度随着距离增加连续衰减。用指数函数最顺手precision mediump float; void main() { vec2 uv gl_PointCoord - vec2(0.5, 0.5); float d length(uv); // d 范围是 0~0.707先归一化到 0~1再衰减 float t d * 1.414; float glow pow(1.0 - clamp(t, 0.0, 1.0), 3.0); if (glow 0.01) { discard; } gl_FragColor vec4(0.6, 0.8, 1.0, glow); }pow(1.0 - t, 3.0)让衰减曲线带一点“中间亮、边缘暗”的感觉幂次越大光晕越聚拢在中心。如果你想让中心有个更亮的核可以把衰减函数拆成两项比如中心核用pow(1.0 - t, 6.0)外围光晕用pow(1.0 - t, 2.0) * 0.5然后相加这属于调参手感活没有绝对标准。光晕点用来做粒子爆炸、星光、环境尘埃特别合适因为这个方案天然半透明多个点叠加时会产生非常自然的亮度堆叠。4.3 圆角方点切比雪夫距离取了半天欧氏距离换个思路用横纵坐标的最大值。max(abs(uv.x), abs(uv.y))得到的是切比雪夫距离等距离线是方形配合一个小阈值切角就能得到圆角方形点precision mediump float; void main() { vec2 uv gl_PointCoord - vec2(0.5, 0.5); float d max(abs(uv.x), abs(uv.y)); float alpha 1.0 - smoothstep(0.42, 0.5, d); if (alpha 0.01) { discard; } gl_FragColor vec4(0.9, 0.4, 0.6, alpha); }这个形状在 UI 界面、像素风游戏、复古 HUD 里很常见。想控制圆角弧度可以再加一层距离场运算对向量做length(max(abs(uv) - vec2(0.3), 0.0))这种 SDF 圆角矩形套路但用smoothstep调过渡带的观感差别不大多数场景够用。三个变体背后的共性可以提炼成一句话先构造距离值再用阈值、差值或函数变换映射成 alpha 和颜色。这个思路熟练之后你看到任何“点状图形”基本都能在脑子里拆出它的距离函数长什么样。我整理了快速对照表形状距离函数保留条件/透明度实心圆length(uv)d 0.5或1-smoothstep圆环length(uv)双阈值区间outer - inner光晕length(uv)pow(1-d, n)连续衰减圆角方点max(abs(uv.x), abs(uv.y))smoothstep边缘过渡5. 移动端与兼容性点大小限制和几个容易翻车的细节圆形点写完之后换到手机上看效果可能发现点变小了、甚至直接消失或者形状变成方块、圆角出现怪异的锯齿。这些大概率不是着色器逻辑问题而是平台对gl_PointSize的限制和精度差异。5.1 先查你设备支持的最大点大小OpenGL ES 2.0 标准只保证gl_PointSize至少支持到 1.0不保证上限。桌面显卡通常支持很大比如 255 甚至 1024但移动 GPU 上限差别很大有的 64有的 128部分低端设备甚至只有 31。你写死 100 像素超出上限后 GPU 会按实现自行处理有的 clamp、有的裁掉、有的直接不绘制。运行前一定要查实际值const range gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE); console.log(Point size range:, range); // 例如 Float32Array [1, 255.0]拿到上限后把u_PointSize限制在range[1]以内或者干脆在片元里做缩放适配不要在顶点着色器里传超限值。我这里说的是 WebGL1WebGL2 对点大小限制同样存在别以为换了版本就万事大吉。5.2 小尺寸点的像素覆盖问题当gl_PointSize小于 1 时GPU 光栅化会用一个近似方式把点覆盖到最近的像素上稍微偏移就可能导致点闪烁或消失。做粒子系统时如果你想让粒子“从无到有”地出现建议不要直接改gl_PointSize到 0.1而是保持点大小固定用 alpha 的渐变来控制显隐。这也是为什么我前面反复强调 alpha 渐变方案的实用价值它不只是为了抗锯齿更是为了规避小点问题。另外高 DPI 屏比如手机的二倍屏、三倍屏上一个 CSS 像素对应多个物理像素。你以为画了 10px 的点实际光栅化可能覆盖 20 个物理像素圆形边缘反而容易糊。解决办法是结合window.devicePixelRatio调整 canvas 的物理尺寸和gl_PointSize保证视觉大小一致。5.3 gl_PointCoord 的精度隐患片元着色器的精度声明用的是precision mediump float在部分移动端 GPU 上mediump 浮点数只有约 10 位精度。当点很小比如 8 像素时gl_PointCoord的量化误差会被放大圆形边缘出现肉眼可见的阶梯。如果对点质量有要求可以改成precision highp float;但 highp 在某些古老设备上不被片元着色器支持稳妥做法是先检查兼容性再决定或者用中等精度但把圆半径计算稍微放大一点比如阈值用 0.49 而不是 0.5给误差留一点余量。5.4 不开启混合时透明边缘等于黑边这是被问得最多的问题明明写了 alpha边缘却是黑色或白色。原因基本是没开gl.enable(gl.BLEND)或者 canvas 的 premultipliedAlpha 设置和着色器输出不匹配。默认 Canvas 是 premultiplied alpha 模式如果你在片元着色器直接输出vec4(r, g, b, a)没有做预乘边缘处a接近 0但r、g、b可能还是原始亮度值预乘后产生偏亮的描边。最简单的处理方案是用非预乘上下文const gl canvas.getContext(webgl, { premultipliedAlpha: false });输出非预乘的vec4(r, g, b, a)混合时注意颜色即可。如果你确实要在预乘模式工作片元着色器输出应该是gl_FragColor vec4(color.rgb * alpha, alpha);。5.5 调试这类问题的小工具习惯我自己排查点渲染问题时会先在片元着色器里写一个“诊断模式”用gl_PointCoord.x和gl_PointCoord.y直接作为颜色输出比如gl_FragColor vec4(gl_PointCoord.x, gl_PointCoord.y, 0.0, 1.0)。这样屏幕上每个点会显示一个从红到绿渐变的方向盘色块一眼能看出 UV 方向和点区域范围。如果看到的是单色方块说明gl_PointCoord根本没生效如果渐变色方向反了说明数据坐标需要翻转。诊断完再换回正式片元逻辑能节省大量调试时间。另一个习惯是绘制点之前先调gl.clearColor(0, 0, 0, 0)并清除颜色缓冲排除背景残留导致视觉误判。把这两层——归一化坐标距离判断、透明度渐变处理边界——掌握之后圆形点就算真正在 WebGL 里“吃透”了。它虽然只是入门系列里的一个小节点但衍生出来的距离场思路在后面做粒子雨、星图、光点河流时会频繁用到。我每次写这类点效果基本上是拿这篇文章里的片元着色器做模板改距离函数和衰减参数适应不同视觉需求。

相关新闻

8位CPU硬件思维训练:数据通路、控制时序与状态迁移实战

8位CPU硬件思维训练:数据通路、控制时序与状态迁移实战

1. 这不是复习提纲,是能直接上手的“硬件思维训练手册”你打开这页,大概率正被《计算机组成原理》折磨得头皮发紧——寄存器堆怎么连?微程序控制器里那堆微指令到底在指挥谁?Cache映射方式一换,性能差出三倍&#xff0…

2026/10/17 20:03:10 阅读更多 →
自动化应届生入行必读:PLC工程师的成长之路与实战经验

自动化应届生入行必读:PLC工程师的成长之路与实战经验

每年春招结束,后台就会涌进一批自动化专业的应届生问我:做PLC工程师有没有前途、第一份工作怎么选、要不要趁年轻多出差。干了12年工控,从接线工到独立带项目的PLC工程师,我想趁这次机会把“自动化应届生入行”这件事一次说透。本…

2026/10/17 20:03:10 阅读更多 →
SAP S4520采购模块核心考点梳理:从主数据到供应商评估

SAP S4520采购模块核心考点梳理:从主数据到供应商评估

简介:这是 SAP S4 HANA S4520 采购模块的中文版教程,贴合 SAPMM 顾问工作场景,面向应用顾问、项目经理和最终用户。内容由浅入深,覆盖采购物料主数据、供货源与条件、货源确定、优化采购、凭证审批、特定采购流程、供应商评估及其…

2026/10/17 20:03:10 阅读更多 →

最新新闻

图像抠图中mask转trimap的原理与三种工程化实现方案

图像抠图中mask转trimap的原理与三种工程化实现方案

1. 什么是mask图生成trimap图?为什么这一步不能跳过在图像抠图(Image Matting)这个领域里,“mask图生成trimap图”不是某个新出的网红玩法,而是所有专业级抠图流程里雷打不动的第一道工序——它就像裁缝做西装前必须先…

2026/10/17 20:44:47 阅读更多 →
LiquidCrystal库函数实战:用TaoToken统一Key打通Arduino字符屏调试链路

LiquidCrystal库函数实战:用TaoToken统一Key打通Arduino字符屏调试链路

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

2026/10/17 20:44:47 阅读更多 →
Windows微信多开:用start命令实现零依赖多账号登录

Windows微信多开:用start命令实现零依赖多账号登录

1. 微信多开这件事,为什么值得单独拿出来聊在Windows上同时登录两个甚至更多微信账号,是很多人日常办公和生活中绕不开的需求。一个账号对接工作客户,一个账号维系亲友社交,还有人专门用一个账号做社群运营、一个账号做私域沉淀。…

2026/10/17 20:44:47 阅读更多 →
标签平滑原理与实战:校准模型置信度的关键技巧

标签平滑原理与实战:校准模型置信度的关键技巧

1. 这个“标签平滑”到底在平什么?——从一个被低估的训练 trick 说起你有没有遇到过这样的情况:模型在训练集上准确率冲到99.5%,验证集却卡在87%不动了,loss曲线在后期突然抖动、泛化能力明显变差?或者分类任务里&…

2026/10/17 20:44:46 阅读更多 →
Cupp、Cewl与Crunch:密码安全的人因建模三件套

Cupp、Cewl与Crunch:密码安全的人因建模三件套

1. 这三款工具不是“黑客神器”,而是密码安全认知的显微镜很多人第一次看到Cupp、Cewl和Crunch,第一反应是:“哦,这是搞社工库爆破用的?”——这种理解既不准确,也容易引发误解。实际上,这三款工…

2026/10/17 20:43:46 阅读更多 →
Kafka接入AI实战:从异步削峰到Agent事件流与性能排查

Kafka接入AI实战:从异步削峰到Agent事件流与性能排查

Kafka接入AI这件事,最近在圈子里讨论热度确实高。我所在的团队上个月也正式完成了Kafka与AI服务链路的全量对接,从最初只敢拿它承接日志数据,到现在AI推理请求、Agent事件流、模型训练样本全都跑在Kafka上,整个过程踩了不少坑&…

2026/10/17 20:43:46 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/17 1:44:59 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 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/16 21:54:38 阅读更多 →
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/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练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/16 14:23:36 阅读更多 →