一步一步带你使用threejs实现3D看房,切换场景(超详细)

1,152 阅读2分钟

开始吧

首先我们必须需要知道 threejs 最基本的4个要素,详情见threejs文档

  1. scene (场景)
  2. camera (相机)
  3. renderer (渲染器)
  4. mesh (网格)

使用vite搭建项目

  1. npm create vite@latest house-3d-view --template
  2. 选择 VueJavaScript
  3. cd house-3d-view
  4. npm i
  5. 项目初始化完成

coding

1.完成一个基本的页面

<template>
    <div class="home">
        <div class="view-container" ref="threeDBox"></div>
  </div>
</template>

<script setup>
import { onMounted, ref } from '@vue/runtime-core'
import livingImg from './assets/image/livingRoom.jpg'
import kitchenImg from './assets/image/kitchen.jpg'
import * as THREE from 'three'
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls'

let renderer,camera,mesh,scene,controller
let threeDBox = ref(null)

 onMounted(()=>{
    init()
    render()
 })
function init(){
    initRenderer()   //初始化渲染器
    initCamera()   // 初始化相机
    initScene()    // 初始化场景
    initMesh()     // 初始化网格
    initController()   // 初始化控制器
}
// 初始化渲染器
function initRenderer() {
  renderer = new THREE.WebGLRenderer({
    antialias: true
  })
  renderer.setPixelRatio(window.devicePixelRatio)
  renderer.setSize(window.innerWidth, window.innerHeight)
  threeDBox.value.appendChild(renderer.domElement)
}

// 初始化镜头
function initCamera() {
  camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000)
  camera.position.z = 0.0001
}
// 初始化场景
function initScene(){
  scene = new THREE.Scene()
}
// 初始化网格,  网格单元需要几何体和材质
function initMesh(){
  const geomatry = new THREE.SphereGeometry(16,50,50)
  const texture = new THREE.TextureLoader().load(livingImg)
  const material = new THREE.MeshBasicMaterial({map:texture})
  mesh = new THREE.Mesh(geomatry, material)
  mesh.geometry.scale(16,16,-16)
  scene.add(mesh)
}
// 控制器
function initController(){
  controller = new OrbitControls(camera,renderer.domElement)
  controller.enableDamping = true
}
// 渲染函数
function render(){
  renderer.render(scene, camera)
  requestAnimationFrame(render)
}
//页面大小发生变化
window.addEventListener("resize",()=>{
  camera.aspect = window.innerWidth/window.innerHeight
  camera.updateProjectionMatrix()
  renderer.setSize(window.innerWidth,window.innerHeight)
})
</script>

<style lang="scss">
.home {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;

  .view-container {
    width: 100%;
    height: 100%;
    overflow: hidden;
  }
}
</style>

基本页面完成我们已经可以看见最基本的3d看房效果,可以通过控制器控制视角,接下来我们需要完成场景的切换**

2.给页面添加精灵(提示)

import tipImg from './assets/image/tip.png'

function init(){
    initRenderer()
    initCamera()
    initScene()
    initMesh()
    initController()
    addTipsSprite()
}

function addTipsSprite(){
  let map = new THREE.TextureLoader().load(tipImg)
  let material = new THREE.SpriteMaterial({map:map})
  let sprit = new THREE.Sprite(material)
  
  sprit.scale.set(10,10,10)
  sprit.position.set(-200,0,-145)
  scene.add(sprit)
}

这一步完成我们可以看见场景中的精灵图,并且可以通过设置position来调整精灵图的位置

3.切换场景

我们将上面的代码稍微更改一部分。根据index的值来渲染不同页面

let dataList = [
  {
    image: livingImg,
    tip:{
        position: { x: -200, y:0, z: -145 },
        image:1
      }
  },
  {
    image: kitchenImg,
    tip: {
        position: { x: -199, y: -24, z: 145 },
        image:0
      }
  }
];


// 在场景中添加精灵
function addTipsSprite(index=0){
  let map = new THREE.TextureLoader().load(tipImg)
  let material = new THREE.SpriteMaterial({map:map})
  let sprite = new THREE.Sprite(material)

  let tip = dataList[index].tip
  sprite.scale.set(10,10,10)
  sprite.position.set(tip.position.x,tip.position.y,tip.position.z)
  sprite.image = tip.image
  scene.add(sprite)
}
// 切换场景,根据index切换不同场景
function changeScene(index=1){
  //将场景中的精灵图清除,留下基础元素
  scene.children = scene.children.filter(item=>item.type!=='Sprite')

  // 渲染切换后的场景
  let texture = new THREE.TextureLoader().load(dataList[index].image)
  let sphereMaterial = new THREE.MeshBasicMaterial({
    map: texture,
    transparent: true,
    opacity: 0,
  });
  mesh.material = sphereMaterial
  camera.updateProjectionMatrix();
  addTipsSprite(index)
}

到这一步我们已经可以通过更改index为不同的值,渲染不同的页面了,最后应该把点击事件添加到场景中的精灵图上。

4.给精灵图添加点击事件(准确点应该是给window添加点击事件,通过Raycaster光线投影获取鼠标在场景中的相对位置)

window.addEventListener("click",function (e){
  e.preventDefault();

  //这里通过raycaster光线投影抓取点击目标
  let raycaster = new THREE.Raycaster()
  let mouse = new THREE.Vector2()
  mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
  raycaster.setFromCamera(mouse, camera);

  let  intersects = raycaster.intersectObjects(scene.children)
  if (intersects.length > 0 && intersects[0].object.type === 'Sprite') {
      changeScene(intersects[0].object.image);
  }
},false)

最后添加过渡动画优化一下了

import gsap from "gsap";


// 切换场景,根据index切换不同场景
function changeScene(index=0){
  //将场景中的精灵图清除,留下基础元素
  scene.children = scene.children.filter(item=>item.type!=='Sprite')

  // 渲染切换后的场景
  let texture = new THREE.TextureLoader().load(dataList[index].image)
  let sphereMaterial = new THREE.MeshBasicMaterial({map:texture})
  mesh.material = sphereMaterial
  
  gsap.to(sphereMaterial, { transparent: true, opacity: 1, duration: 2 });
  camera.updateProjectionMatrix();
  addTipsSprite(index)
}

全部代码

<template>
    <div class="home">
        <div class="view-container" ref="threeDBox"></div>
  </div>
</template>

<script setup>
import { onMounted, ref } from '@vue/runtime-core'
import livingImg from './assets/image/livingRoom.jpg'
import kitchenImg from './assets/image/kitchen.jpg'
import tipImg from './assets/image/tip.png'
import * as THREE from 'three'
import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls'
import gsap from 'gsap'

let renderer,camera,mesh,scene,controller
let threeDBox = ref(null)
let spriteList = []
let dataList = [
  {
    image: livingImg,
    tip:{
        position: { x: -200, y:0, z: -145 },
          image: 1,
      }
  },
  {
    image: kitchenImg,
    tip: {
        position: { x: -199, y: -24, z: 145 },
          image: 0,
      }
  }
];
 onMounted(()=>{
    init()
    render()
 })
function init(){
    initRenderer()
    initCamera()
    initScene()
    initMesh()
    initController()
    addTipsSprite()
    changeScene()
}
// 初始化渲染器
function initRenderer() {
  renderer = new THREE.WebGLRenderer({
    antialias: true
  })
  renderer.setPixelRatio(window.devicePixelRatio)
  renderer.setSize(window.innerWidth, window.innerHeight)
  threeDBox.value.appendChild(renderer.domElement)
}

// 初始化镜头
function initCamera() {
  camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000)
  camera.position.z = 0.0001
}
// 初始化场景
function initScene(){
  scene = new THREE.Scene()
}
// 初始化网格,  网格单元需要几何体和材质
function initMesh(){
  const geomatry = new THREE.SphereGeometry(16,50,50)
  const texture = new THREE.TextureLoader().load(livingImg)
  const material = new THREE.MeshBasicMaterial({map:texture})
  mesh = new THREE.Mesh(geomatry, material)
  mesh.geometry.scale(16,16,-16)
  scene.add(mesh)
}
// 控制器
function initController(){
  controller = new OrbitControls(camera,renderer.domElement)
  controller.enableDamping = true
}
// 渲染函数
function render(){
  controller.update();
  renderer.render(scene, camera)
  requestAnimationFrame(render)
}
// 在场景中添加精灵
function addTipsSprite(index=0){
  let map = new THREE.TextureLoader().load(tipImg)
  let material = new THREE.SpriteMaterial({map:map})
  let sprite = new THREE.Sprite(material)

  let tip = dataList[index].tip
  sprite.scale.set(10,10,10)
  sprite.position.set(tip.position.x,tip.position.y,tip.position.z)
  sprite.image = tip.image
  spriteList.push(sprite)
  scene.add(sprite)
}
// 切换场景,根据index切换不同场景
function changeScene(index=0){
  //将场景中的精灵图清除,留下基础元素
  scene.children = scene.children.filter(item=>item.type!=='Sprite')
  // 渲染切换后的场景
  let texture = new THREE.TextureLoader().load(dataList[index].image)
  let sphereMaterial = new THREE.MeshBasicMaterial({map:texture,transparent:true,opacity:0})
  mesh.material = sphereMaterial
  gsap.to(sphereMaterial,{transparent:true,opacity:1,duration:2})
  camera.updateProjectionMatrix();
  addTipsSprite(index)
}

//页面大小发生变化
window.addEventListener("resize",()=>{
  camera.aspect = window.innerWidth/window.innerHeight
  camera.updateProjectionMatrix()
  renderer.setSize(window.innerWidth,window.innerHeight)
})
window.addEventListener("click",function (e){
  e.preventDefault();
  //这里通过raycaster光线投影抓取点击目标
  let raycaster = new THREE.Raycaster()
  let mouse = new THREE.Vector2()
  mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
  raycaster.setFromCamera(mouse, camera);

  let  intersects = raycaster.intersectObjects(scene.children)
  if (intersects.length > 0 && intersects[0].object.type === 'Sprite') {
      changeScene(intersects[0].object.image);
  }
},false)
</script>

<style lang="scss">
.home {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;

  .view-container {
    width: 100%;
    height: 100%;
    overflow: hidden;
  }
}
</style>