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()
结束
实现了基础的预览效果,还有一些效果没做:预览提示、场景切换,后面有时间再做吧