声明式开发Threejs(二)

543 阅读3分钟

上一节我们基本环境已经搭建好了,并且目标已经确认,这节我们继续开发

MyCanvas

WebGLRenderer关联Canvas

我们通过传入的canvas来拿到DOM元素

  const webGLRendererConstructorParameters = computed<WebGLRendererParameters>(
    () => ({
      canvas: unrefElement(canvas),
    })
  );

把WebGLRenderer和Canvas关联上

  const renderer = shallowRef<any>(
    new WebGLRenderer(webGLRendererConstructorParameters.value)
  );

循环更新

我们创建一个循环拥有更新每帧

import { useRenderLoop } from "./useRenderLoop";
// 返回三个方法供我们使用,pause暂停、resume恢复、onLoop循环
const { pause, resume, onLoop } = useRenderLoop();

循环我们使用 useRafFn

参考useRafFn我们先在useRenderLoop.ts里定义好输出内容

import type { EventHookOn,Fn } from "@vueuse/core";
import { Clock } from 'three'

export interface RenderLoop {
    delta: number
    elapsed: number
    clock: Clock
  }

export interface UseRenderLoopReturn {
  onLoop: EventHookOn<RenderLoop>;
  pause: Fn;
  resume: Fn;
}

export const useRenderLoop = (): UseRenderLoopReturn => ({
  onLoop: onLoop.on,
  pause,
  resume,
});

对上面内容进行一下说明,为什么是这样,我们核心的做法就是通过createEventHook创建钩子,然后循环通过useRafFn循环触发并传入相关参数,跟我们以前 animation 其实是类似的

接下来我们书写一下,先创建事件钩子

// 创建事件钩子,触发trigger,on监听
const onLoop = createEventHook<RenderLoop>();

创建循环

// 创建时钟
const clock = new Clock();
let delta = 0;
let elapsed = 0;

// useRafFn 每秒都执行,pause 暂停,resume 继续
const { pause, resume } = useRafFn(
  () => {
    onLoop.trigger({ delta, elapsed, clock });
  },
  { immediate: false }
);

完整代码

import type { EventHookOn, Fn } from "@vueuse/core";
import { Clock } from "three";
import { createEventHook, useRafFn } from "@vueuse/core";

export interface RenderLoop {
  delta: number;
  elapsed: number;
  clock: Clock;
}

export interface UseRenderLoopReturn {
  onLoop: EventHookOn<RenderLoop>;
  pause: Fn;
  resume: Fn;
}
// 创建事件钩子,触发trigger,on监听
const onLoop = createEventHook<RenderLoop>();

// 创建时钟
const clock = new Clock();
let delta = 0;
let elapsed = 0;

// useRafFn 每秒都执行,pause 暂停,resume 继续
const { pause, resume } = useRafFn(
  () => {
    onLoop.trigger({ delta, elapsed, clock });
  },
  { immediate: false }
);

export const useRenderLoop = (): UseRenderLoopReturn => ({
  onLoop: onLoop.on,
  pause,
  resume,
});

image.png

在 useMyContextProvider.ts 验证下是否生效

  onLoop(() => {
    console.log("rendering");
  });
  
  resume();

打开控制台,可以看到已经生效了 image.png

在循环中我们需要实现下面内容

image.png

观察我们上面写的内容,我们已经有了 renderer、scene,目前我们还缺少 camera,虽然相机是通过标签传递进来的,但是我们可以做一个默认的相机,先来完事我们的内容,然后再处理标签传递过来的内容

这块内容需要我们构思一下:我们新增返回camera、registerCamera,当我们没有传递标签的时候,就registerCamera注册一下我们默认的

image.png

// src/components/MyCanvas.vue
  const { camera, registerCamera } = context.value;
  const addDefaultCamera = () => {
    const camera = new PerspectiveCamera(
      45,
      window.innerWidth / window.innerHeight,
      0.1,
      1000
    );
    camera.position.set(3, 3, 3);
    camera.lookAt(0, 0, 0);
    registerCamera(camera);
  };
  // 注册默认相机
  if (!camera.value) {
    addDefaultCamera();
  }

image.png

创建 useCamera.ts 来处理上述内容

import { ref, computed } from "vue";
import { Camera } from "three";

export const useCamera = () => {
  const cameras = ref<Camera[]>([]);
  const camera = computed<Camera | undefined>(() => cameras.value[0]);

  const registerCamera = (newCamera: Camera) => {
    cameras.value.push(newCamera);
  };

  return {
    camera,
    cameras,
    registerCamera,
  };
};

说明一下上面代码:当我们 registerCamera 我们相机的时候,cameras 长度发生变化,camera 是响应式的,然后在上面我们设置的循环中就可以使用到了

image.png

然后我们在 useMyContextProvider.ts 中添加我们的相机

import { useCamera } from "./useCamera";
const { camera, registerCamera } = useCamera();

在我们循环中就可以实现下面这个内容了

image.png

  onLoop(() => {
    if (camera.value) {
      renderer.value.render(scene, camera.value);
    }
  });

再上面我们的camera 有ts类型报错,我们修复一下

// src/components/myInterface.ts
import type { ComputedRef, MaybeRef } from "vue";
import type { Scene } from "three";
import { Camera } from "three";

export interface MyContext {
  scene: Scene;
  canvas: MaybeRef<HTMLCanvasElement>;
  camera: ComputedRef<Camera | undefined>
  registerCamera: (camera: Camera) => void;
}

image.png

现在观察我们的页面

image.png

看着和最开始不太一样了,最开始是白色,现在是黑色了,如果还不确定,我们可以修改一下背景色看看是否生效了

renderer.value.setClearColor("red"); // 设置背景色,这里是红色。

下面是完整代码

// src/components/useMyContextProvider.ts
import { computed, shallowRef } from "vue";
import { MyContext } from "./myInterface";
import type { WebGLRendererParameters, Scene } from "three";
import { unrefElement } from "@vueuse/core";
import { WebGLRenderer } from "three";
import { useRenderLoop } from "./useRenderLoop";
import type { MaybeRef } from "vue";
import { useCamera } from "./useCamera";

export function useMyContextProvider({
  canvas,
  scene,
}: {
  canvas: MaybeRef<HTMLCanvasElement>;
  scene: Scene;
}): MyContext {
  const webGLRendererConstructorParameters = computed<WebGLRendererParameters>(
    () => ({
      canvas: unrefElement(canvas),
    })
  );

  const renderer = shallowRef<any>(
    new WebGLRenderer(webGLRendererConstructorParameters.value)
  );
  renderer.value.setClearColor("red"); // 设置背景色,这里是红色。
  const { camera, registerCamera } = useCamera();

  const { pause, resume, onLoop } = useRenderLoop();

  onLoop(() => {
    if (camera.value) {
      renderer.value.render(scene, camera.value);
    }
  });

  resume();

  return {
    canvas,
    scene,
    camera,
    registerCamera,
  };
}

发现我们的效果生效了,说明已经成功了 image.png

接下来就是传入形状标签,生成对应的形状,相机可以放在最后了,因为我们已经实现了一个默认相机