likes
comments
collection
share

threejs做3D游戏(12)—— 10分钟实现一个牵引器概述 在加载场景模型后,场景中还会有些需要不断修改位置、大小

作者站长头像
站长
· 阅读数 52

概述

在加载场景模型后,场景中还会有些需要不断修改位置、大小、方向的物体,游戏世界中,各个互动物品的摆放一直是个令人头疼的问题,因为它很重要但无疑十分繁琐。

以前我曾经在《半条命》这款游戏中看到过一把名叫重力枪(零点能量场牵引器)的武器,它可以随意移动和放置游戏世界的物体,让我印象深刻。

threejs做3D游戏(12)—— 10分钟实现一个牵引器概述 在加载场景模型后,场景中还会有些需要不断修改位置、大小

所以我们今天就来实现一个牵引器,让它像重力枪一样可以对场景中的各种物体进行移动和放置。

功能分析

这个名为迁移器的组件,应该包含如下功能。

  1. 获取当前物体的详细信息,名称、位置、旋转、缩放。
  2. 能方便的在3维空间中,直观的修改和观察这些信息,而非是仅仅修改数据。
  3. 修改这些的信息,物体会直接响应

获取当前物体

在react-three-fiber中,直接在模型上定义点击事件,在点击事件中获取到当前模型即可,可以使用useState或zustand的useStore钩子去存放模型对象,再从别处读取。 代码如下:

export defualt ()=>{
    const [target,setTarget] =useState<Object3D | null>(null)
  
    function onClick(e: ThreeEvent<MouseEvent>) {
      e.stopPropagation();
      setTarget(e.eventObject);
    }

    return <Gltf src={path} onClick={onClick} />
}

创建gui界面显示信息

这里你可以使用drei中的Html组件构建一个视图浮在模型所在的cnavas视图层级之上,作为gui界面(用户输入)。

我这里为方便直接使用 leva 库。

npm i leva

这个库用于控制场景中的数据变量,并提供实时反馈。

threejs做3D游戏(12)—— 10分钟实现一个牵引器概述 在加载场景模型后,场景中还会有些需要不断修改位置、大小

import { useControls } from "leva";

export default ()=>{
    ...
  const [{位置, 旋转, 缩放 }, set] = useControls(
    '迁引器',
    () => ({
      所选模型: object.name||"未选择",
      位置: object.position,
      旋转: object.rotation,
      缩放: object.scale,
    }),
  );
   ...
 }

threejs做3D游戏(12)—— 10分钟实现一个牵引器概述 在加载场景模型后,场景中还会有些需要不断修改位置、大小

这里为了方便用户的使用习惯,我将变量变更为了中文,这是考虑到国内用户使用时不习惯英文。如果你只是提供给开发者使用,最好使用英文,避免在某些平台系统的编码问题。

空间中迁移模型

在threejs中有个很好用的控制器,平移控制器TransformControls,在react-three中它得到了完全的继承和扩展。我们可以直接使用这个组件TransformControls

<TransformControls object={mesh} mode="translate" /> 
<mesh ref={mesh} />

如上我们使用到2个参数,mode是当前控制器的模式,其他还有"rotate""scale",object是当前控制的模型。

我们把模式控制加入到leva的控制中,方便在gui界面上切换模式。

...
export default function Tractor(object){
  const [{ 模式, 位置, 旋转, 缩放 }, set] = useControls(
    '迁引器',
    () => ({
      所选模型: object.name||"未选择",
      模式: {
        value: "移动",
        options: ["移动", "旋转", "缩放"],
      },
      位置: object.position,
      旋转: object.rotation,
      缩放: object.scale,
    }),
  );
 ...
 
 return <TransformControls object={object} mode={模式} /> 
}

threejs做3D游戏(12)—— 10分钟实现一个牵引器概述 在加载场景模型后,场景中还会有些需要不断修改位置、大小

gui界面数据与模型数据的双向绑定

从模型到界面的数据流动

理论上从模型到界面的数据流动应该是很自然的,但是leva组件内部的数据是独立的,它不能监听到object数据的改变,所以我们需要使用其提供的set函数来手动改变leva组件内部的数据,让视图知道数据更新了。

提供一个更新gui界面的函数,在每次模型变动时更新gui界面。

...
  const [{ 模式, 位置, 旋转, 缩放 }, set] = useControls(...);
  
  function updatePanel(){
     set({
      所选模型: name,
      位置: position,
      旋转: rotation,
      缩放: scale,
    });
   }
   
   return  <TransformControls
        mode={{
              移动: "translate",
              旋转: "rotate",
              缩放: "scale",
            }[模式]}
        object={object}
        onObjectChange={() => {
          updatePanel();
        }}
      />
   ...

此外leva不接受Vector3数据类型,所以我们需要做一下数据转换,把Vector3类型变为Vector3Tuple类型。

 function updatePanel(){
     set({
      所选模型: name,
      位置: vectorToTuple(object.position),
      旋转: vectorToTuple(object.rotation),
      缩放: vectorToTuple(object.scale),
    });
   }

function vectorToTuple(vector) {
   return [vector.x,vector.y,vector.z]
}

从界面到模型的数据流动

这一步比较简单,我们只要监听GUI的数据改变,再对应修改模型的数据

  const [{ 位置, 旋转, 缩放 }, set] = useControls(...);
  
  useEffect(() => {
    object.position.set(...位置);
    object.rotation.set(...旋转);
    object.scale.set(...缩放);
  }, [位置, 旋转, 缩放]);

配置文件载入

这样就完成了GUI面板与模型数据的双向绑定, 在这里修改获得的空间数据,最后需要填写到配置文件中,以便场景中的模型应用它。

完整代码

const MODE = {
  移动: "translate",
  旋转: "rotate",
  缩放: "scale",
};
export function Tractor({ object }: { object: Object3D }) {
  const [{ 模式, 位置, 旋转, 缩放 }, set] = useControls("迁引器", () => ({
    所选模型: object.name || "未选择",
    模式: {
      value: "移动",
      options: ["移动", "旋转", "缩放"],
    },
    位置: vectorToTuple(object.position),
    旋转: vectorToTuple(object.rotation),
    缩放: vectorToTuple(object.scale),
  }));

  useEffect(() => {
    updatePanel();
  }, [object]);

  useEffect(() => {
    object.position.set(...位置);
    object.rotation.set(...旋转);
    object.scale.set(...缩放);
  }, [位置, 旋转, 缩放]);

  function updatePanel() {
    set({
      所选模型: object.name,
      位置: vectorToTuple(object.position),
      旋转: vectorToTuple(object.rotation),
      缩放: vectorToTuple(object.scale),
    });
  }

  function vectorToTuple(vector: {x: number; y: number;z: number;}): [number, number, number] {
    return [vector.x, vector.y, vector.z];
  }

  return (
    <TransformControls
      mode={MODE[模式 as keyof typeof MODE] as "translate" | "rotate" | "scale"}
      object={object}
      onObjectChange={() => {
        updatePanel();
      }}
    />
  );
}

完善功能

  • 考虑会误操作的可能,为了方便添加一个撤销的功能。
  • 在迁引过程中,以防将来我们需要做些什么,所以对外暴露一个迁引时执行的函数。

撤销

将相应模式下的数据还原到牵引器使用前,为此要保存一份原始数据。

我们默认模型加载进牵引器时的数据为元数据,同时将原始数据作为牵引器的暴露参数,如果从外部传递了原始数据,就优先使用原始数据,否则使用元数据。

export default function Tractor({name="牵引器",object,metaData}){
...
  const data = useMemo(
   () => ({
     position: object.position,
     rotation: object.rotation,
     scale: object.scale.y,
     ...metaData,
   }),
   [object, metaData]
 );
 
 const [{ 模式, 撤销,... }, set] = useControls(
   name,
   () => ({
     ...
     撤销: false,
   }),
 );
 
   useEffect(() => {
   撤销 && resetObj();
 }, [撤销]);

 ...
}

分别撤销对应模式下的操作,恢复到之前的状态。

 function resetObj() {
   const { position, rotation, scale } = data;
   switch (模式) {
     case "移动":
       object.position.set(...position);
       set({ 位置: position });
       break;
     case "旋转":
       object.rotation.set(...rotation);
       set({ 旋转: rotation });
       break;
     case "缩放":
       object.scale.setScalar(scale);
       set({ 缩放: scale });
       break;
     default:
       console.error("无法重置未知模式!");
       break;
   }
   set({
     撤销: false,
   });
 }

牵引函数

类似平移控制器,我们将牵引器的牵引函数暴露出去,以便使用

export default function Tractor({
 name = "牵引器",
 metaData,
 object,
 track,
}){
...

useEffect(() => {
   track && track();
 }, [位置, 旋转, 缩放, 撤销]);

return <TransformControls
       mode={MODE[模式]}
       object={object}
       onObjectChange={() => {
         track && track();
         updatePanel();
       }}
     />

优化后的代码

/**
 * 牵引调试器
 *
 * 用于在调试面板上显示模型的牵引调试信息。
 * @param track - 模型被牵引时调用
 */
export default function Tractor({
  name = "牵引器",
  object,
  metaData,
  track,
}: {
  name?: string; // gui面板名
  metaData?: Partial<ObjectProps>; // 模型原始数据
  object: Object3D; // 受控模型
  track?: () => void; // 模型被牵引时调用的函数
}) {
  const data = useMemo(
    () => ({
      position: vectorToTuple(object.position),
      rotation: vectorToTuple(object.rotation),
      scale: vectorToTuple(object.scale),
      ...metaData,
    }),
    [object, metaData]
  );

  const [{ 模式, 位置, 旋转, 缩放, 撤销 }, set] = useControls("迁引器", () => ({
    所选模型: object.name || "未选择",
    模式: {
      value: "移动",
      options: ["移动", "旋转", "缩放"],
    },
    位置: data.position,
    旋转: data.rotation,
    缩放: data.scale,
    撤销: false,
  }));

  useEffect(() => {
    updatePanel();
  }, [object]);

  useEffect(() => {
    object.position.set(...位置);
    object.rotation.set(...旋转);
    object.scale.set(...缩放);
    track && track();
  }, [位置, 旋转, 缩放]);

  useEffect(() => {
    撤销 && resetObj();
    track && track();
  }, [撤销]);

  function resetObj() {
    const { position, rotation, scale } = data;
    switch (模式) {
      case "移动":
        object.position.set(...position);
        set({ 位置: position });
        break;
      case "旋转":
        object.rotation.set(...rotation);
        set({ 旋转: rotation });
        break;
      case "缩放":
        object.scale.set(...scale);
        set({ 缩放: scale });
        break;
      default:
        console.error("无法重置未知模式!");
        break;
    }
    set({
      撤销: false,
    });
  }

  function updatePanel() {
    set({
      所选模型: name,
      位置: vectorToTuple(object.position),
      旋转: vectorToTuple(object.rotation),
      缩放: vectorToTuple(object.scale),
    });
  }

  function vectorToTuple(vector: {
    x: number;
    y: number;
    z: number;
  }): [number,number,number] {
    return [vector.x, vector.y, vector.z];
  }
  return (
    <TransformControls
      mode={ {
          移动: "translate",
          旋转: "rotate",
          缩放: "scale",
        };[模式]}
      object={object}
      onObjectChange={() => {
        track && track();
        updatePanel();
      }}
    />
  );
}

使用

export defualt ()=>{
    const [target,setTarget] =useState<Object3D | null>(null)
    
    function onClick(e: ThreeEvent<MouseEvent>) {
      e.stopPropagation();
      setTarget(e.eventObject);
    }

    return <group>
        <Gltf src={path} onClick={onClick} />
        {target && <Tractor object={target} />}
     </group>
}

在线体验

牵引器在线体验

转载自:https://juejin.cn/post/7412545166539145225
评论
请登录