趁着暂时没有项目,抄了一下大佬的3d小房子。对three又加深了一点理解。

42 阅读6分钟

上源代码:主流程代码AboutView.vue

<template>
  <div class="current-page">
    <canvas id="draw" `class`="draw" style="border: 1px solid; background-color: #000"></canvas>
  </div>
</template>
<script setup lang="ts">
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import * as THREE from 'three'
import { onMounted, onUnmounted } from 'vue'
import grassUrl from '@/assets/img/grass.jpg'
import floorUrl from '@/assets/img/floor.jpeg'
import skyUrl from '@/assets/img/sky.jpeg'

import { getThreeControls } from './interactiveControls.ts'
import { getHoursData } from './intehours.ts'

// Three.js相关对象
let { scene, camera, renderer, canvas, group, doorMesh, doorGroup, marker, handleDoorClick } =
  getThreeControls()
let { houseSize, housePosition, housePositiontwo } = getHoursData()

// 新增:射线投射器和指针变量
const raycaster = new THREE.Raycaster()
const pointer = new THREE.Vector2()
// 尺寸常量
const width = 1200
const height = 800

// 创建一个小木屋模型mui 的模型
function inithouse(sizeData, positionData) {
  createFloor(sizeData, positionData)
  createWalls(sizeData, positionData)
  createNoDoorWall(sizeData, positionData)
  createDoorWall(sizeData, positionData)
  createRoof(sizeData, positionData)
  createDoor(sizeData, positionData) // 新增创建门
}

function initThree() {
  // 修复:使用全局canvas变量,不使用const重新声明
  canvas = document.querySelector('#draw') as HTMLCanvasElement

  // 创建场景
  scene = new THREE.Scene()

  // 创建相机
  camera = new THREE.PerspectiveCamera(125, width / height, 1, 2000)
  camera.position.set(50, 10, 50)

  // 添加环境光
  const hjLight = new THREE.AmbientLight(0xffffff)

  scene.add(hjLight)

  scene.add(marker)

  // 添加建筑组
  scene.add(group)

  // 创建渲染器
  renderer = new THREE.WebGLRenderer({
    canvas,
    antialias: true,
    alpha: true,
  })
  renderer.setSize(width, height)

  // 添加轨道控制器
  const controls = new OrbitControls(camera, canvas)
  controls.addEventListener('change', () => {
    if (renderer && scene && camera) {
      renderer.render(scene, camera)
    }
  })

  // 初始渲染
  renderer.render(scene, camera)
}
// 创建地面
function createGround() {
  if (!scene) return

  const groundTexture = new THREE.TextureLoader().load(grassUrl)
  groundTexture.wrapS = groundTexture.wrapT = THREE.RepeatWrapping
  groundTexture.repeat.set(100, 100)

  const ground = new THREE.CircleGeometry(500, 100)
  const groundMaterial = new THREE.MeshLambertMaterial({
    side: THREE.DoubleSide,
    map: groundTexture,
  })

  const groundMesh = new THREE.Mesh(ground, groundMaterial)
  groundMesh.name = 'ground'
  groundMesh.rotateX(-Math.PI / 2)
  scene.add(groundMesh)
}
// 创建地板
function createFloor(sizeData, PositionData) {
  if (!group) return

  const texture = new THREE.TextureLoader().load(floorUrl)
  const floor = new THREE.BoxGeometry(sizeData.baseWidth, 1, sizeData.baseLength)
  const material = new THREE.MeshPhongMaterial({ map: texture })
  const mesh = new THREE.Mesh(floor, material)

  mesh.position.set(PositionData.x, PositionData.y, PositionData.z)
  mesh.name = 'floor'
  group.add(mesh)
}

function createWall(width, height, thickness, imgUrl) {
  const wallTexture = new THREE.TextureLoader().load(imgUrl)
  const wall = new THREE.BoxGeometry(width, height, thickness)
  const material = new THREE.MeshPhongMaterial({ map: wallTexture })
  return new THREE.Mesh(wall, material)
}

function createWalls(sizeData, PositionData) {
  if (!group) return
  // 左侧墙
  const leftWall = createWall(
    sizeData.baseWidth,
    sizeData.baseHeight,
    sizeData.baseThickness,
    sizeData.baseImg,
  )
  leftWall.name = 'leftWall'
  leftWall.position.set(PositionData.x, 10, sizeData.baseLength / 2 + PositionData.z)
  group.add(leftWall)

  // 右侧墙
  const rightWall = createWall(
    sizeData.baseWidth,
    sizeData.baseHeight,
    sizeData.baseThickness,
    sizeData.baseImg,
  )
  rightWall.name = 'rightWall'
  rightWall.position.set(PositionData.x, 10, -sizeData.baseLength / 2 + PositionData.z)
  group.add(rightWall)
}

function genwallShapeNo(sizeData) {
  const shape = new THREE.Shape()
  let height = houseSize.baseHeight // 使用统一的高度
  // 绘制墙体轮廓
  shape.moveTo(0, 0)
  shape.lineTo(0, height)
  shape.lineTo(
    sizeData.baseLength / 2 - sizeData.peakWidth / 2,
    sizeData.baseHeight + sizeData.peakHeight - 1,
  )
  shape.lineTo(
    sizeData.baseLength / 2 - sizeData.peakWidth / 2,
    sizeData.baseHeight + sizeData.peakHeight,
  )
  shape.lineTo(
    sizeData.baseLength / 2 + sizeData.peakWidth / 2,
    sizeData.baseHeight + sizeData.peakHeight,
  )
  shape.lineTo(
    sizeData.baseLength / 2 + sizeData.peakWidth / 2,
    sizeData.baseHeight + sizeData.peakHeight - 1,
  )
  shape.lineTo(sizeData.baseLength, sizeData.baseHeight)
  shape.lineTo(sizeData.baseLength, 0)
  shape.lineTo(0, 0)
  return shape
}

function createIrregularWall(shape, position, textureUrl) {
  const extrudeSettings = {
    depth: houseSize.baseThickness, // 使用统一的厚度
    bevelEnabled: false,
  }
  const wallTexture = new THREE.TextureLoader().load(textureUrl)
  const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings)
  wallTexture.wrapS = wallTexture.wrapT = THREE.RepeatWrapping
  wallTexture.repeat.set(0.05, 0.05)
  const material = new THREE.MeshPhongMaterial({
    map: wallTexture,
    side: THREE.DoubleSide,
  })
  const mesh = new THREE.Mesh(geometry, material)
  mesh.position.set(position[0], position[1], position[2])
  group.add(mesh)
  return mesh
}

function createNoDoorWall(sizeData, positionData) {
  const shape = genwallShapeNo(sizeData)
  let mesh = createIrregularWall(
    shape,
    [
      -sizeData.baseWidth / 2 + positionData.x,
      0 + positionData.y,
      sizeData.baseLength / 2 + positionData.z,
    ],
    sizeData.baseImg,
  )
  mesh.name = 'backWall'
  mesh.rotation.y = Math.PI / 2
}

function createDoorWall(sizeData, positionData) {
  const shape = genwallShapeNo(sizeData)
  const door = new THREE.Path()
  const doorOffsetX = sizeData.baseLength / 2

  door.moveTo(doorOffsetX - sizeData.doorWidth / 2, 0)
  door.lineTo(doorOffsetX - sizeData.doorWidth / 2, sizeData.doorHeight)
  door.lineTo(doorOffsetX + sizeData.doorWidth / 2, sizeData.doorHeight)
  door.lineTo(doorOffsetX + sizeData.doorWidth / 2, 0)

  shape.holes.push(door)

  const mesh = createIrregularWall(
    shape,
    [
      sizeData.baseWidth / 2 + positionData.x,
      0 + positionData.y,
      sizeData.baseLength / 2 + positionData.z,
    ],
    sizeData.baseImg,
  )
  mesh.name = 'doorWall'
  mesh.rotation.y = Math.PI / 2
}

function createRoof(sizeData, positionData) {
  if (!group) return

  // 屋顶参数
  const roofPeakHeight = sizeData.peakHeight // 屋顶顶点高度
  const roofEaveLength = 5 // 屋檐延伸长度

  // 计算屋顶实际尺寸
  const roofWidth = Math.sqrt(sizeData.baseWidth ** 2 + roofPeakHeight ** 2) + roofEaveLength
  const roofLength = sizeData.baseWidth - 6 // 比墙面略长
  const roofThickness = 1 // 屋顶厚度

  // 屋顶高度位置 (墙高 + 屋顶厚度)
  const roofPositionY = sizeData.baseHeight + roofThickness + 1

  // 创建屋顶几何体
  const geometry = new THREE.BoxGeometry(roofLength, roofWidth, roofThickness)

  // 加载屋顶纹理
  const texture = new THREE.TextureLoader().load(sizeData.tileUrl)
  texture.wrapS = texture.wrapT = THREE.RepeatWrapping
  texture.repeat.set(2, 2)

  // 创建屋顶材质
  const material = new THREE.MeshPhongMaterial({
    map: texture,
    side: THREE.DoubleSide,
  })

  // 创建右侧屋顶
  const rightRoof = new THREE.Mesh(geometry, material)
  rightRoof.rotation.set(
    80 * (Math.PI / 180), // X轴旋转角度(弧度)
    0 * (Math.PI / 180), // Y轴旋转180度
    90 * (Math.PI / 180), // Z轴旋转45度
  )
  rightRoof.position.set(
    -sizeData.baseWidth / 2 + 20 + positionData.x,
    roofPositionY + positionData.y,
    positionData.z - 16.5,
  )
  rightRoof.name = 'rightRoof'
  group.add(rightRoof)

  // 创建左侧屋顶(对称)
  const leftRoof = new THREE.Mesh(geometry, material)
  leftRoof.rotation.set(
    100 * (Math.PI / 180), // X轴旋转角度(弧度)
    0 * (Math.PI / 180), // Y轴旋转180度
    90 * (Math.PI / 180), // Z轴旋转45度
  )
  leftRoof.position.set(
    sizeData.baseWidth / 2 - 20 + positionData.x,
    roofPositionY + positionData.y,
    positionData.z + 16.5,
  )
  leftRoof.name = 'leftRoof'
  group.add(leftRoof)

  return {
    roofs: [rightRoof, leftRoof],
    width: roofWidth,
  }
}

function createDoor(sizeData, positionData) {
  if (!group) return

  // 创建门组
  doorGroup = new THREE.Group()
  doorGroup.name = 'doorGroup'

  // 加载门纹理
  const texture = new THREE.TextureLoader().load(sizeData.doorImg)

  // 创建门几何体 - 宽度使用sizeData.doorWidth,高度使用sizeData.doorHeight
  const door = new THREE.BoxGeometry(
    sizeData.doorWidth,
    sizeData.doorHeight,
    0.5, // 厚度
  )

  // 创建门材质
  const material = new THREE.MeshPhongMaterial({
    map: texture,
    transparent: true,
    opacity: 1,
  })

  // 创建门网格
  doorMesh = new THREE.Mesh(door, material)
  doorMesh.name = 'door'
  // 关键修改:将门网格在组中偏移,使旋转轴位于左侧边缘
  doorMesh.position.x = sizeData.doorWidth / 2 // 将门向右移动宽度的一半,使左侧边缘位于原点
  // 将门添加到门组
  doorGroup.add(doorMesh)

  // 设置门组位置和旋转
  doorGroup.position.set(
    sizeData.baseWidth / 2 + positionData.x,
    sizeData.doorHeight / 2 + positionData.y,
    6 + positionData.z - sizeData.baseThickness,
  )
  doorGroup.rotation.y = Math.PI / 2

  // 将门组添加到场景组
  group.add(doorGroup)
}

function animate() {
  requestAnimationFrame(animate)
  if (!camera || !marker) return // 添加null检查
  // 让物体始终在 camera 前方 5 单位
  marker.position.copy(camera.position)
  marker.position.add(new THREE.Vector3(0, 0, -15).applyQuaternion(camera.quaternion))
  if (renderer && scene && camera) {
    renderer.render(scene, camera)
  }
}
function createSkyBox() {
  const texture = new THREE.TextureLoader().load(skyUrl)
  texture.wrapS = texture.wrapT = THREE.RepeatWrapping
  // texture.repeat.set(1, 1);
  const skyBox = new THREE.SphereGeometry(500, 100, 100)
  const material = new THREE.MeshPhongMaterial({
    map: texture,
    side: THREE.BackSide,
  })
  const skyBoxMesh = new THREE.Mesh(skyBox, material)
  group.add(skyBoxMesh)
}

// 新增:设置事件监听器函数
function setupEventListeners() {
  if (!canvas) {
    console.error('Canvas not initialized!') // 添加错误日志
    return
  }

  console.log('22222222222') // 调试日志

  // 移除旧监听器避免重复添加
  canvas.removeEventListener('pointermove', onPointerMove)
  canvas.removeEventListener('click', onClick)
  window.removeEventListener('keydown', handleKeyDown) // 键盘事件需绑定到window
  // 鼠标移动事件
  canvas.addEventListener('pointermove', onPointerMove)
  // 点击事件
  canvas.addEventListener('click', onClick, false)
  // 监听全局键盘事件
  window.addEventListener('keydown', handleKeyDown)
}

// 新增:鼠标移动事件处理
function onPointerMove(event: MouseEvent) {
  if (!canvas) return

  // 将鼠标位置归一化为设备坐标 (-1 到 +1)
  pointer.x = (event.clientX / canvas.clientWidth) * 2 - 1
  pointer.y = -(event.clientY / canvas.clientHeight) * 2 + 1
}

// 新增:点击事件处理
function onClick() {
  if (!scene || !camera) return

  // 更新射线投射器
  raycaster.setFromCamera(pointer, camera)

  // 计算与场景对象的交点
  const intersects = raycaster.intersectObjects(scene.children, true)

  if (intersects.length > 0) {
    const clickedObject = intersects[0].object
    console.log('点击了:', clickedObject.name)

    // 检查是否点击了门
    if (clickedObject.name === 'door') {
      handleDoorClick(clickedObject) // 修改:传入点击对象
    }
  }
}
// 新增:键盘事件处理函数
function handleKeyDown(event: KeyboardEvent) {
  console.log('Key pressed:', event.key)
  // 示例:用WASD控制相机移动
  const moveSpeed = 5
  if (!camera) return
  console.log('相机的坐标:', camera.position)

  switch (event.key.toLowerCase()) {
    case 'w':
      camera.position.z -= moveSpeed
      break
    case 's':
      camera.position.z += moveSpeed
      break
    case 'a':
      camera.position.x -= moveSpeed
      break
    case 'd':
      camera.position.x += moveSpeed
      break
    case ' ':
      // 空格键重置相机位置
      camera.position.set(-30, 30, 50)
      break
  }

  // 触发重新渲染
  if (renderer && scene && camera) {
    renderer.render(scene, camera)
  }
}

onMounted(() => {
  initThree()
  createGround()
  animate()
  setupEventListeners() // 新增:设置事件监听器
  createSkyBox()
  inithouse(houseSize, housePosition)
  inithouse(houseSize, housePositiontwo)
})
// 组件卸载时移除事件监听器
onUnmounted(() => {
  if (canvas) {
    canvas.removeEventListener('pointermove', onPointerMove)
    canvas.removeEventListener('click', onClick)
    window.removeEventListener('keydown', handleKeyDown) // 键盘事件需绑定到window
  }
})
</script>
<style scoped>
.current-page {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
}

.draw {
  width: 100%;
  height: 100%;
}
</style>

操作事件代码:interactiveControls.ts

// interactiveControls.ts
import { ref } from 'vue'
import * as THREE from 'three'

// 中间件传递typeZi
// const typeZi = ref<string>()

// Three.js相关对象
let scene: THREE.Scene | null = null
let camera: THREE.PerspectiveCamera | null = null
let renderer: THREE.WebGLRenderer | null = null
let canvas: HTMLCanvasElement | null = null
const group = new THREE.Group()
let doorMesh: THREE.Mesh | null = null
const doorGroup = new THREE.Group()
const marker = new THREE.Mesh(
  new THREE.SphereGeometry(0.5, 16, 16),
  new THREE.MeshBasicMaterial({ color: 0xff0000 }),
)
// 更新typeZi的方法
// const getTypeZi = async (value: string) => {
//   typeZi.value = value
// }

// 新增:门点击处理函数
function handleDoorClick(doorObject: THREE.Object3D) {
  console.log('门被点击了!')

  // 获取门的父对象(如果是门组)
  const doorParent = doorObject.parent || doorObject
  const rotationSpeed = 0.05 // 旋转速度
  const maxRotation = Math.PI / 2 // 最大旋转角度(90度)

  // 检查当前门的状态(使用绝对值比较,避免方向问题)
  const isOpen = Math.abs(doorParent.rotation.y) >= maxRotation

  // 清除之前的动画(防止多次点击造成动画叠加)
  if (doorParent.animationInterval) {
    clearInterval(doorParent.animationInterval)
  }

  if (isOpen) {
    // 关门动画
    console.log('关门中...')
    doorParent.animationInterval = setInterval(() => {
      // 根据当前方向决定旋转方向
      if (doorParent.rotation.y > 0) {
        doorParent.rotation.y = Math.max(0, doorParent.rotation.y - rotationSpeed)
      } else {
        doorParent.rotation.y = Math.min(0, doorParent.rotation.y + rotationSpeed)
      }

      // 检查是否到达关闭位置
      if (Math.abs(doorParent.rotation.y) < 0.01) {
        doorParent.rotation.y = 0
        clearInterval(doorParent.animationInterval)
      }
    }, 1000 / 60)
  } else {
    // 开门动画
    console.log('开门中...')
    doorParent.animationInterval = setInterval(() => {
      // 根据当前方向决定旋转方向
      if (doorParent.rotation.y >= 0) {
        doorParent.rotation.y += rotationSpeed
        // 检查是否到达最大开度
        if (doorParent.rotation.y >= maxRotation) {
          doorParent.rotation.y = maxRotation
          clearInterval(doorParent.animationInterval)
        }
      } else {
        doorParent.rotation.y -= rotationSpeed
        // 检查是否到达最大开度
        if (doorParent.rotation.y <= -maxRotation) {
          doorParent.rotation.y = -maxRotation
          clearInterval(doorParent.animationInterval)
        }
      }
    }, 1000 / 60)
  }
}

// 或者如果需要更新对象,可以导出函数形式
export function getThreeControls() {
  return {
    scene,
    camera,
    renderer,
    canvas,
    group,
    doorMesh,
    doorGroup,
    marker,
    handleDoorClick
  }
}

配置数据代码:intehours.ts

      // interactiveControls.ts
import { ref } from 'vue'
import * as THREE from 'three'
import wallbUrl from '@/assets/img/bmqiang.jpg'
import tileUrl from '@/assets/img/tile.jpg'
import doorUrl from '@/assets/img/door.jpg' // 新增门纹理导入


// 房子的尺寸参数
const houseSize = {
  baseWidth: 40, // 房屋宽度
  baseLength: 60, // 房屋长度
  baseHeight: 20, // 墙面高度
  baseThickness: 1, // 墙面厚度
  peakHeight: 6, // 顶面高度(顶点突出高度)
  peakWidth: 2, // 顶面宽度(顶点宽度)
  doorWidth: 10, // 门宽度
  doorHeight: 16, // 门高度
  baseImg: wallbUrl, // 墙面图片
  tileUrl: tileUrl, // 瓷砖图片
  doorImg: doorUrl, // 新增门图片
}

// 房子的坐标
const housePosition = {
  x: 0,
  y: 1,
  z: 0,
}
// 房子的坐标2
const housePositiontwo = {
  x: 100,
  y: 1,
  z: 100,
}
// 或者如果需要更新对象,可以导出函数形式
export function getHoursData() {
  return {
    houseSize,
    housePosition,
    housePositiontwo
  }
}   
                                             图片自己找一下