threejs做3D游戏(12)—— 10分钟实现一个牵引器概述 在加载场景模型后,场景中还会有些需要不断修改位置、大小
概述
在加载场景模型后,场景中还会有些需要不断修改位置、大小、方向的物体,游戏世界中,各个互动物品的摆放一直是个令人头疼的问题,因为它很重要但无疑十分繁琐。
以前我曾经在《半条命》这款游戏中看到过一把名叫重力枪(零点能量场牵引器)的武器,它可以随意移动和放置游戏世界的物体,让我印象深刻。
所以我们今天就来实现一个牵引器,让它像重力枪一样可以对场景中的各种物体进行移动和放置。
功能分析
这个名为迁移器的组件,应该包含如下功能。
- 获取当前物体的详细信息,名称、位置、旋转、缩放。
- 能方便的在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
这个库用于控制场景中的数据变量,并提供实时反馈。
import { useControls } from "leva";
export default ()=>{
...
const [{位置, 旋转, 缩放 }, set] = useControls(
'迁引器',
() => ({
所选模型: object.name||"未选择",
位置: object.position,
旋转: object.rotation,
缩放: object.scale,
}),
);
...
}
这里为了方便用户的使用习惯,我将变量变更为了中文,这是考虑到国内用户使用时不习惯英文。如果你只是提供给开发者使用,最好使用英文,避免在某些平台系统的编码问题。
空间中迁移模型
在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={模式} />
}
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