声明式开发Threejs(三)

上一节我们把canvas和webgl关联起来了,并且设置了相机和验证了效果,这节我们来对传入的标签进行处理

js 复制代码
// src/App.vue
<script setup lang="ts">
import MyCanvas from "./components/MyCanvas.vue";
</script>

<template>
  <MyCanvas>
    <MyMesh>
      <MyConeGeometry />
      <MyMeshToonMaterial />
    </MyMesh>
  </MyCanvas>
</template>

<style scoped></style>

创建自定义渲染器

通过上面内容我们知道了,我们需要解析出来slots里面的内容,然后渲染出我们自己的内容,所以我们需要自定义渲染器,来实现这些内容

Vue 自身的 @vue/runtime-dom 也是利用这套 API 实现的。要想了解一个简单一些的实现,请参考 @vue/runtime-test,这是一个 Vue 自己做单元测试的私有包。

上面是vue官网的内容,我们参考这个来实现我们自己的内容

创建自定义渲染函数,通过宏defineSlots,接受slots,通过defineComponent创建组件,通过h创建虚拟DOM,通过render把我们的内容渲染到我们的场景上

js 复制代码
import { render } from "./renderer.ts";

const slots = defineSlots<{
  default: () => any;
}>();

const createInternalComponent = () =>
  defineComponent({
    setup() {
      return () => h(Fragment, null, slots?.default ? slots.default() : []);
    },
  });
  
const mountCustomRenderer = () => {
  const InternalComponent = createInternalComponent();

  render(h(InternalComponent), scene.value);
};

mountCustomRenderer();

在 renderer.ts 中我们只需要完成 render 并导就可以,

javascript 复制代码
import { createRenderer } from "vue";

const nodeOps = {

}

export const { render } = createRenderer(nodeOps);

vue 自定义渲染器

js 复制代码
function createRenderer<HostNode, HostElement>(
  options: RendererOptions<HostNode, HostElement>
): Renderer<HostElement>

interface Renderer<HostElement> {
  render: RootRenderFunction<HostElement>
  createApp: CreateAppFunction<HostElement>
}

interface RendererOptions<HostNode, HostElement> {
  patchProp(
    el: HostElement,
    key: string,
    prevValue: any,
    nextValue: any,
    // 其余部分在大多数自定义渲染器中是不会使用的
    isSVG?: boolean,
    prevChildren?: VNode<HostNode, HostElement>[],
    parentComponent?: ComponentInternalInstance | null,
    parentSuspense?: SuspenseBoundary | null,
    unmountChildren?: UnmountChildrenFn
  ): void
  insert(
    el: HostNode,
    parent: HostElement,
    anchor?: HostNode | null
  ): void
  remove(el: HostNode): void
  createElement(
    type: string,
    isSVG?: boolean,
    isCustomizedBuiltIn?: string,
    vnodeProps?: (VNodeProps & { [key: string]: any }) | null
  ): HostElement
  createText(text: string): HostNode
  createComment(text: string): HostNode
  setText(node: HostNode, text: string): void
  setElementText(node: HostElement, text: string): void
  parentNode(node: HostNode): HostElement | null
  nextSibling(node: HostNode): HostNode | null

  // 可选的, DOM 特有的
  querySelector?(selector: string): HostElement | null
  setScopeId?(el: HostElement, id: string): void
  cloneNode?(node: HostNode): HostNode
  insertStaticContent?(
    content: string,
    parent: HostElement,
    anchor: HostNode | null,
    isSVG: boolean
  ): [HostNode, HostNode]
}

然后我们根据内容来写入相应的属性方法,也就是nodeOps对象,也可以参考github上怎么写的

js 复制代码
import type { RendererOptions } from '@vue/runtime-core'

export const svgNS = 'http://www.w3.org/2000/svg'
export const mathmlNS = 'http://www.w3.org/1998/Math/MathML'

const doc = (typeof document !== 'undefined' ? document : null) as Document

const templateContainer = doc && /*#__PURE__*/ doc.createElement('template')

export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
  insert: (child, parent, anchor) => {
    parent.insertBefore(child, anchor || null)
  },

  remove: child => {
    const parent = child.parentNode
    if (parent) {
      parent.removeChild(child)
    }
  },

  createElement: (tag, namespace, is, props): Element => {
    const el =
      namespace === 'svg'
        ? doc.createElementNS(svgNS, tag)
        : namespace === 'mathml'
          ? doc.createElementNS(mathmlNS, tag)
          : doc.createElement(tag, is ? { is } : undefined)

    if (tag === 'select' && props && props.multiple != null) {
      ;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
    }

    return el
  },

  createText: text => doc.createTextNode(text),

  createComment: text => doc.createComment(text),

  setText: (node, text) => {
    node.nodeValue = text
  },

  setElementText: (el, text) => {
    el.textContent = text
  },

  parentNode: node => node.parentNode as Element | null,

  nextSibling: node => node.nextSibling,

  querySelector: selector => doc.querySelector(selector),

  setScopeId(el, id) {
    el.setAttribute(id, '')
  },

  // __UNSAFE__
  // Reason: innerHTML.
  // Static content here can only come from compiled templates.
  // As long as the user only uses trusted templates, this is safe.
  insertStaticContent(content, parent, anchor, namespace, start, end) {
    // <parent> before | first ... last | anchor </parent>
    const before = anchor ? anchor.previousSibling : parent.lastChild
    // #5308 can only take cached path if:
    // - has a single root node
    // - nextSibling info is still available
    if (start && (start === end || start.nextSibling)) {
      // cached
      while (true) {
        parent.insertBefore(start!.cloneNode(true), anchor)
        if (start === end || !(start = start!.nextSibling)) break
      }
    } else {
      // fresh insert
      templateContainer.innerHTML =
        namespace === 'svg'
          ? `<svg>${content}</svg>`
          : namespace === 'mathml'
            ? `<math>${content}</math>`
            : content

      const template = templateContainer.content
      if (namespace === 'svg' || namespace === 'mathml') {
        // remove outer svg/math wrapper
        const wrapper = template.firstChild!
        while (wrapper.firstChild) {
          template.appendChild(wrapper.firstChild)
        }
        template.removeChild(wrapper)
      }
      parent.insertBefore(template, anchor)
    }
    return [
      // first
      before ? before.nextSibling! : parent.firstChild!,
      // last
      anchor ? anchor.previousSibling! : parent.lastChild!,
    ]
  },
}

根据上面代码,我们了解了整体结构,和一个大概书写的内容,我们来实现我们自己的内容,先把结构书写出来

js 复制代码
const nodeOps: RendererOptions<MyObject, MyObject> = {
  createElement: () => noop("createElement"),
  insert: () => noop("insert"),
  remove: () => noop("remove"),
  patchProp: () => noop("patchProp"),
  parentNode(node) {
    return node?.parent || null;
  },
  createText: () => noop("createText"),
  createComment: () => noop("createComment"),

  setText: () => noop("setText"),

  setElementText: () => noop("setElementText"),
  nextSibling: () => noop("nextSibling"),

  querySelector: () => noop("querySelector"),

  setScopeId: () => noop("setScopeId"),
  cloneNode: () => noop("cloneNode"),

  insertStaticContent: () => noop("insertStaticContent"),
};

接着我们实现具体细节

细节

createElement

我们需要根据传入的tag解析出对应的threejs的内容,并创建实例,然后设置一个标识来区分是几何体还是材质

ini 复制代码
  createElement(tag, _isSVG, _anchor, props) {
    let instance;
    let name = tag.replace("My", "");
    const target = catalogue.value[name];
    instance = new target();
    if (props?.attach === undefined) {
      if (instance.isMaterial) {
        instance.attach = "material";
      } else if (instance.isBufferGeometry) {
        instance.attach = "geometry";
      }
    }
    return instance;
  }

insert

在这里我们知道了scene、mesh、material、geometry,只需要把mesh添加到scene里,material + geometry 组成mesh即可

ini 复制代码
  insert(child, parent) {
    if (parent && parent.isScene) {
      scene = parent as unknown as any;
    }
    const parentObject = parent || scene;
    if (child?.isObject3D && parentObject?.isObject3D) {
      parentObject.add(child);
    } else if (typeof child?.attach === "string") {
      if (parentObject) {
        parentObject[child.attach] = child;
      }
    }
  },

renderer.ts

完整代码

js 复制代码
import * as THREE from "three";
import { createRenderer } from "vue";
import type { RendererOptions } from "vue";
import { MyObject } from "./myInterface";
import { catalogue, extend } from "./catalogue";

function noop(fn: string): any {
  fn;
}

let scene: any;

const nodeOps: RendererOptions<MyObject, MyObject> = {
  createElement(tag, _isSVG, _anchor, props) {
    let instance;
    let name = tag.replace("My", "");
    const target = catalogue.value[name];
    instance = new target();
    if (props?.attach === undefined) {
      if (instance.isMaterial) {
        instance.attach = "material";
      } else if (instance.isBufferGeometry) {
        instance.attach = "geometry";
      }
    }
    return instance;
  },
  insert(child, parent) {
    if (parent && parent.isScene) {
      scene = parent as unknown as any;
    }
    const parentObject = parent || scene;
    if (child?.isObject3D && parentObject?.isObject3D) {
      parentObject.add(child);
    } else if (typeof child?.attach === "string") {
      if (parentObject) {
        parentObject[child.attach] = child;
      }
    }
  },
  patchProp(node, prop, _prevValue, nextValue) {},
  parentNode(node) {
    return node?.parent || null;
  },
  remove: () => noop("remove"),
  createText: () => noop("createText"),
  createComment: () => noop("createComment"),

  setText: () => noop("setText"),

  setElementText: () => noop("setElementText"),
  nextSibling: () => noop("nextSibling"),

  querySelector: () => noop("querySelector"),

  setScopeId: () => noop("setScopeId"),
  cloneNode: () => noop("cloneNode"),

  insertStaticContent: () => noop("insertStaticContent"),
};

export const { render } = createRenderer(nodeOps);

extend(THREE);

效果

可以看到我们的内容已经出来了,但是有些锯齿,这是我们最开始设置webgl配置的问题

js 复制代码
  const renderer = shallowRef<any>(
    new WebGLRenderer({
        ...webGLRendererConstructorParameters.value,
        antialias: true,
    })
  );

然后我们再看一下效果,已经有些改善了

后续我们继续优化和添加其它功能,目前我们的首要目标已经完成

相关推荐
学不会•2 小时前
css数据不固定情况下,循环加不同背景颜色
前端·javascript·html
活宝小娜4 小时前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点4 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow4 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
我开心就好o4 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app
开心工作室_kaic5 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
刚刚好ā5 小时前
js作用域超全介绍--全局作用域、局部作用、块级作用域
前端·javascript·vue.js·vue
沉默璇年7 小时前
react中useMemo的使用场景
前端·react.js·前端框架
yqcoder7 小时前
reactflow 中 useNodesState 模块作用
开发语言·前端·javascript
2401_882727577 小时前
BY组态-低代码web可视化组件
前端·后端·物联网·低代码·数学建模·前端框架