Bläddra i källkod

Add VirtualCamerasPlugin and examples, fix autoAspect initialization, Add renderCamera in RootScene indicating the current render camera, Add onEnd in popmotion AnimationOptions, support target for every camera in ProgressivePlugin, Add renderToScreen option in RenderManager.render, Add '*' event in ThreeViewer, Add HierarchyUiPlugin in tweakpane-editor

master
Palash Bansal 2 år sedan
förälder
incheckning
d3810941aa
Inget konto är kopplat till bidragsgivarens mejladress

+ 196
- 5
README.md Visa fil

@@ -40,7 +40,7 @@ To make changes and run the example, click on the CodePen button on the top righ
## Table of Contents

- [ThreePipe](#threepipe)
- [Examples](#examples)
- [Examples](https://threepipe.org/examples/)
- [Table of Contents](#table-of-contents)
- [Getting Started](#getting-started)
- [HTML/JS Quickstart (CDN)](#htmljs-quickstart-cdn)
@@ -101,7 +101,12 @@ To make changes and run the example, click on the CodePen button on the top righ
- [VignettePlugin](#vignetteplugin) - Add Vignette effect by patching the final screen pass
- [ChromaticAberrationPlugin](#chromaticaberrationplugin) - Add Chromatic Aberration effect by patching the final screen pass
- [FilmicGrainPlugin](#filmicgrainplugin) - Add Filmic Grain effect by patching the final screen pass
- [NoiseBumpMaterialPlugin](#noisebumpmaterialplugin) - Sparkle Bump/Noise Bump material extension for PhysicalMaterial
- [CustomBumpMapPlugin](#custombumpmapplugin) - Custom Bump Map material extension for PhysicalMaterial
- [ClearcoatTintPlugin](#clearcoattintplugin) - Clearcoat Tint material extension for PhysicalMaterial
- [FragmentClippingExtensionPlugin](#fragmentclippingextensionplugin) - Fragment/SDF Clipping material extension for PhysicalMaterial
- [HDRiGroundPlugin](#hdrigroundplugin) - Add support for ground projected hdri/skybox to the webgl background shader.
- [VirtualCamerasPlugin](#virtualcamerasplugin) - Add support for rendering virtual cameras before the main one every frame.
- [Rhino3dmLoadPlugin](#rhino3dmloadplugin) - Add support for loading .3dm files
- [PLYLoadPlugin](#plyloadplugin) - Add support for loading .ply files
- [STLLoadPlugin](#stlloadplugin) - Add support for loading .stl files
@@ -2232,9 +2237,10 @@ const anim1 = popmotion.animate({
},
onComplete: () => isMovedUp = true,
onStop: () => throw(new Error('Animation stopped')),
onEnd: () => console.log('Animation ended'), // This runs after both onComplete and onStop
})

// await for animation. This promise will reject only if an exception is thrown in onStop
// await for animation. This promise will reject only if an exception is thrown in onStop or onComplete. onStop rejects if throwOnStop is true
await anim.promise.catch((e)=>{
console.log(e, 'animation stopped before completion')
});
@@ -2481,15 +2487,163 @@ filmicGrainPlugin.intensity = 10
filmicGrainPlugin.multiply = false
```

## NoiseBumpMaterialPlugin

[//]: # (todo: image)

Example: https://threepipe.org/examples/#noise-bump-material-plugin/

Source Code: [src/plugins/material/NoiseBumpMaterialPlugin.ts](./src/plugins/material/NoiseBumpMaterialPlugin.ts)

API Reference: [NoiseBumpMaterialPlugin](https://threepipe.org/docs/classes/NoiseBumpMaterialPlugin.html)

NoiseBumpMaterialPlugin adds a material extension to PhysicalMaterial to add support for sparkle bump / noise bump by creating procedural bump map from noise to simulate sparkle flakes.
It uses voronoise function from blender along with several additions to generate the noise for the generation.
It also adds a UI to the material to edit the settings.
It uses `WEBGI_materials_noise_bump` glTF extension to save the settings in glTF/glb files.

```typescript
import {ThreeViewer, NoiseBumpMaterialPlugin} from 'threepipe'

const viewer = new ThreeViewer({...})

const noiseBump = viewer.addPluginSync(NoiseBumpMaterialPlugin)

// Add noise bump to a material
NoiseBumpMaterialPlugin.AddNoiseBumpMaterial(material, {
flakeScale: 300,
})

// Change properties with code or use the UI
material.userData._noiseBumpMat!.bumpNoiseParams = [1, 1]
material.setDirty()

// Disable
material.userData._noiseBumpMat!.hasBump = false
material.setDirty()
```

## CustomBumpMapPlugin

[//]: # (todo: image)

Example: https://threepipe.org/examples/#custom-bump-map-plugin/

Source Code: [src/plugins/material/CustomBumpMapPlugin.ts](./src/plugins/material/CustomBumpMapPlugin.ts)

API Reference: [CustomBumpMapPlugin](https://threepipe.org/docs/classes/CustomBumpMapPlugin.html)

CustomBumpMapPlugin adds a material extension to PhysicalMaterial to support custom bump maps.
A Custom bump map is similar to the built-in bump map, but allows using an extra bump map and scale to give a combined effect.
This plugin also has support for bicubic filtering of the custom bump map and is enabled by default.
It also adds a UI to the material to edit the settings.
It uses `WEBGI_materials_custom_bump_map` glTF extension to save the settings in glTF/glb files.

```typescript
import {ThreeViewer, CustomBumpMapPlugin} from 'threepipe'

const viewer = new ThreeViewer({...})

const customBump = viewer.addPluginSync(CustomBumpMapPlugin)

// Add noise bump to a material
customBump.enableCustomBump(material, bumpMap, 0.2)

// Change properties with code or use the UI
material.userData._customBumpMat = texture
material.setDirty()

// Disable
material.userData._hasCustomBump = false
// or
material.userData._customBumpMat = null
material.setDirty()
```

## ClearcoatTintPlugin

[//]: # (todo: image)

Example: https://threepipe.org/examples/#clearcoat-tint-plugin/

Source Code: [src/plugins/material/ClearcoatTintPlugin.ts](./src/plugins/material/ClearcoatTintPlugin.ts)

API Reference: [ClearcoatTintPlugin](https://threepipe.org/docs/classes/ClearcoatTintPlugin.html)

ClearcoatTintPlugin adds a material extension to PhysicalMaterial which adds tint and thickness to the built-in clearcoat properties.
It also adds a UI to the material to edit the settings.
It uses `WEBGI_materials_clearcoat_tint` glTF extension to save the settings in glTF/glb files.

```typescript
import {ThreeViewer, ClearcoatTintPlugin} from 'threepipe'

const viewer = new ThreeViewer({...})

const clearcoatTint = viewer.addPluginSync(ClearcoatTintPlugin)

material.clearcoat = 1
// add initial properties
ClearcoatTintPlugin.AddClearcoatTint(material, {
tintColor: '#ff0000',
thickness: 1,
})

// Change properties with code or use the UI
material.userData._clearcoatTint!.tintColor = '#ff0000'
material.setDirty()

// Disable
material.userData._clearcoatTint.enableTint = false
material.setDirty()
```

## FragmentClippingExtensionPlugin

[//]: # (todo: image)

Example: https://threepipe.org/examples/#fragment-clipping-extension-plugin/

Source Code: [src/plugins/materials/FragmentClippingExtensionPlugin.ts](./src/plugins/material/FragmentClippingExtensionPlugin.ts)

API Reference: [FragmentClippingExtensionPlugin](https://threepipe.org/docs/classes/FragmentClippingExtensionPlugin.html)

FragmentClippingExtensionPlugin adds a material extension to PhysicalMaterial to add support for fragment clipping.
Fragment clipping allows to clip fragments of the material in screen space or world space based on a circle, rectangle, plane, sphere, etc.
It uses fixed SDFs with params defined by the user for clipping.
It also adds a UI to the material to edit the settings.
It uses `WEBGI_materials_fragment_clipping_extension` glTF extension to save the settings in glTF/glb files.

```typescript
import {ThreeViewer, FragmentClippingExtensionPlugin} from 'threepipe'

const viewer = new ThreeViewer({...})

const fragmentClipping = viewer.addPluginSync(FragmentClippingExtensionPlugin)

// add initial properties
FragmentClippingExtensionPlugin.AddFragmentClipping(material, {
clipPosition: new Vector4(0.5, 0.5, 0, 0),
clipParams: new Vector4(0.1, 0.05, 0, 1),
})

// Change properties with code or use the UI
material.userData._fragmentClipping!.clipPosition.set(0, 0, 0, 0)
material.setDirty()

// Disable
material.userData._clearcoatTint.clipEnabled = false
material.setDirty()
```

## HDRiGroundPlugin

[//]: # (todo: image)

Example: https://threepipe.org/examples/#hdri-ground-plugin/

Source Code: [src/plugins/pipeline/HDRiGroundPlugin.ts](./src/plugins/extras/HDRiGroundPlugin.ts)
Source Code: [src/plugins/extras/HDRiGroundPlugin.ts](./src/plugins/extras/HDRiGroundPlugin.ts)

API Reference: [FrameFadePlugin](https://threepipe.org/docs/classes/HDRiGroundPlugin.html)
API Reference: [HDRiGroundPlugin](https://threepipe.org/docs/classes/HDRiGroundPlugin.html)

HDRiGroundPlugin patches the background shader in the renderer to add support for ground projected environment map/skybox. Works simply by setting the background same as the environemnt and enabling the plugin.

@@ -2499,7 +2653,7 @@ The plugin is disabled by default when added. Set `.enabled` to enable it or pas
If the background is not the same as the environment when enabled, the user will be prompted for this, unless `promptOnBackgroundMismatch` is set to `false` in the plugin.

```typescript
import {ThreeViewer, FrameFadePlugin} from 'threepipe'
import {ThreeViewer, HDRiGrounPlugin} from 'threepipe'

const viewer = new ThreeViewer({...})

@@ -2518,6 +2672,43 @@ hdriGround.enabled = true

Check the [example](https://threepipe.org/examples/#hdri-ground-plugin/) for a demo.

## VirtualCamerasPlugin

[//]: # (todo: image)

Example: https://threepipe.org/examples/#virtual-cameras-plugin/

Source Code: [src/plugins/rendering/VirtualCamerasPlugin.ts](./src/plugins/rendering/VirtualCamerasPlugin.ts)

API Reference: [VirtualCamerasPlugin](https://threepipe.org/docs/classes/VirtualCamerasPlugin.html)

VirtualCamerasPlugin adds support for rendering to multiple virtual cameras in the viewer. These cameras are rendered in preRender callback just before the main camera is rendered. The virtual cameras can be added to the plugin and removed from it.

The feed to the virtual camera is rendered to a Render Target texture which can be accessed and re-rendered in the scene or used in other plugins.

```typescript
import {ThreeViewer, VirtualCamerasPlugin} from 'threepipe'

const viewer = new ThreeViewer({...})

const hdriGround = viewer.addPluginSync(new VirtualCamerasPlugin())

const camera = new PerspectiveCamera2('orbit', viewer.canvas, false, 45, 1)
camera.name = name
camera.position.set(0, 5, 0)
camera.target.set(0, 0.25, 0)
camera.userData.autoLookAtTarget = true // automatically look at the target (in setDirty)
camera.setDirty()
camera.addEventListener('update', ()=>{
viewer.setDirty() // if the camera is not added to the scene it wont update automatically when camera.setDirty is called(like from the UI)
})

const vCam = virtualCameras.addCamera(camera)
console.log(vCam.target) // target is a WebGLRenderTarget/IRenderTarget
```

Check the [virtual camera](https://threepipe.org/examples/#hdri-ground-plugin/) example for using the texture in the scene.

## Rhino3dmLoadPlugin

Example: https://threepipe.org/examples/#rhino3dm-load/

+ 9
- 0
examples/index.html Visa fil

@@ -231,6 +231,8 @@
<li><a href="./depth-buffer-plugin/">Depth Buffer Plugin </a></li>
<li><a href="./normal-buffer-plugin/">Normal Buffer Plugin </a></li>
<li><a href="./custom-pipeline/">Custom Pipeline specification </a></li>
<li><a href="./virtual-cameras-plugin/">Virtual Cameras Plugin </a></li>
<li><a href="./virtual-camera/">Virtual Camera (Animated) </a></li>
</ul>
<h2 class="category">Interaction</h2>
<ul>
@@ -279,6 +281,13 @@
<li><a href="./gltf-camera-animation/">glTF Camera Animation </a></li>
<li><a href="./gltf-animation-page-scroll/">glTF Animation Page Scroll </a></li>
</ul>
<h2 class="category">Materials</h2>
<ul>
<li><a href="./clearcoat-tint-plugin/">Clearcoat Tint Plugin</a></li>
<li><a href="./fragment-clipping-extension-plugin/">Fragment Clipping Extension Plugin </a></li>
<li><a href="./noise-bump-material-plugin/">SparkleBump(NoiseBump) Material Plugin </a></li>
<li><a href="./custom-bump-map-plugin/">Custom Bump Map Plugin </a></li>
</ul>
<h2 class="category">Utils</h2>
<ul>
<li><a href="./hdri-ground-plugin/">HDRi Ground Plugin <br/>(Projected Skybox)</a></li>

+ 14
- 3
examples/tweakpane-editor/script.ts Visa fil

@@ -2,9 +2,12 @@ import {
_testFinish,
CameraViewPlugin,
ChromaticAberrationPlugin,
ClearcoatTintPlugin,
CustomBumpMapPlugin,
DepthBufferPlugin,
DropzonePlugin,
FilmicGrainPlugin,
FragmentClippingExtensionPlugin,
FrameFadePlugin,
FullScreenPlugin,
GLTFAnimationPlugin,
@@ -13,6 +16,7 @@ import {
HemisphereLight,
KTX2LoadPlugin,
KTXLoadPlugin,
NoiseBumpMaterialPlugin,
NormalBufferPlugin,
PickingPlugin,
PLYLoadPlugin,
@@ -26,9 +30,10 @@ import {
USDZLoadPlugin,
ViewerUiConfigPlugin,
VignettePlugin,
VirtualCamerasPlugin,
} from 'threepipe'
import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane'
import {TweakpaneEditorPlugin} from '@threepipe/plugin-tweakpane-editor'
import {HierarchyUiPlugin, TweakpaneEditorPlugin} from '@threepipe/plugin-tweakpane-editor'
import {BlendLoadPlugin} from '@threepipe/plugin-blend-importer'
import {extraImportPlugins} from '@threepipe/plugin-extra-importers'

@@ -56,6 +61,11 @@ async function init() {
PickingPlugin,
CameraViewPlugin,
ViewerUiConfigPlugin,
ClearcoatTintPlugin,
FragmentClippingExtensionPlugin,
NoiseBumpMaterialPlugin,
CustomBumpMapPlugin,
VirtualCamerasPlugin,
// new SceneUiConfigPlugin(), // this is already in ViewerUiPlugin
new DepthBufferPlugin(HalfFloatType, true, true),
new NormalBufferPlugin(HalfFloatType, false),
@@ -72,6 +82,7 @@ async function init() {
STLLoadPlugin,
USDZLoadPlugin,
BlendLoadPlugin,
HierarchyUiPlugin,
...extraImportPlugins,
])

@@ -81,11 +92,11 @@ async function init() {

editor.loadPlugins({
['Viewer']: [ViewerUiConfigPlugin, SceneUiConfigPlugin, DropzonePlugin, FullScreenPlugin],
['Interaction']: [PickingPlugin],
['Interaction']: [HierarchyUiPlugin, PickingPlugin],
['GBuffer']: [DepthBufferPlugin, NormalBufferPlugin],
['Post-processing']: [TonemapPlugin, ProgressivePlugin, FrameFadePlugin, VignettePlugin],
['Animation']: [GLTFAnimationPlugin, CameraViewPlugin],
['Extras']: [HDRiGroundPlugin, Rhino3dmLoadPlugin],
['Extras']: [HDRiGroundPlugin, Rhino3dmLoadPlugin, ClearcoatTintPlugin, FragmentClippingExtensionPlugin, NoiseBumpMaterialPlugin, CustomBumpMapPlugin, VirtualCamerasPlugin],
['Debug']: [RenderTargetPreviewPlugin],
})


+ 36
- 0
examples/virtual-camera/index.html Visa fil

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Virtual Camera</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"
}
}

</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>

+ 69
- 0
examples/virtual-camera/script.ts Visa fil

@@ -0,0 +1,69 @@
import {
_testFinish,
IObject3D,
Mesh,
PerspectiveCamera2,
PhysicalMaterial,
PlaneGeometry,
PopmotionPlugin,
ProgressivePlugin,
Texture,
ThreeViewer,
VirtualCamerasPlugin,
} from 'threepipe'

async function init() {

const viewer = new ThreeViewer({
canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
debug: true,
plugins: [new ProgressivePlugin(16)],
})
const virtualCameras = viewer.addPluginSync(VirtualCamerasPlugin)
const popmotion = viewer.addPluginSync(PopmotionPlugin)

await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr')
await viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', {
autoCenter: true,
autoScale: true,
})

const ground = new Mesh(
new PlaneGeometry(5, 5)
.translate(0, 0, -4),
new PhysicalMaterial({
color: '#ffffff',
})
)
ground.castShadow = false
ground.receiveShadow = true
viewer.scene.addObject(ground)

const camera = new PerspectiveCamera2('', viewer.canvas, false, 45, 1)
camera.position.set(0, 0, 5)
camera.target.set(0, 0.25, 0)
camera.userData.autoLookAtTarget = true
camera.near = 1
camera.far = 10
camera.setDirty()
const vCam = virtualCameras.addCamera(camera)
ground.material.map = vCam.target.texture as Texture

popmotion.animate({
from: 0,
to: 1,
repeat: Infinity,
duration: 6000,
onUpdate: (v)=>{
// Set camera position xz in a circle around the target
const angle = v * Math.PI * 2 + Math.PI / 2
const radius = 5
camera.position.set(Math.cos(angle) * radius, 0, Math.sin(angle) * radius)
camera.setDirty()
viewer.setDirty() // since camera is not in the scene
},
})

}

init().then(_testFinish)

+ 36
- 0
examples/virtual-cameras-plugin/index.html Visa fil

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Virtual Cameras 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"
}
}

</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>

+ 69
- 0
examples/virtual-cameras-plugin/script.ts Visa fil

@@ -0,0 +1,69 @@
import {
_testFinish,
HemisphereLight,
IObject3D,
PerspectiveCamera2,
ProgressivePlugin,
RenderTargetPreviewPlugin,
ThreeViewer,
Vector3,
VirtualCamerasPlugin,
} from 'threepipe'
import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane'

async function init() {

const viewer = new ThreeViewer({
canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
debug: true,
plugins: [new ProgressivePlugin(16)],
})
const virtualCameras = viewer.addPluginSync(VirtualCamerasPlugin)

viewer.scene.addObject(new HemisphereLight(0xffffff, 0x444444, 10))
await viewer.load<IObject3D>('https://threejs.org/examples/models/fbx/Samba Dancing.fbx', {
autoCenter: true,
autoScale: true,
})

viewer.scene.mainCamera.position.set(5, 5, 5)
viewer.scene.mainCamera.target.set(0, 0.25, 0)
viewer.scene.mainCamera.setDirty()

const views = [
[new Vector3(5, 0, 0), 'right'],
[new Vector3(0, 5, 0), 'top'],
[new Vector3(0, 0, 5), 'front'],
] as const

const rt = viewer.addPluginSync(RenderTargetPreviewPlugin)

const ui = viewer.addPluginSync(TweakpaneUiPlugin, true)
ui.appendChild(viewer.scene.mainCamera.uiConfig)

for (const [view, name] of views) {
const camera = new PerspectiveCamera2('', viewer.canvas, false, 45, 1)
camera.name = name
camera.position.copy(view)
camera.target.set(0, 0.25, 0)
camera.userData.autoLookAtTarget = true
camera.setDirty()
camera.addEventListener('update', ()=>{
viewer.setDirty() // since the camera is not added to the scene it wont update automatically when setDirty is called(like from the UI)
})

viewer.scene.mainCamera.addEventListener('update', ()=>{
camera.target.copy(viewer.scene.mainCamera.target) // sync the lookAt target of all the cameras
camera.setDirty()
})

const vCam = virtualCameras.addCamera(camera)

rt.addTarget(()=>vCam.target, name, false, false, true)

ui.appendChild(camera.uiConfig)
}

}

init().then(_testFinish)

+ 2
- 1
plugins/tweakpane-editor/package.json Visa fil

@@ -7,7 +7,8 @@
},
"dependencies": {
"threepipe": "file:./../../src/",
"@threepipe/plugin-tweakpane": "file:./../tweakpane/src/"
"@threepipe/plugin-tweakpane": "file:./../tweakpane/src/",
"treejs": "git://github.com/repalash/treejs.git#d303016bb74e75725d13e97291ac1d4727985918"
},
"clean-package": {
"remove": [

+ 139
- 0
plugins/tweakpane-editor/src/HierarchyUiPlugin.ts Visa fil

@@ -0,0 +1,139 @@
import {createDiv, createStyles, css, timeout} from 'ts-browser-helpers'
import Tree from 'treejs'
import {AViewerPluginSync, IObject3D, Object3D, ThreeViewer} from 'threepipe'
import {UiObjectConfig} from 'uiconfig.js'

export class HierarchyUiPlugin extends AViewerPluginSync<''> {
enabled = true
public static readonly PluginType = 'HierarchyUiPlugin'

toJSON: any = undefined

treeView: any = undefined

hierarchyDiv = createDiv({
innerHTML: '',
id: 'tpHierarchyContainer',
addToBody: false,
})

constructor(enabled = true) {
super()
this.enabled = enabled
this.reset = this.reset.bind(this)
this._postFrame = this._postFrame.bind(this)
this.uiConfig.domChildren = [this.hierarchyDiv]
createStyles(css`
#tpHierarchyContainer{
width: 100%;
height: auto;
background-color: transparent;
color: #e9e9ed;
margin-top: 0;
}
`)

}

reset(e?: any) {
if (e?.fromHierarchyPlugin) return // for infinite loop
if (!e?.hierarchyChanged) return
this._needsReset = true
}
protected async _reset() {
this._needsReset = false

while (this.hierarchyDiv.firstChild) this.hierarchyDiv.firstChild.remove()

const obj = this._viewer?.scene.modelRoot
if (!obj) return

const data = obj.children.reduce(this._buildData, [])
const visible = obj.children.reduce(this._findVisible, [])
let firstChange = false
return new Promise<void>((resolve) => {
this.treeView = new Tree(this.hierarchyDiv, {
closeDepth: 1,
data,
// values: visible, // uuids of visible nodes
loaded: function() {
this.values = visible
resolve()
},
onChange: () => {
if (!firstChange) { // first time called when loaded
firstChange = true
return
}
timeout(200).then(() => { // wait for the UI to update
if (this.treeView)
this._setVisible(this.treeView.values)
})
},
onItemLabelClick: (item: any) => {
const obj1 = this._viewer?.scene.modelRoot.getObjectByProperty('uuid', item)
if (!obj1 || !obj.visible) return
obj1.dispatchEvent({type: 'select', value: obj1, ui: true})
},
})
})
}

onAdded(viewer: ThreeViewer) {
super.onAdded(viewer)
this.reset()
viewer.scene.addEventListener('sceneUpdate', this.reset)
viewer.addEventListener('postFrame', this._postFrame)
}

onRemove(viewer: ThreeViewer) {
// todo: remove UI element.
viewer.scene.removeEventListener('sceneUpdate', this.reset)
viewer.removeEventListener('postFrame', this._postFrame)
return super.onRemove(viewer)
}

protected _needsReset = false
protected _postFrame() {
if (this._needsReset) this._reset()
}

dispose() {
// todo destroy UI element.
}

uiConfig: UiObjectConfig = {
type: 'folder',
label: 'Hierarchy',
children: [],
}

private _buildData = (data: any[], obj: IObject3D): any[] => {
data.push({
text: obj.name || 'unnamed',
id: obj.uuid,
children: obj.children.reduce(this._buildData, []),
})
return data
}
private _findVisible = (data: any[], obj: Object3D): any[] => { // only leaf.
if (!obj.visible) return data
if (obj.children.length < 1) data.push(obj.uuid)
else data.push(...obj.children.reduce(this._findVisible, []))
return data
}
private _setVisible = (values: string[]): void => {
this._viewer?.doOnce('postFrame', () => {
const obj = this._viewer?.scene.modelRoot
if (!obj || values === undefined || values === null) return
const ancestorSet: Set<Object3D> = new Set()
obj.traverse((o)=>{
if (o === obj) return
o.visible = values.includes(o.uuid)
if (o.visible) o.traverseAncestors(p=>ancestorSet.add(p))
})
ancestorSet.forEach(o=> o.visible = true)
this._viewer?.scene?.setDirty({refreshScene: true, fromHierarchyPlugin: true, updateGround: false})
})
}
}

+ 1
- 0
plugins/tweakpane-editor/src/index.ts Visa fil

@@ -1 +1,2 @@
export {TweakpaneEditorPlugin} from './TweakpaneEditorPlugin'
export {HierarchyUiPlugin} from './HierarchyUiPlugin'

+ 8
- 1
src/core/IScene.ts Visa fil

@@ -51,7 +51,7 @@ export interface AddObjectOptions {

// | string
export type ISceneEventTypes = IObject3DEventTypes | 'sceneUpdate' | 'addSceneObject' |
'mainCameraChange' | 'mainCameraUpdate' | 'environmentChanged' | 'backgroundChanged' |
'mainCameraChange' | 'mainCameraUpdate' | 'environmentChanged' | 'backgroundChanged' | 'renderCameraChange' |
'update' | // todo: deprecate, use 'sceneUpdate' instead
'textureAdded' | // todo remove
'activeCameraChange' | 'activeCameraUpdate' | // todo: deprecate
@@ -80,7 +80,14 @@ export interface IScene<E extends ISceneEvent = ISceneEvent, ET extends ISceneEv
extends Scene<E, ET>, IObject3D<E, ET>, IShaderPropertiesUpdater {
readonly visible: boolean;
readonly isScene: true;
/**
* Main camera that the user controls
*/
mainCamera: ICamera;
/**
* Camera that in currently being rendered.
*/
renderCamera: ICamera;
type: 'Scene';

toJSON(): any; // todo

+ 1
- 1
src/core/camera/PerspectiveCamera2.ts Visa fil

@@ -104,7 +104,7 @@ export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera {
constructor(controlsMode?: TCameraControlsMode, domElement?: HTMLCanvasElement, autoAspect?: boolean, fov?: number, aspect?: number) {
super(fov, aspect)
this._canvas = domElement
this.autoAspect = autoAspect || !!domElement
this.autoAspect = autoAspect ?? !!domElement

iCameraCommons.upgradeCamera.call(this) // todo: test if autoUpgrade = false works as expected if we call upgradeObject3D externally after constructor, because we have setDirty, refreshTarget below.


+ 10
- 0
src/core/object/RootScene.ts Visa fil

@@ -101,6 +101,16 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I
this.setDirty()
}

private _renderCamera: ICamera | undefined
get renderCamera() {
return this._renderCamera ?? this.mainCamera
}
set renderCamera(camera: ICamera) {
const cam = this._renderCamera
this._renderCamera = camera
this.dispatchEvent({type: 'renderCameraChange', lastCamera: cam, camera})
}

/**
* Create a scene instance. This is done automatically in the {@link ThreeViewer} and must not be created separately.
* @param camera

+ 3
- 0
src/plugins/animation/CameraViewPlugin.ts Visa fil

@@ -280,6 +280,9 @@ export class CameraViewPlugin extends AViewerPluginSync<'viewChange'|'startViewC
})

this._animating = false

this._viewer?.setDirty()

this.dispatchEvent({type: 'viewChange', view})

await timeout(10)

+ 12
- 0
src/plugins/animation/PopmotionPlugin.ts Visa fil

@@ -164,6 +164,15 @@ export class PopmotionPlugin extends AViewerPluginSync<''> {
}
this.animations[uuid] = a
a.promise = new Promise<void>((resolve, reject) => {
const end2 = ()=>{
try {
options.onEnd && options.onEnd()
} catch (e: any) {
reject(e)
return false
}
return true
}
const opts: AnimationOptions<V> = {
driver: this.defaultDriver,
...options,
@@ -171,15 +180,18 @@ export class PopmotionPlugin extends AViewerPluginSync<''> {
try {
options.onComplete && options.onComplete()
} catch (e: any) {
if (!end2()) return
reject(e)
return
}
if (!end2()) return
resolve()
},
onStop: ()=>{
try {
options.onStop && options.onStop()
} catch (e: any) {
if (!end2()) return
reject(e)
return
}

+ 3
- 0
src/plugins/index.ts Visa fil

@@ -48,5 +48,8 @@ export {NoiseBumpMaterialPlugin} from './material/NoiseBumpMaterialPlugin'
export {CustomBumpMapPlugin} from './material/CustomBumpMapPlugin'
export {FragmentClippingExtensionPlugin, FragmentClippingMode} from './material/FragmentClippingExtensionPlugin'

// extras
export {VirtualCamerasPlugin} from './rendering/VirtualCamerasPlugin'

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

+ 4
- 1
src/plugins/pipeline/FrameFadePlugin.ts Visa fil

@@ -153,7 +153,10 @@ export class FrameFadePlugin
}

get canFrameFade() {
return this._target && this._pointerEnabled && this.enabled && this.dirty && this._pass && this._pass.fadeTimeState > 0.001
return this._target && this._pointerEnabled &&
this.enabled && this.dirty && this._pass &&
this._pass.fadeTimeState > 0.001 &&
this._viewer && this._viewer.scene.renderCamera === this._viewer.scene.mainCamera
}

get lastFrame() {

+ 56
- 23
src/plugins/pipeline/ProgressivePlugin.ts Visa fil

@@ -5,7 +5,7 @@ import {PipelinePassPlugin} from '../base/PipelinePassPlugin'
import {uiFolderContainer, uiImage, uiInput} from 'uiconfig.js'
import {ICamera, IRenderManager, IScene, IWebGLRenderer} from '../../core'
import {AddBlendTexturePass} from '../../postprocessing/AddBlendTexturePass'
import {serialize, ValOrFunc} from 'ts-browser-helpers'
import {getOrCall, serialize, ValOrFunc} from 'ts-browser-helpers'

export type ProgressivePluginEventTypes = ''
export type ProgressivePluginTarget = WebGLRenderTarget
@@ -24,12 +24,33 @@ export class ProgressivePlugin
readonly passId = 'progressive'
public static readonly PluginType = 'ProgressivePlugin'

target?: ProgressivePluginTarget
protected _targets = new Map<string, ProgressivePluginTarget>()

@serialize() @uiInput('Frame count') maxFrameCount: number
// todo: deserialize jitter

@uiImage('Last Texture' /* {readOnly: true}*/) texture?: Texture
// @uiImage('Last Texture' /* {readOnly: true}*/) texture?: Texture

get texture(): Texture | undefined {
return this.target?.texture
}

get target(): ProgressivePluginTarget | undefined {
return this._viewer ? this._targets.get(this._viewer.scene.renderCamera.uuid) : undefined
}

getTarget(camera?: ICamera) {
return this._viewer ? this._targets.get((camera ? camera : this._viewer.scene.renderCamera).uuid) : undefined
}

get textures() {
return this._viewer ? Array.from(this._targets.values()).map(t => t.texture) : []
}

@uiImage('Last Texture' /* {readOnly: true}*/)
get mainTexture() {
return this._viewer ? this.getTarget(this._viewer.scene.mainCamera)?.texture : undefined
}

// @onChange2(ProgressivePlugin.prototype._createTarget)
// @uiDropdown('Buffer Type', threeConstMappings.TextureDataType.uiConfig)
@@ -46,30 +67,36 @@ export class ProgressivePlugin
this.bufferType = bufferType
}

protected _createTarget(recreate = true) {
protected _createTarget(camera?: ICamera, recreate = false) {
if (!this._viewer) return
if (recreate) this._disposeTarget()
if (!this.target) this.target = this._viewer.renderManager.composerTarget.clone(true) as WebGLRenderTarget

this.texture = this.target.texture
this.texture.name = 'progressiveLastBuffer'

if (this._pass) this._pass.target = this.target
camera = camera ?? this._viewer.scene.renderCamera
if (recreate) this._disposeTarget(camera)
if (this._targets.has(camera.uuid)) return this._targets.get(camera.uuid)
const target = this._viewer.renderManager.composerTarget.clone(true) as WebGLRenderTarget
target.texture.name = 'progressiveLastBuffer_' + camera.uuid
// target.texture.type = this.bufferType
this._targets.set(camera.uuid, target)
// if (this._pass) this._pass.target = this.target
return target
}

protected _disposeTarget() {
protected _disposeTarget(camera?: ICamera) {
if (!this._viewer) return
if (this.target) {
this._viewer.renderManager.disposeTarget(this.target)
this.target = undefined
if (!camera) {
this._targets.forEach((t) => this._viewer!.renderManager.disposeTarget(t))
this._targets.clear()
} else {
const t = this._targets.get(camera.uuid)
if (t) {
this._viewer!.renderManager.disposeTarget(t)
this._targets.delete(camera.uuid)
}
}
this.texture = undefined
}

protected _createPass() {
this._createTarget(true)
if (!this.target) throw new Error('ProgressivePlugin: target not created')
const pass = new ProgressiveBlendPass(this.passId, this.target)
// this._createTarget(true)
const pass = new ProgressiveBlendPass(this.passId, ()=>this.target ?? this._createTarget()) // todo: disposeTarget somewhere
pass.dirty = () => (this._viewer?.renderManager.frameCount || 0) < this.maxFrameCount // todo use isConverged function
return pass
}
@@ -109,14 +136,20 @@ class ProgressiveBlendPass extends AddBlendTexturePass implements IPipelinePass
after = ['render']
required = ['render']
dirty: ValOrFunc<boolean> = () => false
constructor(public readonly passId: IPassID, public target: WebGLRenderTarget) {
constructor(public readonly passId: IPassID, public target?: ValOrFunc<WebGLRenderTarget|undefined>) {
super()
}
render(renderer: IWebGLRenderer, writeBuffer: WebGLRenderTarget, readBuffer: WebGLRenderTarget, deltaTime: number, maskActive: boolean) {
if (!this.enabled) return
const target = getOrCall(this.target)
if (!target) {
console.warn('ProgressiveBlendPass: target not defined')
return
}
if (renderer.renderManager.frameCount < 1) {
this.needsSwap = false
if (readBuffer?.texture)
renderer.renderManager.blit(this.target, {
renderer.renderManager.blit(target, {
source: readBuffer.texture,
respectColorSpace: false,
})
@@ -124,7 +157,7 @@ class ProgressiveBlendPass extends AddBlendTexturePass implements IPipelinePass
}
this.needsSwap = true
super.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive)
renderer.renderManager.blit(this.target, {
renderer.renderManager.blit(target, {
source: writeBuffer.texture,
respectColorSpace: false,
})
@@ -140,7 +173,7 @@ class ProgressiveBlendPass extends AddBlendTexturePass implements IPipelinePass
this.uniforms.weight.value.set(f, f, f, f)
f = 1. - f
this.uniforms.weight2.value.set(f, f, f, f)
this.uniforms.tDiffuse2.value = this.target.texture
this.uniforms.tDiffuse2.value = getOrCall(this.target)?.texture
this.material.uniformsNeedUpdate = true
}


+ 58
- 0
src/plugins/rendering/VirtualCamerasPlugin.ts Visa fil

@@ -0,0 +1,58 @@
import {AViewerPluginSync} from '../../viewer'
import {IRenderTarget} from '../../rendering'
import {ICamera} from '../../core'
import {uiFolderContainer, uiToggle} from 'uiconfig.js'

export interface VirtualCamera {
camera: ICamera
target: IRenderTarget
enabled: boolean
}
@uiFolderContainer('Virtual Cameras')
export class VirtualCamerasPlugin extends AViewerPluginSync<''> {
public static readonly PluginType = 'VirtualCamerasPlugin'

@uiToggle()
enabled = true

toJSON: any = undefined // disable serialization

constructor(enabled = true) {
super()
this.enabled = enabled
}

cameras: VirtualCamera[] = []

protected _viewerListeners = {
preRender: () => {
if (!this.enabled || !this._viewer) return
const viewer = this._viewer
for (const v of this.cameras) {
if (!v.enabled) continue
const camera = v.camera
try {
viewer.scene.renderCamera = camera
viewer.renderManager.render(viewer.scene, false)
const source = viewer.renderManager.composer.readBuffer.texture
viewer.renderManager.blit(v.target, {source})
} catch (e: any) {
viewer.console.error(e)
v.enabled = false
if (viewer.debug) throw e
}
}
},
}

addCamera(camera: ICamera) {
if (!this._viewer) throw 'Plugin not added to viewer'
const target = this._viewer.renderManager.composerTarget.clone(true)
target.name = camera.name + '_virtualCamTarget'
const vCam: VirtualCamera = {camera, target, enabled: true}
this.cameras.push(vCam)
// todo: track for jitter in progressive or something else for jittering
return vCam
}

}

+ 5
- 1
src/postprocessing/ScreenPass.ts Visa fil

@@ -35,6 +35,10 @@ export class ScreenPass extends ExtendedShaderPass implements IPipelinePass<'scr
this.material.addEventListener('materialUpdate', this.setDirty)
}

/**
* Output Color Space
* Note: this is ignored when renderToScreen is false (it will take the color space of the render target)
*/
@uiDropdown('Output Color Space', threeConstMappings.ColorSpace.uiConfig, (t: ScreenPass)=>({onChange: t.setDirty}))
outputColorSpace: ColorSpace = SRGBColorSpace

@@ -43,7 +47,7 @@ export class ScreenPass extends ExtendedShaderPass implements IPipelinePass<'scr
render(renderer: IWebGLRenderer, writeBuffer?: WebGLMultipleRenderTargets | WebGLRenderTarget | null, readBuffer?: WebGLMultipleRenderTargets | WebGLRenderTarget, deltaTime?: number, maskActive?: boolean) {
const colorSpace = renderer.outputColorSpace
if (!writeBuffer || this.renderToScreen) renderer.outputColorSpace = this.outputColorSpace
else console.warn('ScreenPass: outputColorSpace is ignored when renderToScreen is false')
// else console.warn('ScreenPass: outputColorSpace is ignored when renderToScreen is false')
super.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive)
this._lastReadBuffer = readBuffer
renderer.outputColorSpace = colorSpace

+ 8
- 4
src/rendering/RenderManager.ts Visa fil

@@ -192,17 +192,21 @@ export class RenderManager extends RenderTargetManager<IRenderManagerEvent, IRen
// // todo gizmos
// }

render(scene: IScene): void {
render(scene: IScene, renderToScreen = true): void {
if (this._passesNeedsUpdate) {
this._refreshPipeline()
this.refreshPasses()
}
for (const pass of this._passes) {
if (pass.enabled && pass.beforeRender) pass.beforeRender(scene, scene.mainCamera, this)
if (pass.enabled && pass.beforeRender) pass.beforeRender(scene, scene.renderCamera, this)
}
this._composer.renderToScreen = renderToScreen
this._composer.render()
this._frameCount += 1
this._totalFrameCount += 1
this._composer.renderToScreen = true
if (renderToScreen) {
this._frameCount += 1
this._totalFrameCount += 1
}
this._dirty = false
}


+ 22
- 0
src/utils/animation.ts Visa fil

@@ -22,6 +22,14 @@ import {timeout} from 'ts-browser-helpers'
import {MathUtils} from 'three'

export {animate}

declare module 'popmotion'{
interface PlaybackOptions<V> {
// throwOnStop?: boolean; // instead of this, user can simply throw an error in onStop.
onEnd?: () => void;
}
}

export type {AnimationOptions, KeyframeOptions, Easing}

function easeInOutSine(x: number): number {
@@ -100,24 +108,38 @@ export async function animateTarget<V>(target: any, key: string, options: Animat
export async function animateAsync<V=number>(options: AnimationOptions<V>, animations?: AnimateResult[]) {
const complete = options.onComplete
const stop = options.onStop
const end = options.onEnd
options = {...options}
return new Promise<void>((resolve, reject) => {
const end2 = ()=>{
try {
end?.()
} catch (e: any) {
reject(e)
return false
}
return true
}
options.onComplete = ()=>{
try {
complete?.()
} catch (e: any) {
if (!end2()) return
reject(e)
return
}
if (!end2()) return
resolve()
}
options.onStop = ()=>{
try {
stop?.()
} catch (e: any) {
if (!end2()) return
reject(e)
return
}
if (!end2()) return
resolve()
}
const an = animate(options)

+ 15
- 2
src/viewer/AViewerPlugin.ts Visa fil

@@ -1,6 +1,7 @@
import type {ISerializedConfig, ThreeViewer} from './ThreeViewer'
import type {ISerializedConfig, IViewerEvent, ThreeViewer} from './ThreeViewer'
import {IViewerEventTypes} from './ThreeViewer'
import {Event, EventDispatcher} from 'three'
import {SerializationMetaType, ThreeSerialization} from '../utils'
import {PartialRecord, SerializationMetaType, ThreeSerialization} from '../utils'
import {IViewerPlugin, IViewerPluginAsync} from './IViewerPlugin'
import {UiObjectConfig} from 'uiconfig.js'

@@ -57,6 +58,14 @@ export abstract class AViewerPlugin<T extends string = string, TViewer extends T
else this.fromJSON?.(state)
}

protected _viewerListeners: PartialRecord<IViewerEventTypes, (e: Event)=>void> = {}
protected _onViewerEvent = (e: IViewerEvent)=> {
const et = e.eType
et && this._viewerListeners[et]?.(e)
return e
}


// todo: move to ThreeViewer
// storeState(prefix?: string, storage?: Storage, data?: any): void {
// storage = storage || (window ? window.localStorage : undefined)
@@ -114,9 +123,11 @@ export abstract class AViewerPluginSync<T extends string, TViewer extends ThreeV

onAdded(viewer: TViewer): void {
this._viewer = viewer
this._viewer.addEventListener('*', this._onViewerEvent)
}
onRemove(viewer: TViewer): void {
if (this._viewer !== viewer) viewer.console.error('Wrong viewer')
this._viewer?.removeEventListener('*', this._onViewerEvent)
this._viewer = undefined
}

@@ -131,9 +142,11 @@ export abstract class AViewerPluginAsync<T extends string, TViewer extends Three

async onAdded(viewer: TViewer): Promise<void> {
this._viewer = viewer
this._viewer.addEventListener('*', this._onViewerEvent)
}
async onRemove(viewer: TViewer): Promise<void> {
if (this._viewer !== viewer) viewer.console.error('Wrong viewer')
this._viewer?.removeEventListener('*', this._onViewerEvent)
this._viewer = undefined
}
}

+ 15
- 9
src/viewer/ThreeViewer.ts Visa fil

@@ -61,7 +61,9 @@ import {Easing} from 'popmotion'
import {OrbitControls3} from '../three'

export type IViewerEvent = BaseEvent & {
type: 'update'|'preRender'|'postRender'|'preFrame'|'postFrame'|'dispose'|'addPlugin'|'renderEnabled'|'renderDisabled'
type: '*'|'update'|'preRender'|'postRender'|'preFrame'|'postFrame'|'dispose'|'addPlugin'|'renderEnabled'|'renderDisabled'
eType?: '*'|'update'|'preRender'|'postRender'|'preFrame'|'postFrame'|'dispose'|'addPlugin'|'renderEnabled'|'renderDisabled'
[p: string]: any
}
export type IViewerEventTypes = IViewerEvent['type']
export interface ISerializedConfig {
@@ -612,14 +614,13 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes

this.dispatchEvent({type: 'preRender', target: this})

if (this.debug) this.renderManager.render(this._scene)
else {
try {
this.renderManager.render(this._scene)
} catch (e) {
this.console.error(e)
// this.enabled = false
}
try {
this._scene.renderCamera = this._scene.mainCamera
this.renderManager.render(this._scene)
} catch (e) {
this.console.error(e)
if (this.debug) throw e
// this.enabled = false
}

this.dispatchEvent({type: 'postRender', target: this})
@@ -1049,6 +1050,11 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
})
}

dispatchEvent(event: IViewerEvent) {
super.dispatchEvent(event)
super.dispatchEvent({...event, type: '*', eType: event.type})
}

private _setActiveCameraView(event: any = {}): void {
if (event.type === 'setView') {
if (!event.camera) {

Laddar…
Avbryt
Spara