| dist | dist | ||||
| docs | docs | ||||
| index.html | |||||
| ./index.html | |||||
| examples/**/*.js | examples/**/*.js | ||||
| examples/**/*.js.map | examples/**/*.js.map | ||||
| content: '+'; | content: '+'; | ||||
| line-height: 20px; | line-height: 20px; | ||||
| } | } | ||||
| .RenderTargetPreviewPluginContextMenu{ | |||||
| position: absolute; | |||||
| background: #333e; | |||||
| min-width: 8rem; | |||||
| color: white; | |||||
| font-size: 0.8rem; | |||||
| overflow: hidden; | |||||
| border-radius: 8px; | |||||
| box-shadow: 2px 2px 10px #6666; | |||||
| z-index: 100000; | |||||
| font-family: monospace; | |||||
| } | |||||
| .RenderTargetPreviewPluginContextMenu div{ | |||||
| cursor: pointer; | |||||
| padding: 6px 10px; | |||||
| transition: background-color 0.25s ease-in-out; | |||||
| } | |||||
| .RenderTargetPreviewPluginContextMenu div:hover{ | |||||
| background: #1a1a1c; | |||||
| } |
| import {ThreeViewer} from '../../viewer' | |||||
| import {AViewerPluginSync, ThreeViewer} from '../../viewer' | |||||
| import {IRenderTarget} from '../../rendering' | import {IRenderTarget} from '../../rendering' | ||||
| import {createDiv, createStyles, getOrCall, onChange, ValOrFunc} from 'ts-browser-helpers' | import {createDiv, createStyles, getOrCall, onChange, ValOrFunc} from 'ts-browser-helpers' | ||||
| import {Vector4} from 'three' | |||||
| import {Vector4, WebGLRenderTarget} from 'three' | |||||
| import styles from './RenderTargetPreviewPlugin.css' | import styles from './RenderTargetPreviewPlugin.css' | ||||
| import {AViewerPluginSync} from '../../viewer/AViewerPlugin' | |||||
| export class RenderTargetPreviewPlugin <TEvent extends string> extends AViewerPluginSync<TEvent> { | export class RenderTargetPreviewPlugin <TEvent extends string> extends AViewerPluginSync<TEvent> { | ||||
| static readonly PluginType = 'RenderTargetPreviewPlugin' | static readonly PluginType = 'RenderTargetPreviewPlugin' | ||||
| if (!this._viewer) return | if (!this._viewer) return | ||||
| for (const target of this.targetBlocks) { | for (const target of this.targetBlocks) { | ||||
| if (!target.visible) return | |||||
| if (!target.visible) continue | |||||
| const rt = getOrCall(target.target) | const rt = getOrCall(target.target) | ||||
| if (!rt) { | if (!rt) { | ||||
| // todo draw white or pink | // todo draw white or pink | ||||
| else div.classList.remove('RenderTargetPreviewPluginCollapsed') | else div.classList.remove('RenderTargetPreviewPluginCollapsed') | ||||
| this._viewer?.setDirty() | this._viewer?.setDirty() | ||||
| } | } | ||||
| header.oncontextmenu = (e) => { | |||||
| e.preventDefault() | |||||
| e.stopPropagation() | |||||
| const menu = document.createElement('div') | |||||
| menu.classList.add('RenderTargetPreviewPluginContextMenu') | |||||
| menu.style.left = e.clientX + 'px' | |||||
| menu.style.top = e.clientY + 'px' | |||||
| const download = document.createElement('div') | |||||
| download.innerText = 'Download' | |||||
| download.onclick = () => { | |||||
| this.downloadTarget(target) | |||||
| menu.remove() | |||||
| } | |||||
| const remove = document.createElement('div') | |||||
| remove.innerText = 'Remove' | |||||
| remove.onclick = () => { | |||||
| this.removeTarget(target) | |||||
| menu.remove() | |||||
| } | |||||
| const cancel = document.createElement('div') | |||||
| cancel.innerText = 'Cancel' | |||||
| cancel.onclick = () => { | |||||
| menu.remove() | |||||
| } | |||||
| menu.appendChild(download) | |||||
| menu.appendChild(remove) | |||||
| menu.appendChild(cancel) | |||||
| document.body.appendChild(menu) | |||||
| } | |||||
| div.appendChild(header) | div.appendChild(header) | ||||
| this.mainDiv.appendChild(div) | this.mainDiv.appendChild(div) | ||||
| this.targetBlocks.push(targetDef) | this.targetBlocks.push(targetDef) | ||||
| this.refreshUi() | this.refreshUi() | ||||
| return this | return this | ||||
| } | } | ||||
| downloadTarget(target1: ValOrFunc<IRenderTarget|undefined>): this { | |||||
| if (!this._viewer) return this | |||||
| const target = getOrCall(target1) | |||||
| if (!target) return this | |||||
| const tex = target.texture | |||||
| if (Array.isArray(tex)) { | |||||
| // todo support multi target | |||||
| console.warn('todo: multi target') | |||||
| return this | |||||
| } | |||||
| const canvas = this._viewer?.canvas | |||||
| if (!canvas) return this | |||||
| // todo: encoding? | |||||
| const dataUrl = this._viewer.renderManager.renderTargetToDataUrl(target as WebGLRenderTarget, 'image/jpeg') | |||||
| const link = document.createElement('a') | |||||
| document.body.appendChild(link) | |||||
| link.style.display = 'none' | |||||
| link.href = dataUrl | |||||
| link.download = 'image.png' | |||||
| link.click() | |||||
| document.body.removeChild(link) | |||||
| return this | |||||
| } | |||||
| refreshUi(): void { | refreshUi(): void { | ||||
| if (!this.mainDiv) return | if (!this.mainDiv) return | ||||
| if (!this.mainDiv.parentElement) this._viewer.container?.appendChild(this.mainDiv) | if (!this.mainDiv.parentElement) this._viewer.container?.appendChild(this.mainDiv) | ||||
| this.mainDiv.style.display = this.enabled ? 'flex' : 'none' | this.mainDiv.style.display = this.enabled ? 'flex' : 'none' | ||||
| this.mainDiv.style.zIndex = parseInt(this._viewer.canvas.style.zIndex || '0') + 1 + '' | this.mainDiv.style.zIndex = parseInt(this._viewer.canvas.style.zIndex || '0') + 1 + '' | ||||
| this._viewer?.setDirty() | |||||
| } | } | ||||
| dispose() { | dispose() { |
| import { | import { | ||||
| HalfFloatType, | |||||
| IUniform, | IUniform, | ||||
| NoColorSpace, | NoColorSpace, | ||||
| NoToneMapping, | NoToneMapping, | ||||
| WebGLRenderer, | WebGLRenderer, | ||||
| WebGLRenderTarget, | WebGLRenderTarget, | ||||
| } from 'three' | } from 'three' | ||||
| import {IPassID, IPipelinePass, sortPasses} from '../postprocessing' | |||||
| import {EffectComposer2, IPassID, IPipelinePass, sortPasses} from '../postprocessing' | |||||
| import {IRenderTarget} from './RenderTarget' | import {IRenderTarget} from './RenderTarget' | ||||
| import {EffectComposer2} from '../postprocessing/EffectComposer2' | |||||
| import {RenderTargetManager} from './RenderTargetManager' | import {RenderTargetManager} from './RenderTargetManager' | ||||
| import {IShaderPropertiesUpdater} from '../materials' | import {IShaderPropertiesUpdater} from '../materials' | ||||
| import { | import { | ||||
| } | } | ||||
| } | } | ||||
| /** | |||||
| * Only to be used for testing. To do it properly, render the target to the main canvas(with proper encoding and type conversion) and call canvas.toDataURL() | |||||
| * @param target | |||||
| * @param mimeType | |||||
| * @param quality | |||||
| */ | |||||
| renderTargetToDataUrl(target: WebGLRenderTarget, mimeType = 'image/png', quality = 90): string { | renderTargetToDataUrl(target: WebGLRenderTarget, mimeType = 'image/png', quality = 90): string { | ||||
| const buffer = new Uint8Array(target.width * target.height * 4) | |||||
| this._renderer.readRenderTargetPixels(target, 0, 0, target.width, target.height, buffer) | |||||
| const canvas = document.createElement('canvas') | const canvas = document.createElement('canvas') | ||||
| canvas.width = target.width | canvas.width = target.width | ||||
| canvas.height = target.height | canvas.height = target.height | ||||
| const ctx = canvas.getContext('2d') | const ctx = canvas.getContext('2d') | ||||
| if (!ctx) throw new Error('Unable to get 2d context') | if (!ctx) throw new Error('Unable to get 2d context') | ||||
| const imageData = ctx.createImageData(target.width, target.height, {colorSpace: ['display-p3', 'srgb'].includes(target.texture.colorSpace) ? <PredefinedColorSpace>target.texture.colorSpace : undefined}) | const imageData = ctx.createImageData(target.width, target.height, {colorSpace: ['display-p3', 'srgb'].includes(target.texture.colorSpace) ? <PredefinedColorSpace>target.texture.colorSpace : undefined}) | ||||
| imageData.data.set(buffer) | |||||
| if (target.texture.type === HalfFloatType) { | |||||
| const buffer = new Uint16Array(target.width * target.height * 4) | |||||
| this._renderer.readRenderTargetPixels(target, 0, 0, target.width, target.height, buffer) | |||||
| for (let i = 0; i < buffer.length; i++) { | |||||
| imageData.data[i] = buffer[i] / 15360 * 255 // todo check packing | |||||
| } | |||||
| } else { | |||||
| // todo: handle rgbm to srgb conversion? | |||||
| this._renderer.readRenderTargetPixels(target, 0, 0, target.width, target.height, imageData.data) | |||||
| } | |||||
| ctx.putImageData(imageData, 0, 0) | ctx.putImageData(imageData, 0, 0) | ||||
| const string = canvas.toDataURL(mimeType, quality) | const string = canvas.toDataURL(mimeType, quality) | ||||
| canvas.remove() | canvas.remove() |
| if (Array.isArray(this.texture)) { | if (Array.isArray(this.texture)) { | ||||
| this.texture.forEach(t => { | this.texture.forEach(t => { | ||||
| t.colorSpace = options.colorSpace | t.colorSpace = options.colorSpace | ||||
| t.toJSON = () => ({}) | |||||
| t.toJSON = () => { | |||||
| console.warn('Multiple render target texture.toJSON not supported yet.') | |||||
| return {} | |||||
| } | |||||
| }) | }) | ||||
| } else { | } else { | ||||
| this.texture.toJSON = () => ({}) // so that it doesn't get serialized | |||||
| this.texture.toJSON = () => ({ // todo use readRenderTargetPixels as data url or data buffer. | |||||
| isRenderTargetTexture: true, | |||||
| }) // so that it doesn't get serialized | |||||
| } | } | ||||
| } | } | ||||