几何图形绘制
绘制模式
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 个顶点 |
图元示意图
code
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);三角形带的顶点顺序:
code
顶点 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);三角形扇示意图:
code
顶点 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_BYTE | 0 - 255 |
| 无符号短整型 | gl.UNSIGNED_SHORT | 0 - 65535 |
| 无符号整型 | gl.UNSIGNED_INT | 0 - 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() - 顶点属性:位置、颜色、纹理坐标等属性配置
- 索引绘制:重用顶点,减少数据量
性能建议
- 优先使用三角形带:减少顶点数量
- 合理使用索引:对于共享顶点的图形
- 批处理绘制:减少绘制调用次数
- 优化顶点数据:选择合适的数据类型和布局
下一步
继续学习纹理与光照,为几何图形添加材质效果。