{T}

几何图形绘制

绘制模式

WebGL 支持多种基本图元类型,通过 gl.drawArrays()gl.drawElements() 指定绘制模式。

基本图元类型

模式常量说明顶点数要求
gl.POINTS绘制独立的点每个点 1 个顶点
线段gl.LINES绘制独立的线段每条线 2 个顶点
线条带gl.LINE_STRIP连续的线段≥ 2 个顶点
线环gl.LINE_LOOP闭合的线环≥ 3 个顶点
三角形gl.TRIANGLES独立的三角形每个三角形 3 个顶点
三角形带gl.TRIANGLE_STRIP连续的三角形带≥ 3 个顶点
三角形扇gl.TRIANGLE_FAN三角形扇≥ 3 个顶点

图元示意图

plaintext
POINTS(点):
  ●    ●    ●    ●
 
LINES(线段):
  ●────●    ●────●
 
LINE_STRIP(线条带):
  ●────●────●────●
 
LINE_LOOP(线环):
  ●────●
  │    │
  ●────●
 
TRIANGLES(三角形):
  ●      ●
  ▲      ▲
●─●    ●─●
 
TRIANGLE_STRIP(三角形带):
  ●───●
  │╱ ╱│
  ●───●
  │╱ ╱│
  ●───●
 
TRIANGLE_FAN(三角形扇):

     ╱│╲
    ╱ │ ╲
  ●───●───●

绘制点

基本点绘制

javascript
// 顶点着色器
const vertexShaderSource = `
  attribute vec4 a_Position;
  
  void main() {
    gl_Position = a_Position;
    gl_PointSize = 10.0;  // 设置点的大小(像素)
  }
`;
 
// 片段着色器
const fragmentShaderSource = `
  precision mediump float;
  uniform vec4 u_Color;
  
  void main() {
    gl_FragColor = u_Color;  // 设置点的颜色
  }
`;
 
// JavaScript 代码
function main() {
  const canvas = document.getElementById('glCanvas');
  const gl = canvas.getContext('webgl');
  
  // ... 创建着色器程序代码 ...
  
  // 设置点的位置
  const vertices = new Float32Array([
    0.0,  0.5,   // 第一个点
   -0.5, -0.5,   // 第二个点
    0.5, -0.5    // 第三个点
  ]);
  
  const buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
  
  const a_Position = gl.getAttribLocation(program, 'a_Position');
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(a_Position);
  
  // 设置点的颜色
  const u_Color = gl.getUniformLocation(program, 'u_Color');
  gl.uniform4f(u_Color, 1.0, 0.0, 0.0, 1.0);  // 红色
  
  // 清除画布
  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  gl.clear(gl.COLOR_BUFFER_BIT);
  
  // 绘制点
  gl.drawArrays(gl.POINTS, 0, 3);  // 绘制 3 个点
}

圆形点

在片段着色器中绘制圆形点:

glsl
// 片段着色器
precision mediump float;
 
void main() {
  // 计算片段到点中心的距离
  float dist = distance(gl_PointCoord, vec2(0.5, 0.5));
  
  // 如果距离大于 0.5,丢弃片段
  if (dist > 0.5) {
    discard;
  }
  
  // 设置颜色
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

渐变圆形点

glsl
precision mediump float;
 
void main() {
  float dist = distance(gl_PointCoord, vec2(0.5, 0.5));
  
  if (dist > 0.5) {
    discard;
  }
  
  // 根据距离设置透明度
  float alpha = 1.0 - dist * 2.0;
  gl_FragColor = vec4(1.0, 0.0, 0.0, alpha);
}

绘制线段

基本线段

javascript
// 顶点数据:两两组成线段
const vertices = new Float32Array([
 -0.5,  0.5,    // 线段 1 起点
 -0.5, -0.5,    // 线段 1 终点
  0.5,  0.5,    // 线段 2 起点
  0.5, -0.5     // 线段 2 终点
]);
 
// 绘制线段
gl.drawArrays(gl.LINES, 0, 4);  // 4 个顶点,2 条线段

线宽设置

WebGL 的线宽受硬件限制,默认宽度为 1.0:

javascript
// 设置线宽(可能不支持)
gl.lineWidth(3.0);
 
// 检查实际线宽
const lineWidth = gl.getParameter(gl.LINE_WIDTH);
console.log('线宽:', lineWidth);
 
const lineWidthRange = gl.getParameter(gl.ALIASED_LINE_WIDTH_RANGE);
console.log('线宽范围:', lineWidthRange);  // 通常为 [1, 1]

彩色线段

javascript
// 顶点着色器
const vertexShaderSource = `
  attribute vec4 a_Position;
  attribute vec4 a_Color;
  varying vec4 v_Color;
  
  void main() {
    gl_Position = a_Position;
    v_Color = a_Color;
  }
`;
 
// 片段着色器
const fragmentShaderSource = `
  precision mediump float;
  varying vec4 v_Color;
  
  void main() {
    gl_FragColor = v_Color;
  }
`;
 
// 顶点数据(位置 + 颜色)
const vertices = new Float32Array([
  // x, y, r, g, b, a
 -0.5,  0.5,  1.0, 0.0, 0.0, 1.0,  // 红色
 -0.5, -0.5,  0.0, 1.0, 0.0, 1.0,  // 绿色
  0.5,  0.5,  0.0, 0.0, 1.0, 1.0,  // 蓝色
  0.5, -0.5,  1.0, 1.0, 0.0, 1.0   // 黄色
]);
 
// 设置属性指针
const FSIZE = vertices.BYTES_PER_ELEMENT;
 
const a_Position = gl.getAttribLocation(program, 'a_Position');
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 6, 0);
gl.enableVertexAttribArray(a_Position);
 
const a_Color = gl.getAttribLocation(program, 'a_Color');
gl.vertexAttribPointer(a_Color, 4, gl.FLOAT, false, FSIZE * 6, FSIZE * 2);
gl.enableVertexAttribArray(a_Color);

绘制三角形

基本三角形

javascript
// 顶点数据
const vertices = new Float32Array([
  0.0,  0.5,   // 顶点 0
 -0.5, -0.5,   // 顶点 1
  0.5, -0.5    // 顶点 2
]);
 
// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, 3);  // 3 个顶点,1 个三角形

三角形带

三角形带更高效,共享边:

javascript
// 顶点顺序:0-1-2, 1-2-3, 2-3-4, ...
const vertices = new Float32Array([
 -0.5,  0.5,   // 顶点 0
 -0.5, -0.5,   // 顶点 1
  0.0,  0.5,   // 顶点 2
  0.0, -0.5,   // 顶点 3
  0.5,  0.5,   // 顶点 4
  0.5, -0.5    // 顶点 5
]);
 
// 绘制三角形带(4 个三角形)
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 6);

三角形带的顶点顺序:

plaintext
顶点 0 ──── 顶点 2 ──── 顶点 4
  │   ╲    │   ╲    │
  │    ╲   │    ╲   │
顶点 1 ──── 顶点 3 ──── 顶点 5
 
形成三角形:0-1-2, 2-1-3, 2-3-4, 4-3-5

三角形扇

三角形扇以第一个顶点为中心:

javascript
// 顶点顺序:0-1-2, 0-2-3, 0-3-4, ...
const vertices = new Float32Array([
  0.0,  0.0,   // 中心顶点(顶点 0)
  0.0,  0.5,   // 顶点 1
  0.5,  0.0,   // 顶点 2
  0.5, -0.5,   // 顶点 3
  0.0, -0.5,   // 顶点 4
 -0.5,  0.0,   // 顶点 5
 -0.5,  0.5    // 顶点 6
]);
 
// 绘制三角形扇(5 个三角形)
gl.drawArrays(gl.TRIANGLE_FAN, 0, 7);

三角形扇示意图:

plaintext
        顶点 1

    顶点 6 └──── 顶点 0(中心)
         ╱ ╲   ╱
    顶点 5   ╲ ╱
              顶点 2
         ╱ ╲
    顶点 4   ╲
              顶点 3
 
形成三角形:0-1-2, 0-2-3, 0-3-4, 0-4-5, 0-5-6

索引绘制

使用索引可以重用顶点,减少数据量。

基本索引绘制

javascript
// 顶点数据(矩形的 4 个顶点)
const vertices = new Float32Array([
 -0.5,  0.5,   // 顶点 0
 -0.5, -0.5,   // 顶点 1
  0.5, -0.5,   // 顶点 2
  0.5,  0.5    // 顶点 3
]);
 
// 索引数据(定义两个三角形)
const indices = new Uint16Array([
  0, 1, 2,  // 第一个三角形
  0, 2, 3   // 第二个三角形
]);
 
// 创建顶点缓冲区
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
 
// 配置顶点属性
const a_Position = gl.getAttribLocation(program, 'a_Position');
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);
 
// 创建索引缓冲区
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
 
// 绘制
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);

索引数据类型

类型常量顶点数量范围
无符号字节gl.UNSIGNED_BYTE0 - 255
无符号短整型gl.UNSIGNED_SHORT0 - 65535
无符号整型gl.UNSIGNED_INT0 - 4294967295(WebGL 2.0)

立方体索引绘制

javascript
// 立方体 8 个顶点
const vertices = new Float32Array([
  // 前面
 -1.0,  1.0,  1.0,   // 顶点 0
 -1.0, -1.0,  1.0,   // 顶点 1
  1.0, -1.0,  1.0,   // 顶点 2
  1.0,  1.0,  1.0,   // 顶点 3
  
  // 后面
 -1.0,  1.0, -1.0,   // 顶点 4
 -1.0, -1.0, -1.0,   // 顶点 5
  1.0, -1.0, -1.0,   // 顶点 6
  1.0,  1.0, -1.0    // 顶点 7
]);
 
// 索引(6 个面,每个面 2 个三角形)
const indices = new Uint16Array([
  0, 1, 2,    0, 2, 3,    // 前面
  4, 5, 6,    4, 6, 7,    // 后面
  0, 4, 7,    0, 7, 3,    // 上面
  1, 5, 6,    1, 6, 2,    // 下面
  0, 1, 5,    0, 5, 4,    // 左面
  3, 2, 6,    3, 6, 7     // 右面
]);
 
// 绘制立方体
gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0);

顶点属性详解

属性配置参数

javascript
gl.vertexAttribPointer(location, size, type, normalized, stride, offset);
参数说明示例值
location属性位置gl.getAttribLocation() 返回值
size每个顶点的分量数1, 2, 3, 4
type数据类型gl.FLOAT, gl.INT
normalized是否归一化true, false
stride步长(字节)0 或自定义值
offset偏移量(字节)0 或自定义值

单个属性

javascript
// 仅位置属性
const vertices = new Float32Array([
  0.0,  0.5,  0.0,  // 顶点 0: x, y, z
 -0.5, -0.5,  0.0,  // 顶点 1
  0.5, -0.5,  0.0   // 顶点 2
]);
 
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, 0, 0);

多个属性(交错存储)

javascript
// 位置 + 颜色交错存储
const vertices = new Float32Array([
  // x, y, z, r, g, b
  0.0,  0.5, 0.0,  1.0, 0.0, 0.0,  // 顶点 0: 位置 + 红色
 -0.5, -0.5, 0.0,  0.0, 1.0, 0.0,  // 顶点 1: 位置 + 绿色
  0.5, -0.5, 0.0,  0.0, 0.0, 1.0   // 顶点 2: 位置 + 蓝色
]);
 
const FSIZE = vertices.BYTES_PER_ELEMENT;
const stride = FSIZE * 6;  // 每个顶点的总字节数
 
// 位置属性
const a_Position = gl.getAttribLocation(program, 'a_Position');
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, stride, 0);
gl.enableVertexAttribArray(a_Position);
 
// 颜色属性
const a_Color = gl.getAttribLocation(program, 'a_Color');
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, stride, FSIZE * 3);
gl.enableVertexAttribArray(a_Color);

多个属性(分离缓冲区)

javascript
// 位置数据
const positions = 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,
  0.0, 1.0, 0.0,
  0.0, 0.0, 1.0
]);
 
// 位置缓冲区
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);
 
// 颜色缓冲区
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Color);

交错存储 vs 分离存储

特性交错存储分离存储
内存布局紧凑分散
缓存效率
更新灵活性
代码复杂度中等简单

选择建议

  • 静态数据、不变数据 → 交错存储
  • 频繁更新的数据 → 分离存储

绘制参数详解

gl.drawArrays()

javascript
gl.drawArrays(mode, first, count);
参数说明
mode图元类型(gl.POINTS, gl.LINES, gl.TRIANGLES 等)
first起始顶点索引(从 0 开始)
count绘制的顶点数量

示例:

javascript
// 绘制前 3 个顶点
gl.drawArrays(gl.TRIANGLES, 0, 3);
 
// 从第 3 个顶点开始,绘制 6 个顶点
gl.drawArrays(gl.TRIANGLES, 3, 6);

gl.drawElements()

javascript
gl.drawElements(mode, count, type, offset);
参数说明
mode图元类型
count索引数量
type索引数据类型(gl.UNSIGNED_BYTE, gl.UNSIGNED_SHORT)
offset索引缓冲区偏移量(字节)

示例:

javascript
// 绘制所有索引
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
 
// 从第 6 个索引开始绘制
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 6 * 2);

复杂图形示例

绘制圆

javascript
/**
 * 生成圆形顶点数据
 * @param {number} radius - 半径
 * @param {number} segments - 分段数
 * @param {number} centerX - 中心 X 坐标
 * @param {number} centerY - 中心 Y 坐标
 * @returns {Object} - 顶点和索引数据
 */
function createCircle(radius, segments, centerX = 0, centerY = 0) {
  const vertices = [centerX, centerY];  // 中心点
  const indices = [];
  
  // 生成圆周顶点
  for (let i = 0; i <= segments; i++) {
    const angle = (i / segments) * Math.PI * 2;
    vertices.push(
      centerX + Math.cos(angle) * radius,
      centerY + Math.sin(angle) * radius
    );
  }
  
  // 生成索引(三角形扇)
  for (let i = 1; i <= segments; i++) {
    indices.push(0, i, i + 1);
  }
  
  return {
    vertices: new Float32Array(vertices),
    indices: new Uint16Array(indices)
  };
}
 
// 使用示例
const circle = createCircle(0.5, 32);
 
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, circle.vertices, gl.STATIC_DRAW);
 
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, circle.indices, gl.STATIC_DRAW);
 
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);
 
gl.drawElements(gl.TRIANGLES, circle.indices.length, gl.UNSIGNED_SHORT, 0);

绘制球体

javascript
/**
 * 生成球体顶点数据
 * @param {number} radius - 半径
 * @param {number} latBands - 纬度分段数
 * @param {number} longBands - 经度分段数
 */
function createSphere(radius, latBands, longBands) {
  const positions = [];
  const normals = [];
  const texCoords = [];
  const indices = [];
  
  for (let lat = 0; lat <= latBands; lat++) {
    const theta = lat * Math.PI / latBands;
    const sinTheta = Math.sin(theta);
    const cosTheta = Math.cos(theta);
    
    for (let lon = 0; lon <= longBands; lon++) {
      const phi = lon * 2 * Math.PI / longBands;
      const sinPhi = Math.sin(phi);
      const cosPhi = Math.cos(phi);
      
      const x = cosPhi * sinTheta;
      const y = cosTheta;
      const z = sinPhi * sinTheta;
      
      const u = lon / longBands;
      const v = lat / latBands;
      
      positions.push(radius * x, radius * y, radius * z);
      normals.push(x, y, z);
      texCoords.push(u, v);
    }
  }
  
  for (let lat = 0; lat < latBands; lat++) {
    for (let lon = 0; lon < longBands; lon++) {
      const first = lat * (longBands + 1) + lon;
      const second = first + longBands + 1;
      
      indices.push(first, second, first + 1);
      indices.push(second, second + 1, first + 1);
    }
  }
  
  return {
    positions: new Float32Array(positions),
    normals: new Float32Array(normals),
    texCoords: new Float32Array(texCoords),
    indices: new Uint16Array(indices)
  };
}

绘制圆柱体

javascript
/**
 * 生成圆柱体顶点数据
 * @param {number} radiusTop - 顶部半径
 * @param {number} radiusBottom - 底部半径
 * @param {number} height - 高度
 * @param {number} radialSegments - 径向分段数
 */
function createCylinder(radiusTop, radiusBottom, height, radialSegments) {
  const positions = [];
  const normals = [];
  const indices = [];
  const halfHeight = height / 2;
  
  // 侧面顶点
  for (let i = 0; i <= radialSegments; i++) {
    const angle = (i / radialSegments) * Math.PI * 2;
    const sin = Math.sin(angle);
    const cos = Math.cos(angle);
    
    // 顶部顶点
    positions.push(cos * radiusTop, halfHeight, sin * radiusTop);
    normals.push(cos, 0, sin);
    
    // 底部顶点
    positions.push(cos * radiusBottom, -halfHeight, sin * radiusBottom);
    normals.push(cos, 0, sin);
  }
  
  // 侧面索引
  for (let i = 0; i < radialSegments; i++) {
    const a = i * 2;
    const b = a + 1;
    const c = a + 2;
    const d = a + 3;
    
    indices.push(a, b, c);
    indices.push(b, d, c);
  }
  
  return {
    positions: new Float32Array(positions),
    normals: new Float32Array(normals),
    indices: new Uint16Array(indices)
  };
}

实例:绘制彩色三角形

完整示例代码:

html
<!DOCTYPE html>
<html>
<head>
  <title>彩色三角形</title>
  <style>
    canvas { border: 1px solid #ccc; }
  </style>
</head>
<body>
  <canvas id="glCanvas" width="600" height="400"></canvas>
  
  <script>
    const vertexShaderSource = `
      attribute vec3 a_Position;
      attribute vec3 a_Color;
      varying vec3 v_Color;
      
      void main() {
        gl_Position = vec4(a_Position, 1.0);
        v_Color = a_Color;
      }
    `;
    
    const fragmentShaderSource = `
      precision mediump float;
      varying vec3 v_Color;
      
      void main() {
        gl_FragColor = vec4(v_Color, 1.0);
      }
    `;
    
    function createShader(gl, type, source) {
      const shader = gl.createShader(type);
      gl.shaderSource(shader, source);
      gl.compileShader(shader);
      return shader;
    }
    
    function createProgram(gl, vs, fs) {
      const program = gl.createProgram();
      gl.attachShader(program, vs);
      gl.attachShader(program, fs);
      gl.linkProgram(program);
      return program;
    }
    
    function main() {
      const canvas = document.getElementById('glCanvas');
      const gl = canvas.getContext('webgl');
      
      const vs = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
      const fs = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
      const program = createProgram(gl, vs, fs);
      gl.useProgram(program);
      
      // 顶点数据:位置 + 颜色
      const vertices = new Float32Array([
         0.0,  0.5, 0.0,  1.0, 0.0, 0.0,  // 顶点 0
        -0.5, -0.5, 0.0,  0.0, 1.0, 0.0,  // 顶点 1
         0.5, -0.5, 0.0,  0.0, 0.0, 1.0   // 顶点 2
      ]);
      
      const FSIZE = vertices.BYTES_PER_ELEMENT;
      
      const buffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
      gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
      
      const a_Position = gl.getAttribLocation(program, 'a_Position');
      gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
      gl.enableVertexAttribArray(a_Position);
      
      const a_Color = gl.getAttribLocation(program, 'a_Color');
      gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
      gl.enableVertexAttribArray(a_Color);
      
      gl.clearColor(0.0, 0.0, 0.0, 1.0);
      gl.clear(gl.COLOR_BUFFER_BIT);
      
      gl.drawArrays(gl.TRIANGLES, 0, 3);
    }
    
    main();
  </script>
</body>
</html>

小结

核心要点

  • 图元类型:POINTS、LINES、TRIANGLES 等基本图元
  • 绘制方法gl.drawArrays()gl.drawElements()
  • 顶点属性:位置、颜色、纹理坐标等属性配置
  • 索引绘制:重用顶点,减少数据量

性能建议

  1. 优先使用三角形带:减少顶点数量
  2. 合理使用索引:对于共享顶点的图形
  3. 批处理绘制:减少绘制调用次数
  4. 优化顶点数据:选择合适的数据类型和布局

下一步

继续学习纹理与光照,为几何图形添加材质效果。