news 2026/10/15 15:10:51

如何在片段着色器中检测被裁剪的三角形:WebGL 顶点裁剪判定实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
如何在片段着色器中检测被裁剪的三角形:WebGL 顶点裁剪判定实战指南
  • 图形学

【免费下载链接】webgl-fundamentals

WebGL lessons that start with the basics

项目地址:https://gitcode.com/gh_mirrors/we/webgl-fundamentals
点击查看免费下载

本指南源于 webgl-fundamentals 项目 Q&A 系列中的经典问题:当三角形的部分顶点落在裁剪空间(clip space)之外时,如何在片段着色器中丢弃这些被裁剪的片段。文章将给出两种完整可运行的实现方案——基于 varying 插值的逐像素判定,以及更高效的整三角形剔除方案,并深入讲解裁剪空间的数学原理与 WebGL 渲染管线的数据流向,读者读完后可以在自己的 WebGL 项目中直接套用这两种着色器模板。

问题场景:为什么片段着色器"看不到"顶点

在 WebGL 中,一个三角形经过顶点着色器后,会被光栅化(rasterize)成一堆覆盖的像素,随后每个像素都会调用一次片段着色器来询问颜色。正如 WebGL 工作原理 中所讲,片段着色器默认只能拿到极少的逐像素信息:当前像素在画布/帧缓冲上的屏幕坐标(即gl_FragCoord),以及深度缓冲值。顶点本身并不会自动传到片段着色器。

因此,如果你在片段着色器中尝试"访问某个顶点的裁剪状态",会直接撞上数据隔离的墙。想要在片段着色器里拿到顶点信息,唯一的途径是在顶点着色器中把数据算好,通过 varying 传递并插值到片段着色器。

前置知识:如何判断一个顶点被裁剪

要"检测裁剪",先要明确"裁剪"的数学定义。顶点着色器输出的gl_Position处于裁剪空间(clip space),WebGL 之后会自动将xyz除以w,得到位于 -1 到 +1 之间的标准设备坐标(NDC)。这个自动除以w的行为,在 3D 透视教程 中有详细推导:gl_Position = vec4(position.xyz, zToDivideBy)与手动除法完全等价。

一个顶点只有同时满足以下条件才位于可见区域内(不会被裁剪):

-w <= x <= w -w <= y <= w -w <= z <= w

用 GLSL 的内建函数可以把它写成一句布尔表达式:

bool vertexClipped(vec4 clipspace) { return any(lessThan(clipspace.xyz, -clipspace.www)) || any(greaterThan(clipspace.xyz, clipspace.www)); }

lessThan/greaterThan对vec3逐分量比较,返回bvec3;any只要有一个分量为 true 就整体为 true。只要x、y、z中任意一个分量超出[-w, w]范围,这个顶点就被判定为裁剪。

方案一:用 varying 把裁剪标志插值给片段着色器

最简单直接的思路是:在顶点着色器中计算当前顶点是否被裁剪,把结果作为 varying 传出;varying 在光栅化时会被线性插值,因此只要三角形任一顶点被裁剪,插值结果必然大于 0,片段着色器据此discard。

完整的可运行示例见 示例 1:varying 插值判定,核心着色器代码如下:

// 顶点着色器 attribute vec4 position; uniform mat4 matrix; varying float clipped; void main() { gl_Position = matrix * position; clipped = ( any(lessThan(gl_Position.xyz, -gl_Position.www)) || any(greaterThan(gl_Position.xyz, gl_Position.www)) ) ? 1.0 : 0.0; }
// 片段着色器 precision highp float; varying float clipped; void main() { if (clipped > 0.0) { discard; } gl_FragColor = gl_FrontFacing ? vec4(1, 0, 0, 1) : vec4(0, 0, 1, 1); }

这段代码的运作原理非常巧妙:

  • 顶点着色器中,被裁剪的顶点把clipped设为1.0,未被裁剪的设为0.0;
  • 光栅化时,WebGL 会按 着色器与 GLSL 中描述的 varying 机制,对三角形三个顶点的值做插值。如果三个顶点全部在可见区域内,三角形内所有像素的clipped都是0.0;只要任一顶点被裁剪,靠近该顶点的像素插值结果就会大于0.0;
  • 片段着色器中用clipped > 0.0而不是== 1.0判断,正是利用了插值特性——绝大多数像素得到的都是介于 0 和 1 之间的中间值。

方案二:整三角形剔除,让被裁剪的三角形根本不参与光栅化

varying 方案虽然正确,但存在一个效率问题:它仍然会光栅化那些"部分被裁剪"的三角形,只是事后在片段着色器里逐像素discard。对于大三角形来说,可能要白跑成千上万个像素的片段着色器。

更高效的做法是:把三角形的三个顶点同时传给顶点着色器,只要其中任意一个被裁剪,就把gl_Position直接挪到屏幕外,让硬件在光栅化阶段就把整个三角形剔除掉。

完整可运行示例见 示例 2:整三角形剔除,核心着色器如下:

// 顶点着色器 attribute vec4 position; attribute vec4 position1; // 三角形的第二个顶点 attribute vec4 position2; // 三角形的第三个顶点 uniform mat4 matrix; bool vertexClipped(vec4 clipspace) { return any(lessThan(clipspace.xyz, -clipspace.www)) || any(greaterThan(clipspace.xyz, clipspace.www)); } void main() { gl_Position = matrix * position; vec4 clipPosition1 = matrix * position1; vec4 clipPosition2 = matrix * position2; bool clipped = vertexClipped(gl_Position) || vertexClipped(clipPosition1) || vertexClipped(clipPosition2); if (clipped) { gl_Position = vec4(vec3(2), 1); // 移到屏幕外的离屏值 } }
// 片段着色器 precision highp float; void main() { gl_FragColor = gl_FrontFacing ? vec4(1, 0, 0, 1) : vec4(0, 0, 1, 1); }

片段着色器里完全不需要discard了,因为被裁剪的三角形在光栅化之前就被移出屏幕。vec4(vec3(2), 1)中的2远超 NDC 的 [-1, 1] 范围,除以w之后仍然在屏幕外,不会产生任何可见像素。

方案二的代价:需要重建三角形顶点数据

整三角形剔除方案虽然省去了逐像素判定,但代价是数据量变大:顶点着色器每次调用都要同时收到三角形的全部 3 个顶点。示例 2 的 JavaScript 部分展示了如何用 twgl 工具库准备这样的数据:

const verts = twgl.primitives.deindexVertices( twgl.primitives.createSphereVertices(1, 8, 8)); // 复制一份位置数据,用于存放错位后的三角形顶点 const position1 = new Float32Array(verts.position); const position2 = new Float32Array(verts.position); // 每个三角形 3 个顶点(9 个 float),循环错位 for (let i = 0; i < position1.length; i += 9) { { // 0, 1, 2 => 1, 2, 0 const temp = position1.slice(i, i + 3); position1.set(position1.slice(i + 3, i + 9), i); position1.set(temp, i + 6); } { // 0, 1, 2 => 2, 0, 1 const temp = position2.slice(i + 6, i + 9); position2.set(position2.slice(i + 0, i + 6), i + 3); position2.set(temp, i); } } verts.position1 = position1; verts.position2 = position2; const bufferInfo = twgl.createBufferInfoFromArrays(gl, verts);

这段代码的意图是:球体网格顶点先经过deindexVertices去索引化,变成每 3 个顶点一组、每组构成一个三角形的线性数组;然后复制两份位置数据,通过循环错位让position1保存"每个三角形的第二个顶点"、position2保存"每个三角形的第三个顶点"。这样顶点着色器的三次调用分别拿到(v0, v1, v2)、(v1, v2, v0)、(v2, v0, v1),任何一个顶点被裁剪时,三条边上的判定结果一致,三角形整体被移出屏幕。

两个示例都使用twgl完成其余样板代码(createProgramInfo、setBuffersAndAttributes、drawBufferInfo),渲染逻辑本身保持一致:每帧更新一个 60° 视场角的透视矩阵,让球体随时间旋转和平移,越过near/far裁剪面时即可直观看到两种方案的剔除效果。相关渲染参数可对照 3D 透视教程 中的perspective(fov, aspect, near, far)矩阵构造方式。

两种方案对比与选型建议

对比维度方案一:varying 插值方案二:整三角形剔除
实现位置顶点着色器 + 片段着色器discard顶点着色器(片段着色器无额外逻辑)
数据量只需原始顶点位置每个三角形需同时传入 3 个顶点
被裁剪三角形是否光栅化是(事后逐像素丢弃)否(光栅化前移出屏幕)
性能特征像素多时开销大整体剔除,省去逐像素检查
代码复杂度低中(需要重建顶点缓冲)

选型建议:如果你的三角形数量少、屏幕像素多(例如少量大尺寸平面),方案二能明显减少片段着色器的工作量;如果你的顶点数据来自索引缓冲、不方便重构为"三角面三顶点平铺"布局,方案一改动最小、最易落地。此外,方案二的思路还可以进一步推广——把判定逻辑放到 CPU 端完成,则整个三角形连顶点着色器都不用进,但代价是把顶点数据每帧回读,这属于另一层面的取舍。

补充说明

  • 两个示例的完整源代码均位于仓库中:示例 1 与 示例 2,可直接打开运行观察球体穿越近/远裁剪面时的剔除效果。
  • 本篇文章对应的原始问题文档为 webgl-qna-how-to-detect-clipped-triangles-in-the-framgment-shader.md,更多 WebGL Q&A 收录于 webgl-qna.md 索引。
  • 若你对 varying 插值机制、裁剪空间与w分量的数学细节还不熟悉,建议先阅读 WebGL 工作原理、着色器与 GLSL 和 3D 透视教程,它们是理解本文两套方案的前提。
  • 图形学

【免费下载链接】webgl-fundamentals

WebGL lessons that start with the basics

项目地址:https://gitcode.com/gh_mirrors/we/webgl-fundamentals
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 15:10:15

Forge 1.20.1模组开发全流程:环境配置、方块注册与避坑指南

简介&#xff1a;资源为Minecraft 1.20.1版本配套的Forge模组开发工具包&#xff08;MDK&#xff09;&#xff0c;面向准备在该版本开展模组开发、或希望深入学习Forge工程结构的玩家和Java开发者&#xff0c;无论是初次接触模组开发的新手&#xff0c;还是需要在该版本上进行功…

作者头像 李华
网站建设 2026/10/15 15:10:11

位运算技巧:lowbit原理与应用解析

1. 题目背景与核心思路LeetCode 3314这道题涉及位运算中的经典操作lowbit&#xff0c;这是一个在算法竞赛和面试中频繁出现的技巧。我第一次遇到这个问题是在准备某大厂面试时&#xff0c;当时被要求优化一个统计二进制中1的数量的算法。lowbit操作正是解决这类问题的利器。low…

作者头像 李华
网站建设 2026/10/15 15:09:30

YOLOv5单目测距:从检测框到三维距离的工程实践

简介&#xff1a;本资源面向计算机视觉初学者与自动驾驶、机器人导航方向的开发者&#xff0c;提供一套基于YOLOv5与单目深度估计的距离测算Python实现方案&#xff0c;帮助在无额外深度硬件的情况下从单张彩色图像中推断目标距离。压缩包共132个文件&#xff0c;约13.87MB&…

作者头像 李华
网站建设 2026/10/15 15:08:56

Node.js纯JS实现Modbus协议栈:RTU与TCP采集服务实战

简介&#xff1a;这份资源是 NodeJS 环境下 MODBUS-RTU 与 MODBUS-TCP 协议的纯 JavaScript 实现&#xff0c;基于 node-serialport 构建&#xff0c;面向需要与 RS485、RS232 等串行设备或工业电子设备通信的开发者&#xff0c;也适合用 Arduino 控制机器人、灌溉控制器等场景…

作者头像 李华
网站建设 2026/10/15 15:08:47

AI 动手之前先让人拍板:Laravel AI 工具审批机制完整指南

【免费下载链接】ai The Laravel AI SDK provides a unified, expressive API for interacting with AI providers such as OpenAI, Anthropic, Gemini, and more. 项目地址&#xff1a; https://gitcode.com/gh_mirrors/ai89/ai 点击查看 免费下载 在 Laravel AI 中&#xff…

作者头像 李华
网站建设 2026/10/15 15:08:36

Windows NTP时间同步实战:从SNTP到NTP客户端模式升级

简介&#xff1a;这是一款面向Windows系统管理员与C开发者的轻量级NTP时间同步客户端工具&#xff0c;解决局域网或公网环境下系统时钟漂移、手动校时低效等问题&#xff0c;适用于IT运维、工业控制终端时间统一、嵌入式测试环境等对时间精度有基础要求的场景。资源包为114KB的…

作者头像 李华