@needle-tools/engine
Version:
Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.
159 lines • 9.05 kB
JavaScript
import { Color, Mesh, MeshBasicMaterial } from "three";
import { GameObject } from "../../../Component.js";
import { BaseUIComponent } from "../../../ui/BaseUIComponent.js";
import { Canvas } from "../../../ui/Canvas.js";
import { RenderMode } from "../../../ui/Canvas.js";
import { CanvasGroup } from "../../../ui/CanvasGroup.js";
import { RectTransform } from "../../../ui/RectTransform.js";
import { $shadowDomOwner } from "../../../ui/Symbols.js";
import { Text } from "../../../ui/Text.js";
import { USDObject } from "../ThreeUSDZExporter.js";
import { TextExtension } from "./USDZText.js";
export class USDZUIExtension {
get extensionName() {
return "tmui";
}
// TODO would probably be better to export each object instead of the entire Canvas
// so that we don't export them twice (once as regular hierarchy, once as part of Canvas export)
onExportObject(object, model, _context) {
const canvas = GameObject.getComponent(object, Canvas);
if (canvas && canvas.enabled && canvas.renderMode === RenderMode.WorldSpace) {
const textExt = new TextExtension();
const rt = GameObject.getComponent(object, RectTransform);
const canvasGroup = GameObject.getComponent(object, CanvasGroup);
// we have to do some temporary changes (enable UI component so that they're building
// their shadow hierarchy, then revert them back to the original state)
const revertActions = new Array();
let width = 100;
let height = 100;
if (rt) {
// Workaround: since UI components are only present in the shadow hierarchy when objects are on,
// we need to enable them temporarily to export them including their potential child components.
// For example, Text can have multiple child objects (e.g. rich text, panels, ...)
if (!GameObject.isActiveSelf(object)) {
const wasActive = GameObject.isActiveSelf(object);
GameObject.setActive(object, true);
rt.onEnable();
rt.updateTransform();
revertActions.push(() => {
rt.onDisable();
GameObject.setActive(object, wasActive);
});
}
object.traverse((child) => {
if (!GameObject.isActiveInHierarchy(child)) {
const wasActive = GameObject.isActiveSelf(child);
GameObject.setActive(child, true);
const baseUIComponent = GameObject.getComponent(child, BaseUIComponent);
if (baseUIComponent) {
baseUIComponent.onEnable();
revertActions.push(() => {
baseUIComponent.onDisable();
});
}
const rectTransform = GameObject.getComponent(child, RectTransform);
if (rectTransform) {
rectTransform.onEnable();
rectTransform.updateTransform();
// This method bypasses the checks for whether the object is enabled etc.
// so that we can ensure even a disabled object has the correct layout.
rectTransform["onApplyTransform"]();
revertActions.push(() => {
rectTransform.onDisable();
});
}
const text = GameObject.getComponent(child, Text);
if (text) {
text.onEnable();
revertActions.push(() => {
text.onDisable();
});
}
revertActions.push(() => {
GameObject.setActive(child, wasActive);
});
}
});
width = rt.width;
height = rt.height;
const shadowRootModel = USDObject.createEmpty();
const shadowComponent = rt.shadowComponent;
model.add(shadowRootModel);
if (shadowComponent) {
const mat = shadowComponent.matrix;
shadowRootModel.setMatrix(mat);
const usdObjectMap = new Map();
const opacityMap = new Map();
usdObjectMap.set(shadowComponent, shadowRootModel);
opacityMap.set(shadowComponent, canvasGroup ? canvasGroup.alpha : 1);
shadowComponent.traverse((child) => {
// console.log("traversing UI shadow components", shadowComponent.name + " ->" + child.name);
if (child === shadowComponent)
return;
const childModel = USDObject.createEmpty();
childModel.setMatrix(child.matrix);
const childParent = child.parent;
const isText = !!childParent && typeof childParent["textContent"] === "string" && childParent["textContent"].length > 0;
let hierarchyOpacity = opacityMap.get(childParent) || 1;
// TODO CanvasGroup doesn't render something but modifies opacity
// get CanvasGroup and modify alpha here
const canvasGroup = GameObject.getComponent(child, CanvasGroup);
if (canvasGroup)
hierarchyOpacity *= canvasGroup.alpha;
if (child instanceof Mesh && isText) {
// get shadoDomOwner so we can export Text from the text extension directly
const shadowDomOwner = child[$shadowDomOwner];
if (!shadowDomOwner)
console.error("Error when exporting UI: shadow component owner not found. This is likely a bug.", child);
else
textExt.exportText(shadowDomOwner.gameObject, childModel, _context);
}
if (child instanceof Mesh && !isText) {
// UI behaves weird: it seems it's always flipped right now,
// and three magically fixes it when rendering
// see https://github.com/mrdoob/three.js/pull/12720#issue-275923930
// https://github.com/mrdoob/three.js/issues/17361
// So we need to fix the winding order after applying the negative scale.
const clonedGeo = child.geometry.clone();
clonedGeo.scale(1, 1, -1);
this.flipWindingOrder(clonedGeo);
childModel.geometry = clonedGeo;
const color = new Color();
const ownOpacity = child.material.opacity;
color.copy(child.material.color);
// Calculate opacity from parent chain and own alpha
childModel.material = new MeshBasicMaterial({
color: color,
opacity: ownOpacity * hierarchyOpacity,
map: child.material.map,
transparent: true,
});
}
usdObjectMap.set(child, childModel);
opacityMap.set(child, hierarchyOpacity);
const parentUsdzObject = usdObjectMap.get(childParent);
if (!parentUsdzObject) {
console.error("Error when exporting UI: shadow component parent not found!", child, child.parent);
return;
}
parentUsdzObject.add(childModel);
});
}
}
// revert temporary changes that we did here
for (const revert of revertActions) {
revert();
}
}
}
flipWindingOrder(geometry) {
const index = geometry.index.array;
for (let i = 0, il = index.length / 3; i < il; i++) {
const x = index[i * 3];
index[i * 3] = index[i * 3 + 2];
index[i * 3 + 2] = x;
}
geometry.index.needsUpdate = true;
}
}
//# sourceMappingURL=USDZUI.js.map