在HTML5技术栈中,实现3D建模绘制球体通常依赖WebGL相关技术,核心思路是通过计算球面的顶点、纹理坐标和索引数据,再结合渲染管线完成绘制。下面会详细介绍球体绘制的具体步骤,同时补充其他基础几何体的创建方法。

一、绘制球体的核心步骤
1. 准备WebGL上下文
首先需要在HTML页面中创建<canvas>元素,获取WebGL渲染上下文,这是后续所有绘制操作的基础。
<!DOCTYPE html>
<html>
<head>
<title>HTML5球体绘制</title>
</head>
<body>
<canvas id="glCanvas" width="800" height="600"></canvas>
<script>
// 获取canvas元素
const canvas = document.getElementById('glCanvas');
// 获取WebGL上下文
const gl = canvas.getContext('webgl');
if (!gl) {
console.error('当前浏览器不支持WebGL');
}
</script>
</body>
</html>
2. 计算球体几何数据
球体可以通过经纬度划分的方式生成顶点,设定球的半径、经线分段数、纬线分段数,计算每个顶点的坐标、法线和纹理坐标。
// 生成球体顶点数据
function createSphereData(radius, widthSegments, heightSegments) {
const positions = [];
const normals = [];
const texCoords = [];
const indices = [];
// 遍历纬线
for (let y = 0; y <= heightSegments; y++) {
const v = y / heightSegments;
const phi = v * Math.PI; // 纬度对应的角度,0到π
const sinPhi = Math.sin(phi);
const cosPhi = Math.cos(phi);
// 遍历经线
for (let x = 0; x <= widthSegments; x++) {
const u = x / widthSegments;
const theta = u * Math.PI * 2; // 经度对应的角度,0到2π
const sinTheta = Math.sin(theta);
const cosTheta = Math.cos(theta);
// 计算顶点坐标
const px = radius * sinPhi * cosTheta;
const py = radius * cosPhi;
const pz = radius * sinPhi * sinTheta;
positions.push(px, py, pz);
// 法线就是顶点坐标归一化,因为球心在原点
const length = Math.sqrt(px * px + py * py + pz * pz);
normals.push(px / length, py / length, pz / length);
// 纹理坐标
texCoords.push(u, v);
}
}
// 生成索引数据
for (let y = 0; y < heightSegments; y++) {
for (let x = 0; x < widthSegments; x++) {
const a = x + (widthSegments + 1) * y;
const b = x + (widthSegments + 1) * (y + 1);
const c = (x + 1) + (widthSegments + 1) * (y + 1);
const d = (x + 1) + (widthSegments + 1) * y;
// 两个三角形组成一个四边形
indices.push(a, b, d);
indices.push(b, c, d);
}
}
return {
positions: new Float32Array(positions),
normals: new Float32Array(normals),
texCoords: new Float32Array(texCoords),
indices: new Uint16Array(indices)
};
}
3. 编写着色器程序
需要顶点着色器和片元着色器来完成顶点的变换和颜色的计算,这里使用简单的光照模型让球体更有立体感。
// 顶点着色器代码
const vertexShaderSource = `
attribute vec3 aPosition;
attribute vec3 aNormal;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
uniform mat3 uNormalMatrix;
varying vec3 vNormal;
varying vec3 vPosition;
void main() {
vNormal = uNormalMatrix * aNormal;
vPosition = (uModelViewMatrix * vec4(aPosition, 1.0)).xyz;
gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0);
}
`;
// 片元着色器代码
const fragmentShaderSource = `
precision mediump float;
varying vec3 vNormal;
varying vec3 vPosition;
uniform vec3 uLightPosition;
uniform vec3 uColor;
void main() {
vec3 normal = normalize(vNormal);
vec3 lightDir = normalize(uLightPosition - vPosition);
float diff = max(dot(normal, lightDir), 0.0);
vec3 diffuse = diff * uColor;
vec3 ambient = 0.2 * uColor;
gl_FragColor = vec4(ambient + diffuse, 1.0);
}
`;
// 编译着色器函数
function compileShader(gl, source, type) {
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 = compileShader(gl, vertexSource, gl.VERTEX_SHADER);
const fragmentShader = compileShader(gl, fragmentSource, gl.FRAGMENT_SHADER);
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));
return null;
}
return program;
}
4. 渲染球体
将生成的几何数据和着色器程序结合,设置矩阵和光照参数,完成球体的绘制。
// 初始化并绘制球体
function initAndDrawSphere(gl) {
// 创建着色器程序
const program = createProgram(gl, vertexShaderSource, fragmentShaderSource);
gl.useProgram(program);
// 生成球体数据
const sphereData = createSphereData(1.0, 32, 32);
// 创建缓冲区并传入顶点数据
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, sphereData.positions, gl.STATIC_DRAW);
const aPosition = gl.getAttribLocation(program, 'aPosition');
gl.vertexAttribPointer(aPosition, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(aPosition);
// 传入法线数据
const normalBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
gl.bufferData(gl.ARRAY_BUFFER, sphereData.normals, gl.STATIC_DRAW);
const aNormal = gl.getAttribLocation(program, 'aNormal');
gl.vertexAttribPointer(aNormal, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(aNormal);
// 创建索引缓冲区
const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, sphereData.indices, gl.STATIC_DRAW);
// 设置矩阵相关参数
const uModelViewMatrix = gl.getUniformLocation(program, 'uModelViewMatrix');
const uProjectionMatrix = gl.getUniformLocation(program, 'uProjectionMatrix');
const uNormalMatrix = gl.getUniformLocation(program, 'uNormalMatrix');
const uLightPosition = gl.getUniformLocation(program, 'uLightPosition');
const uColor = gl.getUniformLocation(program, 'uColor');
// 投影矩阵
const projectionMatrix = mat4.create();
mat4.perspective(projectionMatrix, Math.PI / 4, gl.canvas.width / gl.canvas.height, 0.1, 100.0);
gl.uniformMatrix4fv(uProjectionMatrix, false, projectionMatrix);
// 模型视图矩阵
const modelViewMatrix = mat4.create();
mat4.translate(modelViewMatrix, modelViewMatrix, [0, 0, -5]);
gl.uniformMatrix4fv(uModelViewMatrix, false, modelViewMatrix);
// 法线矩阵
const normalMatrix = mat3.create();
mat3.normalFromMat4(normalMatrix, modelViewMatrix);
gl.uniformMatrix3fv(uNormalMatrix, false, normalMatrix);
// 光照和颜色参数
gl.uniform3fv(uLightPosition, [2, 2, 2]);
gl.uniform3fv(uColor, [0.8, 0.2, 0.2]);
// 开启深度测试
gl.enable(gl.DEPTH_TEST);
// 设置背景色
gl.clearColor(0.1, 0.1, 0.1, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 绘制元素
gl.drawElements(gl.TRIANGLES, sphereData.indices.length, gl.UNSIGNED_SHORT, 0);
}
// 调用初始化函数,需要引入gl-matrix库来处理矩阵运算
// 实际使用时可以在页面中引入<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix-min.js"></script>
// 然后执行initAndDrawSphere(gl);
二、其他基础几何体创建思路
1. 立方体
立方体的创建相对简单,只需要定义8个顶点,然后按照面的顺序生成索引即可,每个面是2个三角形。
// 生成立方体数据
function createCubeData(size) {
const halfSize = size / 2;
const positions = [
// 前面
-halfSize, -halfSize, halfSize,
halfSize, -halfSize, halfSize,
halfSize, halfSize, halfSize,
-halfSize, halfSize, halfSize,
// 后面
-halfSize, -halfSize, -halfSize,
halfSize, -halfSize, -halfSize,
halfSize, halfSize, -halfSize,
-halfSize, halfSize, -halfSize
];
const indices = [
0, 1, 2, 0, 2, 3, // 前面
4, 6, 5, 4, 7, 6, // 后面
0, 4, 5, 0, 5, 1, // 底面
2, 6, 7, 2, 7, 3, // 顶面
0, 3, 7, 0, 7, 4, // 左面
1, 5, 6, 1, 6, 2 // 右面
];
return {
positions: new Float32Array(positions),
indices: new Uint16Array(indices)
};
}
2. 圆柱体
圆柱体可以分为顶面、底面和侧面三部分,侧面通过分段的方式生成顶点,和球体的经线划分思路类似。
核心步骤是计算顶面圆心、底面圆心和侧面的分段顶点,分别生成对应的索引数据即可。
三、注意事项
- 分段数越高,球体表面越光滑,但顶点数量也会成比例增加,需要根据性能需求合理设置。
- WebGL需要浏览器支持,开发时最好做兼容性判断。
- 矩阵运算可以借助成熟的库比如gl-matrix,避免重复造轮子,减少出错概率。
- 纹理坐标计算正确才能实现正确的纹理映射,如果有贴图需求需要额外处理纹理数据的传入。