3D室内场景构建

143 阅读6分钟

目录


构建一个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的关键。