vue3+threejs新手从零开发卡牌游戏(八):关联卡组和手牌区、添加初始化卡组和初始化手牌逻辑

首先我们优化下之前的代码,先加载游戏资源,然后再初始化场景,由于目前只有一个font字体需要加载,所以我们将之前game/deck/p1.vue中的font相关代码迁移到game/index.vue下,同时使用async和await处理异步加载,即当资源加载完后再执行场景的初始化,这里font我存入了store供全局使用,后面有对应的store代码:

import { FontLoader } from 'three/addons/loaders/FontLoader.js';


// 字体加载器
const fontLoader = new FontLoader();

onMounted(async () => {
  await initResource()
  initScene()

  // 监听浏览器窗口变化进行场景自适应
  window.addEventListener('resize', onWindowResize, false);
})

// 资源加载
const initResource = () => {
  // 字体加载
  return new Promise((resolve, reject) => {
    fontLoader.load('fonts/helvetiker_regular.typeface.json', (font: any) => {
      commonStore.loadFont(font)
      resolve(true)
    });
  })
}

...

然后我们思考下卡组的抽卡逻辑:
1.移除一张卡组顶的卡牌

2.将卡牌从卡组位置移动到手牌区位置(动效)

3.将手牌加入手牌区

首先我们可以自定义一个测试卡组,然后存放到store中:

stores/common.ts代码:

import { ref, computed } from 'vue'
import { defineStore } from 'pinia'

export const useCommonStore = defineStore('common', () => {

  const _font = ref() // 字体

  const p1Deck = ref([] as any) // 卡组
  const p2Deck = ref([] as any) // 卡组

  
  // 加载字体
  function loadFont(data: any) {
    _font.value = data
  }

  // 更新己方卡组
  function updateP1Deck(data: any) {
    p1Deck.value = data
  }

  // 更新对方卡组
  function updateP2Deck(data: any) {
    p2Deck.value = data
  }


  return {
    _font,
    p1Deck,
    p2Deck,

    loadFont,
    updateP1Deck,
    updateP2Deck,

  }
}, {
  persist: true
})

然后在game/index.vue初始化卡组:

// 初始化卡组
const initDeck = () => {
  return new Promise((resolve, reject) => {
    let p1Deck = [
      "YZ-01",
      "YZ-02",
      "YZ-03",
      "YZ-04",
      "YZ-01",
      "YZ-02",
      "YZ-03",
      "YZ-04",
      "YZ-01",
      "YZ-02",
      "YZ-03",
      "YZ-04",
    ]
    // 洗牌
    p1Deck.sort(() => {
      return Math.random() - 0.5
    })
    let newDeck: any = []
    p1Deck.forEach((v: any, i: any) => {
      let obj = CARD_DICT.find((b: any) => b.card_id === v)
      if (obj) {
        newDeck.push({
          card_id: v,
          name: `${obj.name}_${i}`
        })
      }
    })
    // console.log("p1Deck", newDeck)
    commonStore.updateP1Deck(newDeck)
    
    nextTick(() => {
      handRef.value.init()
      deckRef.value.init()
      resolve(true)
    })
  })
}

其中我们在newDeck数组中保存了这局游戏中所用卡组中所有卡牌的card_id和name,其中name通过名称+索引的方式让每张卡牌有了唯一的name值,这个步骤是为了处理卡组中的同名卡牌,这样我们可以保证每张卡牌都有唯一标识,并且按照优先级我们应该先初始化卡组,然后才能初始化手牌,所以这里也用了Promise,然后初始化卡组存入store中的p1Deck,修改deck/p1.vue代码,将store中的p1Deck传入进来:

import { useCommonStore } from "@/stores/common.ts"

const commonStore = useCommonStore()

const init = () => {
  setDeckPos()
  commonStore.$state.p1Deck.forEach((v: any, i: any) => {
    let obj = CARD_DICT.find((b: any) => b.card_id === v.card_id)
    if (obj) {
      let card = new Card(obj)
      let mesh = card.init()
      mesh.position.set(0, 0.005 * i, 0)
      mesh.rotateX(180 * (Math.PI / 180)) // 弧度
      mesh.name = v.name
      deckGroup.add( mesh );
    }
  })

  renderText()

}

// 渲染文字
const renderText = () => {
  const geometry = new TextGeometry( `${commonStore.$state.p1Deck.length}`, {
    font: commonStore.$state._font,
    size: 0.4,
    height: 0,
    curveSegments: 4,
    bevelEnabled: true,
    bevelThickness: 0,
    bevelSize: 0,
    bevelSegments: 0
  });
  geometry.center()
  const material = new THREE.MeshBasicMaterial( { color: new THREE.Color("white") } )
  const mesh = new THREE.Mesh( geometry, material ) ;
  mesh.position.set(0, 0.005 * commonStore.$state.p1Deck.length + 0.01, 0) // 弧度
  mesh.rotateX(-90 * (Math.PI / 180)) // 弧度
  mesh.name = "卡组数量"

  // 阴影
  let shadowGeometry = geometry.clone()
  shadowGeometry.translate(0.02, 0.02, 0);
  let shadowMaterial = new THREE.MeshBasicMaterial( { color: new THREE.Color("black") } );
  let shadowMesh = new THREE.Mesh(shadowGeometry, shadowMaterial);
  shadowMesh.position.set(0, 0.005 * commonStore.$state.p1Deck.length, 0) // 弧度
  shadowMesh.rotateX(-90 * (Math.PI / 180)) // 弧度
  shadowMesh.name = "卡组数量阴影"

  deckGroup.add(mesh)
  deckGroup.add(shadowMesh)
}

// 设置卡组位置
const setDeckPos = () => {
  nextTick(() => {
    let plane = scene.getObjectByName("地面")
    let point = transPos(window.innerWidth - 15, window.innerHeight - 15) // 卡组起始位置的屏幕坐标
    // 
    raycaster.setFromCamera( point, camera );
    const intersects1 = raycaster.intersectObject( plane );
    if (intersects1.length > 0) {
      let point = intersects1[0].point
      // deckGroup.position.set(point.x, point.y, point.z)
      deckGroup.position.set(point.x - 0.5, point.y, point.z - 0.7)
    }
  })
}

此时页面效果如下:

可以看到右侧卡组的数量和你传入的测试卡组数量保持一致了。

然后我们可以思考下,如何从卡组顶移除一张卡牌

1.找到卡组顶的卡牌

2.将它从卡组group中移除

3.更新卡组数量和厚度

我们继续修改game/deck/p1.vue,这个方法是传入一个obj(这个对象就是p1Deck中要移除的那个卡牌对象,里面只有card_id和name两个字段),然后根据obj中的name找到卡组中对应的卡牌mesh,然后根据type判断是要往卡组里添加还是移除这个mesh,如果是移除的话,那么我们先找到卡组Group,然后删除卡组Group中的这个mesh,并进行更新;同时我们删除了文字mesh和对应的文字阴影mesh,将新的卡组数量文字绘制上去,然后将这个方法暴露出去:

// 修改卡组
const editDeckCard = (obj: any, type: any) => {
  let group = scene.getObjectByName("p1_deckGroup")
  let text = group.children.find((v: any) => v.name === "卡组数量")
  let shadowText = group.children.find((v: any) => v.name === "卡组数量阴影")
  // console.log(22, group.children, commonStore.$state.p1Deck)
  if (type === "remove") { // 删除卡组中的卡牌
    let child = group.children.find((v: any) => v.name === obj.name)
    if (child) {
      group.remove(child)
    }
  }
  group.remove(text)
  group.remove(shadowText)
  group.children.forEach((v: any, i: any) => {
    v.position.set(0, 0.005 * i, 0)
  })
  renderText()
}

defineExpose({
  init,
  editDeckCard
})

因为这里还有一层上级目录,所以需要修改game/deck/index.vue,将p1.vue中的方法暴露出去供game/index.vue使用:

// 修改卡组
const editDeckCard = (obj: any, type: any) => {
  p1Ref.value.editDeckCard(obj, type)
}

defineExpose({
  init,
  editDeckCard
})

在game/index.vue中,我们添加一个初始化手牌的方法:

// 初始化手牌
const initHand = () => {
  let cardNumber = 4
  let _number = 0
  let p1Deck = JSON.parse(JSON.stringify(commonStore.$state.p1Deck))
  let deckGroup = scene.getObjectByName("p1_deckGroup")
  let _interval = setInterval(function() {
    // console.log(123, p1Deck)
    if (_number < cardNumber) {
      let obj = p1Deck[p1Deck.length - 1]
      p1Deck.splice(p1Deck.length-1, 1)
      commonStore.updateP1Deck(p1Deck)
      // 修改卡组
      deckRef.value.editDeckCard(obj, "remove")
      // 手牌区添加手牌
      handRef.value.addHandCard(obj, deckGroup)
    } else {
      clearInterval(_interval)
    }
    _number++
  }, 200)

}

这里的逻辑是,比如游戏开始时,要从卡顶抽4张牌,这里我设置每200ms抽一张牌,每抽一张牌就把p1Deck里对应的卡牌删掉,然后调用editDeckCard方法进行移除卡牌操作,然后将抽出来的卡牌加入手牌区,我们先把手牌加入手牌区的方法注释掉,此时刷新页面效果如下:

然后我们在game/hand/p1.vue中编写手牌区添加手牌的逻辑,注意我这里先将卡牌mesh加入场景中(方便在世界坐标系中做卡牌移动动效),然后等卡牌移动到手牌区后再真正的将卡牌加入手牌区Group中:

// 添加手牌
const addHandCard = (obj: any, origin: any) => {
  let position = origin.position
  let cardObj = CARD_DICT.find((v: any) => v.card_id === obj.card_id)
  if (cardObj) {
    let card = new Card(cardObj)
    let mesh = card.init()
    mesh.position.set(position.x, position.y, position.z)
    mesh.material.forEach((v: any) => {
      v.transparent = true
    })
    scene.add( mesh );
    updateCardPos(mesh)
  }

}

// 更新卡牌位置
const updateCardPos = (mesh: any) => {
  const tw = new TWEEN.Tween({
    x: mesh.position.x,
    y: mesh.position.y,
    z: mesh.position.z,
    opacity: 0.9,
    mesh
  })
  tw.to({
    x: handGroup.position.x,
    y: handGroup.position.y,
    z: handGroup.position.z,
    opacity: 0
  }, 200)
  tw.easing(TWEEN.Easing.Quadratic.Out)
  tw.onUpdate((obj: any) => {
    obj.mesh.position.set(obj.x, obj.y, obj.z)
    obj.mesh.material.forEach((v: any) => {
      v.opacity = obj.opacity
    })
  })
  tw.onComplete(function() {
    //动画结束:关闭允许透明,恢复到模型原来状态
    TWEEN.remove(tw)
    scene.remove( mesh );
    
    mesh.material.forEach((v: any) => {
      v.transparent = false
      v.opacity = 1
    })
    handGroup.add(mesh)
    // 计算叠放间距
    let space = ((_width.value - 1) / (handGroup.children.length - 1)) <= 1 ? (_width.value - 1) / (handGroup.children.length - 1) : 1
    handGroup.children.forEach((v: any, i: any) => {
      v.position.set(i * space, 0.005 * i, 0)
    })
  })

  tw.start();
}

defineExpose({
  init,
  addHandCard
})

addHandCard方法的参数obj指的是从p1Deck中移除的那张卡牌对象,里面只包含card_id和name两个字段,origin指的是来源对象,比如如果是从卡组移入手牌,那么来源对象就是卡组,如果是从墓地移入手牌那么来源就是墓地,这个来源对象是方便记录起始点的位置(结束点我设置的是手牌区的起始点,用来做卡牌的移动特效),上面的updateCardPos方法就是用TWEEN做的移动效果,需要注意的是,结尾一定要写tw.start()方法,否则动画不会执行,同时需要在game/index.vue的动画循环中加入TWEEN.update(),否则动画不会更新,依然看不到动画效果,动画教程可以参考:tween.js user guide | tween.js

// 用requestAnimationFrame进行渲染循环
const animate = () => {
  requestAnimationFrame( animate );
  TWEEN.update()
  renderer.render( scene, camera );
}

最后别忘了修改hand/index.vue方法,将addHandCard方法暴露出去:

const addHandCard = (obj: any, origin: any) => {
  p1Ref.value.addHandCard(obj, origin)
}

defineExpose({
  init,
  addHandCard
})

最后的效果如下:

附:
game/index.vue完整代码:

<template>
  <div ref="sceneRef" class="scene"></div>
  <!-- 手牌 -->
  <Hand ref="handRef"/>
  <!-- 卡组 -->
  <Deck ref="deckRef"/>
</template>

<script setup lang="ts">
import { reactive, ref, onMounted, onBeforeUnmount, watch, defineComponent, getCurrentInstance, nextTick } from 'vue'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // 轨道控制器
import { FontLoader } from 'three/addons/loaders/FontLoader.js';
import { useCommonStore } from "@/stores/common.ts"
import { Card } from "./Card.ts"
import { CARD_DICT } from "@/utils/dict/card.ts"
import Hand from "./hand/index.vue"
import Deck from "./deck/index.vue"

// 引入threejs变量
const {proxy} = getCurrentInstance()
const THREE = proxy['THREE']
const scene = proxy['scene']
const camera = proxy['camera']
const renderer = proxy['renderer']
const TWEEN = proxy['TWEEN']

const commonStore = useCommonStore()

// 场景ref
const sceneRef = ref()
const handRef = ref()
const deckRef = ref()

// 坐标轴辅助
const axesHelper = new THREE.AxesHelper(5);
// 创建轨道控制器
const controls = new OrbitControls( camera, renderer.domElement );
// 字体加载器
const fontLoader = new FontLoader();

onMounted(async () => {
  await initResource()
  initScene()
  initGame()

  // 监听浏览器窗口变化进行场景自适应
  window.addEventListener('resize', onWindowResize, false);
})

// 资源加载
const initResource = () => {
  // 字体加载
  return new Promise((resolve, reject) => {
    fontLoader.load('fonts/helvetiker_regular.typeface.json', (font: any) => {
      commonStore.loadFont(font)
      resolve(true)
    });
  })
}

// 初始化场景
const initScene = () => {
  renderer.setSize( window.innerWidth, window.innerHeight );
  sceneRef.value.appendChild( renderer.domElement );
  scene.add(axesHelper)

  // camera.position.set( 5, 5, 5 );
  camera.position.set( 0, 6.5, 0 );
  camera.lookAt(0, 0, 0)

  addPlane()

  animate();
}

// scene中添加plane几何体
const addPlane = () => {
  const geometry = new THREE.PlaneGeometry( 20, 20);
  const material = new THREE.MeshBasicMaterial( {
    color: new THREE.Color("gray"), 
    side: THREE.FrontSide, 
    alphaHash: true,
    // alphaTest: 0,
    opacity: 0
  } );
  const plane = new THREE.Mesh( geometry, material );
  plane.rotateX(-90 * (Math.PI / 180)) // 弧度
  plane.name = "地面"
  scene.add( plane );
}

// 用requestAnimationFrame进行渲染循环
const animate = () => {
  requestAnimationFrame( animate );
  TWEEN.update()
  renderer.render( scene, camera );
}

// 场景跟随浏览器窗口大小自适应
const onWindowResize = () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}

// 初始化游戏
const initGame = async () => {
  // 初始化卡组
  await initDeck()
  // 初始化手牌
  initHand()
}

// 初始化卡组
const initDeck = () => {
  return new Promise((resolve, reject) => {
    let p1Deck = [
      "YZ-01",
      "YZ-02",
      "YZ-03",
      "YZ-04",
      "YZ-01",
      "YZ-02",
      // "YZ-03",
      // "YZ-04",
      // "YZ-01",
      // "YZ-02",
      // "YZ-03",
      // "YZ-04",
    ]
    // 洗牌
    p1Deck.sort(() => {
      return Math.random() - 0.5
    })
    let newDeck: any = []
    p1Deck.forEach((v: any, i: any) => {
      let obj = CARD_DICT.find((b: any) => b.card_id === v)
      if (obj) {
        newDeck.push({
          card_id: v,
          name: `${obj.name}_${i}`
        })
      }
    })
    // console.log("p1Deck", newDeck)
    commonStore.updateP1Deck(newDeck)
    
    nextTick(() => {
      handRef.value.init()
      deckRef.value.init()
      resolve(true)
    })
  })
}

// 初始化手牌
const initHand = () => {
  let cardNumber = 4
  let _number = 0
  let p1Deck = JSON.parse(JSON.stringify(commonStore.$state.p1Deck))
  let deckGroup = scene.getObjectByName("p1_deckGroup")
  let _interval = setInterval(function() {
    // console.log(123, p1Deck)
    if (_number < cardNumber) {
      let obj = p1Deck[p1Deck.length - 1]
      p1Deck.splice(p1Deck.length-1, 1)
      commonStore.updateP1Deck(p1Deck)
      // 修改卡组
      deckRef.value.editDeckCard(obj, "remove")
      // 手牌区添加手牌
      handRef.value.addHandCard(obj, deckGroup)
    } else {
      clearInterval(_interval)
    }
    _number++
  }, 200)

}
</script>

<style lang="scss" scoped>
.scene {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100vh;
}
</style>

game/deck/p1.vue完整代码:

<template>
  <div></div>
</template>

<script setup lang="ts">
import { reactive, ref, onMounted, onBeforeUnmount, watch, defineComponent, getCurrentInstance, nextTick } from 'vue'
import { useCommonStore } from "@/stores/common.ts"
import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
import { Card } from "@/views/game/Card.ts"
import { CARD_DICT } from "@/utils/dict/card.ts"
import { transPos } from "@/utils/common.ts"

// 引入threejs变量
const {proxy} = getCurrentInstance()
const THREE = proxy['THREE']
const scene = proxy['scene']
const camera = proxy['camera']
const renderer = proxy['renderer']
const TWEEN = proxy['TWEEN']

const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();

const commonStore = useCommonStore()

// 卡组group
const deckGroup = new THREE.Group()
deckGroup.name = "p1_deckGroup"
scene.add(deckGroup)

const init = () => {
  setDeckPos()
  commonStore.$state.p1Deck.forEach((v: any, i: any) => {
    let obj = CARD_DICT.find((b: any) => b.card_id === v.card_id)
    if (obj) {
      let card = new Card(obj)
      let mesh = card.init()
      mesh.position.set(0, 0.005 * i, 0)
      mesh.rotateX(180 * (Math.PI / 180)) // 弧度
      mesh.name = v.name
      deckGroup.add( mesh );
    }
  })

  renderText()

}

// 渲染文字
const renderText = () => {
  const geometry = new TextGeometry( `${commonStore.$state.p1Deck.length}`, {
    font: commonStore.$state._font,
    size: 0.4,
    height: 0,
    curveSegments: 4,
    bevelEnabled: true,
    bevelThickness: 0,
    bevelSize: 0,
    bevelSegments: 0
  });
  geometry.center()
  const material = new THREE.MeshBasicMaterial( { color: new THREE.Color("white") } )
  const mesh = new THREE.Mesh( geometry, material ) ;
  mesh.position.set(0, 0.005 * commonStore.$state.p1Deck.length + 0.01, 0) // 弧度
  mesh.rotateX(-90 * (Math.PI / 180)) // 弧度
  mesh.name = "卡组数量"

  // 阴影
  let shadowGeometry = geometry.clone()
  shadowGeometry.translate(0.02, 0.02, 0);
  let shadowMaterial = new THREE.MeshBasicMaterial( { color: new THREE.Color("black") } );
  let shadowMesh = new THREE.Mesh(shadowGeometry, shadowMaterial);
  shadowMesh.position.set(0, 0.005 * commonStore.$state.p1Deck.length, 0) // 弧度
  shadowMesh.rotateX(-90 * (Math.PI / 180)) // 弧度
  shadowMesh.name = "卡组数量阴影"

  deckGroup.add(mesh)
  deckGroup.add(shadowMesh)
}

// 设置卡组位置
const setDeckPos = () => {
  nextTick(() => {
    let plane = scene.getObjectByName("地面")
    let point = transPos(window.innerWidth - 15, window.innerHeight - 15) // 卡组起始位置的屏幕坐标
    // 
    raycaster.setFromCamera( point, camera );
    const intersects1 = raycaster.intersectObject( plane );
    if (intersects1.length > 0) {
      let point = intersects1[0].point
      // deckGroup.position.set(point.x, point.y, point.z)
      deckGroup.position.set(point.x - 0.5, point.y, point.z - 0.7)
    }
  })
}

// 修改卡组
const editDeckCard = (mesh: any, type: any) => {
  let group = scene.getObjectByName("p1_deckGroup")
  let text = group.children.find((v: any) => v.name === "卡组数量")
  let shadowText = group.children.find((v: any) => v.name === "卡组数量阴影")
  // console.log(22, group.children, commonStore.$state.p1Deck)
  if (type === "remove") { // 删除卡组中的卡牌
    let child = group.children.find((v: any) => v.name === mesh.name)
    if (child) {
      group.remove(child)
    }
  }
  group.remove(text)
  group.remove(shadowText)
  group.children.forEach((v: any, i: any) => {
    v.position.set(0, 0.005 * i, 0)
  })
  renderText()
}

defineExpose({
  init,
  editDeckCard
})
</script>

<style lang="scss" scoped>
</style>

game/hand/p1.vue完整代码:

<template>
  <div></div>
</template>

<script setup lang="ts">
import { reactive, ref, onMounted, onBeforeUnmount, watch, defineComponent, getCurrentInstance, nextTick } from 'vue'
import { useCommonStore } from "@/stores/common.ts"
import { DragControls } from 'three/addons/controls/DragControls.js';
import { Card } from "@/views/game/Card.ts"
import { CARD_DICT } from "@/utils/dict/card.ts"
import { transPos } from "@/utils/common.ts"

// 引入threejs变量
const {proxy} = getCurrentInstance()
const THREE = proxy['THREE']
const scene = proxy['scene']
const camera = proxy['camera']
const renderer = proxy['renderer']
const TWEEN = proxy['TWEEN']

const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();

const commonStore = useCommonStore()

// 手牌区group
const handGroup = new THREE.Group()
handGroup.name = "p1_handGroup"
scene.add(handGroup)

const controls = new DragControls( handGroup.children, camera, renderer.domElement );

const _width = ref()

const init = () => {
  setHandPos()
}

// 设置手牌区位置
const setHandPos = () => {
  nextTick(() => {
    let plane = scene.getObjectByName("地面")
    let point1 = transPos(10, window.innerHeight - 10) // 手牌区起始位置的屏幕坐标
    let point2 = transPos(window.innerWidth * 0.65, window.innerHeight - 10) // 手牌区结束位置的屏幕坐标
    let x1 = 0 // 手牌区起始位置的世界x坐标
    let x2 = 0 // 手牌区结束位置的世界x坐标
    // 
    raycaster.setFromCamera( point1, camera );
    const intersects1 = raycaster.intersectObject( plane );
    if (intersects1.length > 0) {
      let point = intersects1[0].point
      // 由于卡牌几何体大小设置的是(1, 0.005, 1.4),所以我们对应进行偏移
      // handGroup.position.set(point.x, point.y, point.z)
      handGroup.position.set(point.x + 0.5, point.y, point.z - 0.7)
      x1 = handGroup.position.x
    }
    // 
    raycaster.setFromCamera( point2, camera );
    const intersects = raycaster.intersectObject( plane );
    if (intersects.length > 0) {
      let point = intersects[0].point
      x2 = point.x + 0.5
    }

    // 用绝对值相加得到手牌区长度
    _width.value = Math.abs(x1) + Math.abs(x2)

  })
}

// 添加手牌
const addHandCard = (obj: any, origin: any) => {
  let position = origin.position
  // console.log(666, deckGroupPos)
  let cardObj = CARD_DICT.find((v: any) => v.card_id === obj.card_id)
  if (cardObj) {
    let card = new Card(cardObj)
    let mesh = card.init()
    mesh.position.set(position.x, position.y, position.z)
    mesh.material.forEach((v: any) => {
      v.transparent = true
    })
    scene.add( mesh );
    updateCardPos(mesh)
  }

}

// 更新卡牌位置
const updateCardPos = (mesh: any) => {
  const tw = new TWEEN.Tween({
    x: mesh.position.x,
    y: mesh.position.y,
    z: mesh.position.z,
    opacity: 0.9,
    mesh
  })
  tw.to({
    x: handGroup.position.x,
    y: handGroup.position.y,
    z: handGroup.position.z,
    opacity: 0
  }, 200)
  tw.easing(TWEEN.Easing.Quadratic.Out)
  tw.onUpdate((obj: any) => {
    obj.mesh.position.set(obj.x, obj.y, obj.z)
    obj.mesh.material.forEach((v: any) => {
      v.opacity = obj.opacity
    })
  })
  tw.onComplete(function() {
    //动画结束:关闭允许透明,恢复到模型原来状态
    TWEEN.remove(tw)
    scene.remove( mesh );
    
    mesh.material.forEach((v: any) => {
      v.transparent = false
      v.opacity = 1
    })
    handGroup.add(mesh)
    // 计算叠放间距
    let space = ((_width.value - 1) / (handGroup.children.length - 1)) <= 1 ? (_width.value - 1) / (handGroup.children.length - 1) : 1
    handGroup.children.forEach((v: any, i: any) => {
      v.position.set(i * space, 0.005 * i, 0)
    })
  })

  tw.start();
}

controls.addEventListener( 'dragstart', function ( event: any ) {
  event.object.position.y += 0.04

} );

controls.addEventListener( 'drag', function ( event: any ) {
  event.object.position.y += 0.04
  // console.log(event)

} );

controls.addEventListener( 'dragend', function ( event: any ) {
  event.object.position.y -= 0.04
} );

defineExpose({
  init,
  addHandCard
})
</script>

<style lang="scss" scoped>
</style>

  • 17
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
使用Vue3和Three.js渲染室内地图有几个主要步骤。 首先,你需要准备好室内地图的数据。这可以是一个包含房间、墙壁、家具等元素的3D模型文件,如OBJ或GLTF格式。你还可以考虑将地图数据转换为JSON格式,并使用自定义脚本生成3D对象。 接下来,在Vue3中创建一个Three.js的场景(Scene)。你可以使用Vue Composition API来创建一个自定义的Vue组件,该组件将负责Three.js的初始化和场景的绘制。在这个组件中,你将使用Three.js的PerspectiveCamera来设置透视投影相机,并使用OrbitControls插件来实现用户交互控制。 然后,你需要加载地图数据并将其转换为Three.js的3D对象。你可以使用Three.js提供的加载器(Loader)来加载3D模型文件。完成加载后,你可以将模型添加到场景中,并设置其位置、旋转等属性。 最后,你可以根据需要添加光源、阴影效果、材质等来提高渲染效果。你可以通过创建Three.js的光源对象,如DirectionalLight或SpotLight,来模拟现实世界的光照。你可以使用Three.js的材质(Material)来定义模型的外观和反射属性,如颜色、贴图等。 通过以上步骤,你可以在Vue3应用中使用Three.js渲染室内地图。不过,请注意,使用Three.js进行复杂的渲染可能对性能有一定要求,你可能需要优化性能,如使用LOD(多级细分)模型、合并几何体等技术来提高性能并避免卡顿。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

清岚_lxn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值