上一节我们把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);
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,
})
);
然后我们再看一下效果,已经有些改善了
后续我们继续优化和添加其它功能,目前我们的首要目标已经完成