目录
- 初始化WebGL上下文
- 清除缓冲区
- 缓冲区对象
- 着色器和程序
- 属性指针
- 渲染
- 事件监听
- 错误处理
- 纹理映射
- Framebuffers和Renderbuffers
- Uniforms和Attributes
- 扩展和特性检测
- 性能监控
- 动画和帧率控制
- 多重渲染目标
- 3D数学库
- 灯光和阴影
- 交互和碰撞检测
WebGL编程通常围绕几个核心接口和函数进行,这些接口和函数允许开发者与WebGL上下文交互,创建和管理图形资源,以及渲染3D场景。以下是一些常用的WebGL接口和事件:
初始化WebGL上下文
首先,需要获取一个HTML <canvas> 元素的WebGL渲染上下文。
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) {
alert('Your browser does not support WebGL');
}
清除缓冲区
使用clearColor, clearDepth, clearStencil设置清除颜色、深度和模板缓冲区的值,然后调用clear方法清除它们。
const clearColor = [0.0, 0.0, 0.0, 1.0]; // RGBA
gl.clearColor(...clearColor);
gl.clearDepth(1.0); // 深度值范围0.0到1.0
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
缓冲区对象
创建顶点缓冲区对象(VBO)来存储顶点数据。
// 创建缓冲区对象
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 将数据填充到缓冲区
const vertices = [-1.0, 1.0, 0.0, -1.0, -1.0, 0.0, 1.0, -1.0, 0.0];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
// 解绑缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, null);
着色器和程序
创建顶点着色器和片段着色器,然后链接它们到一个程序对象。
const vertexShaderSource = `
attribute vec3 aPosition;
void main() {
gl_Position = vec4(aPosition, 1.0);
}
`;
const fragmentShaderSource = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderSource);
gl.compileShader(vertexShader);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderSource);
gl.compileShader(fragmentShader);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
属性指针
将顶点属性与着色器中的attribute变量关联。
const positionAttributeLocation = gl.getAttribLocation(program, 'aPosition');
gl.enableVertexAttribArray(positionAttributeLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.vertexAttribPointer(positionAttributeLocation, 3, gl.FLOAT, false, 0, 0);
渲染
调用drawArrays或drawElements来渲染图形。
gl.drawArrays(gl.TRIANGLES, 0, 3); // 使用顶点数组直接渲染
// 或者使用索引渲染
// const indexBuffer = ...; // 创建索引缓冲区
// gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
// gl.drawElements(gl.TRIANGLES, numIndices, gl.UNSIGNED_SHORT, 0);
事件监听
虽然WebGL自身不直接处理事件,但可以通过JavaScript监听DOM事件,如鼠标、键盘事件,然后在事件处理函数中调用WebGL相关函数来响应用户输入。
canvas.addEventListener('mousemove', (event) => {
const x = event.clientX;
const y = event.clientY;
// 根据鼠标位置更新WebGL场景
});
错误处理
使用gl.getError()检查WebGL错误,通常在关键操作后调用。
if (gl.getError() !== gl.NO_ERROR) {
console.error('WebGL error detected');
}
纹理映射
纹理映射为3D模型提供了丰富的表面细节。下面是如何加载和应用纹理的示例:
// 加载纹理
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
const image = new Image();
image.onload = () => {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.generateMipmap(gl.TEXTURE_2D);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.bindTexture(gl.TEXTURE_2D, null);
};
image.src = 'texture.png';
// 在着色器中使用纹理
const textureUniformLocation = gl.getUniformLocation(program, 'uTexture');
gl.uniform1i(textureUniformLocation, 0); // 告诉着色器纹理单元0用于采样
// 绘制时激活纹理
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
Framebuffers和Renderbuffers
Framebuffers允许你将渲染结果保存到纹理或其他缓冲区中,这对于后期处理、阴影映射等非常有用。
const framebuffer = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
const renderbuffer = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer);
const textureForFramebuffer = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, textureForFramebuffer);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, textureForFramebuffer, 0);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, renderbuffer);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, width, height);
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
console.error('Framebuffer is not complete!');
} else {
// 绑定framebuffer开始渲染
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
// ... 渲染逻辑 ...
// 渲染完成后恢复默认帧缓冲
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}
Uniforms和Attributes
Uniforms和Attributes是着色器中两种类型的变量,用于传递数据。Uniforms是全局的,所有顶点共享;Attributes则与每个顶点相关联。
// 设置uniform变量
const timeUniformLocation = gl.getUniformLocation(program, 'uTime');
gl.uniform1f(timeUniformLocation, currentTimeInSeconds);
// 更新attribute变量
const colorAttributeLocation = gl.getAttribLocation(program, 'aColor');
const colors = [1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0]; // 三个顶点的颜色
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
gl.vertexAttribPointer(colorAttributeLocation, 4, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(colorAttributeLocation);
扩展和特性检测
WebGL支持许多可选扩展,用于提供额外的功能。在使用之前,应先检查是否可用。
const ext = gl.getExtension('EXT_color_buffer_float');
if (ext) {
console.log('EXT_color_buffer_float is supported');
} else {
console.warn('EXT_color_buffer_float is not supported');
}
性能监控
使用WEBGL_debug_renderer_info扩展可以获取底层图形API的信息,帮助诊断性能问题。
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
console.log(`Vendor: ${vendor}, Renderer: ${renderer}`);
动画和帧率控制
在WebGL中,通常通过循环渲染每一帧来实现动画效果。你可以使用requestAnimationFrame来平滑地更新场景并保持流畅的动画。
let lastTime = performance.now();
function animate() {
const now = performance.now();
const delta = now - lastTime;
updateScene(delta / 1000); // 更新场景,参数是时间差(秒)
drawScene(); // 绘制场景
lastTime = now;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
多重渲染目标(MRT)
多重渲染目标允许在一次渲染调用中将输出写入多个颜色附件,这对于实现复杂的后期处理效果很有用。这通常涉及使用framebuffer对象和多个颜色附件。
const attachments = [
gl.COLOR_ATTACHMENT0,
gl.COLOR_ATTACHMENT1
];
const fb = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
gl.drawBuffers(attachments);
// 创建并绑定两个纹理作为颜色附件
const tex1 = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex1);
// ... 设置纹理参数 ...
const tex2 = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex2);
// ... 设置纹理参数 ...
// 将纹理绑定到颜色附件
gl.framebufferTexture2D(gl.FRAMEBUFFER, attachments[0], gl.TEXTURE_2D, tex1, 0);
gl.framebufferTexture2D(gl.FRAMEBUFFER, attachments[1], gl.TEXTURE_2D, tex2, 0);
// 检查framebuffer是否完整
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
console.error('Framebuffer is not complete!');
} else {
// ... 渲染到两个纹理 ...
gl.bindFramebuffer(gl.FRAMEBUFFER, null); // 渲染完成后恢复默认framebuffer
}
3D数学库
在WebGL编程中,3D数学运算(如向量、矩阵操作)是常见的。许多库如gl-matrix提供了便利的数学函数。
import * as glm from 'gl-matrix';
// 创建4x4矩阵
const matrix = glm.mat4.create();
// 旋转矩阵
glm.mat4.rotate(matrix, glm.radians(45), [0, 1, 0]);
// 将矩阵应用于顶点
for (let i = 0; i < vertices.length; i += 3) {
const transformedVertex = glm.vec3.transformMat4([vertices[i], vertices[i + 1], vertices[i + 2]], matrix);
vertices[i] = transformedVertex[0];
vertices[i + 1] = transformedVertex[1];
vertices[i + 2] = transformedVertex[2];
}
灯光和阴影
WebGL支持多种类型的灯光,如点光源、方向光和聚光灯。实现阴影通常涉及多重渲染目标和深度纹理。
// 创建光源
const lightPosition = [10, 10, 10];
const lightDirection = glm.vec3.normalize(glm.vec3.sub([0, 0, 0], lightPosition));
// 在着色器中计算光照
vec3 lightColor = vec3(1.0, 1.0, 1.0);
vec3 ambientColor = lightColor * ambientIntensity;
vec3 diffuseColor = max(dot(normal, lightDirection), 0.0) * lightColor * diffuseIntensity;
vec3 result = ambientColor + diffuseColor;
// 阴影映射(简化示例)
const shadowMapFBO = ...; // 创建framebuffer对象和深度纹理
renderSceneFromLightPerspective(shadowMapFBO); // 从光源视角渲染场景到深度纹理
交互和碰撞检测
通过监听DOM事件和计算几何碰撞,可以实现与3D场景的交互。
canvas.addEventListener('mousedown', (event) => {
const mousePos = getMousePosition(event, canvas);
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mousePos, camera);
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
// 处理点击事件
}
});
function getMousePosition(event, canvas) {
const rect = canvas.getBoundingClientRect();
return new THREE.Vector2(
(event.clientX - rect.left) / canvas.width * 2 - 1,
-(event.clientY - rect.top) / canvas.height * 2 + 1
);
}