尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

WebGPU 玻璃材质实现:从折射到菲涅尔的透明渲染指南

WebGPU 玻璃材质实现:从折射到菲涅尔的透明渲染指南 第一次用 WebGPU 做玻璃材质的时候我犯过一个典型错误把一个玻璃杯模型的透明度调成 0.2然后盯着屏幕上那只灰蒙蒙的杯子愣了很久。它确实“透明”了但完全没有玻璃感。真正的玻璃感靠的不是半透明而是折射、反射和菲涅尔三样东西叠加在一起。这里要说的 WebGPU Glass Material就是在 WebGPU 渲染管线里把这三样东西做出来的过程。这个主题适合两类人一类是做 Web 端 3D 产品展示想把玻璃、水晶、水面这类透明材质做得更真实另一类是在学 WebGPU 渲染想搞明白透明材质和普通 PBR 材质到底差在哪里。读完你会得到一个能运行的玻璃球体知道 IOR、粗糙度、菲涅尔、环境贴图这些参数分别在调什么也清楚哪些地方容易出问题、为什么出问题。我把整个实现过程拆成六个部分先理解问题再准备环境接着写渲染管线和着色器然后调参数再做性能优化最后给一套排查思路。每部分都会带具体的代码片段或判断标准方便你直接落到项目里复现。1. 先确认玻璃材质到底在解决什么渲染问题1.1 “半透明”和“折射”是两回事普通 3D 场景里调 alpha得到的效果是整个模型变成一层半透明膜。背景不会错位也不会变形边缘更没有玻璃特有的高光轮廓。原因很简单alpha 混合只是在颜色上做比例混合光线方向没有改变。玻璃材质的核心是光线进入介质后方向发生偏折。你透过一个玻璃杯看背后物体杯身里的边缘会被扭曲扭曲程度由折射率决定。WebGPU 里实现这种效果需要在片元着色器里拿到物体法线、视线方向然后用折射公式计算新的采样方向再去环境贴图或场景纹理中取色。这一步才是玻璃感的来源。所以如果你只想让物体“透过去看到后面”普通 alpha 就够了但如果你想让物体看起来“像玻璃”就必须处理折射方向。1.2 WebGPU 里没有现成的“玻璃着色器”WebGPU 不是 DCC 软件没有类似 Blender 或者 Unity 里那种一键添加的玻璃材质预设。它给你的是渲染管线、着色器、纹理、采样器、缓冲区这些底层能力。要做玻璃效果本质上是自己写一个片元着色器把反射、折射、透射和菲涅尔混合起来。很多新手第一次接触时会在文档里找“transparent”开关结果发现 WebGPU 只提供混合状态和深度状态具体的透明逻辑完全由自己控制。这意味着你把blend配好只是让物体可以半透明要真正像玻璃还必须自己实现折射和反射逻辑。1.3 和普通 PBR 材质体系的差异传统 PBR 材质参数是基础色、金属度、粗糙度、法线贴图等。玻璃属于绝缘体金属度非常低但没有透明度或折射参数也不够。一些引擎会提供 transmission 或 ior 扩展用来描述半透明材质。WebGPU 原生没有这套封装你需要自己把这类参数作为 uniform 传进着色器。也就是说你编写的不是“玻璃材质”四个字而是一套能表现玻璃的光传输模型。对新手来说这个差异要先想清楚。否则后面调参时会一直以为是 WebGPU 某个内置开关没打开实际只是着色器逻辑里少了折射计算。1.4 适用场景和限制WebGPU 玻璃材质最适合中小型场景比如产品展示里放一两件玻璃器皿、水晶摆件或者做实时预览编辑器。如果场景里到处是玻璃就要重点考虑性能预算和渲染顺序。另一个限制是浏览器兼容性。WebGPU 目前已经逐步普及但移动端和老设备仍不一定支持。如果是对外发布的 Web 应用需要准备一个回退方案比如把玻璃替换成普通半透明材质或纯色材质。这一点在项目预研阶段就要列进排期不要等页面在用户手机上变成黑块后才补。2. 环境准备跑效果之前先确认好四件事2.1 浏览器怎么判断是否支持 WebGPU做 WebGPU 开发时第一件事不是写着色器而是确认浏览器是否暴露了navigator.gpu。如果这个对象不存在后面所有代码都不会执行。打开浏览器控制台输入navigator.gpu能返回对象说明基础支持是有的。代码里最好做一次防御判断if (!navigator.gpu) { console.error(当前浏览器不支持 WebGPU); return; } const adapter await navigator.gpu.requestAdapter(); if (!adapter) { console.error(无法获取 WebGPU 适配器请检查 GPU 驱动或浏览器设置); return; } const device await adapter.requestDevice();这段代码可以避免后续报错难以定位。有时候浏览器版本支持但 GPU 驱动有问题requestAdapter也会返回 null。所以不要把两段判断合在一起写。常见情况是 Chrome 和 Edge 在桌面端支持较好Safari 和 Firefox 的支持情况要看具体版本。我一般建议第一版测试先用 Chrome 最新稳定版跑通之后再去其他浏览器验证差异。2.2 硬件资源配置和判断标准玻璃材质需要采样环境贴图通常用到 Cube 纹理和 mipmap还要有一张渲染目标纹理做离屏折射。这些都会占用显存。如果显存太小创建大纹理时容易失败。建议从 512 或 1024 的纹理尺寸开始验证能跑通之后再逐步提升到 2048。不要一上来就用 4096 的 HDR 环境贴图尤其不要同时开多张。运行时打开任务管理器或 GPU 面板看显存占用。如果createTexture报错优先降低纹理尺寸或减少中间缓冲。判断硬件是否够用的标准很简单空场景能稳定跑加入环境贴图和玻璃球后不掉到 20 帧以下说明可以继续做如果一加就掉到个位数先把分辨率降下来再调参数。2.3 项目结构和 WGSL 语法准备做 WebGPU 开发用 TypeScript 加 Vite 搭项目会比较顺手。WebGPU 的着色器语言默认是 WGSL和传统 WebGL 里常用的 GLSL 语法差异不小。有 Three.js 背景的人刚接触 WGSL 时容易不习惯比如关键字不同、内建函数名不同、struct和fn的写法也不同。但不要为了绕过这部分就去加 GLSL 转换链。第一版直接用 WGSL 写出问题时浏览器控制台会直接报告着色器编译错误定位反而更快。建议把顶点着色器和片元着色器拆成独立函数放在一个.wgsl文件里。在 TypeScript 中用字符串或 import 方式引入都可以关键是保持结构清晰方便像普通代码一样排查。2.4 调试面板和错误作用域Chrome 的 WebGPU 面板能看到渲染管线、绑定组、缓冲区、纹理等对象关键时刻很管用。但 WebGPU 的很多错误不是立刻抛到 console而是通过异步错误作用域报告。常见做法是device.pushErrorScope(validation); // 创建资源或执行绘制 const error await device.popErrorScope(); if (error) { console.error(WebGPU validation error:, error.message); }这段代码能帮你把验证错误抓出来。否则图像全黑时你只能靠猜。生产环境里还可以把错误信息上报到日志系统方便远程排查。3. 动手实现从建管线到调出第一颗玻璃球3.1 准备场景和几何数据先用一个球体做实验最合适。WebGPU 没有内置几何体 API需要自己生成顶点数据。最简单的做法是写一个经纬球函数生成 position 和 normal。我习惯先做细分至少 32x32 的球体保证法线足够平滑。法线很关键因为折射和反射都要依靠法线方向。如果法线是平坦的玻璃球看起来就是一个多边形体没有光滑的折射形变。生成顶点时注意把法线和顶点位置对应好。不要只生成 position忘记生成 normal。有些新手会漏掉第二个顶点属性结果渲染时只显示黑块排查半天才发现是法线没有进管线。3.2 创建渲染管线创建渲染管线时要指定顶点缓冲布局、着色器入口、深度状态和混合状态。玻璃物体通常要开启颜色混合同时关闭深度写入否则多个透明物体之间会互相遮挡。const pipeline device.createRenderPipeline({ // ... primitive: { topology: triangle-list, cullMode: back, }, depthStencil: { format: depth24plus, depthWriteEnabled: false, depthCompare: less-equal, }, fragment: { targets: [{ format: bgra8unorm, // 以 context.getPreferredFormat() 为准 blend: { color: { srcFactor: src-alpha, dstFactor: one-minus-src-alpha, }, alpha: { srcFactor: one, dstFactor: one-minus-src-alpha, }, }, }], }, });格式不要写死。更稳妥的方式是用画布上下文返回的getPreferredFormat()。不然在部分浏览器上可能因为格式不匹配导致生成管线失败。关闭depthWriteEnabled是透明物体的常见处理。深度测试仍然保留保证玻璃不会穿到墙后面但玻璃彼此之间需要靠排序来保证遮挡关系而不是靠深度写入这一点后面会专门讲。3.3 顶点着色器在 WGSL 中顶点着色器负责把模型坐标转换为裁剪坐标同时把世界坐标和法线传给片元着色器。struct VSInput { location(0) position: vec3f32, location(1) normal: vec3f32, }; struct VSOutput { builtin(position) clip: vec4f32, location(0) worldPos: vec3f32, location(1) normal: vec3f32, }; vertex fn vs_main(in: VSInput) - VSOutput { var out: VSOutput; out.clip cameraProjView * vec4f32(in.position, 1.0); out.worldPos modelMatrix * vec4f32(in.position, 1.0); out.normal normalize(mat3(modelMatrix) * in.normal); return out; }代码里的cameraProjView、modelMatrix需要从 uniform 中获取这里省略了完整的绑定声明。法线变换如果涉及非等比缩放需要使用逆转置矩阵否则高光和折射方向会错。球体如果只做了等比缩放手动用mat3(modelMatrix)也够用但放到复杂物体上就要留意。3.4 片元着色器核心折射、反射、菲涅尔混合片元着色器是玻璃效果的核心。先做清晰玻璃暂时不考虑粗糙度。核心逻辑分三步计算视线方向。计算折射和反射方向。采样环境贴图用菲涅尔系数混合。一个简化版 WGSL 片元着色器如下group(0) binding(0) var envTexture: texture_cubef32; group(0) binding(1) var envSampler: sampler; fragment fn fs_main(in: VSOutput) - location(0) vec4f32 { let viewDir normalize(cameraPos - in.worldPos); let normal normalize(in.normal); let eta 1.0 / ior; // 从空气进入玻璃 let refractDir refract(-viewDir, normal, eta); let reflectDir reflect(-viewDir, normal); let reflected textureSample(envTexture, envSampler, reflectDir).rgb; let refracted textureSample(envTexture, envSampler, refractDir).rgb; let fresnel 0.04 0.96 * pow(1.0 - max(dot(normal, -viewDir), 0.0), 5.0); let color mix(refracted, reflected, fresnel); return vec4f32(color, 1.0); }这里cameraPos、ior是从 uniform 传入的。F0取 0.04是绝缘体玻璃的常见近似值。refract内置函数返回折射方向reflect返回反射方向。视线越靠近掠射角fresnel越大反射越强玻璃边缘就更明显。这个示例假设环境贴图是 Cube 纹理。如果你暂时没有 HDR 环境图可以先用一个纯色或渐变 Cube 纹理做验证。重要的是先把折射和反射的流程跑通。3.5 设置 uniform 绑定组玻璃物体需要用到相机位置、模型矩阵、IOR、F0 等参数。这些参数通常放在 uniform buffer 中通过绑定组传入着色器。创建绑定组时要提前声明 bind group layout。示例const uniformBuffer device.createBuffer({ size: 64, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, });在渲染循环里使用queue.writeBuffer更新 uniform。注意不要每帧创建新 buffer不然会不断累积内存碎片。一次创建循环里只更新内容这是 WebGPU 性能优化的基本习惯。3.6 第一颗玻璃球的验证标准把玻璃球绘制到画布上成功效果是球体边缘有明显环境反射。背景环境在球内出现一定程度的扭曲。转动相机时球内的扭曲方向会变化。球体质感不像普通半透明塑料而是有通透感和反射感。如果球体全黑优先检查环境贴图绑定和采样方向。如果只看到反射看不到折射可能菲涅尔系数偏大或者refractDir方向不对。先把代码精简到只采样refractDir确认背景会扭曲再加反射和菲涅尔。4. 关键参数IOR、菲涅尔、粗糙度、混合顺序4.1 IOR 到底怎么调IOR 是折射率。常见介质参考值介质IOR空气1.0水1.333玻璃1.5石英1.46钻石2.417大多数产品展示场景里玻璃取 1.5 就够了。IOR 越大折射偏折越明显物体的透明区域扭曲感越强。如果取到 2.4 左右会接近钻石的高折射效果。在 WGSL 的refract函数中eta是入射介质折射率除以折射介质折射率。从空气到玻璃入射介质是空气折射介质是玻璃所以eta 1.0 / 1.5。很多人把eta直接写成 1.5结果折射方向反了玻璃球看起来像在发光或者边缘变形方向不对。判断折射方向是否正确可以看球体边缘背后的背景变形。如果背景的直线边缘在球体内被扭曲成一个方向反过来对比真实玻璃杯方向一致就说明对。4.2 菲涅尔公式和 F0菲涅尔效应是指视线角度变化时反射强度不同。垂直看玻璃反射很弱擦着边缘看反射很强。玻璃边缘高光就是菲涅尔效应的结果。绝缘体材料的 F0 可以取 0.04。Schlick 近似写法let fresnel 0.04 0.96 * pow(1.0 - max(dot(normal, -viewDir), 0.0), 5.0);如果 F0 取太高玻璃会变得像镜子取太低玻璃高光不明显。做产品展示时F0 可以保持在 0.04 到 0.08 之间。想拉高边缘对比度可以调指数比如从 5.0 改成 3.0反射过渡会更柔和。4.3 粗糙度怎么影响玻璃清晰玻璃的粗糙度接近 0。磨砂玻璃需要模糊反射和折射方向。但实时渲染里真正对每个反射和折射方向做模糊计算很贵。比较低成本的做法是采样环境贴图时使用 mipmap level。粗糙度越大采样的 mip level 越高画面越模糊。在 WGSL 中可以用textureSampleLevel指定 level。这种方法能快速模拟磨砂玻璃但不会产生散射级的物理真实效果适合大多数展示场景。如果场景里有清晰玻璃和磨砂玻璃建议分成两种渲染管线不要用同一个 shader 强行兼容。否则你会陷入分支判断和性能损失最后两种效果都不理想。4.4 透明物体混合和排序顺序WebGPU 不会自动给透明物体排序。透明物体之间如果互相遮挡绘制顺序会影响最终结果。常见做法是先渲染场景中所有不透明物体。再将所有透明物体按相机距离从远到近排序。按排序结果逐个绘制透明物体。在 CPU 端你需要根据相机位置计算每个玻璃物体的距离并排序。不要把排序逻辑写到着色器里。如果多个玻璃物体嵌套比如一个玻璃杯里放另一个玻璃球简单距离排序会失效。这个时候需要离屏折射或深度剥离工程复杂度会明显增加。第一版建议避免嵌套透明物体。把玻璃物体放在不透明背景前先跑出正确观感再做复杂交互。5. 性能优化从单个玻璃球到批量场景5.1 先建立单个物体的性能基线不要一开始就铺开 100 个玻璃物体。先把单个玻璃球跑稳定记录 FPS 和 GPU 时间。在 Chrome 性能面板里可以看到 Rasterize 和 GPU frame 的时间。一个玻璃物体通常需要进行环境贴图采样可能不止一次。如果单个物体只有 30 帧那继续增加数量会很危险。先把单物体优化到 60 帧再考虑批量。玻璃材质本身比普通 PBR 材质更贵因为需要额外计算折射方向和反射方向两次环境采样。这个开销要用性能测试方式确认而不是靠直觉。5.2 降低环境贴图开销Cube 纹理在内存和采样成本上都比 2D 纹理高。尤其是大分辨率 Cube 纹理每一层 mipmap 都占显存。建议先用 512 或 1024 验证再逐步提升。如果反射内容不要求动态变化可以提前把环境贴图生成好并做模糊运行时只采样。不需要每帧重建。一个判断标准是把环境贴图从 2048 降到 512FPS 有明显提升说明当前瓶颈在纹理采样带宽。如果帧率没变可能要查其他地方的资源分配。5.3 屏幕空间折射的工程思路如果想让玻璃折射场景里的真实物体而不是只折射环境贴图通常用屏幕空间折射。简单流程是先用不透明管线和深度清除把场景渲染到离屏颜色纹理 A。再渲染玻璃物体片元着色器根据当前像素坐标加折射偏移量采样纹理 A。把采样结果和反射、菲涅尔混合。这个方案比物理上对每个像素做完整光追便宜很多也是很多实时项目采用的做法。但它有局限屏幕空间只能采样已经渲染出来的可见表面对于被遮挡的信息、屏幕边缘信息会缺失。因此要给折射偏移量设一个最大范围超出范围就回退到环境贴图采样。在 WebGPU 里离屏纹理可以用createTexture创建并作为 render pass 的 color attachment。渲染完不透明场景后把纹理作为bind_group传入玻璃管线。5.4 实例化和合批当玻璃物体数量变多比如一排玻璃装饰球每次提交一个 draw call 就会很浪费。可以改用实例化绘制把每个物体的位移、缩放、旋转矩阵放到实例属性中一次 draw call 绘制多个物体。如果物体使用相同的玻璃材质IOR 和粗糙度也相同实例化很合适。如果需要每个物体有自己的参数可以把参数放进另一个实例化缓冲区按 instance index 读取。这个优化建议在核心效果稳定后再做不要一开始就引入复杂的数据布局。6. 常见问题排查玻璃效果不对时按这个顺序查6.1 先做“固定颜色测试”片元着色器临时改成return vec4f32(1.0, 0.0, 0.0, 1.0);如果物体变成红色说明渲染管线基本正常问题在后续采样或计算逻辑。如果还是黑色或透明说明顶点、绑定组、混合或深度状态有问题。这个测试能快速隔离开问题区间比直接盯着一堆日志高效很多。6.2 检查环境贴图和采样方向固定颜色测试通过后恢复采样但先不要做折射只采样reflectDir。看球体表面有没有环境反射。如果没有说明环境贴图没有绑定或者采样坐标不对。如果反射出现但折射方向很怪检查 normal 和 viewDir 是否在同一个坐标系。常见问题是 normal 在物体局部空间viewDir 在世界空间两者直接点乘和相减结果会出现光斑和偏移。解决方法是先在顶点着色器里把 normal 变换到世界空间并归一化然后到片元阶段再使用。6.3 玻璃变成全白或全透明如果玻璃全白可能是颜色没有经过任何衰减或者环境贴图采样的是白色背景。可以先输出refracted颜色确认环境贴图数据是否正常。如果全透明检查混合因子和深度写入。把玻璃物体的depthWriteEnabled关掉同时使用src-alpha混合。如果还全透明可能 alpha 值等于 0。你可以临时返回vec4(1.0, 1.0, 1.0, 1.0)验证 alpha 通道是否为 1排除混合状态问题。6.4 性能下降和“掉帧”怎么查先停掉玻璃物体只渲染不透明物体看是否流畅。如果依然卡瓶颈在基础场景如果变流畅说明瓶颈在玻璃 shader 或环境采样。此时降低环境贴图分辨率或者把采样器改成filterable: false的一版看帧率是否变化。如果单帧内不断创建 texture 或 buffer也会造成明显卡顿。打开浏览器的 GPU 任务管理器可以快速判断是否资源分配太频繁。6.5 浏览器差异和 API 演进同一段 WebGPU 代码在 Chrome 上运行正常在 Edge 上报错在 Safari 上效果不同这是正常现象。常见原因是默认纹理格式、Y 轴翻转规则、扩展支持不同。建议使用context.getPreferredFormat()动态获取画布颜色格式而不是写死bgra8unorm。涉及设备适配器和纹理格式时加日志确认当前设备能力。WebGPU API 目前还在演进不同版本之间接口有小幅变化代码如果运行一段时间后报错要优先确认浏览器版本和参考的文档是否一致。注意WebGPU 透明渲染的排序问题并不会因为新 API 自动消失。遇到重影或遮挡错误先检查深度写入、绘制顺序和混合因子。把 WebGPU 玻璃材质从零跑通之后我的体会是它不是“一个效果预设”而是一整套关于渲染顺序、采样方向、混合状态和性能预算的工程问题。第一版用球体加环境贴图就能看到玻璃感不用一开始就做屏幕空间折射。等单物体稳定再逐步加粗糙度、透明排序和实例化。遇到问题先做固定颜色测试再看采样方向最后查资源和绘制顺序大部分情况都能找到原因。如果你在做实际项目记得给不支持 WebGPU 的旧环境准备一个普通半透明材质回退。玻璃材质好看但不要让页面因为一个材质问题而失去可用性。
返回列表