目录
构建一个3D室内场景使用WebGL涉及几个核心步骤,包括设置WebGL上下文、定义顶点和片段着色器、加载模型、设置相机和投影、以及处理纹理和光照。
初始化WebGL上下文
首先,我们需要在HTML中创建一个<canvas>
元素,并在JavaScript中获取该元素,初始化WebGL上下文。
<canvas id="canvas"></canvas>
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) {
alert('Your browser does not support WebGL');
}
创建着色器
接下来,定义顶点着色器和片段着色器。顶点着色器处理顶点的位置,而片段着色器处理像素颜色。
// 顶点着色器
const vertexShaderSource = `
attribute vec3 a_position;
attribute vec2 a_texCoord;
uniform mat4 u_projectionMatrix;
uniform mat4 u_viewMatrix;
uniform mat4 u_modelMatrix;
varying vec2 v_texCoord;
void main() {
gl_Position = u_projectionMatrix * u_viewMatrix * u_modelMatrix * vec4(a_position, 1.0);
v_texCoord = a_texCoord;
}`;
// 片段着色器
const fragmentShaderSource = `
precision mediump float;
uniform sampler2D u_texture;
varying vec2 v_texCoord;
void main() {
gl_FragColor = texture2D(u_texture, v_texCoord);
}`;
加载和编译着色器
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('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('Unable to initialize the shader program: ' + gl.getProgramInfoLog(program));
return;
}
gl.useProgram(program);
准备3D模型数据
这里简化处理,直接定义一些顶点数据来表示一个简单的室内墙壁或家具。
const positions = [
// 墙壁顶点数据
-1.0, 1.0, 0.0,
-1.0, -1.0, 0.0,
1.0, -1.0, 0.0,
1.0, 1.0, 0.0,
// ... 更多顶点数据
];
const texCoords = [
0.0, 0.0,
0.0, 1.0,
1.0, 1.0,
1.0, 0.0,
// ... 更多纹理坐标
];
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
const texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texCoords), gl.STATIC_DRAW);
链接顶点属性
const positionAttributeLocation = gl.getAttribLocation(program, 'a_position');
const texCoordAttributeLocation = gl.getAttribLocation(program, 'a_texCoord');
gl.enableVertexAttribArray(positionAttributeLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(positionAttributeLocation, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(texCoordAttributeLocation);
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.vertexAttribPointer(texCoordAttributeLocation, 2, gl.FLOAT, false, 0, 0);
设置相机、投影和模型矩阵
这部分涉及到更复杂的数学,包括创建透视投影矩阵、视图矩阵和模型矩阵。
function perspective(out, fovy, aspect, near, far) {
// ... 实现透视投影矩阵
}
function lookAt(out, eye, target, up) {
// ... 实现视图矩阵
}
const projectionMatrix = mat4.create();
perspective(projectionMatrix, 45 * Math.PI / 180, canvas.width / canvas.height, 0.1, 100.0);
const viewMatrix = mat4.create();
lookAt(viewMatrix, [0, 0, 5], [0, 0, 0], [0, 1, 0]);
const modelMatrix = mat4.create();
mat4.translate(modelMatrix, modelMatrix, [0, 0, 0]); // 根据需要调整模型位置
const u_projectionMatrixLocation = gl.getUniformLocation(program, 'u_projectionMatrix');
const u_viewMatrixLocation = gl.getUniformLocation(program, 'u_viewMatrix');
const u_modelMatrixLocation = gl.getUniformLocation(program, 'u_modelMatrix');
gl.uniformMatrix4fv(u_projectionMatrixLocation, false, projectionMatrix);
gl.uniformMatrix4fv(u_viewMatrixLocation, false, viewMatrix);
gl.uniformMatrix4fv(u_modelMatrixLocation, false, modelMatrix);
加载纹理
假设你有墙壁纹理图片。
function loadTexture(gl, url) {
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
const level = 0;
const internalFormat = gl.RGBA;
const width = 1;
const height = 1;
const border = 0;
const srcFormat = gl.RGBA;
const srcType = gl.UNSIGNED_BYTE;
const pixel = new Uint8Array([0, 0, 255, 255]); // 临时占位像素
gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, width, height, border, srcFormat, srcType, pixel);
const image = new Image();
}
image.src = url;
image.onload = function () {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, internalFormat, srcType, image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
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.bindTexture(gl.TEXTURE_2D, null); // 解绑纹理
};
绘制3D场景
在主渲染循环中,使用drawArrays或drawElements绘制3D几何体。
function render() {
requestAnimationFrame(render);
// 更新模型矩阵(例如,根据时间进行旋转)
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 绘制墙壁或其他3D物体
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, positions.length / 3); // 假设4个顶点构成一个三角带
// ... 绘制更多3D物体
}
render();
光照处理和阴影
光照和阴影的处理通常在着色器中完成,涉及到对表面法线、光源位置和颜色的计算。这里仅提供一个简单的光照示例,实际场景可能需要更复杂的光照模型。
// 片段着色器
const fragmentShaderSource = `
precision mediump float;
uniform sampler2D u_texture;
uniform vec3 u_lightPosition;
uniform vec3 u_lightColor;
varying vec2 v_texCoord;
varying vec3 v_surfaceNormal;
vec3 calculateLight(vec3 surfaceNormal, vec3 lightPosition, vec3 lightColor) {
vec3 lightVector = normalize(lightPosition - v_worldPosition);
float diffuse = max(dot(surfaceNormal, lightVector), 0.0);
return diffuse * lightColor;
}
void main() {
vec3 ambient = u_lightColor * 0.1; // 环境光
vec3 diffuse = calculateLight(v_surfaceNormal, u_lightPosition, u_lightColor);
vec3 color = ambient + diffuse;
gl_FragColor = vec4(texture2D(u_texture, v_texCoord).rgb * color, 1.0);
}`;
用户交互和动画
为了让用户能够与场景交互,你需要监听键盘和鼠标事件,然后更新相机位置或旋转。以下是一个简单的相机平移示例:
let cameraPosition = [0, 0, 5]; // 初始相机位置
let cameraRotation = [0, 0, 0]; // 初始相机旋转
document.addEventListener('keydown', (event) => {
switch (event.key) {
case 'ArrowLeft':
cameraRotation[1] += 0.1;
break;
case 'ArrowRight':
cameraRotation[1] -= 0.1;
break;
case 'ArrowUp':
cameraPosition[2] += 1;
break;
case 'ArrowDown':
cameraPosition[2] -= 1;
break;
// ... 处理其他按键
}
});
function updateCameraMatrix() {
const cameraMatrix = mat4.create();
mat4.identity(cameraMatrix);
mat4.translate(cameraMatrix, cameraMatrix, cameraPosition);
mat4.rotateX(cameraMatrix, cameraMatrix, cameraRotation[0]);
mat4.rotateY(cameraMatrix, cameraMatrix, cameraRotation[1]);
mat4.rotateZ(cameraMatrix, cameraMatrix, cameraRotation[2]);
const inverseCameraMatrix = mat4.invert(mat4.create(), cameraMatrix);
const viewMatrix = mat4.create();
mat4.mul(viewMatrix, inverseCameraMatrix, modelMatrix);
gl.uniformMatrix4fv(u_viewMatrixLocation, false, viewMatrix);
}
在渲染循环中调用updateCameraMatrix
以更新视图矩阵。
高级特性
- 阴影贴图:实现阴影贴图可以增加场景的真实感。这涉及到在不同的渲染通道中生成和应用阴影贴图。
- 环境映射:使用环境映射可以模拟物体表面反射周围环境的效果,例如使用立方体贴图。
- 后期处理:通过在所有3D渲染之后应用各种效果,如抗锯齿、模糊、色彩校正等,可以增强视觉效果。
- 碰撞检测:为了实现交互,需要检测物体之间的碰撞,以便做出相应的反应。
- 物理引擎:集成物理引擎可以让物体遵循真实的物理规律运动,如重力、碰撞反弹等。
性能优化
- 批处理:将相似的几何体组合在一起,减少渲染调用次数。
- LOD(细节层次):根据物体与相机的距离,动态调整模型的细节级别,以节省资源。
- 剔除:避免渲染不可见的物体,如背面剔除和视锥体剔除。
- 缓冲区对象:使用VBO(顶点缓冲对象)和IBO(索引缓冲对象)提高数据传输效率。
- 分块加载:大型场景可以按需加载,而不是一次性加载所有内容。
总结
构建一个3D室内场景涉及许多方面,包括基本的WebGL设置、着色器编程、模型加载、光照和阴影处理、用户交互、性能优化等。随着技能的提升,你还可以添加更多的复杂功能,使场景更加真实和互动。对于初学者,理解这些基本概念并逐步扩展是学习WebGL的关键。