从零开始写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](https://link.juejin.cn?target=https%3A%2F%2Fthreejs.org%2Fdocs%2Findex.html%3Fq%3Dcontr%23examples%2Fzh%2Fcontrols%2FOrbitControls "https://threejs.org/docs/index.html?q=contr#examples/zh/controls/OrbitControls"))(轨道控制器)可以使得相机围绕目标进行轨道运动。是的,有了它就可以更方便观察效果了:) 这是一个官网上的栗子🐿️ ![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/50ed3d3bb0d3466da63c89c9fc240132~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRXhjaGFy:q75.awebp) #### 网格帮助类(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() ``` 效果是这样的 ![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6f2120f3dc4945b890212f6e36af5fc7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRXhjaGFy:q75.awebp) ### 完成基本预览效果 分别使用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 ); ``` 运行!效果是这样的 ![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/270722565f1341debb4c22ff55069e1b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRXhjaGFy:q75.awebp) 好,我们准备直接给六个面贴上材质图片 ```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) ``` 运行!效果是这样的 ![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ecb5fbbc471d40529394a03906a57ec7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRXhjaGFy:q75.awebp) 然后使用Object3D的scale方法,缩放整个物体 ![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/955d8acc98b8499ea107d9ab19a0d1aa~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRXhjaGFy:q75.awebp) ```typescript cube.geometry.scale(5,5,-5) ``` 缩放方法传入负值z轴,会有贴图反转的功能,就像是这样 效果是这样的 ![效果图](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/474d8bc3b7644e0dba6218e7b6e5b530~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRXhjaGFy:q75.awebp) 为什么直接就变成这样了?可以把轨道控制器的缩放功能开着,看一下什么样: ![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/11e68b4213f045f7893311ddbe752722~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRXhjaGFy:q75.awebp) 就是这么神奇! 下面是完整的子类代码:) ```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() ``` 运行!效果是这样的 ![效果图](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/2ac840f1a478465d8b6240606374f30b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRXhjaGFy:q75.awebp) 看着有点糊,因为贴图分辨率不高,图片来源是从互联网扒的,将个就吧 #### 全景图片贴图🌏 替换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) } ``` 运行!效果是这样的 ![](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/3a7095416eba4ff281a4be9ccad37a73~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRXhjaGFy:q75.awebp) 如法炮制 ```scss // 添加贴图之后缩放物体 mesh.geometry.scale(5,5,-5) ``` 效果是这样的 ![效果图](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e6cd45535e0d40ab8fdf7a762553e984~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRXhjaGFy:q75.awebp) 看着要比天空盒好一点 基础效果完成🎉🎉🎉 ### 完整子类代码 ```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() ``` ### 结束 实现了基础的预览效果,还有一些效果没做:预览提示、场景切换,后面有时间再做吧

相关推荐
EndingCoder1 小时前
2025年JavaScript性能优化全攻略
开发语言·javascript·性能优化
a濯7 小时前
element plus el-table多选框跨页多选保留
javascript·vue.js
H309199 小时前
vue3+dhtmlx-gantt实现甘特图展示
android·javascript·甘特图
CodeCraft Studio9 小时前
数据透视表控件DHTMLX Pivot v2.1发布,新增HTML 模板、增强样式等多个功能
前端·javascript·ui·甘特图
llc的足迹9 小时前
el-menu 折叠后小箭头不会消失
前端·javascript·vue.js
九月TTS10 小时前
TTS-Web-Vue系列:移动端侧边栏与响应式布局深度优化
前端·javascript·vue.js
积极向上的龙10 小时前
首屏优化,webpack插件用于给html中js自动添加异步加载属性
javascript·webpack·html
Bl_a_ck11 小时前
开发环境(Development Environment)
开发语言·前端·javascript·typescript·ecmascript
ai产品老杨11 小时前
AI赋能安全生产,推进数智化转型的智慧油站开源了。
前端·javascript·vue.js·人工智能·ecmascript
程序员Bears12 小时前
从零打造个人博客静态页面与TodoList应用:前端开发实战指南
java·javascript·css·html5