浏览代码

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

master
Palash Bansal 2 年前
父节点
当前提交
77a4228f46
没有帐户链接到提交者的电子邮件

+ 69
- 2
README.md 查看文件

- [DepthBufferPlugin](#depthbufferplugin) - Pre-rendering of depth buffer - [DepthBufferPlugin](#depthbufferplugin) - Pre-rendering of depth buffer
- [NormalBufferPlugin](#normalbufferplugin) - Pre-rendering of normal buffer - [NormalBufferPlugin](#normalbufferplugin) - Pre-rendering of normal buffer
- [GBufferPlugin](#gbufferplugin) - Pre-rendering of depth-normal and flags buffers in a single pass - [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 - [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 - [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 - [GLTFAnimationPlugin](#gltfanimationplugin) - Add support for playing and seeking gltf animations
- [PopmotionPlugin](#popmotionplugin) - Integrates with popmotion.io library for animation/tweening - [PopmotionPlugin](#popmotionplugin) - Integrates with popmotion.io library for animation/tweening
- [CameraViewPlugin](#cameraviewplugin) - Add support for saving, loading, animating, looping between camera views - [CameraViewPlugin](#cameraviewplugin) - Add support for saving, loading, animating, looping between camera views
// Use the normal target by accessing `normalTarget.texture`. // Use the normal target by accessing `normalTarget.texture`.
``` ```



## GBufferPlugin ## GBufferPlugin


[//]: # (todo: image) [//]: # (todo: image)
const gBufferFlags = gBufferPlugin.flagsTexture; 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 ## PickingPlugin


[//]: # (todo: image) [//]: # (todo: image)


[Example](https://threepipe.org/examples/#transform-controls-plugin/) — [Example](https://threepipe.org/examples/#transform-controls-plugin/) —
[Source Code](./src/plugins/interaction/TransformControlsPlugin.ts) — [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. Transform Controls Plugin adds support for moving, rotating and scaling objects in the viewer with interactive widgets.


console.log(transfromControlsPlugin.transformControls) 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 ## GLTFAnimationPlugin



+ 35
- 0
examples/canvas-snapshot-plugin/index.html 查看文件

<!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 查看文件

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 查看文件

<!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 查看文件

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 查看文件

msaa: true, msaa: true,
}) })


// Note: see also: CanvasSnapshotPlugin

async function init() { async function init() {


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

+ 2
- 0
examples/index.html 查看文件

</ul> </ul>
<h2 class="category">Export</h2> <h2 class="category">Export</h2>
<ul> <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="./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="./render-target-export/">EXR, PNG, JPEG, WEBP Export<br/>(Render Target Export) </a></li>
<li><a href="./glb-export/">GLB Export </a></li> <li><a href="./glb-export/">GLB Export </a></li>
</ul> </ul>
<h2 class="category">Utils</h2> <h2 class="category">Utils</h2>
<ul> <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="./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="./render-target-preview/">Render Target Preview Plugin </a></li>
<li><a href="./object3d-generator-plugin/">Object3D Generator Plugin <br/>(Lights, Cameras)</a></li> <li><a href="./object3d-generator-plugin/">Object3D Generator Plugin <br/>(Lights, Cameras)</a></li>

+ 7
- 0
examples/tweakpane-editor/script.ts 查看文件

import { import {
_testFinish, _testFinish,
CameraViewPlugin, CameraViewPlugin,
CanvasSnapshotPlugin,
ChromaticAberrationPlugin, ChromaticAberrationPlugin,
ClearcoatTintPlugin, ClearcoatTintPlugin,
ContactShadowGroundPlugin,
CustomBumpMapPlugin, CustomBumpMapPlugin,
DepthBufferPlugin, DepthBufferPlugin,
DropzonePlugin, DropzonePlugin,


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




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

+ 304
- 0
src/plugins/base/BaseGroundPlugin.ts 查看文件

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 查看文件

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 查看文件

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 查看文件

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 查看文件

// base // base
export {PipelinePassPlugin} from './base/PipelinePassPlugin' export {PipelinePassPlugin} from './base/PipelinePassPlugin'
export {BaseImporterPlugin} from './base/BaseImporterPlugin' export {BaseImporterPlugin} from './base/BaseImporterPlugin'
export {BaseGroundPlugin} from './base/BaseGroundPlugin'


// pipeline // pipeline
export {ProgressivePlugin} from './pipeline/ProgressivePlugin' export {ProgressivePlugin} from './pipeline/ProgressivePlugin'
export {KTXLoadPlugin} from './import/KTXLoadPlugin' export {KTXLoadPlugin} from './import/KTXLoadPlugin'
export {KTX2LoadPlugin} from './import/KTX2LoadPlugin' export {KTX2LoadPlugin} from './import/KTX2LoadPlugin'


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

// postprocessing // postprocessing
export {AScreenPassExtensionPlugin} from './postprocessing/AScreenPassExtensionPlugin' export {AScreenPassExtensionPlugin} from './postprocessing/AScreenPassExtensionPlugin'
export {TonemapPlugin} from './postprocessing/TonemapPlugin' export {TonemapPlugin} from './postprocessing/TonemapPlugin'
export {HDRiGroundPlugin} from './extras/HDRiGroundPlugin' export {HDRiGroundPlugin} from './extras/HDRiGroundPlugin'
export {Object3DWidgetsPlugin} from './extras/Object3DWidgetsPlugin' export {Object3DWidgetsPlugin} from './extras/Object3DWidgetsPlugin'
export {Object3DGeneratorPlugin} from './extras/Object3DGeneratorPlugin' export {Object3DGeneratorPlugin} from './extras/Object3DGeneratorPlugin'
export {ContactShadowGroundPlugin} from './extras/ContactShadowGroundPlugin'

+ 38
- 0
src/three/utils/HVBlurHelper.ts 查看文件

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 查看文件

export {threeConstMappings} from './const-mappings' export {threeConstMappings} from './const-mappings'
export {ObjectPicker} from './ObjectPicker' export {ObjectPicker} from './ObjectPicker'
export {autoGPUInstanceMeshes} from './gpu-instancing' export {autoGPUInstanceMeshes} from './gpu-instancing'
export {HVBlurHelper} from './HVBlurHelper'
export {ViewHelper2, type GizmoOrientation, type DomPlacement} from './ViewHelper2' export {ViewHelper2, type GizmoOrientation, type DomPlacement} from './ViewHelper2'


// export {} from './constants' // export {} from './constants'

+ 143
- 0
src/utils/canvas-snapshot.ts 查看文件

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 查看文件

Vector2, Vector2,
Vector3, Vector3,
} from 'three' } 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 {TViewerScreenShader} from '../postprocessing'
import { import {
AddObjectOptions, AddObjectOptions,
import {IViewerPlugin, IViewerPluginSync} from './IViewerPlugin' import {IViewerPlugin, IViewerPluginSync} from './IViewerPlugin'
import {uiConfig, UiObjectConfig, uiPanelContainer} from 'uiconfig.js' import {uiConfig, UiObjectConfig, uiPanelContainer} from 'uiconfig.js'
import {IRenderTarget} from '../rendering' import {IRenderTarget} from '../rendering'
import type {CameraViewPlugin, ProgressivePlugin} from '../plugins'
import type {CanvasSnapshotPlugin, FileTransferPlugin} from '../plugins'
import {CameraViewPlugin, ProgressivePlugin} from '../plugins'
// noinspection ES6PreferShortImport // noinspection ES6PreferShortImport
import {DropzonePlugin, DropzonePluginOptions} from '../plugins/interaction/DropzonePlugin' import {DropzonePlugin, DropzonePluginOptions} from '../plugins/interaction/DropzonePlugin'
// noinspection ES6PreferShortImport // noinspection ES6PreferShortImport
* Render scale, 1 = full resolution, 0.5 = half resolution, 2 = double resolution. * Render scale, 1 = full resolution, 0.5 = half resolution, 2 = double resolution.
* Same as pixelRatio in three.js * Same as pixelRatio in three.js
* Can be set to `window.devicePixelRatio` to render at device resolution in browsers. * 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 debug?: boolean


zPrepass: options.zPrepass ?? options.useGBufferDepth ?? false, zPrepass: options.zPrepass ?? options.useGBufferDepth ?? false,
depthBuffer: !(options.zPrepass ?? options.useGBufferDepth ?? false), depthBuffer: !(options.zPrepass ?? options.useGBufferDepth ?? false),
screenShader: options.screenShader, 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('animationLoop', this._animationLoop as any)
this.renderManager.addEventListener('resize', ()=> this._scene.mainCamera.refreshAspect()) this.renderManager.addEventListener('resize', ()=> this._scene.mainCamera.refreshAspect())
} }


async getScreenshotBlob({mimeType = 'image/jpeg', quality = 90} = {}): Promise<Blob | null | undefined> { 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) => { const blobPromise = async()=> new Promise<Blob|null>((resolve) => {
this._canvas.toBlob((blob) => { this._canvas.toBlob((blob) => {
resolve(blob) resolve(blob)
}) })
} }


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) if (!this.renderEnabled) return this._canvas.toDataURL(mimeType, quality)
return await this.doOnce('postFrame', () => this._canvas.toDataURL(mimeType, quality)) return await this.doOnce('postFrame', () => this._canvas.toDataURL(mimeType, quality))
} }
super.dispatchEvent({...event, type: '*', eType: event.type}) 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 { private _setActiveCameraView(event: any = {}): void {
if (event.type === 'setView') { if (event.type === 'setView') {
if (!event.camera) { if (!event.camera) {

正在加载...
取消
保存