初学ThreeJs 实战项目2:寂静的小屋

168 阅读4分钟

一、项目简介

在 3D 图形领域中,Three.js 是一款功能强大的工具。本文将详细介绍如何利用 Three.js 构建一个名为《寂静的小屋》的 3D 项目,该项目融合了多种元素以营造独特的氛围。

在线预览地址

源码地址

效果图: image.png

二、准备工作与资源加载

1. 材质贴图加载

在构建场景前,需加载各类材质贴图以增强场景的真实感。

// 加载砖块贴图
const bricksColorTexture = new THREE.TextureLoader().load(bricksColor);
const bricksAmbientOcclusionTexture = new THREE.TextureLoader().load(bricksAmbientOcclusion);
// 加载门、草地等其他材质贴图的代码类似

三、场景构建

1. 房屋模型构建

// 创建屋子
const createHouse = (screen: THREE.Scene) => {
    const house = new THREE.Group();

    // 墙
    const wall = new THREE.Mesh(
        new THREE.BoxGeometry(4, 2.5, 4),
        new THREE.MeshStandardMaterial({
            color: "#ac8e82",
            map: bricksColorTexture,
            aoMap: bricksAmbientOcclusionTexture,
            normalMap: bricksNormalTexture,
            roughnessMap: bricksRoughnessTexture
        })
    );
    wall.geometry.setAttribute("uv2", new THREE.Float32BufferAttribute(wall.geometry.attributes.uv.array, 2));
    wall.position.y = wall.geometry.parameters.height / 2;
    wall.castShadow = true;
    house.add(wall);

    // 屋顶
    const roof = new THREE.Mesh(
        new THREE.ConeGeometry(4, 1.5, 4),
        new THREE.MeshStandardMaterial({
            color: "#b35f45",
        })
    );
    roof.rotation.y = Math.PI / 4;
    roof.castShadow = true;
    roof.position.y = wall.geometry.parameters.height + roof.geometry.parameters.height / 2;
    house.add(roof);

    // 门
    const door = new THREE.Mesh(
        new THREE.PlaneGeometry(2.25, 2.25),
        new THREE.MeshStandardMaterial({
            map: doorColorTexture,
            aoMap: doorAmbientOcclusionTexture,
            aoMapIntensity: 1,
            normalMap: doorNormalTexture,
            displacementMap: doorDisplacementTexture,
            displacementScale: 0.01,
            metalnessMap: doorMetalnessTexture,
            roughnessMap: doorRoughnessTexture,
            transparent: true,
            alphaMap: doorAlphaTexture
        })
    );
    door.castShadow = true;
    door.position.y = 1;
    door.position.z = wall.geometry.parameters.depth / 2 + 0.001;
    door.geometry.setAttribute("uv2", new THREE.Float32BufferAttribute(door.geometry.attributes.uv.array, 2));
    house.add(door);

    // 灌木
    const bushGeometry = new THREE.SphereGeometry(1, 16, 16);
    const bushMaterial = new THREE.MeshStandardMaterial({
        color: "#89c854",
        map: grassColorTexture,
        aoMap: grassAmbientOcclusionTexture,
        normalMap: grassNormalTexture,
        roughnessMap: grassRoughnessTexture
    });
    
    //创建四个球形的灌木,然后放置在门口位置上;
    const bush1 = new THREE.Mesh(bushGeometry, bushMaterial);
    bush1.scale.set(0.5, 0.5, 0.5);
    bush1.position.set(0.8, 0.2, 2.2);
    bush1.castShadow = true;

    const bush2 = new THREE.Mesh(bushGeometry, bushMaterial);
    bush2.scale.set(0.25, 0.25, 0.25);
    bush2.position.set(1.4, 0.1, 2.1);
    bush2.castShadow = true;

    const bush3 = new THREE.Mesh(bushGeometry, bushMaterial);
    bush3.scale.set(0.4, 0.4, 0.4);
    bush3.position.set(-0.8, 0.1, 2.2);
    bush3.castShadow = true;

    const bush4 = new THREE.Mesh(bushGeometry, bushMaterial);
    bush4.scale.set(0.15, 0.15, 0.15);
    bush4.position.set(-1, 0.05, 2.6);
    bush4.castShadow = true;

    house.add(bush1, bush2, bush3, bush4);

    // 门口的光源 营造氛围
    const doorLight = new THREE.PointLight("#ff7d46", 1, 7);
    doorLight.position.set(0, 2.2, 2.7);
    doorLight.castShadow = true;
    house.add(doorLight);

    // 优化阴影性能
    doorLight.shadow.mapSize.width = 256;
    doorLight.shadow.mapSize.height = 256;
    doorLight.shadow.camera.far = 7;
};

AOMap(Ambient Occlusion Map,环境光遮蔽贴图)

  • 作用:它可以为场景增加环境光遮蔽效果。即使在全局光照环境下,物体的角落、缝隙等区域也会显得更暗,而突出部分会更亮,从而增加物体的层次感和真实感。仅有颜色贴图(map)时,这些细节难以体现。
  • 示例:在这个墙的例子中,使用 aoMap 可以让砖块之间的缝隙看起来更暗,就像现实中光线较难到达这些区域一样。

NormalMap(法线贴图)

  • 作用:通过改变模型表面的法线方向,来模拟出复杂的几何细节,而无需增加实际的几何面数。这可以在不增加计算量的情况下让物体看起来有更多的细节和立体感。
  • 示例:对于墙面,可以使砖块表面看起来有凹凸不平的效果,仿佛有真实的纹理深度,而不仅仅是一个平面颜色。

RoughnessMap(粗糙度贴图)

  • 作用:定义了材质表面的粗糙度。不同区域的粗糙度不同,会导致光线在表面的反射和散射效果不同。较粗糙的区域会使光线散射得更厉害,看起来更暗淡;较光滑的区域则会有更集中的反射。
  • 示例:在墙的材质中,可能有些砖块的表面比较光滑,有些比较粗糙,通过 roughnessMap 可以模拟出这种差异。

2. 地面模型构建

// 地面
const createGround = (screen: THREE.Scene) => {
    const floor = new THREE.Group();

    // 草地
    const grass = new THREE.Mesh(
        new THREE.PlaneGeometry(20, 20),
        new THREE.MeshStandardMaterial({
            map: grassColorTexture,
            aoMap: grassAmbientOcclusionTexture,
            normalMap: grassNormalTexture,
            roughnessMap: grassRoughnessTexture
        })
    );
    grass.rotation.x = -Math.PI * 0.5;
    grass.receiveShadow = true;
    grass.geometry.setAttribute("uv2", new THREE.Float32BufferAttribute(grass.geometry.attributes.uv.array, 2));
    floor.add(grass);

    // 碑石
    const stoneGeometry = new THREE.BoxGeometry(0.8, 1.2, 0.4);
    const stoneMaterial = new THREE.MeshStandardMaterial({
        color: "#b2b6b1"
    });
    for (let i = 0; i < 80; i++) {
        const angle = Math.random() * Math.PI * 2;
        const radius = 4 + Math.random() * 4.5; //将房子排除在范围外
        const x = Math.sin(angle) * radius;
        const z = Math.cos(angle) * radius;
        const stone = new THREE.Mesh(stoneGeometry, stoneMaterial);
        stone.position.set(x, stoneGeometry.parameters.height / 2 - 0.1, z);
        stone.rotation.y = Math.random() * (Math.PI / 8);
        stone.rotation.z = Math.random() * (Math.PI / 10);
        stone.castShadow = true;
        floor.add(stone);
        //以上可以理解为 在一个圆环上面随机分布 80个碑石;
    };
};

四、灯光与阴影设置

1. 灯光布置

// 幽灵灯光
const ghost1 = new THREE.PointLight("#ff00ff", 2, 3);
scene.add(ghost1);
const ghost2 = new THREE.PointLight("#00ffff", 2, 3);
scene.add(ghost2);
const ghost3 = new THREE.PointLight("#ffff00", 2, 3);
scene.add(ghost3);
ghost1.castShadow = true;
ghost2.castShadow = true;
ghost3.castShadow = true;

// 创建灯光
const light = new THREE.AmbientLight("#b9d5ff", 0.12);
light.castShadow = true;
scene.add(light);

// 定向灯
const moonLight = new THREE.DirectionalLight("#b9d5ff", 0.12);
moonLight.position.set(4, 5, -2);
moonLight.castShadow = true;
scene.add(moonLight);

2. 阴影优化

renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

ghost1.shadow.mapSize.width = 256;
ghost1.shadow.mapSize.height = 256;
ghost1.shadow.camera.far = 7;

// 其他灯光的阴影优化类似

五、动画效果添加

// 控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.minPolarAngle = 0;
controls.maxPolarAngle = Math.PI / 2 - 0.1;

// 添加渲染循环
const clock = new THREE.Clock();
const animate = () => {
    const elapsedTime = clock.getElapsedTime();
    const ghost1Angle = elapsedTime * 0.5;
    
    //让幽灵灯光在场景中动起来~
    ghost1.position.x = Math.sin(ghost1Angle) * 4;
    ghost1.position.z = Math.cos(ghost1Angle) * 4;
    ghost1.position.y = Math.sin(elapsedTime * 3);

    const ghost2Angle = -elapsedTime * 0.32;
    ghost2.position.x = Math.sin(ghost2Angle) * 5;
    ghost2.position.z = Math.cos(ghost2Angle) * 5;
    ghost2.position.y = Math.sin(elapsedTime * 4) + Math.sin(elapsedTime * 2.5);

    const ghost3Angle = -elapsedTime * 0.18;
    ghost3.position.x = Math.sin(ghost3Angle) * 4 + (Math.sin(elapsedTime * 0.32) + 3);
    ghost3.position.z = Math.cos(ghost3Angle) * 4 + (Math.sin(elapsedTime * 0.32) + 3);
    ghost3.position.y = Math.sin(elapsedTime * 5) + Math.sin(elapsedTime * 2.5);

    // 更新控制器
    controls.update();

    renderer.render(scene, camera);
    requestAnimationFrame(animate);
};
animate();

六、项目效果与总结

1. 项目效果

运行项目后,呈现出一座寂静的小屋位于草地之上,周围有碑石。门口有灯光,空中幽灵飘动,营造出神秘氛围。通过控制器可多角度查看场景。

2. 总结

通过《寂静的小屋》项目展示了 Three.js 在构建复杂 3D 场景方面的能力。从材质加载、模型构建、灯光阴影到动画效果等方面,共同构建了逼真的 3D 世界。未来可进一步优化和扩展,如添加交互元素等。