WebGL常用接口和事件

548 阅读5分钟

目录


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

UniformsAttributes是着色器中两种类型的变量,用于传递数据。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
    );
}