Forráskód Böngészése

feat: 构建第一人称视角

bill 1 hónapja
szülő
commit
c69f251abc

+ 86 - 81
src/core/renderer-three/hook/use-controls.ts

@@ -7,31 +7,51 @@ import {
 } from "./use-stage";
 import { OrbitControls } from "three/examples/jsm/Addons.js";
 import { listener } from "@/utils/event";
-import { PerspectiveCamera, Vector3 } from "three";
-import { frameEebounce, frameInterval, mergeFuns } from "@/utils/shared";
+import {  Vector3 } from "three";
+import { mergeFuns } from "@/utils/shared";
 import { useMouseEventRegister } from "./use-event";
 import { subgroupName } from "../container";
 import { useCameraAnimation } from "./use-animation";
-import { getDownKeys } from "@/core/hook/use-global-vars";
-import { getMoveDirectrionByKeys } from "./use-downkeys";
-
-const getModelControls = (
-  container: HTMLDivElement,
-  camera: PerspectiveCamera,
-  render: () => void
-) => {
-  const controls = new OrbitControls(camera, container);
+import {
+  getMoveDirectrionByKeys,
+  useFigureMoveCollision,
+} from "./use-move";
+
+const useModelControls = () => {
+  const container = useContainer();
+  const camera = useCamera();
+  const _render = useRender();
+  const render = () => {
+    camera.bus.emit("change");
+    _render();
+  };
+
+  const controls = new OrbitControls(camera);
   controls.target.set(0, 5, 0);
-  controls.update();
+  controls.enabled = false;
 
   const unListener = listener(controls as any, "change", render);
   let prevOrigin: Vector3 | null = null;
   let prevDire: Vector3 | null = null;
 
+  watch(
+    container,
+    (container, _, onCleanup) => {
+      if (container) {
+        controls.domElement = container;
+        controls.connect();
+        onCleanup(() => {
+          controls.disconnect();
+        });
+      }
+    },
+    { immediate: true }
+  );
+
   return {
     controls,
     onDestory() {
-      controls.dispose();
+      controls.domElement && controls.dispose();
       unListener();
     },
     syncCamera() {
@@ -54,68 +74,66 @@ const getModelControls = (
   };
 };
 
-const getRoamingControls = (
-  container: HTMLDivElement,
-  camera: PerspectiveCamera,
-  render: () => void
-) => {
-  const initDirection = camera.getWorldDirection(new Vector3());
-  const controls = new OrbitControls(camera, container);
-  controls.rotateSpeed = -0.3;
-  controls.enableZoom = false;
-
+const roamingEysHeight = 120;
+const useRoamingControls = () => {
+  const container = useContainer();
+  const camera = useCamera();
+  const _render = useRender();
+  const render = () => {
+    camera.bus.emit("change");
+    _render();
+  };
+  const enabled = ref(false);
   const syncCamera = (direction = camera.getWorldDirection(new Vector3())) => {
     controls.target.copy(direction.add(camera.position));
     controls.update();
   };
-  syncCamera(initDirection);
-  render();
-  let prevOrigin: Vector3 | null = null;
-  let prevDire: Vector3 | null = null;
-
-  const unListener = listener(controls as any, "change", render);
+  
   const { direction, onDestory: onDownDestory } = getMoveDirectrionByKeys();
-  const stopWatchDirection = watch(direction, (dire, _, onCleanup) => {
-    if (!controls.enabled || !dire) return;
-    onCleanup(frameInterval(() => {
-      const cameraDire = camera.getWorldDirection(new Vector3())
-      cameraDire.y = 0
-      cameraDire.normalize()
-
-      const rightDire = new Vector3(0, 1, 0).cross(cameraDire)
-      const moveDire = new Vector3()
-      if (dire.y) {
-        moveDire.addScaledVector(cameraDire, dire.y)
-      }
+  const move = useFigureMoveCollision(camera, direction, syncCamera);
+  move.pause()
 
-      if (dire.x) {
-        moveDire.addScaledVector(rightDire, -dire.x)
-      }
+  const controls = new OrbitControls(camera);
+  controls.rotateSpeed = -0.3;
+  controls.enableZoom = false;
 
-      if (moveDire.length() > 0) {
-        camera.position.addScaledVector(moveDire.normalize(), 2)
-        syncCamera();
-        render();
-      }
-    }));
-  });
+  let prevOrigin: Vector3 | null = null;
+  let prevDire: Vector3 | null = null;
 
   return {
     controls,
-    onDestory() {
-      controls.dispose();
-      unListener();
-      onDownDestory();
-      stopWatchDirection();
-    },
+    onDestory: mergeFuns(
+      watchEffect(() => (controls.enabled = enabled.value)),
+      watch(
+        container,
+        (container, _, onCleanup) => {
+          if (container) {
+            controls.domElement = container;
+            controls.connect();
+            onCleanup(() => {
+              controls.disconnect();
+            });
+          }
+        },
+        { immediate: true }
+      ),
+      move.destory,
+      () => {
+        controls.domElement && controls.dispose();
+      },
+      listener(controls as any, "change", render),
+      onDownDestory
+    ),
     syncCamera,
     disable() {
       prevOrigin = camera.position.clone();
       prevDire = camera.getWorldDirection(new Vector3());
       controls.enabled = false;
+      move.pause()
     },
     enable() {
       controls.enabled = true;
+      move.continue()
       render();
     },
     get current() {
@@ -128,41 +146,29 @@ const getRoamingControls = (
 };
 
 const controlsFactory = {
-  model: getModelControls,
-  roaming: getRoamingControls,
+  model: useModelControls,
+  roaming: useRoamingControls,
 };
 export type ControlsType = keyof typeof controlsFactory;
 export type Controls = ReturnType<(typeof controlsFactory)[ControlsType]>;
 
 export const useControls = installThreeGlobalVar(() => {
   const container = useContainer();
-  const camera = useCamera();
-  const render = useRender();
   const type = ref<ControlsType>();
   const controls = ref<Controls>();
-  let controlsMap: Partial<Record<ControlsType, Controls>> = {};
+  const controlsMap = {} as Record<ControlsType, Controls>;
+
+  for (const [type, factory] of Object.entries(controlsFactory)) {
+    controlsMap[type as ControlsType] = factory();
+  }
 
-  const ctRender = () => {
-    camera.bus.emit("change");
-    render();
-  };
   const stopWatch = watch(
     [container, type],
     ([container, type], _, onCleanup) => {
       if (!(type && container)) return;
 
-      let ct: Controls;
-      if (type in controlsMap) {
-        ct = controlsMap[type]!;
-        ct.enable();
-      } else {
-        ct = controlsMap[type] = controlsFactory[type](
-          container,
-          camera,
-          ctRender
-        );
-      }
-
+      const ct = controlsMap[type];
+      ct.enable();
       controls.value = ct;
       onCleanup(() => {
         ct.disable();
@@ -179,7 +185,6 @@ export const useControls = installThreeGlobalVar(() => {
       for (const controls of Object.values(controlsMap)) {
         controls.onDestory();
       }
-      controlsMap = {};
     },
   };
 });
@@ -191,7 +196,7 @@ export const useFlyRoaming = installThreeGlobalVar(() => {
 
   return async (point: Vector3) => {
     type.value = undefined;
-    const position = point.clone().add({ x: 0, y: 100, z: 0 });
+    const position = point.clone().add({ x: 0, y: roamingEysHeight, z: 0 });
     const direction = camera.getWorldDirection(new Vector3());
     const target = position.clone().add(direction);
     await cameraAnimation(position, target);
@@ -228,7 +233,6 @@ export const installAutoSwitchControls = installThreeGlobalVar(() => {
   const flyRoaming = useFlyRoaming();
   const flyModel = useFlyModel();
 
-  type.value = "model";
   const onDestroy = mergeFuns(
     listener(document.documentElement, "keydown", (ev) => {
       if (ev.key === "Escape" && type.value !== "model") {
@@ -237,6 +241,7 @@ export const installAutoSwitchControls = installThreeGlobalVar(() => {
     }),
     mouseRegister(subgroupName, "dblclick", ({ point }) => flyRoaming(point))
   );
+  type.value = "model";
 
   return { onDestroy };
 });

+ 0 - 36
src/core/renderer-three/hook/use-downkeys.ts

@@ -1,36 +0,0 @@
-import { getDownKeys } from "@/core/hook/use-global-vars";
-import { mergeFuns } from "@/utils/shared";
-import { Vector3 } from "three";
-import { shallowRef, watch } from "vue";
-
-
-export const getMoveDirectrionByKeys = () => {
-  const { var: keys, onDestroy: onDownDestory } = getDownKeys();
-  const direction = shallowRef<Vector3>()
-
-  const stopWatch = watch(keys, (keys) => {
-    let dire = new Vector3()
-    if (keys.has('a')) {
-      dire.setX(-1)
-    }
-    if (keys.has('d')) {
-      dire.setX(1)
-    }
-    if (keys.has('w')) {
-      dire.setY(1)
-    }
-    if (keys.has('s')) {
-      dire.setY(-1)
-    }
-    if (dire.x || dire.y) {
-      direction.value = dire.normalize()
-    } else {
-      direction.value = undefined
-    }
-  }, { deep: true });
-
-  return {
-    direction,
-    onDestory: mergeFuns(stopWatch, onDownDestory)
-  }
-}

+ 3 - 1
src/core/renderer-three/hook/use-getter.ts

@@ -7,8 +7,10 @@ export const useGetIntersectObject = () => {
   const scene = useScene();
   const raycaster = useRaycaster();
 
-  return (origin: Vector3, direction: Vector3) => {
+  return (origin: Vector3, direction: Vector3, far = 10000, near = 0) => {
     raycaster.set(origin, direction);
+    raycaster.far = far
+    raycaster.near = near
     return raycaster.intersectObject(scene);
   };
 };

+ 200 - 0
src/core/renderer-three/hook/use-move.ts

@@ -0,0 +1,200 @@
+import { getDownKeys } from "@/core/hook/use-global-vars";
+import { frameInterval, mergeFuns } from "@/utils/shared";
+import { Object3D, Vector2, Vector3 } from "three";
+import { ref, Ref, ShallowRef, shallowRef, watch } from "vue";
+import { useGetIntersectObject, useRaycaster } from "./use-getter";
+
+export const getMoveDirectrionByKeys = () => {
+  const { var: keys, onDestroy: onDownDestory } = getDownKeys();
+  const direction = shallowRef<Vector3>();
+
+  const stopWatch = watch(
+    keys,
+    (keys) => {
+      let dire = new Vector3();
+      if (keys.has("a")) {
+        dire.setX(-1);
+      }
+      if (keys.has("d")) {
+        dire.setX(1);
+      }
+      if (keys.has("w")) {
+        dire.setZ(1);
+      }
+      if (keys.has("s")) {
+        dire.setZ(-1);
+      }
+      if (keys.has(" ")) {
+        dire.setY(1);
+      }
+      if (dire.x || dire.y || dire.z) {
+        direction.value = dire.normalize();
+      } else {
+        direction.value = undefined;
+      }
+    },
+    { deep: true }
+  );
+
+  return {
+    direction,
+    onDestory: mergeFuns(stopWatch, onDownDestory),
+  };
+};
+
+export const jumpFactory = (initY = 0, endY = initY, jumpForce = 5) => {
+  const gravity = -0.3; // 重力加速度
+  let velocityY = jumpForce; // 垂直速度
+  let y = initY;
+
+  const update = () => {
+    // 5. 应用重力
+    velocityY += gravity;
+    y += velocityY + Math.sin(Date.now() * 0.01) * 0.01;
+
+    if (y <= endY) {
+      y = endY;
+      velocityY = 0;
+      return { height: y, final: true, velocityY };
+    } else {
+      return { height: y, final: false, velocityY };
+    }
+  };
+
+  return update;
+};
+
+const up = new Vector3(0, 1, 0);
+export const useFigureMoveCollision = (
+  figure: Object3D,
+  dire: Ref<Vector3 | undefined>,
+  render: () => void,
+  scale = 2.5,
+  height = 120,
+  srang = 10
+) => {
+  const getIntersect = useGetIntersectObject();
+  const pause = ref(false);
+  const rangeCount = 10;
+  const offset = 10;
+  const rangeHeight: number[] = [];
+  for (let i = 0; i < rangeCount; i++) {
+    rangeHeight.push((i / (rangeCount - 1)) * (height - offset) + offset);
+  }
+
+  const getJumpPoint = (position = figure.position) => {
+    const objects = getIntersect(position, jumpRayDire, height);
+    return objects.length > 0 ? objects[0].point : 0;
+  };
+
+  const jumpRayDire = new Vector3(0, -1, 0);
+  const jump = (velocityY = 5) => {
+    const updateJump = jumpFactory(figure.position.y, height, velocityY);
+    return () => {
+      const j = updateJump();
+      figure.position.setY(j.height);
+      if (j.velocityY < 0) {
+        const p = getJumpPoint();
+        if (p) {
+          figure.position.setY(height + p.y);
+          return true;
+        }
+      }
+      return j.final;
+    };
+  };
+
+  const checkRange = (dire: Vector3) => {
+    const p = figure.position;
+    for (const offset of rangeHeight) {
+      const objects = getIntersect(
+        new Vector3(p.x, p.y - height + offset, p.z),
+        dire,
+        srang
+      );
+      if (objects.length) {
+        return false;
+      }
+    }
+    return true;
+  };
+
+  const move = (moveDire: Vector2) => {
+    const figureDire = figure.getWorldDirection(new Vector3());
+    figureDire.setY(0).normalize();
+
+    const rightDire = up.clone().cross(figureDire);
+    const finalDire = new Vector3();
+
+    if (moveDire.y) {
+      const xMoveDire = figureDire.clone().multiplyScalar(moveDire.y);
+      checkRange(xMoveDire) && finalDire.add(xMoveDire);
+    }
+
+    if (moveDire.x) {
+      const yMoveDire = rightDire.multiplyScalar(-moveDire.x);
+      checkRange(yMoveDire) && finalDire.add(yMoveDire);
+    }
+
+    if (figureDire.length() > 0) {
+      figure.position.addScaledVector(finalDire.normalize(), scale);
+      return true;
+    } else {
+      return false;
+    }
+  };
+
+  let stopJump: (() => void) | null = null;
+  const startJump = (velocityY = 5) => {
+    const update = jump(velocityY);
+    stopJump = frameInterval(() => {
+      if (update()) {
+        stopJump && stopJump();
+        stopJump = null;
+      }
+      render();
+    });
+  };
+
+  let stopMove: (() => void) | null = null;
+  const stopFMove = watch([dire, pause], ([dire, pause]) => {
+    if (stopMove) {
+      stopMove();
+      stopMove = null;
+    }
+
+    if (!dire || pause) {
+      return;
+    }
+
+    if (dire.y && !stopJump) {
+      startJump();
+    }
+
+    stopMove = frameInterval(() => {
+      move(new Vector2(dire.x, dire.z));
+      render();
+      if (!stopJump) {
+        // console.log(getJumpPoint());
+      }
+    });
+  });
+
+  return {
+    destory: () => {
+      stopFMove();
+      stopJump && stopJump();
+      stopMove && stopMove();
+    },
+    pause: () => {
+      pause.value = true
+      stopJump && stopJump();
+      stopMove && stopMove();
+      stopJump = null
+      stopMove = null
+    },
+    continue() {
+      pause.value = false
+    }
+  };
+};

+ 6 - 5
src/core/renderer-three/hook/use-stage.ts

@@ -6,10 +6,11 @@ import {
   stackVar,
 } from "@/core/hook/use-global-vars";
 import { listener } from "@/utils/event";
-import { frameEebounce } from "@/utils/shared";
+import { frameThrottling } from "@/utils/shared";
 import mitt, { Emitter } from "mitt";
 import {
   Color,
+  Fog,
   Object3D,
   PCFSoftShadowMap,
   PerspectiveCamera,
@@ -114,8 +115,7 @@ export const useCamera = installThreeGlobalVar(() => {
     0.1,
     500000
   ) as PerspectiveCamera & { bus: Emitter<{ change: void }> };
-  camera.position.set(0, 2, 0);
-  camera.position.multiplyScalar(800);
+  camera.position.set(1, 1, 1).multiplyScalar(1000);
   camera.lookAt(new Vector3(0, 1, 0));
   camera.bus = mitt();
 
@@ -131,6 +131,7 @@ export const useCamera = installThreeGlobalVar(() => {
 export const useScene = installThreeGlobalVar(() => {
   const scene = new Scene();
   scene.background = new Color("white");
+  scene.fog = new Fog( 0xffffff, 0, 7500 );
   return scene;
 });
 
@@ -139,10 +140,10 @@ export const useRender = installThreeGlobalVar(() => {
   const renderer = useRenderer();
   const scene = useScene();
   const camera = useCamera();
-  const render = frameEebounce(() => {
+  const render = frameThrottling(() => {
     loop.trigger();
     renderer.render(scene, camera);
-  }, true);
+  });
 
   renderer.bus.on("sizeChange", render);
   return render;

+ 17 - 2
src/utils/shared.ts

@@ -136,18 +136,33 @@ export const debounce = <T extends (...args: any) => any>(
 };
 
 // 防抖
-export const frameEebounce = <T extends (...args: any) => any>(fn: T, enforce = false) => {
+export const frameEebounce = <T extends (...args: any) => any>(fn: T) => {
   let count = 0;
   return function (...args: Parameters<T>) {
     let current = ++count;
     requestAnimationFrame(() => {
-      if (enforce || current === count) {
+      if (current === count) {
         fn.apply(null, args);
       }
     });
   };
 };
 
+// 节流
+export const frameThrottling = <T extends (...args: any) => any>(fn: T) => {
+  let iswait = false
+  return function (...args: Parameters<T>) {
+    if (iswait) {
+      return;
+    }
+    iswait = true
+    requestAnimationFrame(() => {
+      iswait = false
+      fn.apply(null, args);
+    });
+  };
+};
+
 export const frameInterval = (fn: () => void) => {
   let isStop = false
   const animation = () => {