从零开始写THREEJS DEMO(一)3D看房<一>

THREE.js 3D看房案例 (一)

基础知识准备

我们知道,threejs是封装了基础的webgl库,threejs三要素:场景、相机(正交相机透视相机)、渲染器,物体三要素:几何体(Geometry)材质(Material)、网格(Mesh),还可以结合dom事件完成一些交互操作、还能导入模型播放模型动作、配合物理引擎做物理效果等,要做3D看房效果,在THREE.js中有两种主流的实现效果:全景图片贴图(就是一个球体物体加全景图贴图)skyBox天空盒(就是用一个正方体,六个面贴上单个方向的全景图),准备好物体后,使用几何体的Geometry.box.geometry.scale()方法缩放物体,把相机包围,相当于将视觉移动到物体内部,就完成预览效果:)

这里有一个转换全景图的工具:HDRI to CubeMap

代码仓库:GitHub - Exchar/threejs-demo: 一个threejs demo集合

基础类准备

我们先来实现一个基础的效果,使用场景、相机、渲染器,结合轨道控制器(OrbitControls)

从0开始:使用vite,不使用MVVM框架,使用原生写法,使用typescript

项目搭建

先准备一个文件夹

进入文件夹后先初始化一个package.json

bash 复制代码
pnpm init

安装vite

bash 复制代码
pnpm add vite -D

然后在package.json的scripts节点中写一个"dev": "vite"

最终基础文件结构是这样的

vite.config.ts

typescript 复制代码
import {defineConfig} from 'vite'
export default defineConfig({
    build:{
        target: 'esnext'
    },
    server:{
        port: 9800
    },
    root: './'
})

index.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        *{
            margin: 0;
            padding: 0;
        }
        html,body{
            width: 100%;
            height: 100%;
        }
    </style>
</head>
<body>
    <script type="module" src="./src/pages/home/index.js"></script>
</body>
</html>

在utils/index.ts中编写BaseRender类

typescript 复制代码
import * as THREE from 'three'
interface Option{
    width?:number,
    height?:number,
    antialias?:boolean
}
export class BaseRender{
    canvasEl:HTMLCanvasElement|null = null
    renderer: THREE.WebGLRenderer
    scene: THREE.Scene
    camera: THREE.PerspectiveCamera

    constructor(el:string | HTMLCanvasElement|undefined,options:Option){
        this.canvasEl = el ? this.getQueryElement(el): null;

        // 创建threejs场景
        this.scene = new THREE.Scene()
        this.scene.background = new THREE.Color(0xbfbfbf);

        // 创建threejs相机
        // 参数值为 视野角度,长宽比,近截面,远截面
        this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        this.camera.position.z = 0

        // 创建webgl渲染器
        this.renderer = new THREE.WebGLRenderer({
            canvas: this.canvasEl || undefined,
            antialias: options.antialias || false
        });
        if(!this.canvasEl){
            document.body.appendChild(this.renderer.domElement);
        }
        // 设置canvasEl
        this.canvasEl = this.canvasEl || this.renderer.domElement;
        // 设置渲染尺寸
        this.renderer.setSize(options.width || window.innerWidth, options.height ||window.innerHeight);
        this.renderer.render(this.scene,this.camera)

        this.startResizeWatcher();

    }
    startRender(){
        // console.log(this.renderer)
        this.renderer.render(this.scene, this.camera);
        requestAnimationFrame(()=>this.startRender());
    }
    getQueryElement(el:string | HTMLCanvasElement):HTMLCanvasElement|null{
        return typeof el==='string' ? document.querySelector(el) : el
    }
    startResizeWatcher(){
        window.addEventListener('resize',()=>{
            this.renderer.setSize(window.innerWidth,window.innerHeight)
        })
    }
}

简单解释一下,为了方便后面调用,将场景scene、相机camera、渲染器renderer都放到类的属性里,为了拿到最终渲染的canvasElement,在调用创建渲染器后又获取渲染器实例的domElement重新赋值,startRender是一个每隔一段时间都在调用渲染器实例的render函数的递归函数,为了使其运行更加流畅,使用requestAnimationFrame,相比定时器它更加智能,能根据当前电脑的负载和性能确定什么时候执行渲染,默认不自动执行,子类继承后手动调用startRender函数,startResizeWater是一个监听window窗口尺寸然后实时给画布canvas尺寸设置的函数

然后在文件src/pages/home/index.ts中继承并实例化一下

typescript 复制代码
import { BaseRender } from "../../utils";
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

class CutstomRender extends BaseRender {
    controls: OrbitControls
    constructor(){
        super(undefined,{});

        // 调用更新函数
        this.startRender()
    }
}


//实例化
new CustomRender()

现在的效果如下

为什么什么都没有?因为没有添加物体光源阴影什么的

为了方便观察,添加一个轨道控制器轨道控制器(OrbitControls)和一个网格帮助类GridHelper

这里简单介绍一下轨道控制器和网格帮助类

轨道控制器(OrbitControls)

[Orbit controls](three.js docs)(轨道控制器)可以使得相机围绕目标进行轨道运动。是的,有了它就可以更方便观察效果了:) 这是一个官网上的栗子🐿️

网格帮助类(GridHelper)

坐标格辅助对象. 坐标格实际上是2维线数组.

完成基础搭建

最终完成基础搭建,在子类继承时加入辅助对象,完整的子类代码如下:文件路径/src/pages/home/index.ts

typescript 复制代码
import { BaseRender } from "../../utils";
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

class CutstomRender extends BaseRender {
    controls: OrbitControls
    constructor(){
        super(undefined,{});
        // 初始化完成后添加轨道控制器
        const controls = new OrbitControls(this.camera, this.renderer.domElement);
        this.controls = controls
        this.camera.position.set( 0, 10, 10 );
        controls.update();

        // 添加GridHelper
        const size = 10;
        const divisions = 10;

        const gridHelper = new THREE.GridHelper( size, divisions );
        this.scene.add( gridHelper );

        this.startRender()
    }

    startRender(): void {
        this.renderer.render(this.scene, this.camera);
        this.controls.update()
        this.camera.lookAt(0,0,0)
        requestAnimationFrame(() => this.startRender());
    }
}
new CutstomRender()

效果是这样的

完成基本预览效果

分别使用skyBox和全景图片贴图实现

天空盒(skyBox)📦

先准备一个正方体,准备一个立方体BoxGeometry,准备一个基础材质MeshBasicMaterial,然后创建一个网格对象八物体和材质组合起来,最终添加到场景内

typescript 复制代码
const geometry = new THREE.BoxGeometry( 10, 10, 10 );
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const mesh = new THREE.Mesh( geometry, material );
this.scene.add( mesh );

运行!效果是这样的

好,我们准备直接给六个面贴上材质图片

typescript 复制代码
        // 获取各个面的贴图
        const images = [
            '/src/assets/left.png',
            '/src/assets/right.png',
            '/src/assets/top.png',
            '/src/assets/bottom.png',
            '/src/assets/front.png',
            '/src/assets/back.png',
        ]

        const textureLoader = new THREE.TextureLoader();
        const textures:THREE.MeshBasicMaterial[] = []
        images.forEach((item,index)=>{
            // load函数参数:路径,加载成功的回调,过程回调,加载失败的回调  url : String, onLoad : Function, onProgress : Function, onError : Function 
            textureLoader.load(item,(texture)=>{
                const material = new THREE.MeshBasicMaterial( { map:texture } );
                textures.push(material)
            },undefined,()=>{})
        })
        // const cube = new THREE.Mesh( new THREE.CubeGeometry( 20, 20, 20 ), new THREE.MeshFaceMaterial(textures) );
        const cube = new THREE.Mesh( geometry, textures );
        // this.scene.add( mesh );
        this.scene.add(cube)

运行!效果是这样的

然后使用Object3D的scale方法,缩放整个物体

typescript 复制代码
      cube.geometry.scale(5,5,-5)

缩放方法传入负值z轴,会有贴图反转的功能,就像是这样

效果是这样的

为什么直接就变成这样了?可以把轨道控制器的缩放功能开着,看一下什么样:

就是这么神奇!

下面是完整的子类代码:)

typescript 复制代码
import { BaseRender } from "../../utils";
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

class CutstomRender extends BaseRender {
    controls: OrbitControls
    constructor(){
        super(undefined,{});
        this.addOrbitController()
        this.camera.position.set( 0, 10, 10 );

        // this.addGridHelper()

        // 添加正方体
        this.addViewGeometry()

        this.startRender()
    }
    addOrbitController(){
        // 初始化完成后添加轨道控制器
        const controls = new OrbitControls(this.camera, this.renderer.domElement);
        this.controls = controls
        controls.autoRotate = true
        controls.autoRotateSpeed = 1
        controls.enableDamping = true
        controls.enableZoom = false
    }

    addViewGeometry(){
        // 添加一个正方体
        const geometry = new THREE.BoxGeometry( 10, 10, 10 );
        const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
        const mesh = new THREE.Mesh( geometry, material );
        this.scene.add( mesh );

        
        // 获取各个面的贴图
        const images = [
            '/src/assets/left.png',
            '/src/assets/right.png',
            '/src/assets/top.png',
            '/src/assets/bottom.png',
            '/src/assets/front.png',
            '/src/assets/back.png',
        ]

        const textureLoader = new THREE.TextureLoader();
        const textures:THREE.MeshBasicMaterial[] = []
        images.forEach((item,index)=>{
            console.log(item)
            textureLoader.load(item,(texture)=>{
                console.log(texture)
                const material = new THREE.MeshBasicMaterial( { map:texture } );
                textures.push(material)
            },undefined,()=>{})
        })
        // const cube = new THREE.Mesh( new THREE.CubeGeometry( 20, 20, 20 ), new THREE.MeshFaceMaterial(textures) );
        const cube = new THREE.Mesh( geometry, textures );
        // this.scene.add( mesh );
        this.scene.add(cube)
        this.renderer.render(this.scene, this.camera);
        // 添加贴图之后缩放物体
        cube.geometry.scale(5,5,-5)
    }
    addGridHelper(){
        // 添加GridHelper
        const size = 100;
        const divisions = 20;
        
        const gridHelper = new THREE.GridHelper( size, divisions );
        this.scene.add( gridHelper );
    }

    startRender(): void {
        this.renderer.render(this.scene, this.camera);
        this.controls.update()
        // 渲染时一直让相机固定
        this.camera.lookAt(0,0,0)
        requestAnimationFrame(() => this.startRender());
    }
}
new CutstomRender()

运行!效果是这样的

看着有点糊,因为贴图分辨率不高,图片来源是从互联网扒的,将个就吧

全景图片贴图🌏

替换addViewGeometry方法

typescript 复制代码
    addViewGeometryWithTexture(){
        // 创建一个球体
        const geometry = new THREE.SphereGeometry( 5, 32, 16 );
        // 创建材质
        const material = new THREE.MeshBasicMaterial({
            map: new THREE.TextureLoader().load('/src/assets/livingRoom.jpg')
        })

        const mesh = new THREE.Mesh(geometry,material)
        this.scene.add(mesh)

    }

运行!效果是这样的

如法炮制

scss 复制代码
// 添加贴图之后缩放物体
    mesh.geometry.scale(5,5,-5)

效果是这样的

看着要比天空盒好一点

基础效果完成🎉🎉🎉

完整子类代码

typescript 复制代码
import { BaseRender } from "../../utils";
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

class CutstomRender extends BaseRender {
    controls: OrbitControls
    constructor(){
        super(undefined,{});
        this.addOrbitController()
        this.camera.position.set( 0, 10, 10 );

        // this.addGridHelper()

        // 天空盒实现
        // this.addViewGeometryWidthSkyBox()

        // 全景贴图实现
        this.addViewGeometryWithTexture()

        this.startRender()
    }
    addOrbitController(){
        // 初始化完成后添加轨道控制器
        const controls = new OrbitControls(this.camera, this.renderer.domElement);
        this.controls = controls
        controls.autoRotate = true
        controls.autoRotateSpeed = 1
        controls.enableDamping = true
        // controls.enableZoom = false
    }

    addViewGeometryWithTexture(){
        // 创建一个球体
        const geometry = new THREE.SphereGeometry( 5, 32, 16 );
        // 创建材质
        const material = new THREE.MeshBasicMaterial({
            map: new THREE.TextureLoader().load('/src/assets/livingRoom.jpg')
        })

        const mesh = new THREE.Mesh(geometry,material)
        this.scene.add(mesh)

        // 添加贴图之后缩放物体
        mesh.geometry.scale(5,5,-5)
    }

    // 天空盒实现
    addViewGeometryWidthSkyBox(){
        // 添加一个正方体
        const geometry = new THREE.BoxGeometry( 10, 10, 10 );
        const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
        const mesh = new THREE.Mesh( geometry, material );
        this.scene.add( mesh );

        
        // 获取各个面的贴图
        const images = [
            '/src/assets/left.png',
            '/src/assets/right.png',
            '/src/assets/top.png',
            '/src/assets/bottom.png',
            '/src/assets/front.png',
            '/src/assets/back.png',
        ]

        const textureLoader = new THREE.TextureLoader();
        const textures:THREE.MeshBasicMaterial[] = []
        images.forEach((item,index)=>{
            // load函数参数:路径,加载成功的回调,过程回调,加载失败的回调  url : String, onLoad : Function, onProgress : Function, onError : Function 
            textureLoader.load(item,(texture)=>{
                const material = new THREE.MeshBasicMaterial( { map:texture } );
                textures.push(material)
            },undefined,()=>{})
        })
        // const cube = new THREE.Mesh( new THREE.CubeGeometry( 20, 20, 20 ), new THREE.MeshFaceMaterial(textures) );
        const cube = new THREE.Mesh( geometry, textures );
        // this.scene.add( mesh );
        this.scene.add(cube)
        // this.renderer.render(this.scene, this.camera);
        // 添加贴图之后缩放物体
        cube.geometry.scale(5,5,-5)
    }
    addGridHelper(){
        // 添加GridHelper
        const size = 100;
        const divisions = 20;
        
        const gridHelper = new THREE.GridHelper( size, divisions );
        this.scene.add( gridHelper );
    }

    startRender(): void {
        this.renderer.render(this.scene, this.camera);
        this.controls.update()
        // 渲染时一直让相机固定
        this.camera.lookAt(0,0,0)
        requestAnimationFrame(() => this.startRender());
    }
}
new CutstomRender()

结束

实现了基础的预览效果,还有一些效果没做:预览提示、场景切换,后面有时间再做吧

相关推荐
夜寒花碎15 分钟前
前端事件循环
前端·javascript·面试
阿常1121 分钟前
uni-app基础拓展
前端·javascript·uni-app
壹贰叁肆伍上山打老虎21 分钟前
突发奇想,写了一个有意思的函数,一个有趣的 JavaScript 函数:将数组分割成多维块
前端·javascript
bbb16922 分钟前
react源码分析 setStatae究竟是同步任务还是异步任务
前端·javascript·react.js
言兴23 分钟前
你知道吗?JavaScript中的事件循环机制
前端·javascript
pany28 分钟前
📱 MobVue 致力成为你的移动端 H5 首选
前端·javascript·vue.js
掘金安东尼1 小时前
上周前端发生哪些新鲜事儿? #404
前端·javascript·面试
岁岁岁平安1 小时前
Vue3实战学习(IDEA中打开、启动与搭建Vue3工程极简脚手架教程(2025超详细教程)、Windows系统命令行启动Vue3工程)(2)
javascript·vue.js·vue·idea·vue3项目脚手架
墨菲斯托8881 小时前
Node.js原型链污染
前端·javascript·node.js
雪碧聊技术2 小时前
如何在el-input搜索框组件的最后面,添加图标按钮?
前端·javascript·vue.js·element-plus组件库·el-input搜索框