Переглянути джерело

Add CanvasSnapshotPlugin, ContactShadowGroundPlugin, their examples. Add classes BaseGroundPlugin, FileTransferPlugin, HVBlurHelper, CanvasSnapshot. Add support for renderScale auto in viewer constructor.

master
Palash Bansal 2 роки тому
джерело
коміт
77a4228f46
Аккаунт користувача з таким Email не знайдено

+ 69
- 2
README.md Переглянути файл

@@ -95,8 +95,10 @@ To make changes and run the example, click on the CodePen button on the top righ
- [DepthBufferPlugin](#depthbufferplugin) - Pre-rendering of depth buffer
- [NormalBufferPlugin](#normalbufferplugin) - Pre-rendering of normal buffer
- [GBufferPlugin](#gbufferplugin) - Pre-rendering of depth-normal and flags buffers in a single pass
- [CanvasSnapshotPlugin](#canvassnapshotplugin) - Add support for taking snapshots of the canvas
- [PickingPlugin](#pickingplugin) - Adds support for selecting objects in the viewer with user interactions and selection widgets
- [TransformControlsPlugin](#transformcontrolsplugin) - Adds support for moving, rotating and scaling objects in the viewer with interactive widgets
- [ContactShadowGroundPlugin](#contactshadowgroundplugin) - Adds a ground plane at runtime with contact shadows
- [GLTFAnimationPlugin](#gltfanimationplugin) - Add support for playing and seeking gltf animations
- [PopmotionPlugin](#popmotionplugin) - Integrates with popmotion.io library for animation/tweening
- [CameraViewPlugin](#cameraviewplugin) - Add support for saving, loading, animating, looping between camera views
@@ -2227,7 +2229,6 @@ const normalTarget = normalPlugin.target;
// Use the normal target by accessing `normalTarget.texture`.
```


## GBufferPlugin

[//]: # (todo: image)
@@ -2250,6 +2251,51 @@ const normalDepth = gBufferPlugin.normalDepthTexture;
const gBufferFlags = gBufferPlugin.flagsTexture;
```

## CanvasSnapshotPlugin

[//]: # (todo: image)

[Example](https://threepipe.org/examples/#canvas-snapshot-plugin/) —
[Source Code](./src/plugins/export/CanvasSnapshotPlugin.ts) —
[API Reference](https://threepipe.org/docs/classes/CanvasSnapshotPlugin.html)

Canvas Snapshot Plugin adds support for taking snapshots of the canvas and exporting them as images and data urls. It includes options to take snapshot of a region, mime type, quality render scale and scaling the output image. Check out the interface [CanvasSnapshotOptions](https://threepipe.org/docs/interfaces/CanvasSnapshotOptions.html) for more details.

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

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

const snapshotPlugin = viewer.addPluginSync(new CanvasSnapshotPlugin())

// download a snapshot.
await snapshotPlugin.downloadSnapshot('image.webp', { // all parameters are optional
scale: 1, // scale the final image
timeout: 0, // wait before taking the snapshot, in ms
quality: 0.9, // quality of the image (0-1) only for jpeg and webp
displayPixelRatio: 2, // render scale
mimeType: 'image/webp', // mime type of the image
waitForProgressive: true, // wait for progressive rendering to finish (ProgressivePlugin). true by default
rect: { // region to take snapshot. eg. crop center of the canvas
height: viewer.canvas.clientHeight / 2,
width: viewer.canvas.clientWidth / 2,
x: viewer.canvas.clientWidth / 4,
y: viewer.canvas.clientHeight / 4,
},
})

// get data url (string)
const dataUrl = await snapshotPlugin.getDataUrl({ // all parameters are optional
displayPixelRatio: 2, // render scale
mimeType: 'image/webp', // mime type of the image
})

// get File
const file = await snapshotPlugin.getFile('file.jpeg', { // all parameters are optional
mimeType: 'image/jpeg', // mime type of the image
})
```

## PickingPlugin

[//]: # (todo: image)
@@ -2325,7 +2371,7 @@ pickingPlugin.addEventListener('hoverObjectChanged', (e)=>{

[Example](https://threepipe.org/examples/#transform-controls-plugin/) —
[Source Code](./src/plugins/interaction/TransformControlsPlugin.ts) —
[API Reference](https://threepipe.org/docs/classes/TransformControlsPlugin.html)
[API Reference](https://threepipe.org/docs/classes/TransformControlsPlugin.html)

Transform Controls Plugin adds support for moving, rotating and scaling objects in the viewer with interactive widgets.

@@ -2346,6 +2392,27 @@ const transfromControlsPlugin = viewer.addPluginSync(new TransformControlsPlugin
console.log(transfromControlsPlugin.transformControls)
```

## ContactShadowGroundPlugin

[//]: # (todo: image)

[Example](https://threepipe.org/examples/#contact-shadow-ground-plugin/) —
[Source Code](./src/plugins/extras/ContactShadowGroundPlugin.ts) —
[API Reference](https://threepipe.org/docs/classes/ContactShadowGroundPlugin.html)

Contact Shadow Ground Plugin adds a ground plane with three.js contact shadows to the viewer scene.

The plane is added to the scene root at runtime and not saved with scene export. Instead the plugin settings are saved with the scene.

It inherits from the base class [BaseGroundPlugin](https://threepipe.org/docs/classes/BaseGroundPlugin.html) which provides generic ground plane functionality. Check the source code for more details. With the property `autoAdjustTransform`, the ground plane is automatically adjusted based on the bounding box of the scene.

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

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

viewer.addPluginSync(new ContactShadowGroundPlugin())
```

## GLTFAnimationPlugin


+ 35
- 0
examples/canvas-snapshot-plugin/index.html Переглянути файл

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

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

+ 63
- 0
examples/canvas-snapshot-plugin/script.ts Переглянути файл

@@ -0,0 +1,63 @@
import {_testFinish, CanvasSnapshotPlugin, isWebpExportSupported, ThreeViewer} from 'threepipe'
import {createSimpleButtons} from '../examples-utils/simple-bottom-buttons.js'

const viewer = new ThreeViewer({
canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
msaa: true,
renderScale: 'auto',
})

async function init() {
const snapshotPlugin = viewer.addPluginSync(new CanvasSnapshotPlugin())

await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr')
await viewer.load('https://threejs.org/examples/models/gltf/kira.glb', {
autoCenter: true,
autoScale: true,
})

createSimpleButtons({
['Download snapshot (rect png)']: async(btn: HTMLButtonElement) => {
btn.disabled = true
await snapshotPlugin.downloadSnapshot('snapshot.png', {
scale: 1, // scale the final image
displayPixelRatio: 2, // render scale
mimeType: 'image/png', // mime type of the image
rect: { // region to take snapshot. Crop center of the canvas
height: viewer.canvas.clientHeight / 2,
width: viewer.canvas.clientWidth / 2,
x: viewer.canvas.clientWidth / 4,
y: viewer.canvas.clientHeight / 4,
},
})
btn.disabled = false
},
['Download snapshot (jpeg)']: async(btn: HTMLButtonElement) => {
btn.disabled = true
await snapshotPlugin.downloadSnapshot('snapshot.jpeg', {
mimeType: 'image/jpeg', // mime type of the image
quality: 0.9, // quality of the image (0-1) only for jpeg and webp
displayPixelRatio: 2, // render scale
})
btn.disabled = false
},
['Download snapshot (webp)']: async(btn: HTMLButtonElement) => {
btn.disabled = true
if (!isWebpExportSupported()) {
alert('WebP export is not supported in this browser, try the latest version of chrome, firefox or edge.')
btn.disabled = false
return
}
await snapshotPlugin.downloadSnapshot('snapshot.webp', {
mimeType: 'image/webp', // mime type of the image
scale: 1, // scale the final image
quality: 0.9, // quality of the image (0-1) only for jpeg and webp
displayPixelRatio: 2, // render scale
})
btn.disabled = false
},
})

}

init().then(_testFinish)

+ 36
- 0
examples/contact-shadow-ground-plugin/index.html Переглянути файл

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

+ 22
- 0
examples/contact-shadow-ground-plugin/script.ts Переглянути файл

@@ -0,0 +1,22 @@
import {_testFinish, ContactShadowGroundPlugin, IObject3D, ThreeViewer} from 'threepipe'
import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane'

async function init() {

const viewer = new ThreeViewer({
canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
})

viewer.addPluginSync(ContactShadowGroundPlugin)

await Promise.all([
viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr'),
viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf'),
])

const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true))
ui.setupPluginUi(ContactShadowGroundPlugin, {expanded: true})

}

init().then(_testFinish)

+ 2
- 0
examples/image-snapshot-export/script.ts Переглянути файл

@@ -6,6 +6,8 @@ const viewer = new ThreeViewer({
msaa: true,
})

// Note: see also: CanvasSnapshotPlugin

async function init() {

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

+ 2
- 0
examples/index.html Переглянути файл

@@ -265,6 +265,7 @@
</ul>
<h2 class="category">Export</h2>
<ul>
<li><a href="./canvas-snapshot-plugin/">Canvas Snapshot Plugin<br/>(Image Snapshot) </a></li>
<li><a href="./image-snapshot-export/">PNG, JPEG, WEBP Export<br/>(Image Snapshot) </a></li>
<li><a href="./render-target-export/">EXR, PNG, JPEG, WEBP Export<br/>(Render Target Export) </a></li>
<li><a href="./glb-export/">GLB Export </a></li>
@@ -299,6 +300,7 @@
</ul>
<h2 class="category">Utils</h2>
<ul>
<li><a href="./contact-shadow-ground-plugin/">Contact Shadow Ground Plugin</a></li>
<li><a href="./hdri-ground-plugin/">HDRi Ground Plugin <br/>(Projected Skybox)</a></li>
<li><a href="./render-target-preview/">Render Target Preview Plugin </a></li>
<li><a href="./object3d-generator-plugin/">Object3D Generator Plugin <br/>(Lights, Cameras)</a></li>

+ 7
- 0
examples/tweakpane-editor/script.ts Переглянути файл

@@ -1,8 +1,10 @@
import {
_testFinish,
CameraViewPlugin,
CanvasSnapshotPlugin,
ChromaticAberrationPlugin,
ClearcoatTintPlugin,
ContactShadowGroundPlugin,
CustomBumpMapPlugin,
DepthBufferPlugin,
DropzonePlugin,
@@ -48,6 +50,7 @@ async function init() {

const viewer = new ThreeViewer({
canvas: document.getElementById('mcanvas') as HTMLCanvasElement,
renderScale: 'auto',
msaa: true,
rgbm: true,
zPrepass: false, // set it to true if you only have opaque objects in the scene to get better performance.
@@ -97,6 +100,8 @@ async function init() {
Object3DWidgetsPlugin,
Object3DGeneratorPlugin,
GaussianSplattingPlugin,
ContactShadowGroundPlugin,
CanvasSnapshotPlugin,
...extraImportPlugins,
])

@@ -108,9 +113,11 @@ async function init() {

editor.loadPlugins({
['Viewer']: [ViewerUiConfigPlugin, SceneUiConfigPlugin, DropzonePlugin, FullScreenPlugin, TweakpaneUiPlugin],
['Scene']: [ContactShadowGroundPlugin],
['Interaction']: [HierarchyUiPlugin, TransformControlsPlugin, PickingPlugin, Object3DGeneratorPlugin, GeometryGeneratorPlugin, EditorViewWidgetPlugin, Object3DWidgetsPlugin],
['GBuffer']: [GBufferPlugin, DepthBufferPlugin, NormalBufferPlugin],
['Post-processing']: [TonemapPlugin, ProgressivePlugin, FrameFadePlugin, VignettePlugin, ChromaticAberrationPlugin, FilmicGrainPlugin],
['Export']: [CanvasSnapshotPlugin],
['Animation']: [GLTFAnimationPlugin, CameraViewPlugin],
['Extras']: [HDRiGroundPlugin, Rhino3dmLoadPlugin, ClearcoatTintPlugin, FragmentClippingExtensionPlugin, NoiseBumpMaterialPlugin, CustomBumpMapPlugin, VirtualCamerasPlugin],
['Debug']: [RenderTargetPreviewPlugin],

+ 304
- 0
src/plugins/base/BaseGroundPlugin.ts Переглянути файл

@@ -0,0 +1,304 @@
import {AViewerPluginSync, ThreeViewer} from '../../viewer'
import {IGeometry, iGeometryCommons, IMaterial, ISceneEvent, Mesh2, PhysicalMaterial, UnlitMaterial} from '../../core'
import {BufferAttribute, Euler, InterleavedBufferAttribute, PlaneGeometry, Vector3} from 'three'
import {onChange, onChange2, serialize} from 'ts-browser-helpers'
import {OrbitControls3} from '../../three'
import {uiConfig, uiFolderContainer, uiNumber, uiToggle} from 'uiconfig.js'

@uiFolderContainer('Ground')
export class BaseGroundPlugin<TEvent extends string = ''> extends AViewerPluginSync<TEvent> {
public static readonly PluginType: string = 'BaseGroundPlugin'

get enabled() {
return this.visible
}

set enabled(value) {
this.visible = value
}

protected _geometry: IGeometry&PlaneGeometry

protected _mesh: Mesh2<IGeometry&PlaneGeometry, IMaterial>
private _transformNeedRefresh = true

constructor() {
super()
this._refreshMaterial = this._refreshMaterial.bind(this)
this._refreshTransform = this._refreshTransform.bind(this)
this._refreshCameraLimits = this._refreshCameraLimits.bind(this)
this.refresh = this.refresh.bind(this)
this._refresh2 = this._refresh2.bind(this)
this._onSceneUpdate = this._onSceneUpdate.bind(this)
this._preRender = this._preRender.bind(this)
this._postFrame = this._postFrame.bind(this)
this._geometry = iGeometryCommons.upgradeGeometry.call(new PlaneGeometry(1, 1, 1, 1))
this._geometry.attributes.uv2 = (this._geometry.attributes.uv as any as BufferAttribute | InterleavedBufferAttribute).clone()
this._geometry.attributes.uv2.needsUpdate = true
this._mesh = this._createMesh()
this.refresh()
}

@uiToggle('Visible')
@onChange(BaseGroundPlugin.prototype.refreshTransform)
@serialize() visible = true

@uiNumber('Size')
@onChange2(BaseGroundPlugin.prototype._onSceneUpdate)
@serialize() size = 8

@uiNumber('Height (yOffset)')
@onChange2(BaseGroundPlugin.prototype._onSceneUpdate)
@serialize() yOffset = 0

@uiToggle('Render to Depth')
@onChange(BaseGroundPlugin.prototype._refresh2)
@serialize() renderToDepth = true

/**
* If false, the ground will not be tonemapped in post processing.
* note: this will only work when {@link GBufferPlugin} is being used. Also needs {@link renderToDepth} to be true.
*/
@uiToggle('Tonemap Ground')
@onChange(BaseGroundPlugin.prototype._refresh2)
@serialize() tonemapGround = true

/**
* If true, the camera will be limited to not go below the ground.
* note: this will only work when {@link OrbitControls3} or three.js OrbitControls are being used.
*/
@uiToggle('Limit Camera Above Ground')
@onChange(BaseGroundPlugin.prototype._refreshCameraLimits)
@serialize() limitCameraAboveGround = false

@uiToggle('Auto Adjust Transform')
@onChange(BaseGroundPlugin.prototype.refreshTransform)
@serialize() autoAdjustTransform = true

@serialize('material')
@uiConfig()
protected _material?: PhysicalMaterial

onAdded(viewer: ThreeViewer): void {
super.onAdded(viewer)
// if (viewer.getPlugin('TweakpaneUi')) console.error('TweakpaneUiPlugin must be added after Ground Plugin')

viewer.scene.addObject(this._mesh, {addToRoot: true})
viewer.scene.addEventListener('sceneUpdate', this._onSceneUpdate) // todo: refresh when update...
viewer.scene.addEventListener('addSceneObject', this._onSceneUpdate)
viewer.addEventListener('preRender', this._preRender)
viewer.addEventListener('postFrame', this._postFrame)
this.refresh()
}

onRemove(viewer: ThreeViewer): void {
this._mesh?.dispose(true)
this._removeMaterial()
viewer.scene.removeEventListener('sceneUpdate', this._onSceneUpdate)
viewer.scene.removeEventListener('addSceneObject', this._onSceneUpdate)
viewer.removeEventListener('postFrame', this._postFrame)
viewer.removeEventListener('preRender', this._preRender)
return super.onRemove(viewer)
}

protected _postFrame() {
if (this._transformNeedRefresh) this._refreshTransform()
if (!this._viewer) return
}

protected _preRender() {
if (!this._viewer) return
}

dispose(): void {
this._removeMaterial()
this._geometry.dispose()
this._material?.dispose() // todo
this._mesh?.dispose?.()
super.dispose()
}

protected _removeMaterial() {
if (!this._material) return
// this._manager?.materials?.unregisterMaterial(this._material)
this._material.userData.renderToDepth = this._material.userData.__renderToDepth
this._material.userData.__renderToDepth = undefined
// todo reset gBufferData.tonemapEnabled also
this._material = undefined
}

protected _onSceneUpdate(event?: ISceneEvent) {
if (event?.geometryChanged === false) return
if (event?.updateGround !== false)
this.refreshTransform()
}

/**
* Extra flag for plugins to disable transform refresh like when animating or dragging
*/
enableRefreshTransform = true

refreshTransform(): void {
if (!this.enableRefreshTransform) return
this._transformNeedRefresh = true
}

public refresh(): void {
if (!this._viewer) return
this._refreshMaterial()
this.refreshTransform()
this._refreshCameraLimits()
}

// because of inheritance breaks onChange
private _refresh2(): void {
this.refresh()
}


private _cameraLimitsSet = false
private _cameraLastMaxPolarAngle = Math.PI
private _refreshCameraLimits() {
const orbit = this._viewer?.scene.mainCamera.controls as OrbitControls3
if (!orbit) return
if (orbit.maxPolarAngle === undefined) {
console.warn('refreshCameraLimits only available with orbit controls.')
return
}
if (this.limitCameraAboveGround) {
if (!this._cameraLimitsSet) this._cameraLastMaxPolarAngle = orbit.maxPolarAngle
orbit.maxPolarAngle = Math.PI / 2
this._cameraLimitsSet = true
} else if (this._cameraLimitsSet) {
orbit.maxPolarAngle = this._cameraLastMaxPolarAngle
this._cameraLimitsSet = false
}

}

protected _refreshTransform() {
if (!this._mesh) return
if (!this._viewer) return
let updated = false
if (this.visible !== this._mesh.visible) {
this._mesh.visible = this.visible
updated = true
}
if (this.isDisabled()) {
if (updated) this._viewer?.scene.setDirty()
return
}
if (this.autoAdjustTransform) {
this._mesh.userData.bboxVisible = false
const bbox = this._viewer.scene.getBounds(true)
this._mesh.userData.bboxVisible = true
const v = bbox.getCenter(
new Vector3()).sub(new Vector3(0,
bbox.getSize(new Vector3()).y / 2 + this.yOffset,
0))
updated = updated || v.clone().sub(this._mesh.position).length() > 0.0001
if (updated) {
this._mesh.position.copy(v)
}
}
updated = updated || Math.abs(this._mesh.scale.x - this.size) > 0.0001
// todo: check rotation, someone could externally change it
if (updated) {
this._mesh.scale.setScalar(this.size)
// this._mesh.lookAt(new Vector3().fromArray(this._options.up))
this._mesh.setRotationFromEuler(new Euler(-Math.PI / 2., 0, this._mesh.rotation.z))
this._mesh.matrixWorldNeedsUpdate = true
this._mesh.setDirty()
// this._viewer.scene.setDirty()
}
this._transformNeedRefresh = false
}


protected _createMesh(mesh?: Mesh2<IGeometry&PlaneGeometry, IMaterial>): Mesh2<IGeometry&PlaneGeometry, IMaterial> {
if (!mesh) mesh = new Mesh2(this._geometry, new UnlitMaterial())
else mesh.geometry = this._geometry
if (mesh) {
mesh.userData.physicsMass = 0
mesh.userData.userSelectable = false
mesh.castShadow = true
mesh.receiveShadow = true
mesh.name = 'Ground Plane'
}
return mesh
}

protected _createMaterial(material?: PhysicalMaterial): PhysicalMaterial {
if (!material) material = new PhysicalMaterial({
name: 'BaseGroundMaterial',
color: 0xffffff,
})
material.userData.runtimeMaterial = true
return material
}

protected _refreshMaterial(): boolean {
if (!this._viewer) return false
if (this.isDisabled()) return false
const mat = this._material ?? this._createMaterial()
const isNewMaterial = mat !== this._material
if (isNewMaterial) { // new material
this._removeMaterial()
this._material = mat
const id = this._material?.uuid
if (!id) console.warn('No material found for ground')
this._viewer.scene.setDirty()
if (this._mesh && this._material) {
this._material.roughness = 0.2
this._material.metalness = 0.5
this._mesh.material = this._material // for update event handlers.
}
}
if (this._material) {
if (this._material.userData.__renderToDepth === undefined) {
this._material.userData.__renderToDepth = this._material.userData.renderToDepth
}
if (this._material.userData.renderToDepth !== this.renderToDepth) {
this._material.userData.renderToDepth = this.renderToDepth // required to work with SSR, SSAO etc when the ground is transparent / transmissive
}
if (!this._material.userData.gBufferData) this._material.userData.gBufferData = {}
if (this._material.userData.gBufferData.__tonemapEnabled === undefined) {
this._material.userData.gBufferData.__tonemapEnabled = this._material.userData.gBufferData.tonemapEnabled
}
if (this._material.userData.gBufferData.tonemapEnabled !== this.tonemapGround) {
this._material.userData.gBufferData.tonemapEnabled = this.tonemapGround
}
this._material.userData.ssaoDisabled = true
this._material.userData.sscsDisabled = true

// if (this._material.userData.__postTonemap === undefined) {
// this._material.userData.__postTonemap = this._material.userData.postTonemap
// }
// if (this._material.userData.postTonemap !== this.tonemapGround) {
// this._material.userData.postTonemap = this.tonemapGround
// }
}
this._viewer.setDirty(this) // todo: something else also?
return isNewMaterial
}

get material() {
return this._material
}

get mesh() {
return this._mesh
}

fromJSON(data: any, meta?: any): this | null {
if (data.options) {
console.error('todo: support old webgi v0 file')
}
if (!super.fromJSON(data, meta)) return null
// if (this._material && this._material.transmission >= 0.01) this._material.transparent = true
this.refresh()
// Note: baked shadow reset is done in ShadowMapBaker.fromJSON
return this
}

}

+ 123
- 0
src/plugins/export/CanvasSnapshotPlugin.ts Переглянути файл

@@ -0,0 +1,123 @@
import {serialize, timeout} from 'ts-browser-helpers'
import {AViewerPluginSync} from '../../viewer'
import {uiButton, uiConfig, uiFolderContainer, uiInput} from 'uiconfig.js'
import {CanvasSnapshot, CanvasSnapshotOptions} from '../../utils/canvas-snapshot'
import {ProgressivePlugin} from '../pipeline/ProgressivePlugin'

@uiFolderContainer('Canvas Snapshot (Image Export)')
export class CanvasSnapshotPlugin extends AViewerPluginSync<''> {
static readonly PluginType = 'CanvasSnapshotPlugin'
enabled = true

constructor() {
super()
this.downloadSnapshot = this.downloadSnapshot.bind(this)
this.getDataUrl({})
}

/**
* Returns a File object with screenshot of the viewer canvas
* @param filename default is {@link CanvasSnapshotPlugin.filename}
* @param options waitForProgressive: wait for progressive rendering to finish, default: true
*/
async getFile(filename?: string, options: CanvasSnapshotOptions&{waitForProgressive?: boolean} = {waitForProgressive: true}): Promise<File|undefined> {
options.getDataUrl = false
return await this._getFile(filename || this.filename, options) as File
}

/**
* Returns a data url of the screenshot of the viewer canvas
* @param options waitForProgressive: wait for progressive rendering to finish, default: true
*/
async getDataUrl(options: CanvasSnapshotOptions&{waitForProgressive?: boolean} = {}): Promise<string> {
options.getDataUrl = true
return await this._getFile('', options) as string ?? ''
}

private async _getFile(filename: string, options: CanvasSnapshotOptions&{waitForProgressive?: boolean} = {}): Promise<File|string|undefined> {
const viewer = this._viewer
const canvas = this._viewer?.canvas
if (!viewer || !canvas) return undefined
const dpr = viewer.renderManager.renderScale
if (options.displayPixelRatio !== undefined && options.displayPixelRatio !== dpr) {
viewer.renderManager.renderScale = options.displayPixelRatio
}
if (options.timeout) await timeout(options.timeout)
const progressive = viewer.getPlugin(ProgressivePlugin)
if (options.waitForProgressive !== false && progressive) {
// todo: disable interactions and all so that frameCount is not affected
await new Promise<void>((res)=>{
const listener = () => {
if (!progressive.isConverged(true)) return
viewer.removeEventListener('postFrame', listener)
res()
}
viewer.addEventListener('postFrame', listener)
})
} else await viewer.doOnce('postFrame')
options.displayPixelRatio = 1
const rect = options.rect
if (rect && viewer.renderManager.renderScale !== 1) {
options.rect = {
...rect,
x: rect.x * viewer.renderManager.renderScale,
y: rect.y * viewer.renderManager.renderScale,
width: rect.width * viewer.renderManager.renderScale,
height: rect.height * viewer.renderManager.renderScale,
}
}
const file = await CanvasSnapshot.GetFile(canvas, filename, options)
options.rect = rect
options.displayPixelRatio = viewer.renderManager.renderScale
viewer.renderManager.renderScale = dpr
return file
}

@uiInput('Filename')
@serialize()
filename = 'snapshot.png'

/**
* Only for {@link downloadSnapshot} and functions using that
*/
@uiConfig()
@serialize()
defaultOptions: CanvasSnapshotOptions&{waitForProgressive?: boolean} = {
waitForProgressive: true,
displayPixelRatio: window.devicePixelRatio,
scale: 1,
timeout: 0,
quality: 0.9,
}

@uiButton('Download .png')
async downloadSnapshot(filename?: string, options: CanvasSnapshotOptions&{waitForProgressive?: boolean} = {waitForProgressive: true}): Promise<void> {
if (!this._viewer) return
if (!options.mimeType && !filename) this.filename = this.filename.split('.').slice(0, -1).join('.') + '.png'
const file = await this.getFile(filename, {...this.defaultOptions, ...options})
if (file) await this._viewer.exportBlob(file, file.name)
}

@uiButton('Download .jpeg')
protected async _downloadJpeg(): Promise<void> {
this.filename = this.filename.split('.').slice(0, -1).join('.') + '.jpeg'
return this.downloadSnapshot(undefined, {mimeType: 'image/jpeg'})
}
@uiButton('Download .webp')
protected async _downloadWebp(): Promise<void> {
this.filename = this.filename.split('.').slice(0, -1).join('.') + '.webp'
return this.downloadSnapshot(undefined, {mimeType: 'image/webp'})
}

}

/**
* @deprecated - use {@link CanvasSnapshotPlugin}
*/
export class CanvasSnipperPlugin extends CanvasSnapshotPlugin {
static readonly PluginType: any = 'CanvasSnipper'
constructor() {
super()
console.warn('CanvasSnipperPlugin is deprecated, use CanvasSnapshotPlugin')
}
}

+ 27
- 0
src/plugins/export/FileTransferPlugin.ts Переглянути файл

@@ -0,0 +1,27 @@
import {AViewerPluginSync} from '../../viewer'
import {downloadBlob} from 'ts-browser-helpers'

export class FileTransferPlugin extends AViewerPluginSync<'transferFile'> {
enabled = true

static readonly PluginType = 'FileTransferPlugin'

toJSON: any = undefined

async exportFile(file: File|Blob, name?: string) {
name = name || (file as File).name || 'file_export'
this.dispatchEvent({type: 'transferFile', path: name, state: 'exporting'})
await this.actions.exportFile(file, name, ({state, progress})=>{
this.dispatchEvent({type: 'transferFile', path: name, state: state ?? 'exporting', progress})
})
this.dispatchEvent({type: 'transferFile', path: name, state: 'done'})
}

readonly defaultActions = {
exportFile: async(blob: Blob, name: string, _onProgress?: (d: {state?: string, progress?: number})=>void)=>{
downloadBlob(blob, name)
},
}

actions = {...this.defaultActions}
}

+ 197
- 0
src/plugins/extras/ContactShadowGroundPlugin.ts Переглянути файл

@@ -0,0 +1,197 @@
import {onChange, serialize} from 'ts-browser-helpers'
import {
BasicDepthPacking,
Color,
Euler,
LinearFilter,
MeshDepthMaterial,
NoBlending,
NoColorSpace,
OrthographicCamera,
RGBAFormat,
UnsignedByteType,
Vector3,
WebGLRenderTarget,
} from 'three'
import {BaseGroundPlugin} from '../base/BaseGroundPlugin'
import {GBufferRenderPass} from '../../postprocessing'
import {ThreeViewer} from '../../viewer'
import {IRenderTarget} from '../../rendering'
import {uiFolderContainer, uiSlider, uiToggle} from 'uiconfig.js'
import {HVBlurHelper} from '../../three/utils/HVBlurHelper'
import {shaderReplaceString} from '../../utils'

@uiFolderContainer('Contact Shadow Ground')
export class ContactShadowGroundPlugin extends BaseGroundPlugin {
static readonly PluginType = 'ContactShadowGroundPlugin'

@uiToggle('Contact Shadows')
@onChange(ContactShadowGroundPlugin.prototype.refresh)
@serialize() contactShadows = true

@uiSlider('Shadow Scale', [0, 2])
@serialize()
@onChange(ContactShadowGroundPlugin.prototype._refreshShadowCameraFrustum)
shadowScale = 1

@uiSlider('Shadow Height', [0, 20])
@serialize()
@onChange(ContactShadowGroundPlugin.prototype._refreshShadowCameraFrustum)
shadowHeight = 5

@uiSlider('Blur Amount', [0, 10])
@serialize()
@onChange(ContactShadowGroundPlugin.prototype._setDirty)
blurAmount = 1


shadowCamera = new OrthographicCamera(-1, 1, 1, -1, 0.001, this.shadowHeight)
private _depthPass?: GBufferRenderPass<'contactShadowGround', WebGLRenderTarget>
private _blurHelper?: HVBlurHelper

constructor() {
super()
this._refreshShadowCameraFrustum = this._refreshShadowCameraFrustum.bind(this)
this.refresh = this.refresh.bind(this)
}

onAdded(viewer: ThreeViewer): void {
const target = viewer.renderManager.createTarget<IRenderTarget & WebGLRenderTarget>({
type: UnsignedByteType,
format: RGBAFormat,
colorSpace: NoColorSpace,
size: {width: 512, height: 512},
generateMipmaps: false,
depthBuffer: true,
minFilter: LinearFilter,
magFilter: LinearFilter,
// isAntialiased: this._viewer.isAntialiased,
})
target.texture.name = 'groundContactDepthTexture'

// https://github.com/mrdoob/three.js/blob/master/examples/webgl_shadow_contact.html
const material = new MeshDepthMaterial({
// depthPacking: RGBADepthPacking, // todo
depthPacking: BasicDepthPacking,
transparent: false,
blending: NoBlending,
})
material.onBeforeCompile = (shader) => {
shader.uniforms.opacity.value = 1.
shader.fragmentShader = shaderReplaceString(shader.fragmentShader,
'gl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity );',
'gl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), 1.0 );',
// 'gl_FragColor = vec4( vec3( 0.0 ), ( 1.0 - fragCoordZ ) * darkness );',
)
}

this._depthPass = new GBufferRenderPass('contactShadowGround', target, material, new Color(0, 0, 0), 0)
this._blurHelper = new HVBlurHelper(viewer)
super.onAdded(viewer)
}

onRemove(viewer: ThreeViewer): void {
const target = this._depthPass?.target
if (target) this._viewer?.renderManager.disposeTarget(target)
this._depthPass?.dispose()
this._depthPass = undefined
this._blurHelper?.dispose()
this._blurHelper = undefined
return super.onRemove(viewer)
}
// todo: dispose target, material, pass and stuff

protected _postFrame() {
super._postFrame()
if (!this._viewer) return

}

protected _preRender() {
super._preRender()
if (!this._viewer || !this._depthPass || !this._blurHelper) return

this._depthPass.scene = this._viewer.scene
this._depthPass.camera = this.shadowCamera
this._depthPass.render(this._viewer.renderManager.renderer, null)

const blurTarget = this._viewer.renderManager.getTempTarget<IRenderTarget&WebGLRenderTarget>({
type: UnsignedByteType,
format: RGBAFormat,
colorSpace: NoColorSpace,
size: {width: 1024, height: 1024},
generateMipmaps: false,
depthBuffer: false,
minFilter: LinearFilter,
magFilter: LinearFilter,
// isAntialiased: this._viewer.isAntialiased,
})

this._blurHelper.blur(this._depthPass.target.texture, this._depthPass.target, blurTarget, this.blurAmount / 256)
this._blurHelper.blur(this._depthPass.target.texture, this._depthPass.target, blurTarget, 0.4 * this.blurAmount / 256)

this._viewer.renderManager.releaseTempTarget(blurTarget)

}

protected _refreshTransform() {
super._refreshTransform()

if (!this._mesh) return
if (!this._viewer) return

this.shadowCamera.position.copy(this._mesh.getWorldPosition(new Vector3()))
this.shadowCamera.setRotationFromEuler(new Euler(Math.PI / 2., 0, 0))
this.shadowCamera.updateMatrixWorld()
this._refreshShadowCameraFrustum()

this._mesh.scale.y = -this.size
}

private _refreshShadowCameraFrustum() {
if (!this.shadowCamera) return
this.shadowCamera.left = -this.size / (2 * this.shadowScale)
this.shadowCamera.right = this.size / (2 * this.shadowScale)
this.shadowCamera.top = this.size / (2 * this.shadowScale)
this.shadowCamera.bottom = -this.size / (2 * this.shadowScale)
this.shadowCamera.far = this.shadowHeight
this.shadowCamera.updateProjectionMatrix()
this._setDirty()
}
private _setDirty() {
this._viewer?.setDirty()
}

protected _removeMaterial() {
if (!this._material) return
// todo: remove map or render target thats assigned
super._removeMaterial()
}

public refresh(): void {
if (!this._viewer) return
// todo: shadow enabled check
super.refresh()
}

protected _refreshMaterial(): boolean {
if (!this._viewer) return false
const isNewMaterial = super._refreshMaterial()
if (!this._material) return isNewMaterial
this._material.alphaMap = this._depthPass?.target.texture || null

if (isNewMaterial) {
this._material.roughness = 1
this._material.metalness = 0
this._material.color.set(0x111111)
this._material.transparent = true
this._material.userData.ssreflDisabled = true // todo: unset this in remove material.
this._material.userData.ssreflNonPhysical = false
// this._material.materialObject.userData.inverseAlphaMap = false // this must be false, if getting inverted colors, check clear color of gbuffer render pass.
}

return isNewMaterial

}

}

+ 6
- 0
src/plugins/index.ts Переглянути файл

@@ -1,6 +1,7 @@
// base
export {PipelinePassPlugin} from './base/PipelinePassPlugin'
export {BaseImporterPlugin} from './base/BaseImporterPlugin'
export {BaseGroundPlugin} from './base/BaseGroundPlugin'

// pipeline
export {ProgressivePlugin} from './pipeline/ProgressivePlugin'
@@ -34,6 +35,10 @@ export {STLLoadPlugin} from './import/STLLoadPlugin'
export {KTXLoadPlugin} from './import/KTXLoadPlugin'
export {KTX2LoadPlugin} from './import/KTX2LoadPlugin'

// export
export {CanvasSnapshotPlugin, CanvasSnipperPlugin} from './export/CanvasSnapshotPlugin'
export {FileTransferPlugin} from './export/FileTransferPlugin'

// postprocessing
export {AScreenPassExtensionPlugin} from './postprocessing/AScreenPassExtensionPlugin'
export {TonemapPlugin} from './postprocessing/TonemapPlugin'
@@ -59,3 +64,4 @@ export {VirtualCamerasPlugin} from './rendering/VirtualCamerasPlugin'
export {HDRiGroundPlugin} from './extras/HDRiGroundPlugin'
export {Object3DWidgetsPlugin} from './extras/Object3DWidgetsPlugin'
export {Object3DGeneratorPlugin} from './extras/Object3DGeneratorPlugin'
export {ContactShadowGroundPlugin} from './extras/ContactShadowGroundPlugin'

+ 38
- 0
src/three/utils/HVBlurHelper.ts Переглянути файл

@@ -0,0 +1,38 @@
import {ShaderMaterial, Texture, WebGLRenderTarget} from 'three'
import {HorizontalBlurShader} from 'three/examples/jsm/shaders/HorizontalBlurShader'
import {VerticalBlurShader} from 'three/examples/jsm/shaders/VerticalBlurShader'
import {ThreeViewer} from '../../viewer'
import {IRenderTarget} from '../../rendering'

export class HVBlurHelper {
horizontalBlurMaterial = new ShaderMaterial(HorizontalBlurShader)

verticalBlurMaterial = new ShaderMaterial(VerticalBlurShader)

constructor(private _viewer: ThreeViewer) {
this.horizontalBlurMaterial.depthTest = false
this.verticalBlurMaterial.depthTest = false
}

blur(source: Texture, dest: IRenderTarget & WebGLRenderTarget, tempTarget: IRenderTarget & WebGLRenderTarget, amountMultiplier = 1) {
this.horizontalBlurMaterial.uniforms.h.value = amountMultiplier
this.verticalBlurMaterial.uniforms.v.value = amountMultiplier
this._viewer.renderManager.blit(tempTarget, {
material: this.horizontalBlurMaterial,
clear: true,
source: source, // this._depthPass.target.texture,
})
// this._viewer.renderManager.blit(this._depthPass.target, {
this._viewer.renderManager.blit(dest, {
material: this.verticalBlurMaterial,
clear: true,
source: tempTarget.texture,
})
}

dispose() {
this.horizontalBlurMaterial.dispose()
this.verticalBlurMaterial.dispose()
}

}

+ 1
- 0
src/three/utils/index.ts Переглянути файл

@@ -8,6 +8,7 @@ export {getTextureDataType, textureToCanvas, textureDataToImageData, textureToDa
export {threeConstMappings} from './const-mappings'
export {ObjectPicker} from './ObjectPicker'
export {autoGPUInstanceMeshes} from './gpu-instancing'
export {HVBlurHelper} from './HVBlurHelper'
export {ViewHelper2, type GizmoOrientation, type DomPlacement} from './ViewHelper2'

// export {} from './constants'

+ 143
- 0
src/utils/canvas-snapshot.ts Переглянути файл

@@ -0,0 +1,143 @@
import {now} from 'ts-browser-helpers'

export interface CanvasSnapshotRect {
height: number;
width: number;
x: number;
y: number;
/**
* Use if canvas.width !== canvas.clientWidth or height and rect is based on client rect
*/
assumeClientRect?: boolean;
}

export interface CanvasSnapshotOptions {
getDataUrl?: boolean,
mimeType?: string,
quality?: number, // between 0 and 1, only for image/jpeg or image/webp
rect?: CanvasSnapshotRect,
scale?: number,
timeout?: number, // in ms, if not specified, will be based on progressive rendering or 200ms
displayPixelRatio?: number,
}

export class CanvasSnapshot {
public static Debug = false
public static async GetClonedCanvas(
canvas: HTMLCanvasElement,
{
rect = {x: 0, y: 0, width: canvas.width, height: canvas.height, assumeClientRect: false},
displayPixelRatio = 1,
scale = 1,
}: CanvasSnapshotOptions): Promise<HTMLCanvasElement> {
// return canvas.toDataURL(mimeType);
// in Safari, images are flipped when premultipliedAlpha is true in canvas, so it works with 2d context, see: https://github.com/pixijs/pixi.js/blob/dev/packages/extract/src/Extract.ts and https://github.com/pixijs/pixi.js/issues/2951

const destCanvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas') as HTMLCanvasElement
destCanvas.width = rect.width * scale * displayPixelRatio
destCanvas.height = rect.height * scale * displayPixelRatio

// const iRect = {...rect}

if (rect.assumeClientRect) {
rect.x *= canvas.width / (displayPixelRatio * canvas.clientWidth)
rect.y *= canvas.height / (displayPixelRatio * canvas.clientHeight)
rect.width *= canvas.width / (displayPixelRatio * canvas.clientWidth)
rect.height *= canvas.height / (displayPixelRatio * canvas.clientHeight)
}

const destCtx = destCanvas.getContext('2d')
if (!destCtx) {
console.error('snapshot: cannot create context')
return destCanvas
}

// console.log(canvas.style.background)
const background = canvas.style.background || canvas.parentElement?.style.background || ''
if (background.includes('url')) {
const url = /url\("(.*)"\)/ig.exec(background)?.[1]
if (url) {
const img = new Image()
img.src = url
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve()
img.onerror = () => reject()
if (img.complete) resolve()
})
destCtx.drawImage(img,
img.width * rect.x * displayPixelRatio / canvas.width, img.height * rect.y * displayPixelRatio / canvas.height,
img.width * rect.width * displayPixelRatio / canvas.width, img.height * rect.height * displayPixelRatio / canvas.height,
0, 0,
destCanvas.width,
destCanvas.height,
)

}
} else {
destCtx.fillStyle = canvas.style.background || canvas.parentElement?.style.backgroundColor || '#00000000'
destCtx.fillRect(0, 0, destCanvas.width, destCanvas.height)
}

destCtx?.drawImage(
canvas,
rect.x * displayPixelRatio, rect.y * displayPixelRatio, rect.width * displayPixelRatio, rect.height * displayPixelRatio,
0, 0, destCanvas.width, destCanvas.height,
)

const debug = this.Debug
if (debug) {
// console.log(
// destCanvas,
// )
document.body.appendChild(destCanvas)
destCanvas.style.position = 'absolute'
destCanvas.style.top = '0'
destCanvas.style.left = '0'
destCanvas.style.borderWidth = '2px'
destCanvas.style.borderColor = '#ff00ff'
setTimeout(() => destCanvas.remove(), 5000)
}

return destCanvas
}

public static async GetDataUrl(canvas: HTMLCanvasElement, {mimeType = 'image/png', quality, ...options}: CanvasSnapshotOptions): Promise<string> {
const clone = await this.GetClonedCanvas(canvas, options)
const url = clone.toDataURL(mimeType, quality)
if (!this.Debug) clone.remove()
return url
}

// set one of canvas or context to draw in.
public static async GetImage(canvas: HTMLCanvasElement, options: CanvasSnapshotOptions = {}): Promise<HTMLImageElement> {
const imgUrl = await this.GetDataUrl(canvas, options)
return new Promise<HTMLImageElement>((resolve, reject) => {
const img = new Image()
img.onload = () => resolve(img)
img.onerror = () => reject()
img.src = imgUrl
})
}

public static async GetBlob(canvas: HTMLCanvasElement, options: CanvasSnapshotOptions = {}): Promise<Blob> {
const clone = await this.GetClonedCanvas(canvas, options)

const blob = await new Promise<Blob>((resolve, reject) => {
clone.toBlob((b) => {
if (b) resolve(b)
else reject()
}, options.mimeType ?? 'image/png', options.quality)
})
if (!this.Debug) clone.remove()

return blob
}

public static async GetFile(canvas: HTMLCanvasElement, filename = 'image.png', options: CanvasSnapshotOptions = {}): Promise<File|string> {
return options.getDataUrl ? await this.GetDataUrl(canvas, options) : new File([await this.GetBlob(canvas, options)], filename, {
type: options.mimeType ?? 'image/png',
lastModified: now(),
})
}

}

+ 29
- 6
src/viewer/ThreeViewer.ts Переглянути файл

@@ -10,7 +10,7 @@ import {
Vector2,
Vector3,
} from 'three'
import {Class, createCanvasElement, onChange, serialize, ValOrArr} from 'ts-browser-helpers'
import {Class, createCanvasElement, downloadBlob, onChange, serialize, ValOrArr} from 'ts-browser-helpers'
import {TViewerScreenShader} from '../postprocessing'
import {
AddObjectOptions,
@@ -52,7 +52,8 @@ import {
import {IViewerPlugin, IViewerPluginSync} from './IViewerPlugin'
import {uiConfig, UiObjectConfig, uiPanelContainer} from 'uiconfig.js'
import {IRenderTarget} from '../rendering'
import type {CameraViewPlugin, ProgressivePlugin} from '../plugins'
import type {CanvasSnapshotPlugin, FileTransferPlugin} from '../plugins'
import {CameraViewPlugin, ProgressivePlugin} from '../plugins'
// noinspection ES6PreferShortImport
import {DropzonePlugin, DropzonePluginOptions} from '../plugins/interaction/DropzonePlugin'
// noinspection ES6PreferShortImport
@@ -131,9 +132,9 @@ export interface ThreeViewerOptions {
* Render scale, 1 = full resolution, 0.5 = half resolution, 2 = double resolution.
* Same as pixelRatio in three.js
* Can be set to `window.devicePixelRatio` to render at device resolution in browsers.
* An optimal value is `Math.min(2, window.devicePixelRatio)` to prevent issues on mobile.
* An optimal value is `Math.min(2, window.devicePixelRatio)` to prevent issues on mobile. This is set when 'auto' is passed.
*/
renderScale?: number
renderScale?: number | 'auto'

debug?: boolean

@@ -408,7 +409,9 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
zPrepass: options.zPrepass ?? options.useGBufferDepth ?? false,
depthBuffer: !(options.zPrepass ?? options.useGBufferDepth ?? false),
screenShader: options.screenShader,
renderScale: options.renderScale,
renderScale: typeof options.renderScale === 'string' ? options.renderScale === 'auto' ?
Math.min(2, window.devicePixelRatio) : parseFloat(options.renderScale) :
options.renderScale,
})
this.renderManager.addEventListener('animationLoop', this._animationLoop as any)
this.renderManager.addEventListener('resize', ()=> this._scene.mainCamera.refreshAspect())
@@ -516,6 +519,10 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
}

async getScreenshotBlob({mimeType = 'image/jpeg', quality = 90} = {}): Promise<Blob | null | undefined> {
const plugin = this.getPlugin<CanvasSnapshotPlugin>('CanvasSnapshotPlugin')
if (plugin) {
return plugin.getFile('snapshot.' + mimeType.split('/')[1], {mimeType, quality, waitForProgressive: true})
}
const blobPromise = async()=> new Promise<Blob|null>((resolve) => {
this._canvas.toBlob((blob) => {
resolve(blob)
@@ -530,7 +537,7 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
})
}

async getScreenshotDataUrl({mimeType = 'image/jpeg', quality = 90} = {}): Promise<string | null | undefined> {
async getScreenshotDataUrl({mimeType = 'image/jpeg', quality = 0.9} = {}): Promise<string | null | undefined> {
if (!this.renderEnabled) return this._canvas.toDataURL(mimeType, quality)
return await this.doOnce('postFrame', () => this._canvas.toDataURL(mimeType, quality))
}
@@ -1079,6 +1086,22 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes
super.dispatchEvent({...event, type: '*', eType: event.type})
}

/**
* Uses the {@link FileTransferPlugin} to export a blob. If the plugin is not available, it will download the blob.
* FileTransferPlugin can be configured by other plugins to export the blob to a specific location like local file system, cloud storage, etc.
* @param blob - The blob or file to export/download
* @param name
*/
async exportBlob(blob: Blob|File, name?: string) {
const tr = this.getPlugin<FileTransferPlugin>('FileTransferPlugin')
name = name ?? (blob as File).name ?? 'file'
if (!tr) {
downloadBlob(blob, name)
return
}
await tr.exportFile(blob, name)
}

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

Завантаження…
Відмінити
Зберегти