WebGL基础

WebGL是基于OpenGL ES的Web 3D图形API,可以在浏览器中渲染高性能的2D和3D图形。本章将介绍WebGL的基本概念和使用方法。

WebGL概述

什么是WebGL

const webglOverview = {
    definition: 'WebGL是一种JavaScript API,用于在浏览器中渲染交互式2D和3D图形',
    
    features: [
        '基于OpenGL ES 2.0/3.0',
        '使用GPU加速渲染',
        '跨平台支持',
        '无需插件',
        '与HTML元素集成'
    ],
    
    applications: [
        '3D游戏',
        '数据可视化',
        '虚拟现实',
        '产品展示',
        '科学模拟'
    ]
};

获取WebGL上下文

const canvas = document.getElementById('webgl-canvas');

const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');

if (!gl) {
    alert('您的浏览器不支持WebGL');
}

const gl2 = canvas.getContext('webgl2');

console.log('WebGL版本:', gl.getParameter(gl.VERSION));
console.log('GLSL版本:', gl.getParameter(gl.SHADING_LANGUAGE_VERSION));
console.log('渲染器:', gl.getParameter(gl.RENDERER));
console.log('厂商:', gl.getParameter(gl.VENDOR));

WebGL渲染管线

const renderPipeline = {
    stages: [
        {
            name: '顶点数据',
            description: '顶点坐标、颜色、纹理坐标等'
        },
        {
            name: '顶点着色器',
            description: '处理每个顶点,计算位置'
        },
        {
            name: '图元装配',
            description: '将顶点组装成三角形、线段等'
        },
        {
            name: '光栅化',
            description: '将图元转换为片段'
        },
        {
            name: '片段着色器',
            description: '计算每个像素的颜色'
        },
        {
            name: '深度测试/模板测试',
            description: '决定哪些片段可见'
        },
        {
            name: '帧缓冲',
            description: '最终输出到屏幕'
        }
    ]
};

着色器基础

顶点着色器

attribute vec3 aPosition;
attribute vec4 aColor;

uniform mat4 uModelMatrix;
uniform mat4 uViewMatrix;
uniform mat4 uProjectionMatrix;

varying vec4 vColor;

void main() {
    gl_Position = uProjectionMatrix * uViewMatrix * uModelMatrix * vec4(aPosition, 1.0);
    vColor = aColor;
}

片段着色器

precision mediump float;

varying vec4 vColor;

uniform vec3 uLightPosition;
uniform vec3 uAmbientColor;

void main() {
    gl_FragColor = vColor;
}

编译着色器

function createShader(gl, type, source) {
    const shader = gl.createShader(type);
    gl.shaderSource(shader, source);
    gl.compileShader(shader);
    
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        console.error('着色器编译错误:', gl.getShaderInfoLog(shader));
        gl.deleteShader(shader);
        return null;
    }
    
    return shader;
}

function createProgram(gl, vertexSource, fragmentSource) {
    const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);
    const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
    
    if (!vertexShader || !fragmentShader) {
        return null;
    }
    
    const program = gl.createProgram();
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program);
    
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
        console.error('程序链接错误:', gl.getProgramInfoLog(program));
        gl.deleteProgram(program);
        return null;
    }
    
    gl.deleteShader(vertexShader);
    gl.deleteShader(fragmentShader);
    
    return program;
}

const vertexShaderSource = `
    attribute vec3 aPosition;
    attribute vec4 aColor;
    uniform mat4 uModelViewProjection;
    varying vec4 vColor;
    
    void main() {
        gl_Position = uModelViewProjection * vec4(aPosition, 1.0);
        vColor = aColor;
    }
`;

const fragmentShaderSource = `
    precision mediump float;
    varying vec4 vColor;
    
    void main() {
        gl_FragColor = vColor;
    }
`;

const program = createProgram(gl, vertexShaderSource, fragmentShaderSource);

绘制三角形

顶点数据

const vertices = new Float32Array([
    0.0,  0.5,  0.0,
   -0.5, -0.5,  0.0,
    0.5, -0.5,  0.0
]);

const colors = new Float32Array([
    1.0, 0.0, 0.0, 1.0,
    0.0, 1.0, 0.0, 1.0,
    0.0, 0.0, 1.0, 1.0
]);

const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);

属性绑定

gl.useProgram(program);

const aPosition = gl.getAttribLocation(program, 'aPosition');
const aColor = gl.getAttribLocation(program, 'aColor');

gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.enableVertexAttribArray(aPosition);
gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 0, 0);

gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.enableVertexAttribArray(aColor);
gl.vertexAttribPointer(aColor, 4, gl.FLOAT, false, 0, 0);

渲染循环

function render() {
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    
    gl.useProgram(program);
    
    gl.drawArrays(gl.TRIANGLES, 0, 3);
    
    requestAnimationFrame(render);
}

render();

矩阵变换

矩阵工具函数

const mat4 = {
    create() {
        return new Float32Array([
            1, 0, 0, 0,
            0, 1, 0, 0,
            0, 0, 1, 0,
            0, 0, 0, 1
        ]);
    },
    
    identity(out) {
        out[0] = 1; out[1] = 0; out[2] = 0; out[3] = 0;
        out[4] = 0; out[5] = 1; out[6] = 0; out[7] = 0;
        out[8] = 0; out[9] = 0; out[10] = 1; out[11] = 0;
        out[12] = 0; out[13] = 0; out[14] = 0; out[15] = 1;
        return out;
    },
    
    perspective(out, fovy, aspect, near, far) {
        const f = 1.0 / Math.tan(fovy / 2);
        out[0] = f / aspect;
        out[1] = 0;
        out[2] = 0;
        out[3] = 0;
        out[4] = 0;
        out[5] = f;
        out[6] = 0;
        out[7] = 0;
        out[8] = 0;
        out[9] = 0;
        out[10] = (far + near) / (near - far);
        out[11] = -1;
        out[12] = 0;
        out[13] = 0;
        out[14] = (2 * far * near) / (near - far);
        out[15] = 0;
        return out;
    },
    
    lookAt(out, eye, center, up) {
        let x0, x1, x2, y0, y1, y2, z0, z1, z2, len;
        
        z0 = eye[0] - center[0];
        z1 = eye[1] - center[1];
        z2 = eye[2] - center[2];
        
        len = 1 / Math.sqrt(z0 * z0 + z1 * z1 + z2 * z2);
        z0 *= len; z1 *= len; z2 *= len;
        
        x0 = up[1] * z2 - up[2] * z1;
        x1 = up[2] * z0 - up[0] * z2;
        x2 = up[0] * z1 - up[1] * z0;
        
        len = Math.sqrt(x0 * x0 + x1 * x1 + x2 * x2);
        if (len) {
            len = 1 / len;
            x0 *= len; x1 *= len; x2 *= len;
        }
        
        y0 = z1 * x2 - z2 * x1;
        y1 = z2 * x0 - z0 * x2;
        y2 = z0 * x1 - z1 * x0;
        
        out[0] = x0; out[1] = y0; out[2] = z0; out[3] = 0;
        out[4] = x1; out[5] = y1; out[6] = z1; out[7] = 0;
        out[8] = x2; out[9] = y2; out[10] = z2; out[11] = 0;
        out[12] = -(x0 * eye[0] + x1 * eye[1] + x2 * eye[2]);
        out[13] = -(y0 * eye[0] + y1 * eye[1] + y2 * eye[2]);
        out[14] = -(z0 * eye[0] + z1 * eye[1] + z2 * eye[2]);
        out[15] = 1;
        
        return out;
    },
    
    translate(out, a, v) {
        const x = v[0], y = v[1], z = v[2];
        out[12] = a[0] * x + a[4] * y + a[8] * z + a[12];
        out[13] = a[1] * x + a[5] * y + a[9] * z + a[13];
        out[14] = a[2] * x + a[6] * y + a[10] * z + a[14];
        out[15] = a[3] * x + a[7] * y + a[11] * z + a[15];
        return out;
    },
    
    rotateY(out, a, rad) {
        const s = Math.sin(rad);
        const c = Math.cos(rad);
        const a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3];
        const a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11];
        
        out[0] = a00 * c - a20 * s;
        out[1] = a01 * c - a21 * s;
        out[2] = a02 * c - a22 * s;
        out[3] = a03 * c - a23 * s;
        out[8] = a00 * s + a20 * c;
        out[9] = a01 * s + a21 * c;
        out[10] = a02 * s + a22 * c;
        out[11] = a03 * s + a23 * c;
        
        return out;
    },
    
    multiply(out, a, b) {
        const a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3];
        const a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7];
        const a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11];
        const a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15];
        
        let b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3];
        out[0] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
        out[1] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
        out[2] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
        out[3] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
        
        b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7];
        out[4] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
        out[5] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
        out[6] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
        out[7] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
        
        b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11];
        out[8] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
        out[9] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
        out[10] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
        out[11] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
        
        b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15];
        out[12] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
        out[13] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
        out[14] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
        out[15] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
        
        return out;
    }
};

使用矩阵

const projectionMatrix = mat4.create();
mat4.perspective(projectionMatrix, 
    45 * Math.PI / 180,
    canvas.width / canvas.height,
    0.1,
    100.0
);

const viewMatrix = mat4.create();
mat4.lookAt(viewMatrix,
    [0, 0, 5],
    [0, 0, 0],
    [0, 1, 0]
);

const modelMatrix = mat4.create();
mat4.translate(modelMatrix, modelMatrix, [0, 0, 0]);
mat4.rotateY(modelMatrix, modelMatrix, 0);

const mvpMatrix = mat4.create();
mat4.multiply(mvpMatrix, viewMatrix, modelMatrix);
mat4.multiply(mvpMatrix, projectionMatrix, mvpMatrix);

const uMVP = gl.getUniformLocation(program, 'uModelViewProjection');
gl.uniformMatrix4fv(uMVP, false, mvpMatrix);

纹理映射

加载纹理

function loadTexture(gl, url) {
    return new Promise((resolve) => {
        const texture = gl.createTexture();
        const image = new Image();
        
        image.onload = function() {
            gl.bindTexture(gl.TEXTURE_2D, texture);
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
            
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
            
            resolve(texture);
        };
        
        image.src = url;
    });
}

const texture = await loadTexture(gl, 'texture.png');

gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);

const uTexture = gl.getUniformLocation(program, 'uTexture');
gl.uniform1i(uTexture, 0);

纹理坐标

const textureCoords = new Float32Array([
    0.0, 0.0,
    1.0, 0.0,
    0.0, 1.0,
    1.0, 1.0
]);

const texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, textureCoords, gl.STATIC_DRAW);

const aTexCoord = gl.getAttribLocation(program, 'aTexCoord');
gl.enableVertexAttribArray(aTexCoord);
gl.vertexAttribPointer(aTexCoord, 2, gl.FLOAT, false, 0, 0);

光照

基本光照着色器

const lightingVertexShader = `
    attribute vec3 aPosition;
    attribute vec3 aNormal;
    
    uniform mat4 uModelMatrix;
    uniform mat4 uViewMatrix;
    uniform mat4 uProjectionMatrix;
    uniform mat3 uNormalMatrix;
    
    varying vec3 vNormal;
    varying vec3 vPosition;
    
    void main() {
        vec4 worldPosition = uModelMatrix * vec4(aPosition, 1.0);
        vPosition = worldPosition.xyz;
        vNormal = uNormalMatrix * aNormal;
        
        gl_Position = uProjectionMatrix * uViewMatrix * worldPosition;
    }
`;

const lightingFragmentShader = `
    precision mediump float;
    
    varying vec3 vNormal;
    varying vec3 vPosition;
    
    uniform vec3 uLightPosition;
    uniform vec3 uLightColor;
    uniform vec3 uAmbientColor;
    uniform vec3 uMaterialColor;
    
    void main() {
        vec3 normal = normalize(vNormal);
        vec3 lightDir = normalize(uLightPosition - vPosition);
        
        float diff = max(dot(normal, lightDir), 0.0);
        vec3 diffuse = diff * uLightColor * uMaterialColor;
        
        vec3 ambient = uAmbientColor * uMaterialColor;
        
        vec3 viewDir = normalize(-vPosition);
        vec3 reflectDir = reflect(-lightDir, normal);
        float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
        vec3 specular = spec * uLightColor;
        
        vec3 result = ambient + diffuse + specular;
        gl_FragColor = vec4(result, 1.0);
    }
`;

东巴文小贴士

🎮 WebGL开发建议

  1. 使用库:Three.js、Babylon.js等库简化开发
  2. 调试工具:使用浏览器开发者工具和WebGL Inspector
  3. 性能优化:减少绘制调用、使用实例化渲染
  4. 错误处理:检查着色器编译和程序链接状态

📚 学习路径

  • 基础:理解渲染管线、着色器
  • 进阶:矩阵变换、光照模型
  • 高级:阴影、后处理、计算着色器

下一步

下一章将探讨 SVG图形,学习可缩放矢量图形的使用方法。