当前位置: 首页 > news >正文

为世界添彩 - WebGL 中的颜色与着色器变量

欢迎回来!在第一部分,我们已经了解了 WebGL 的“渲染管线”,以及两个核心的“工人”——顶点着色器与片元着色器。我们通过 attribute 变量,成功地将顶点坐标从 JavaScript “投喂”给了顶点着色器。

今天的目标是:不仅仅告诉 GPU 在哪里画,还要告诉它画什么颜色

新的问题:如何传递颜色?

你可能会想:“很简单,在片元着色器里直接改颜色不就行了?”

// 片元着色器 (旧代码)
void main() {// gl_FragColor = vec4(1.0, 0.0, 0.5, 1.0); // 紫红色gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0); // 现在改成绿色!
}

没错,但这依然是给整个图形上同一个颜色。我们想要的是一个五彩斑斓的三角形,比如顶角是红色,左下角是绿色,右下角是蓝色。

这意味着,颜色数据也必须是跟顶点一一对应的。既然位置是每个顶点都不同的 attribute,那颜色自然也可以!

打通任督二脉:Varying 变量

好,假设我们成功地把每个顶点的颜色,像坐标一样,通过另一个 attribute 传给了顶点着色器

但这里有个关键问题:顶点着色器只负责处理顶点(三个角),而片元着色器负责处理图形内部的每一个像素。顶点着色器知道三个角的颜色,它怎么告诉片元着色器“中间那些像素该涂什么颜色”呢?

答案就是今天的主角:varying 变量

你可以把 varying 想象成一座桥梁,连接着顶点着色器和片元着色器。它的工作机制非常神奇:

  1. 你在顶点着色器里,给一个 varying 变量赋值(比如,把从 attribute 接收到的顶点颜色赋给它)。
  2. GPU 开始绘制图形。当它填充两个顶点之间的像素时,它会自动地、线性地“插值”这个 varying 变量。
  3. 在片元着色器里,你接收这个 varying 变量。此时你收到的值,已经是 GPU 为你平滑计算好的、当前像素点“应该有”的值。

听起来有点抽象?看这张图:

我们只定义了三个角的颜色,中间所有像素的颜色都是 GPU 通过 varying 变量自动“渐变”出来的。这就是 WebGL 中创造平滑渐变的秘密!

GLSL 变量家族小结

现在,我们认识了 GLSL 中负责“通信”的三种主要变量类型:

  1. attribute从 JavaScript 到顶点着色器。用于传递每个顶点都不同的数据,如位置、颜色、纹理坐标等。只能在顶点着色器中使用。
  2. varying从顶点着色器到片元着色器。用于传递经过插值计算的数据。必须在两个着色器中成对声明。
  3. uniform:(我们下一篇会用到) 从 JavaScript 到两个着色器。用于传递对所有顶点都相同的数据,比如一个全局的变换矩阵或光照颜色。

搞清楚它们的职责,是掌握 GLSL 的关键。

开工!改造我们的代码

理论讲完了,我们来动手修改上一篇的代码。

1. 更新顶点着色器

我们需要它接收颜色数据,并把它传递给片元着色器。

// 新增一个 attribute 来接收颜色数据
attribute vec4 a_color;
// 新增一个 varying 变量作为桥梁
varying vec4 v_color;void main() {gl_Position = vec4(a_position, 0.0, 1.0);// 将接收到的颜色直接传递给 varying 变量v_color = a_color;
}

2. 更新片元着色器

它不再使用写死的颜色,而是接收从顶点着色器传来的、经过插值的颜色。

precision mediump float;// 声明同名的 varying 变量来接收数据
varying vec4 v_color;void main() {// 使用插值后的颜色作为当前像素的颜色gl_FragColor = v_color;
}

3. 大改版:JavaScript

JavaScript 的工作要多一些,因为它现在需要管理并发送两份数据(位置和颜色)。

一个常见的、性能更好的做法是数据交错 (Interleaving)。我们不再创建两个独立的数组,而是把一个顶点所有的数据(位置、颜色)都放在一起,存进一个大数组和同一个 Buffer 中。

[X1, Y1, R1, G1, B1, A1, X2, Y2, R2, G2, B2, A2, ...]

这样做的好处是数据更紧凑,GPU 读取效率更高。但这也意味着,我们需要更精确地告诉 WebGL 如何从这一个 Buffer 里,分别解析出位置和颜色。这就是 gl.vertexAttribPointer 函数中 strideoffset 参数大显身手的时候了。

  • stride:告诉 WebGL “一整套顶点数据”有多长(占多少字节)。简单说,就是跳多远才能到下一组数据的开头。
  • offset:告诉 WebGL 当前这个 attribute 的数据,在一套数据里是从哪里(第几个字节)开始的。
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>WebGL 教程 2:彩色三角形</title><style>body { background-color: #333; color: #eee; text-align: center; }canvas { background-color: #000; border: 1px solid #555; }</style>
</head>
<body onload="main()"><h1>为世界添彩 - WebGL 中的颜色!</h1><canvas id="webgl-canvas" width="500" height="500"></canvas><!-- 顶点着色器代码 (已更新) --><script id="vertex-shader" type="x-shader/x-vertex">attribute vec2 a_position;// 新增:接收顶点的颜色 a_color (RGBA)attribute vec4 a_color;// 新增:varying 变量,用于将颜色传递给片元着色器varying vec4 v_color;void main() {gl_Position = vec4(a_position, 0.0, 1.0);// 将从 attribute 接收到的颜色,赋值给 varying 变量v_color = a_color;}</script><!-- 片元着色器代码 (已更新) --><script id="fragment-shader" type="x-shader/x-fragment">precision mediump float;// 新增:接收从顶点着色器传来的、已插值的颜色varying vec4 v_color;void main() {// 使用这个插值后的颜色作为像素的最终颜色gl_FragColor = v_color;}</script><script>function main() {// ... (步骤 1, 2, 3 与上一篇相同:获取上下文、编译链接着色器) ...const canvas = document.getElementById('webgl-canvas');const gl = canvas.getContext('webgl');if (!gl) { alert('WebGL not supported!'); return; }const vertexShaderSource = document.getElementById('vertex-shader').text;const fragmentShaderSource = document.getElementById('fragment-shader').text;const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);const program = createProgram(gl, vertexShader, fragmentShader);// 4. 找到 attribute 的位置const positionAttributeLocation = gl.getAttribLocation(program, "a_position");// 新增:找到 a_color 的位置const colorAttributeLocation = gl.getAttribLocation(program, "a_color");// 5. 创建 Buffer,并存入交错的数据const positionBuffer = gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);// 数据交错:[X1, Y1, R1, G1, B1,  X2, Y2, R2, G2, B2, ... ]// 顶点1 (红色), 顶点2 (绿色), 顶点3 (蓝色)const positionsAndColors = [0.0,  0.5,    1.0, 0.0, 0.0, // 顶点1: 坐标(0, 0.5), 颜色(R=1, G=0, B=0)-0.5, -0.5,    0.0, 1.0, 0.0, // 顶点2: 坐标(-0.5, -0.5), 颜色(R=0, G=1, B=0)0.5, -0.5,    0.0, 0.0, 1.0  // 顶点3: 坐标(0.5, -0.5), 颜色(R=0, G=0, B=1)];gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positionsAndColors), gl.STATIC_DRAW);// ... (步骤 6, 7 与上一篇相同:清空画布、启用程序) ...gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);gl.clearColor(0.1, 0.1, 0.1, 1.0);gl.clear(gl.COLOR_BUFFER_BIT);gl.useProgram(program);// 8. 告诉 WebGL 如何从唯一的 Buffer 中解析出两份数据// 启用两个 attributegl.enableVertexAttribArray(positionAttributeLocation);gl.enableVertexAttribArray(colorAttributeLocation);// 再次绑定 Buffer (好习惯)gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);const FSIZE = (new Float32Array()).BYTES_PER_ELEMENT; // 每个浮点数占4个字节const STRIDE = 5 * FSIZE; // 一套完整顶点数据(X,Y,R,G,B)的总字节数// ---- 指示 a_position 如何解析数据 ----const positionSize = 2; // 每个顶点的位置由 2 个分量组成const positionType = gl.FLOAT;const positionNormalize = false;const positionOffset = 0 * FSIZE; // 位置数据从一套数据的开头(偏移量0)开始gl.vertexAttribPointer(positionAttributeLocation,positionSize,positionType,positionNormalize,STRIDE, // Stride: 告诉它下一组位置数据在多远之后positionOffset // Offset: 告诉它这组数据从哪里开始);// ---- 指示 a_color 如何解析数据 ----const colorSize = 3; // 每个顶点的颜色由 3 个分量组成 (RGB)const colorType = gl.FLOAT;const colorNormalize = false;const colorOffset = 2 * FSIZE; // 颜色数据从偏移量2个浮点数大小之后开始gl.vertexAttribPointer(colorAttributeLocation,colorSize,colorType,colorNormalize,STRIDE, // Stride: 同样是一套完整顶点数据的长度colorOffset // Offset: 告诉它颜色数据在位置数据之后);// 9. 绘制!const primitiveType = gl.TRIANGLES;const drawOffset = 0;const count = 3;gl.drawArrays(primitiveType, drawOffset, count);}// --- 辅助函数 (与上一篇相同) ---function createShader(gl, type, source) {const shader = gl.createShader(type);gl.shaderSource(shader, source);gl.compileShader(shader);if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) return shader;console.error("Shader compile error:", gl.getShaderInfoLog(shader));gl.deleteShader(shader);}function createProgram(gl, vertexShader, fragmentShader) {const program = gl.createProgram();gl.attachShader(program, vertexShader);gl.attachShader(program, fragmentShader);gl.linkProgram(program);if (gl.getProgramParameter(program, gl.LINK_STATUS)) return program;console.error("Program link error:", gl.getProgramInfoLog(program));gl.deleteProgram(program);}</script>
</body>
</html>
总结与展望

现在,你的浏览器里应该出现了一个色彩斑斓的三角形,从红色的顶点平滑地过渡到绿色和蓝色。非常漂亮,不是吗?

今天,我们掌握了 WebGL 中数据流转的又一关键环节:

  • 学会了使用 varying 变量,在顶点着色器和片元着色器之间架起了沟通的桥梁
  • 理解了 GPU 是如何自动插值 varying 变量,从而创造出平滑的渐变效果。
  • 实践了数据交错的技巧,以及如何使用 strideoffset 参数,让 WebGL 从同一个 Buffer 中精确地解析出多种 attribute 数据

我们的三角形已经有了形状和颜色,但它还是一个“死”的东西。在下一篇文章中,我们将学习 2D 图形学中最核心的概念之一——矩阵变换。我们将引入第三位变量家族成员 uniform,并通过它让我们的三角形动起来:平移、旋转、缩放!

敬请期待 《第 3 篇:让图形动起来 - WebGL 2D 变换》

http://www.dtcms.com/a/398014.html

相关文章:

  • 初识MYSQL —— mysql的安装
  • c回顾 01
  • 【LeetCode 每日一题】3484. 设计电子表格——(解法一)二维数组
  • python+django/flask+springboot实践性教学系统 实训任务发布 学生作业提交 教师评阅管理系统
  • 洞悉未来,智驭不确定性:蒙特卡洛模拟决策模型实践
  • 长宁哪里有做网站优化比较好利润在100万到300万之间税率2021
  • 沈阳网站设计外包广西建设网官网桂建云
  • vscode 插件怎么实现编辑器行号处添加图标标记
  • Git 从零到一:以 Gitee 为例的实战与可视化指南
  • React 标准 SPA 项目 入门学习记录
  • HAProxy 完整指南:简介、负载均衡原理与安装配置
  • 领码课堂 | React 核心组件与高级性能优化全实战指南
  • 涡轮丝杆升降机的丝杆材质有哪些?
  • 前端笔记:vue中 Map、Set之间的使用和区别
  • 中美关系最新消息视频重庆seo优化公司
  • 【Cesium 开发实战教程】第六篇:三维模型高级交互:点击查询、材质修改与动画控制
  • 英雄联盟视频网站源码做产品设计之前怎么查资料国外网站
  • Vue3-接入飞书H5应用
  • 四川省建设厅网站川北医学院广告网站怎么建设
  • 七彩喜智慧养老:科技向善,让晚年生活绽放“喜”悦之光
  • 模型驱动的 AI Agent架构:亚马逊云科技的Strands框架技术深度解析
  • 【数据结构】——外部排序(K路归并)
  • 【观成科技】活跃黑产团伙“黑猫”攻击武器加密通信分析
  • 高斯过程(Gaussian Process)回归:一种贝叶斯非参数方法
  • 微算法科技(NASDAQ MLGO)创新基于账户加权图与后量子密码学的区块链
  • 中国银行信息科技岗位笔试
  • WXML 编译错误修复总结
  • 怎么给网站wordpress游戏网站策划书
  • Halcon学习--(3)图像阈值处理
  • 知识导航新体验:Perplexica+cpolar 24小时智能服务