Просмотр исходного кода

Implement Material configurator and switch node base plugins, grid ui plugins and examples.

master
Palash Bansal 2 лет назад
Родитель
Сommit
115e68d2bd
Аккаунт пользователя с таким Email не найден

+ 6
- 1
examples/index.html Просмотреть файл

@@ -349,6 +349,12 @@
<li><a href="./simplify-modifier-plugin/">Simplify Modifier Plugin </a></li>
<li><a href="./meshopt-simplify-modifier-plugin/">MeshOpt Simplify Modifier Plugin </a></li>
</ul>
<h2 class="category">Configurators</h2>
<ul>
<li><a href="./material-configurator-plugin/">Material Configurator Plugin </a></li>
<li><a href="./switch-node-plugin/">Switch Node (Object Configurator) Plugin </a></li>
<li><a href="./gltf-khr-material-variants-plugin/">glTF KHR Material Variants Plugin </a></li>
</ul>
<h2 class="category">Import</h2>
<ul>
<li><a href="./fbx-load/">FBX Load </a></li>
@@ -415,7 +421,6 @@
<li><a href="./object3d-generator-plugin/">Object3D Generator Plugin <br/>(Lights, Cameras)</a></li>
<li><a href="./geometry-generator-plugin/">Geometry Generator Plugin </a></li>
<li><a href="./object3d-widgets-plugin/">Object3D Widgets Plugin <br/>(Lights, Cameras)</a></li>
<li><a href="./gltf-khr-material-variants-plugin/">glTF KHR Material Variants Plugin </a></li>
<li><a href="./geometry-uv-preview/">Geometry UV Preview Plugin </a></li>
<li><a href="./parallel-asset-import/">Parallel Asset Import </a></li>
<li><a href="./obj-to-glb/">Convert OBJ to GLB </a></li>

+ 37
- 0
examples/material-configurator-plugin/index.html Просмотреть файл

@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Material Configurator Plugin</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported -->
<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>

<script type="importmap">
{
"imports": {
"threepipe": "./../../dist/index.mjs",
"@threepipe/plugin-tweakpane": "./../../plugins/tweakpane/dist/index.mjs",
"@threepipe/plugin-configurator": "./../../plugins/configurator/dist/index.mjs"
}
}

</script>
<style id="example-style">
html, body, #canvas-container, #mcanvas {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
}
</style>
<script type="module" src="../examples-utils/simple-code-preview.mjs"></script>
<script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script>
</head>
<body>
<div id="canvas-container">
<canvas id="mcanvas"></canvas>
</div>

</body>

+ 40
- 0
examples/material-configurator-plugin/script.ts Просмотреть файл

@@ -0,0 +1,40 @@
import {_testFinish, FrameFadePlugin, IObject3D, PickingPlugin, SSAAPlugin, ThreeViewer} from 'threepipe'
import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane'
import {MaterialConfiguratorPlugin} from '@threepipe/plugin-configurator'

async function init() {

const viewer = new ThreeViewer({
canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
msaa: true,
plugins: [PickingPlugin, FrameFadePlugin, SSAAPlugin],
dropzone: {
addOptions: {
disposeSceneObjects: true,
},
},
})

const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true))
const materialConfigurator = viewer.addPluginSync(new MaterialConfiguratorPlugin())
materialConfigurator.enableEditContextMenus = true

// await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr')

// This model is already setup in the editor.
// You can use the editor to setup the materials in the UI and then load the model here.
// Another way to load the material variations is to export a json file of the plugin from the editor and load it in the same way after loading the model.
await viewer.load<IObject3D>(
'https://demo-assets.pixotronics.com/pixo/gltf/material_configurator.glb',
// 'https://demo-assets.pixotronics.com/pixo/gltf/classic-watch.glb',
{
autoCenter: true,
autoScale: true,
})
viewer.scene.mainCamera.controls!.enableDamping = true // since its disabled in the file for some reason
ui.setupPluginUi(MaterialConfiguratorPlugin)
ui.setupPluginUi(PickingPlugin)

}

init().finally(_testFinish)

+ 37
- 0
examples/switch-node-plugin/index.html Просмотреть файл

@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Switch Node Plugin (Configurator)</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Import maps polyfill -->
<!-- Remove this when import maps will be widely supported -->
<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>

<script type="importmap">
{
"imports": {
"threepipe": "./../../dist/index.mjs",
"@threepipe/plugin-tweakpane": "./../../plugins/tweakpane/dist/index.mjs",
"@threepipe/plugin-configurator": "./../../plugins/configurator/dist/index.mjs"
}
}

</script>
<style id="example-style">
html, body, #canvas-container, #mcanvas {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
}
</style>
<script type="module" src="../examples-utils/simple-code-preview.mjs"></script>
<script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script>
</head>
<body>
<div id="canvas-container">
<canvas id="mcanvas"></canvas>
</div>

</body>

+ 40
- 0
examples/switch-node-plugin/script.ts Просмотреть файл

@@ -0,0 +1,40 @@
import {_testFinish, FrameFadePlugin, IObject3D, PickingPlugin, SSAAPlugin, ThreeViewer} from 'threepipe'
import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane'
import {SwitchNodePlugin} from '@threepipe/plugin-configurator'

async function init() {

const viewer = new ThreeViewer({
canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
msaa: true,
plugins: [PickingPlugin, FrameFadePlugin, SSAAPlugin],
dropzone: {
addOptions: {
disposeSceneObjects: true,
},
},
})

const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true))
const configurator = viewer.addPluginSync(new SwitchNodePlugin())
configurator.enableEditContextMenus = true

// await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr')

// This model is already setup in the editor.
// You can use the editor to setup the switch-nodes in the UI and then load the model here.
// Another way to load the switch node variation details is to export a json file of the plugin from the editor and load it in the same way after loading the model.
await viewer.load<IObject3D>(
'https://demo-assets.pixotronics.com/pixo/gltf/product_configurator.glb',
// 'https://demo-assets.pixotronics.com/pixo/gltf/classic-watch.glb',
{
autoCenter: true,
autoScale: true,
})
viewer.scene.mainCamera.controls!.enableDamping = true // since its disabled in the file for some reason
ui.setupPluginUi(SwitchNodePlugin)
ui.setupPluginUi(PickingPlugin)

}

init().finally(_testFinish)

+ 1
- 0
examples/tweakpane-editor/index.html Просмотреть файл

@@ -22,6 +22,7 @@
"@threepipe/plugin-extra-importers": "./../../plugins/extra-importers/dist/index.mjs",
"@threepipe/plugin-blend-importer": "./../../plugins/blend-importer/dist/index.mjs",
"@threepipe/plugin-geometry-generator": "./../../plugins/geometry-generator/dist/index.mjs",
"@threepipe/plugin-configurator": "./../../plugins/configurator/dist/index.mjs",
"@threepipe/plugin-gaussian-splatting": "./../../plugins/gaussian-splatting/dist/index.mjs"
}
}

+ 9
- 1
examples/tweakpane-editor/script.ts Просмотреть файл

@@ -57,6 +57,7 @@ import {BlendLoadPlugin} from '@threepipe/plugin-blend-importer'
import {extraImportPlugins} from '@threepipe/plugin-extra-importers'
import {GeometryGeneratorPlugin} from '@threepipe/plugin-geometry-generator'
import {GaussianSplattingPlugin} from '@threepipe/plugin-gaussian-splatting'
import {MaterialConfiguratorPlugin, SwitchNodePlugin} from '@threepipe/plugin-configurator'

async function init() {

@@ -121,11 +122,18 @@ async function init() {
DeviceOrientationControlsPlugin,
PointerLockControlsPlugin,
ThreeFirstPersonControlsPlugin,
// InteractionPromptPlugin, // todo disable when not in Viewer tab, like in webgi
new MeshOptSimplifyModifierPlugin(false), // will auto-initialize on first use.
// new BasicSVGRendererPlugin(false, true),
...extraImportPlugins,
MaterialConfiguratorPlugin,
SwitchNodePlugin,
])

// to show more details in the UI and allow to edit changes in title etc.
viewer.getPlugin(MaterialConfiguratorPlugin)!.enableEditContextMenus = true
viewer.getPlugin(SwitchNodePlugin)!.enableEditContextMenus = true

const rt = viewer.getOrAddPluginSync(RenderTargetPreviewPlugin)
rt.addTarget({texture: viewer.getPlugin(GBufferPlugin)?.normalDepthTexture}, 'normalDepth')
rt.addTarget({texture: viewer.getPlugin(GBufferPlugin)?.flagsTexture}, 'gBufferFlags')
@@ -139,7 +147,7 @@ async function init() {
['GBuffer']: [GBufferPlugin, DepthBufferPlugin, NormalBufferPlugin],
['Post-processing']: [TonemapPlugin, ProgressivePlugin, SSAOPlugin, FrameFadePlugin, VignettePlugin, ChromaticAberrationPlugin, FilmicGrainPlugin],
['Export']: [CanvasSnapshotPlugin],
['Configuration']: [GLTFKHRMaterialVariantsPlugin],
['Configurator']: [MaterialConfiguratorPlugin, SwitchNodePlugin, GLTFKHRMaterialVariantsPlugin],
['Animation']: [GLTFAnimationPlugin, CameraViewPlugin],
['Extras']: [HDRiGroundPlugin, Rhino3dmLoadPlugin, ClearcoatTintPlugin, FragmentClippingExtensionPlugin, NoiseBumpMaterialPlugin, CustomBumpMapPlugin, VirtualCamerasPlugin],
['Debug']: [RenderTargetPreviewPlugin],

+ 40
- 0
plugins/configurator/package-lock.json Просмотреть файл

@@ -0,0 +1,40 @@
{
"name": "@threepipe/plugin-configurator",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@threepipe/plugin-configurator",
"version": "0.1.0",
"license": "Apache-2.0",
"dependencies": {
"threepipe": "file:./../../src/",
"tippy.js": "^6.3.7"
},
"devDependencies": {}
},
"../../src": {},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/threepipe": {
"resolved": "../../src",
"link": true
},
"node_modules/tippy.js": {
"version": "6.3.7",
"resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz",
"integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==",
"dependencies": {
"@popperjs/core": "^2.9.0"
}
}
}
}

+ 59
- 0
plugins/configurator/package.json Просмотреть файл

@@ -0,0 +1,59 @@
{
"name": "@threepipe/plugin-configurator",
"description": "Plugins for creating material and object configurators in threepipe.",
"version": "0.1.0",
"devDependencies": {
},
"dependencies": {
"threepipe": "file:./../../src/",
"tippy.js": "^6.3.7"
},
"clean-package": {
"remove": [
"clean-package",
"scripts",
"devDependencies",
"//",
"markdown-to-html"
],
"replace": {
"dependencies": {},
"peerDependencies": {
"threepipe": "^0.0.30"
}
}
},
"type": "module",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"files": [
"dist",
"src"
],
"scripts": {
"new:pack": "npm run prepare && clean-package && npm pack && clean-package restore",
"new:publish": "npm run prepare && clean-package && npm publish --access public && clean-package restore",
"prepare": "npm run build && npm run docs",
"build": "rimraf dist && vite build",
"dev": "NODE_ENV=development vite build --watch",
"docs": "rimraf docs && npx typedoc"
},
"author": "repalash <palash@shaders.app>",
"license": "Apache-2.0",
"keywords": [
"three",
"three.js",
"threepipe",
"vite",
"plugin"
],
"bugs": {
"url": "https://github.com/repalash/threepipe/issues"
},
"homepage": "https://github.com/repalash/threepipe#readme",
"repository": {
"type": "git",
"url": "git://github.com/repalash/threepipe.git"
}
}

+ 74
- 0
plugins/configurator/src/GridItemList.css Просмотреть файл

@@ -0,0 +1,74 @@

.customContextGrid {
background: #eeeeee55;
border: 0.5px solid rgba(220, 220, 220, 0.2);
width: auto;
height: auto;
position: absolute;
display: flex;
flex-direction: row;
flex-wrap: wrap;
z-index: 200;
padding: 0.35rem 0.35rem;
border-radius: 0.375rem;
min-width: 5rem;
pointer-events: auto;
box-shadow: 0px 2px 6px rgba(12, 12, 12, 0.2);

color: #111111;
font-size: 0.65rem;
font-family: Inter, "Roboto Mono", "Source Code Pro", Menlo, Courier, monospace;
backdrop-filter: blur(20px);
}

.customContextGridItems {
background-color: transparent;
cursor: pointer;
border-radius: 0.25rem;
line-height: 1rem;
font-weight: 500;
overflow: hidden;
margin: 0.12rem;
}

.customContextGridItems:hover {
color: white;
/*background-color: #017AFF;*/
box-shadow: 0 0 7px 0px rgba(64, 64, 64, 0.3);
}

.customContextGridItemImage {
width: 100%;
height: 100%;
}

.customContextGridHeading {
width: 100%;
padding: 5px;
font-size: 0.85rem;
}

.customContextGridParent {
position: absolute;
top: 0;
left: 0;
width: 270px;
height: calc(100% - 100px);
overflow-y: scroll;
z-index: 100;
display: flex;
flex-direction: column;
margin-bottom: 50px;
margin-top: 50px;
}

/* Hide scrollbar for Chrome, Safari and Opera */
.customContextGridParent::-webkit-scrollbar {
display: none;
}

/* Hide scrollbar for IE, Edge and Firefox */
.customContextGridParent {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}

+ 104
- 0
plugins/configurator/src/GridItemList.ts Просмотреть файл

@@ -0,0 +1,104 @@
import {createDiv, createStyles, mobileAndTabletCheck} from 'ts-browser-helpers'
import tippy from 'tippy.js'
import tippyStyles from 'tippy.js/dist/tippy.css?inline'
import styles from './GridItemList.css?inline'

export interface GridItem {
id: string
image: string
onClick?: (id: string) => void
tooltip?: string
}
/**
* Earlier it was called CustomContextGrid.
* Used to create a overlay of grid items over the canvas allowing users to pick from a list of items.
*/
export class GridItemList {
public static Elements: HTMLDivElement[] = []
// private static _inited = false
private static _container = document.createElement('div')

private static _initializeStyles(): void {
// GridItemList._inited = true // since container is cleared
createStyles(styles, GridItemList._container)
createStyles(tippyStyles, GridItemList._container)
GridItemList._container.style.position = 'absolute'
GridItemList._container.style.top = '0'
GridItemList._container.style.left = '0'
GridItemList._container.style.width = '270px'
GridItemList._container.style.height = '100%'
GridItemList._container.style.pointerEvents = 'none'
GridItemList._container.style.zIndex = '100'
GridItemList._container.style.overflowY = 'auto'
}

public static Create<T extends GridItem>(tag: string, title: string, cols: number, x: number, y: number, items: T[], processDiv: (d: HTMLDivElement, item: T, container: HTMLElement) => void): HTMLDivElement {
// if (!GridItemList._inited) GridItemList._initialize()
const isMobile = mobileAndTabletCheck()
const gap = isMobile ? 0.15 : 0.25 // rem
const itemWidth = isMobile ? 1.5 : 2.5 // rem
const margin = isMobile ? 1 : 1 // rem

// if (GridItemList.Element) GridItemList.Remove()

const container = createDiv({
classList: ['customContextGrid'], addToBody: false,
innerHTML: `
<div class="customContextGridHeading"> ${title} </div>
`,
})
container.style.top = y + 'px'
container.style.left = x + 'px'
container.style.gap = gap + 'rem'
container.style.width = (itemWidth + gap) * cols - gap + margin + 'rem' // `calc(${100.0 / cols}%-${gap * cols}rem)`
container.dataset.tag = tag
for (const item of items) {
const d = createDiv({
classList: ['customContextGridItems'], addToBody: false, innerHTML: `
<img src="${item.image}" class="customContextGridItemImage">
`,
})
d.style.width = itemWidth + 'rem'
d.style.height = itemWidth + 'rem'
container.appendChild(d)
d.onclick = () => item.onClick?.(item.id)
if (item.tooltip) tippy(d, {placement: 'bottom', content: item.tooltip})
processDiv(d, item, container)
}

GridItemList.Elements?.push(container)
return container
}

public static RemoveAll(tag?: string): void {
if (!tag) {
for (const element of GridItemList.Elements) element.remove()
GridItemList.Elements = []
} else {
const el = GridItemList.Elements.filter(e => e.dataset.tag === tag)
for (const element of el) element.remove()
GridItemList.Elements = GridItemList.Elements.filter(e => e.dataset.tag !== tag)
}
}

public static RebuildUi(parent?: HTMLElement): void {
if (GridItemList.Elements.length === 0) return
if (!parent) parent = createDiv({addToBody: true, classList: ['customContextGridParent']})
for (const element of GridItemList.Elements) element.remove()
GridItemList._container.innerHTML = ''
GridItemList._initializeStyles()
let y = 20
parent.appendChild(GridItemList._container)
for (const element of GridItemList.Elements) {
element.style.top = y + 'px'
GridItemList._container.appendChild(element)
y += element.clientHeight + 20
}
}
public static Dispose() {
GridItemList.RemoveAll()
GridItemList._container.remove()
GridItemList._container.innerHTML = ''
}

}

+ 22
- 0
plugins/configurator/src/GridItemListPlugin.ts Просмотреть файл

@@ -0,0 +1,22 @@
import {AViewerPluginSync, ThreeViewer} from 'threepipe'
import {GridItemList} from './GridItemList'

/**
* A helper plugin to create a a simple list of small grids like for material or object configurator
*/
export class GridItemListPlugin extends AViewerPluginSync<''> {
enabled = true
toJSON: any = undefined
create = GridItemList.Create
removeAll = GridItemList.RemoveAll
rebuildUi() {
if (!this._viewer) return
GridItemList.RebuildUi(this._viewer.container) // todo throttle?
}

onRemove(viewer: ThreeViewer) {
super.onRemove(viewer)
GridItemList.Dispose()
}
}


+ 108
- 0
plugins/configurator/src/MaterialConfiguratorPlugin.ts Просмотреть файл

@@ -0,0 +1,108 @@
import {CustomContextMenu, MaterialConfiguratorBasePlugin, MaterialVariations} from 'threepipe'
import {GridItemListPlugin} from './GridItemListPlugin'

/**
* Material Configurator Plugin (Basic UI)
* This plugin allows you to create variations of materials mapped to material names or uuids in the scene.
* These variations can be applied to the materials in the scene. (This copies the properties to the same material instances instead of assigning new materials)
* The plugin interfaces with the picking plugin and also provides uiConfig to show and edit the variations.
* This functionality is inherited from `MaterialConfiguratorBasePlugin`
*
* Additionally this plugin adds a Grid UI using {@link GridItemListPlugin} in the DOM over the viewer canvas to show various material variations and allow the user to apply them.
* The UI can also be used in the editor to edit the variations and apply them.
*/
export class MaterialConfiguratorPlugin extends MaterialConfiguratorBasePlugin {
public static PluginType = 'MaterialConfiguratorPlugin'

enableEditContextMenus = false

dependencies = [GridItemListPlugin]

// must be called from preFrame
protected async _refreshUi(): Promise<boolean> {
if (!await super._refreshUi()) return false

const grid = this._viewer?.getPlugin(GridItemListPlugin)
if (!grid) return false

grid.removeAll(MaterialConfiguratorPlugin.PluginType)

for (const variation of this.variations) {
const container = grid.create(MaterialConfiguratorPlugin.PluginType,
variation.title + (this.enableEditContextMenus ? ' (' + variation.uuid + ')' : ''),
5,
20, 0,
variation.materials.map(m => {
const image = this.getPreview(m, variation.preview)
return {
id: m.uuid,
image, // : (m as any).map?.image ? imageBitmapToBase64((m as any).map.image, 100) : makeColorSvg((m as any).color ?? '#ffffff'),
onClick: (id:string) => this.applyVariation(variation, id),
tooltip: m.name || m.uuid,
}
}), (d, item)=> {
// todo test in shadow dom.
d.oncontextmenu = (e) => {
if (!this.enableEditContextMenus) return
e.preventDefault()
e.stopPropagation()
const menu = CustomContextMenu.Create(this.materialContextMenuItems(variation, item.id), e.clientX, e.clientY)
document.body.appendChild(menu)
}
})
container.oncontextmenu = (e) => {
if (!this.enableEditContextMenus) return
e.preventDefault()
e.stopPropagation()
const menu = CustomContextMenu.Create(this.variationsContextMenuItems(variation), e.clientX, e.clientY)
document.body.appendChild(menu)
}
}

grid.rebuildUi()

return true
}

materialContextMenuItems = (variation: MaterialVariations, uuid: string)=>({
['Remove']: async()=>{
const conf = await this._viewer?.dialog.confirm('Remove material: Remove material from this variation list?')
if (!conf) return
variation.materials = variation.materials.filter(m => m.uuid !== uuid)
this.refreshUi()
CustomContextMenu.Remove()
},
// todo set icon url
})
variationsContextMenuItems = (variation: MaterialVariations)=>({
['Rename mapping']: async() => {
const name = await this._viewer?.dialog.prompt('Change name: New material name to map to', variation.uuid, true)
if (name) {
variation.uuid = name
this.refreshUi()
}
},
['Rename title']: async() => {
const name = await this._viewer?.dialog.prompt('Change name: New material name to map to', variation.title, true)
if (name) {
variation.title = name
this.refreshUi()
}
},
['Clear Materials']: async()=>{
const conf = await this._viewer?.dialog.confirm('Remove all: Remove all materials from this variation list?')
if (!conf) return
variation.materials = []
this.refreshUi()
CustomContextMenu.Remove()
},
['Remove Section']: async()=>{
const conf = await this._viewer?.dialog.confirm('Remove variations: Remove this category of variations?')
if (!conf) return
this.removeVariation(variation)
CustomContextMenu.Remove()
},
})

}


+ 79
- 0
plugins/configurator/src/SwitchNodePlugin.ts Просмотреть файл

@@ -0,0 +1,79 @@
import {SwitchNodeBasePlugin} from 'threepipe'
import {GridItemListPlugin} from './GridItemListPlugin'

/**
* Switch Node Plugin (Basic UI)
* This plugin allows you to configure object variations in a file and apply them in the scene.
* Each SwitchNode is a parent object with multiple direct children. Only one child is visible at a time.
* This works by toggling the `visible` property of the children of a parent object.
* The plugin interfaces with the picking plugin and also provides uiConfig to show and edit the variations.
* It also provides a function to create snapshot previews of individual variations. This creates a limited render of the object with the selected child visible.
* To get a proper render, its better to render it offline and set the image as a preview.
* This functionality is inherited from `SwitchNodeBasePlugin`.
*
* Additionally this plugin adds a Grid UI using {@link GridItemListPlugin} in the DOM over the viewer canvas to show various object variations and allow the user to select them.
* The UI can also be used in the editor to edit the variations and apply them.
*/
export class SwitchNodePlugin extends SwitchNodeBasePlugin {
public static readonly PluginType = 'SwitchNodePlugin'

enableEditContextMenus = false

dependencies = [GridItemListPlugin]

protected _refreshUi(): boolean {
if (!super._refreshUi()) return false

const grid = this._viewer?.getPlugin(GridItemListPlugin)
if (!grid) return false

grid.removeAll(SwitchNodePlugin.PluginType)

for (const variation of this.variations) {
const obj = this._viewer!.scene.getObjectByName(variation.name)
if (!obj) {
console.warn('no object found for variation, skipping', variation)
continue
}
if (obj.children.length < 1) {
console.warn('SwitchNode does not have enough children', variation)
}
const container = grid.create(SwitchNodePlugin.PluginType,
variation.title + (this.enableEditContextMenus ? ' (' + variation.name + ')' : ''),
Math.min(5, obj.children.length),
20, 0,
obj.children.map(child => {
return {
id: child.uuid,
image: this.getPreview(variation, child),
onClick: () => {
this.selectNode(variation, child.name || child.uuid)
},
tooltip: child.name || child.uuid,
}
}), (d, _item)=> {
// todo test in shadow dom.
d.oncontextmenu = (e) => {
if (!this.enableEditContextMenus) return
e.preventDefault()
e.stopPropagation()
// todo
// const menu = CustomContextMenu.Create(this.materialContextMenuItems(variation, item.id), e.clientX, e.clientY)
// document.body.appendChild(menu)
}
}
)
container.oncontextmenu = (e) => {
if (!this.enableEditContextMenus) return
e.preventDefault()
e.stopPropagation()
// todo
// const menu = CustomContextMenu.Create(this.variationsContextMenuItems(variation), e.clientX, e.clientY)
// document.body.appendChild(menu)
}
}
grid.rebuildUi()

return true
}
}

+ 40
- 0
plugins/configurator/src/global.d.ts Просмотреть файл

@@ -0,0 +1,40 @@
declare module '*.txt' {
const content: string
export default content
}
declare module '*.glsl' {
const content: string
export default content
}
declare module '*.vert' {
const content: string
export default content
}
declare module '*.frag' {
const content: string
export default content
}
declare module '*.module.scss' {
const content: any
export default content
export const stylesheet: string
}
declare module '*.module.css' {
const content: any
export default content
export const stylesheet: string
}
declare module '*.css' {
const content: string
export default content
}
declare module '*.css?inline' { // for vite
const content: string
export default content
}

// export {}

// hack for typedoc
// eslint-disable-next-line @typescript-eslint/naming-convention
// declare type OffscreenCanvas = HTMLCanvasElement

+ 4
- 0
plugins/configurator/src/index.ts Просмотреть файл

@@ -0,0 +1,4 @@
export {MaterialConfiguratorPlugin} from './MaterialConfiguratorPlugin'
export {GridItemListPlugin} from './GridItemListPlugin'
export {GridItemList} from './GridItemList'
export {SwitchNodePlugin} from './SwitchNodePlugin'

+ 41
- 0
plugins/configurator/tsconfig.json Просмотреть файл

@@ -0,0 +1,41 @@
{
"compilerOptions": {
"baseUrl": "./src",
"rootDir": "./src",
"allowJs": true,
"checkJs": false,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"isolatedModules": true,
"module": "es2020",
"noImplicitAny": true,
"declaration": true,
"declarationMap": true,
"declarationDir": "dist",
"outDir": "dist",
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"removeComments": false,
"preserveConstEnums": true,
"moduleResolution": "node",
"emitDecoratorMetadata": false,
"sourceMap": true,
"target": "ES2021",
"strictNullChecks": true,
"lib": [
"es2020",
"esnext",
"dom"
]
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"**/*.spec.ts",
"dist"
]
}

+ 10
- 0
plugins/configurator/typedoc.json Просмотреть файл

@@ -0,0 +1,10 @@
{
"extends": [
"../../typedoc.json"
],
"entryPoints": [
"src/index.ts"
],
"name": "Threepipe Configurator Plugins",
"readme": "none"
}

+ 91
- 0
plugins/configurator/vite.config.js Просмотреть файл

@@ -0,0 +1,91 @@
import {defineConfig} from 'vite'
import json from '@rollup/plugin-json';
import dts from 'vite-plugin-dts'
import packageJson from './package.json';
import license from 'rollup-plugin-license';
import replace from '@rollup/plugin-replace';
import glsl from 'rollup-plugin-glsl';
import path from 'node:path';

const isProd = process.env.NODE_ENV === 'production'
const { name, version, author } = packageJson
const {main, module, browser} = packageJson

const globals = {
'three': 'threepipe', // just incase someone uses three
'threepipe': 'threepipe',
'@threepipe/plugin-tweakpane': '@threepipe/plugin-tweakpane',
}

export default defineConfig({
optimizeDeps: {
exclude: ['uiconfig.js', 'ts-browser-helpers'],
},
base: '',
// define: {
// 'process.env': process.env
// },
build: {
sourcemap: true,
minify: false,
cssMinify: isProd,
cssCodeSplit: false,
watch: !isProd ? {
buildDelay: 1000,
} : null,
lib: {
entry: 'src/index.ts',
formats: isProd ? ['es', 'umd'] : ['es'],
name: name,
fileName: (format) => (format === 'umd' ? main : module).replace('dist/', ''),
},
outDir: 'dist',
emptyOutDir: isProd,
commonjsOptions: {
exclude: [/uiconfig.js/, /ts-browser-helpers/],
},
rollupOptions: {
output: {
// inlineDynamicImports: false,
globals,
},
external: Object.keys(globals),

},
},
plugins: [
isProd ? dts({tsconfigPath: './tsconfig.json'}) : null,
replace({
'from \'three\'': 'from \'threepipe\'',
delimiters: ['', ''],
}),
replace({
'process.env.NODE_ENV': JSON.stringify(isProd ? 'production' : 'development'),
preventAssignment: true,
}),
glsl({ // todo: minify glsl.
include: 'src/**/*.glsl',
}),
json(),
// postcss({
// modules: false,
// autoModules: true, // todo; issues with typescript import css, because inject is false
// inject: false,
// minimize: isProduction,
// // Or with custom options for `postcss-modules`
// }),
license({
banner: `
@license
${name} v${version}
Copyright 2022<%= moment().format('YYYY') > 2022 ? '-' + moment().format('YYYY') : null %> ${author}
${packageJson.license} License
See ./dependencies.txt for any bundled third-party dependencies and licenses.
`,
thirdParty: {
output: path.join(__dirname, 'dist', 'dependencies.txt'),
includePrivate: true, // Default is false.
},
}),
],
})

+ 298
- 0
src/plugins/configurator/MaterialConfiguratorBasePlugin.ts Просмотреть файл

@@ -0,0 +1,298 @@
import {AViewerPluginSync, ThreeViewer} from '../../viewer'
import {PickingPlugin} from '../interaction/PickingPlugin'
import {imageBitmapToBase64, makeColorSvgCircle, serialize} from 'ts-browser-helpers'
import {UiObjectConfig} from 'uiconfig.js'
import {IMaterial, PhysicalMaterial} from '../../core'
import {MaterialPreviewGenerator} from '../../three'
import {Color} from 'three'

/**
* Material Configurator Plugin (Base)
* This plugin allows you to create variations of materials mapped to material names or uuids in the scene.
* These variations can be applied to the materials in the scene. (This copies the properties to the same material instances instead of assigning new materials)
* The plugin interfaces with the picking plugin and also provides uiConfig to show and edit the variations.
*
* See `MaterialConfiguratorPlugin` in [plugin-configurator](https://threepipe.org/plugins/configurator/docs/index.html) for example on inheriting with a custom UI renderer.
*
* @category Plugins
*/
export class MaterialConfiguratorBasePlugin extends AViewerPluginSync<''> {
enabled = true
public static PluginType = 'MaterialConfiguratorPlugin'
private _picking: PickingPlugin | undefined
protected _previewGenerator: MaterialPreviewGenerator | undefined
private _uiNeedRefresh = false

constructor() {
super()
this.addEventListener('deserialize', this.refreshUi)
this.refreshUi = this.refreshUi.bind(this)
this._refreshUi = this._refreshUi.bind(this)
this._refreshUiConfig = this._refreshUiConfig.bind(this)
}

onAdded(viewer: ThreeViewer) {
super.onAdded(viewer)

// todo subscribe to plugin add event if picking is not added yet.
this._picking = viewer.getPlugin<PickingPlugin>('Picking')
this._previewGenerator = new MaterialPreviewGenerator()

this._picking?.addEventListener('selectedObjectChanged', this._refreshUiConfig)
viewer.addEventListener('preFrame', this._refreshUi)
}

/**
* Apply all variations(by selected index or first item) when a config is loaded
*/
applyOnLoad = true

/**
* Reapply all selected variations again.
* Useful when the scene is loaded or changed and the variations are not applied.
*/
reapplyAll() {
this.variations.forEach(v => this.applyVariation(v, v.materials[v.selectedIndex ?? 0].uuid))
}

fromJSON(data: any, meta?: any): this | Promise<this | null> | null {
this.variations = []
if (!super.fromJSON(data, meta)) return null // its not a promise
if (data.applyOnLoad === undefined) { // old files
this.applyOnLoad = false
}
if (this.applyOnLoad) this.reapplyAll()
return this
}

onRemove(viewer: ThreeViewer) {
this._previewGenerator?.dispose()
this._previewGenerator = undefined

this._picking?.removeEventListener('selectedObjectChanged', this._refreshUiConfig)
this.removeEventListener('deserialize', this.refreshUi)
viewer.removeEventListener('preFrame', this._refreshUi)

this._picking = undefined

return super.onRemove(viewer)
}

findVariation(uuid?: string): MaterialVariations|undefined {
return uuid ? this.variations.find(v => v.uuid === uuid) : undefined
}

getSelectedVariation(): MaterialVariations|undefined {
return this.findVariation(this._selectedMaterial()?.uuid) || this.findVariation(this._selectedMaterial()?.name)
}

/**
* Apply a material variation based on index or uuid.
* @param variations
* @param matUuidOrIndex
*/
applyVariation(variations: MaterialVariations, matUuidOrIndex: string|number): boolean {
const m = this._viewer?.materialManager
if (!m) return false
const material = typeof matUuidOrIndex === 'string' ?
variations.materials.find(m1 => m1.uuid === matUuidOrIndex) :
variations.materials[matUuidOrIndex]
if (!material) return false
variations.selectedIndex = variations.materials.indexOf(material)
return m.applyMaterial(material, variations.uuid)
}

/**
* Get the preview for a material variation
* Should be called from preFrame ideally. (or preRender but set viewerSetDirty = false)
* @param preview - Type of preview. Could be generate:sphere, generate:cube, color, map, emissive, etc.
* @param material - Material or index of the material in the variation.
* @param viewerSetDirty - call viewer.setDirty() after setting the preview. So that the preview is cleared from the canvas.
*/
getPreview(material: IMaterial, preview: string, viewerSetDirty = true): string {
if (!this._viewer) return ''
// const m = typeof material === 'number' ? variation.materials[material] : material
const m = material
if (!m) return ''
let image = ''
if (!preview.startsWith('generate:')) {
const pp = (m as any)[preview] || '#ff00ff'
image = pp.image ? imageBitmapToBase64(pp.image, 100) : ''
if (!image.length) image = makeColorSvgCircle(pp.isColor ? (pp as Color).getHexString() : pp)
} else {
image = this._previewGenerator!.generate(m,
this._viewer.renderManager.renderer,
this._viewer.scene.environment,
preview.split(':')[1]
)
}
if (viewerSetDirty) this._viewer.setDirty() // because called from preFrame
return image
}

/**
* Refreshes the UI in the next frame
*/
refreshUi(): void {
if (!this.enabled || !this._viewer) return
this._uiNeedRefresh = true
}

private _refreshUiConfig() {
if (!this.enabled) return
this.uiConfig.uiRefresh?.() // don't call this.refreshUi here
}

// must be called from preFrame
protected async _refreshUi(): Promise<boolean> {
if (!this.enabled) return false
if (!this._viewer || !this._uiNeedRefresh) return false
this._uiNeedRefresh = false
this._refreshUiConfig()
return true
}

@serialize()
variations: MaterialVariations[] = []

private _selectedMaterial = () => (this._picking?.getSelectedObject()?.material || undefined) as IMaterial | undefined
uiConfig: UiObjectConfig = {
label: 'Material Configurator',
type: 'folder',
// expanded: true,
children: [
() => [
{
type: 'input',
label: 'uuid',
property: [this._selectedMaterial(), 'uuid'],
hidden: () => !this._selectedMaterial(),
disabled: true,
},
{
type: 'input',
label: 'mapping',
hidden: () => !this._selectedMaterial(),
property: () => [this.getSelectedVariation(), 'uuid'],
onChange: async() => this.refreshUi(),
},
{
type: 'input',
label: 'title',
hidden: () => !this._selectedMaterial(),
property: () => [this.getSelectedVariation(), 'title'],
onChange: async() => this.refreshUi(),
},
{
type: 'dropdown',
label: 'Preview Type',
hidden: () => !this._selectedMaterial(),
property: () => [this.getSelectedVariation(), 'preview'],
onChange: async() => this.refreshUi(),
children: ['generate:sphere', 'generate:cube', 'color', 'map', 'emissive', ...Object.keys(PhysicalMaterial.MaterialProperties).filter(x => x.endsWith('Map'))].map(k => ({
label: k,
value: k,
})),
},
...this.getSelectedVariation()?.materials.map(m => {
return m.uiConfig ? Object.assign(m.uiConfig, {expanded: false}) : {}
}) || [],
{
type: 'button',
label: 'Clear variations',
hidden: () => !this._selectedMaterial(),
value: async() => {
const v = this.getSelectedVariation()
if (v && await this._viewer!.dialog.confirm('Material configurator: Remove all variations for this material?')) v.materials = []
this.refreshUi()
},
},
{
type: 'button',
label: 'Remove completely',
hidden: () => !this._selectedMaterial(),
value: async() => {
const v = this.getSelectedVariation()
if (v && await this._viewer!.dialog.confirm('Material configurator: Remove this variation?')) {
this.removeVariation(v)
}
},
},
{
type: 'button',
label: 'Add Variation',
hidden: () => !this._selectedMaterial(),
value: async() => {
const mat = this._selectedMaterial()
if (!mat) return
if (!mat.name && !await this._viewer?.dialog.confirm('Material configurator: Material has no name. Use uuid instead?')) return
this.addVariation(mat)
},
},
{
type: 'button',
label: 'Refresh Ui',
value: () => this.refreshUi(),
},
{
type: 'button',
label: 'Apply All',
value: () => {
this.variations.forEach(v => this.applyVariation(v, v.materials[0].uuid))
},
},
],
],
}

removeVariationForMaterial(material: IMaterial) {
let variation = this.findVariation(material.uuid)
if (!variation && material.name.length > 0) variation = this.findVariation(material.name)
if (variation) this.removeVariation(variation)
}
removeVariation(variation: MaterialVariations) {
if (!variation) return
this.variations.splice(this.variations.indexOf(variation), 1)
this.refreshUi()
}
addVariation(material?: IMaterial) {
const clone = material?.clone?.()
if (material && clone) {
let variation = this.findVariation(material.uuid)
if (!variation && material.name.length > 0) variation = this.findVariation(material.name)
if (!variation) {
variation = this.createVariation(material)
}
variation.materials.push(clone)
this.refreshUi()
}
}

createVariation(material: IMaterial) {
this.variations.push({
uuid: material.name.length > 0 ? material.name : material.uuid,
title: material.name.length > 0 ? material.name : 'No Name',
preview: 'generate:sphere',
materials: [],
})
return this.variations[this.variations.length - 1]
}
}

export interface MaterialVariations {
/**
* The name or the uuid of the material in the scene
*/
uuid: string
/**
* Title to show in the UI
*/
title: string
preview: keyof PhysicalMaterial | 'generate:sphere' | 'generate:cube' | 'generate:cylinder'
materials: IMaterial[]
data?: {
icon?: string,
[key: string]: any
}[]
selectedIndex?: number
}

+ 265
- 0
src/plugins/configurator/SwitchNodeBasePlugin.ts Просмотреть файл

@@ -0,0 +1,265 @@
import {Object3D, Vector3} from 'three'
import {AViewerPluginSync, ThreeViewer} from '../../viewer'
import {PickingPlugin} from '../interaction/PickingPlugin'
import {UiObjectConfig} from 'uiconfig.js'
import {serialize} from 'ts-browser-helpers'
import {snapObject} from '../../three'

/**
* Switch Node Plugin (Base)
* This plugin allows you to configure object variations in a file and apply them in the scene.
* Each SwitchNode is a parent object with multiple direct children. Only one child is visible at a time.
* This works by toggling the `visible` property of the children of a parent object.
* The plugin interfaces with the picking plugin and also provides uiConfig to show and edit the variations.
* It also provides a function to create snapshot previews of individual variations. This creates a limited render of the object with the selected child visible.
* To get a proper render, its better to render it offline and set the image as a preview.
*
* See `SwitchNodePlugin` in [plugin-configurator](https://threepipe.org/plugins/configurator/docs/index.html) for example on inheriting with a custom UI renderer.
*
* @category Plugins
*/
export class SwitchNodeBasePlugin extends AViewerPluginSync<''> {
public static readonly PluginType = 'SwitchNodePlugin'

enabled = true
private _picking: PickingPlugin | undefined
private _uiNeedRefresh = false


constructor() {
super()

this._postFrame = this._postFrame.bind(this)
this.refreshUiConfig = this.refreshUiConfig.bind(this)
this.addEventListener('deserialize', async() => {
// await timeout(200) // not needed actually
this.refreshUi()
})
}

onAdded(viewer: ThreeViewer) {
super.onAdded(viewer)
// todo subscribe to plugin add event if picking is not added yet.
this._picking = viewer.getPlugin<PickingPlugin>('Picking')
this._picking?.addEventListener('selectedObjectChanged', this.refreshUiConfig) // don't call this.refreshUi here
viewer.addEventListener('postFrame', this._postFrame)
}
onRemove(viewer: ThreeViewer) {
this._picking = viewer.getPlugin<PickingPlugin>('Picking')
this._picking?.removeEventListener('selectedObjectChanged', this.refreshUiConfig)
viewer.removeEventListener('postFrame', this._postFrame)
super.onRemove(viewer)
}

protected _postFrame() {
if (this._uiNeedRefresh) this._refreshUi() // only call this from here.
}

/**
* Select a switch node variation with name or uuid.
* @param node
* @param nameOrUuid
* @param setDirty - set dirty in the viewer after update.
*/
selectNode(node: ObjectSwitchNode, nameOrUuid: string|number, setDirty = true) {
const obj = this._viewer?.scene.getObjectByName(node.name)
if (!obj || obj.children.length < 1) return
const child = typeof nameOrUuid === 'number' ?
obj.children[nameOrUuid] :
obj.children.find(c => c.name === nameOrUuid || c.uuid === nameOrUuid)
if (!child) {
this._viewer?.console.warn('SwitchNodePlugin: child not found', nameOrUuid)
return false
}
node.selected = child.name || child.uuid
let changed = false
for (const child1 of obj.children) {
const visible = child1.visible
child1.visible = (child1.name || child1.uuid) === node.selected
changed = changed || visible !== child1.visible
}
if (changed && setDirty) this._viewer!.scene.setDirty({refreshScene: true, frameFade: true})
return changed
}


/**
* Apply all variations(by selected index or first item) when a config is loaded
*/
applyOnLoad = true

/**
* Reapply all selected variations again.
* Useful when the scene is loaded or changed and the variations are not applied.
*/
reapplyAll() {
this.variations.forEach(v => this.selectNode(v, v.selected || 0, false))
this._viewer!.scene.setDirty({refreshScene: true, frameFade: true})
}

fromJSON(data: any, meta?: any): this | Promise<this | null> | null {
this.variations = []
if (!super.fromJSON(data, meta)) return null // its not a promise
if (data.applyOnLoad === undefined) { // old files
this.applyOnLoad = true // setting true because all the items will be visible otherwise.
}
if (this.applyOnLoad) this.reapplyAll()
return this
}

refreshUi() {
if (!this.enabled) return
this._uiNeedRefresh = true
}

protected _refreshUi(): boolean {
if (!this.enabled) return false
if (!this._viewer) return false
this._uiNeedRefresh = false

this.refreshUiConfig()
return true
}

refreshUiConfig() {
if (!this.enabled) return
this.uiConfig.uiRefresh?.()
}

@serialize() variations: ObjectSwitchNode[] = []

protected _selectedSwitchNode = (): Object3D | undefined => {
const obj = this._picking?.getSelectedObject() // (?.material || undefined) as IMaterial | undefined
if (!obj) return undefined
const nodes = this.variations.map(v => v.name)
let found: Object3D | undefined = undefined
obj.traverseAncestors(a => {
if (found) return
if (!a.name) return
if (nodes.includes(a.name)) found = a
})
return found
}

/**
* Get the preview for a switch node variation
* Should be called from preFrame ideally. (or preRender but set viewerSetDirty = false)
* @param child - Child Object to get the preview for
* @param variation - Switch node variation that contains the child.
* @param viewerSetDirty - call viewer.setDirty() after setting the preview. So that the preview is cleared from the canvas.
*/
getPreview(variation: ObjectSwitchNode, child: Object3D, viewerSetDirty = true): string {
if (!this._viewer || !variation) return ''
// const m = typeof material === 'number' ? variation.materials[material] : material
const cv = variation.camView
const camOffset = new Vector3(
(cv.includes('right') ? 1 : 0) - (cv.includes('left') ? 1 : 0),
(cv.includes('top') ? 1 : 0) - (cv.includes('bottom') ? 1 : 0),
(cv.includes('front') ? 1 : 0) - (cv.includes('back') ? 1 : 0)
)
if (!variation.camDistance) variation.camDistance = 1
const image = snapObject(this._viewer!.renderManager.renderer, child, this._viewer?.scene, 7, camOffset.multiplyScalar(variation.camDistance * 2))
if (viewerSetDirty) this._viewer.setDirty() // because called from preFrame
return image
}

addNode(node: ObjectSwitchNode, refreshUi = true) {
this.variations.push(node)
if (refreshUi) this.refreshUi()
}

uiConfig: UiObjectConfig = {
label: 'Switch Node Plugin',
type: 'folder',
// expanded: true,
children: [
{
type: 'checkbox',
label: 'Enabled',
property: [this, 'enabled'],
},
() => [
{
type: 'folder',
label: 'All nodes',
expanded: true,
children: [
this.variations.map(v => ({
type: 'input',
label: v.title,
property: [v, 'name'],
onChange: () => this.refreshUi(),
})),
],
},
{
type: 'button',
label: 'Add Node',
value: () => {
this.addNode({
name: 'switch_node',
selected: '',
title: 'Switch Node',
camView: 'front',
camDistance: 1,
})
},
},
{
type: 'button',
label: 'Refresh UI',
value: () => this.refreshUi(),
},
{
type: 'input',
label: 'Selected node title',
hidden: () => !this._selectedSwitchNode(),
property: () => {
const node = this._selectedSwitchNode()
if (!node) return []
return [this.variations.find(v => v.name === node.name), 'title']
},
onChange: () => this.refreshUi(),
},
{
type: 'slider',
bounds: [0.01, 2],
stepSize: 0.01,
label: 'Cam Distance',
hidden: () => !this._selectedSwitchNode(),
property: () => {
const node = this._selectedSwitchNode()
if (!node) return []
return [this.variations.find(v => v.name === node.name), 'camDistance']
},
// onChange: ()=> this.refreshUi(),
},
{
type: 'dropdown',
label: 'Cam View',
hidden: () => !this._selectedSwitchNode(),
property: () => {
const node = this._selectedSwitchNode()
if (!node) return []
return [this.variations.find(v => v.name === node.name), 'camView']
},
onChange: () => this.refreshUi(),
children: ['top', 'bottom', 'front', 'back', 'left', 'right'].map(k => ({
label: k,
value: k,
})),
},

],
],
}

}

export interface ObjectSwitchNode{
name: string,
title: string,
selected: string,
camView: 'top'|'bottom'|'front'|'back'|'left'|'right'|string,
camDistance: number,
}

+ 4
- 0
src/plugins/index.ts Просмотреть файл

@@ -70,6 +70,10 @@ export {FragmentClippingExtensionPlugin, FragmentClippingMode} from './material/
// rendering
export {VirtualCamerasPlugin} from './rendering/VirtualCamerasPlugin'

// configurator
export {MaterialConfiguratorBasePlugin, type MaterialVariations} from './configurator/MaterialConfiguratorBasePlugin'
export {SwitchNodeBasePlugin, type ObjectSwitchNode} from './configurator/SwitchNodeBasePlugin'

// extras
export {HDRiGroundPlugin} from './extras/HDRiGroundPlugin'
export {Object3DWidgetsPlugin} from './extras/Object3DWidgetsPlugin'

+ 68
- 0
src/three/utils/MaterialPreviewGenerator.ts Просмотреть файл

@@ -0,0 +1,68 @@
import {BoxGeometry, CylinderGeometry, HemisphereLight, Light, Mesh, Scene, SphereGeometry, Vector3} from 'three'
import {IDisposable} from 'ts-browser-helpers'
import {snapObject} from './snapObject'
import {IMaterial, ITexture, IWebGLRenderer} from '../../core'

export class MaterialPreviewGenerator implements IDisposable {
private _scene: Scene
private _channel: number
private _lights: Light[] = []

constructor() {

const scene = new Scene()

this._channel = 7
const hemisphericLight = new HemisphereLight(0xffffff, 0x444444, 1)
hemisphericLight.position.set(0, 10, 0)
hemisphericLight.layers.set(this._channel)
scene.add(hemisphericLight)
this._lights.push(hemisphericLight)

this._scene = scene

}

dispose() {
[...this._lights].forEach(light => light.dispose())
Object.values(this.shapes).forEach(shape => {
if (shape.geometry) shape.geometry.dispose()
})
}

shapes: Record<string, Mesh> = {
sphere: new Mesh(new SphereGeometry(1)),
cube: new Mesh(new BoxGeometry(1, 1, 1)),
cylinder: new Mesh(new CylinderGeometry(0.5, 0.5, 1)),
}

// todo: show an overlay when this is happening
generate(material: IMaterial, renderer: IWebGLRenderer, environment?: ITexture|null, shape = 'sphere'): string {
const object = this.shapes[shape] || new Mesh(new SphereGeometry(1))
object.material = material

if (!object.geometry.attributes.tangent) object.geometry.computeTangents() // for anisotropy

this._scene.add(object)
this._scene.environment = environment ?? null

const envIntensity = material.envMapIntensity
// clamp since we have no tonemapping
if (typeof envIntensity === 'number') {
material.envMapIntensity = Math.max(envIntensity, 2)
}

const snap = snapObject(renderer, object, this._scene, this._channel, new Vector3(0, 0, 1.5))
// const snap = snapObject(this.viewer, (material.userData.__appliedMeshes as Set<Mesh>).values().next().value, undefined, this._channel)

if (typeof envIntensity === 'number')
material.envMapIntensity = envIntensity

this._scene.remove(object)

object.material = undefined as any

return snap
}

}

+ 2
- 0
src/three/utils/index.ts Просмотреть файл

@@ -9,6 +9,8 @@ export {threeConstMappings} from './const-mappings'
export {ObjectPicker} from './ObjectPicker'
export {autoGPUInstanceMeshes} from './gpu-instancing'
export {HVBlurHelper} from './HVBlurHelper'
export {MaterialPreviewGenerator} from './MaterialPreviewGenerator'
export {snapObject} from './snapObject'
export {ViewHelper2, type GizmoOrientation, type DomPlacement} from './ViewHelper2'

// export {} from './constants'

+ 76
- 0
src/three/utils/snapObject.ts Просмотреть файл

@@ -0,0 +1,76 @@
import {Object3D, PerspectiveCamera, Scene, Vector3} from 'three'
import {Box3B} from '../math/Box3B'
import {IWebGLRenderer} from '../../core'

/**
* Returns a snapshot of the object.
* Does a simple render, does not run the full pipeline.
*
* Ideally, call this from preRender and object must be in root, for usage see {@link MaterialPreviewGenerator}.
* @param renderer
* @param object
* @param root
* @param channel
* @param camOffset
* @param camera
*/
export function snapObject(
renderer: IWebGLRenderer,
object: Object3D,
root?: Scene,
channel = 7,
camOffset = new Vector3(0, 0, 1.5),
camera = new PerspectiveCamera(45, 1, 0.1, 1000)
): string {
const oldVisible = object.visible
object.visible = true

const bbox = new Box3B().expandByObject(object, true, true)
const center = bbox.getCenter(new Vector3())
const bboxSize = bbox.getSize(new Vector3())
camera.position.copy(center).add(camOffset.clone().multiplyScalar(Math.max(bboxSize.x, bboxSize.y, bboxSize.z)))
camera.lookAt(center)

if (object) {
object.traverseVisible(obj => {
obj.layers.enable(channel)
})
// console.log((object as any).material)
}
if (channel > 0)
camera.layers.set(channel)
else
camera.layers.enableAll()

// scene.environment = this.viewer.scene.getEnvironment() as any

renderer.setRenderTarget(null)

renderer.clear()
if (typeof renderer.renderWithModes === 'function') {
renderer.renderWithModes({
backgroundRender: false,
// mainRenderPass: false,
// screenSpaceRendering: false,
// shadowMapRender: false,
}, ()=>{
renderer.render(root ?? object, camera)
})
} else {
renderer.render(root ?? object, camera)
}

// renderer.setRenderTarget(target)
// this._renderer.render(root, camera)
// todo use webp when possible.
const snap = renderer.domElement.toDataURL('image/png')

renderer.clear()

object.visible = oldVisible
object.traverseVisible(obj => {
obj.layers.disable(channel)
})
camera.layers.enableAll()
return snap
}

Загрузка…
Отмена
Сохранить