| @@ -1,6 +1,6 @@ | |||
| dist | |||
| docs | |||
| index.html | |||
| ./index.html | |||
| examples/**/*.js | |||
| examples/**/*.js.map | |||
| @@ -47,3 +47,23 @@ | |||
| content: '+'; | |||
| 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; | |||
| } | |||
| @@ -1,9 +1,8 @@ | |||
| import {ThreeViewer} from '../../viewer' | |||
| import {AViewerPluginSync, ThreeViewer} from '../../viewer' | |||
| import {IRenderTarget} from '../../rendering' | |||
| 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 {AViewerPluginSync} from '../../viewer/AViewerPlugin' | |||
| export class RenderTargetPreviewPlugin <TEvent extends string> extends AViewerPluginSync<TEvent> { | |||
| static readonly PluginType = 'RenderTargetPreviewPlugin' | |||
| @@ -47,7 +46,7 @@ export class RenderTargetPreviewPlugin <TEvent extends string> extends AViewerPl | |||
| if (!this._viewer) return | |||
| for (const target of this.targetBlocks) { | |||
| if (!target.visible) return | |||
| if (!target.visible) continue | |||
| const rt = getOrCall(target.target) | |||
| if (!rt) { | |||
| // todo draw white or pink | |||
| @@ -85,6 +84,35 @@ export class RenderTargetPreviewPlugin <TEvent extends string> extends AViewerPl | |||
| else div.classList.remove('RenderTargetPreviewPluginCollapsed') | |||
| 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) | |||
| this.mainDiv.appendChild(div) | |||
| this.targetBlocks.push(targetDef) | |||
| @@ -102,6 +130,29 @@ export class RenderTargetPreviewPlugin <TEvent extends string> extends AViewerPl | |||
| this.refreshUi() | |||
| 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 { | |||
| if (!this.mainDiv) return | |||
| @@ -114,6 +165,7 @@ export class RenderTargetPreviewPlugin <TEvent extends string> extends AViewerPl | |||
| if (!this.mainDiv.parentElement) this._viewer.container?.appendChild(this.mainDiv) | |||
| this.mainDiv.style.display = this.enabled ? 'flex' : 'none' | |||
| this.mainDiv.style.zIndex = parseInt(this._viewer.canvas.style.zIndex || '0') + 1 + '' | |||
| this._viewer?.setDirty() | |||
| } | |||
| dispose() { | |||
| @@ -1,4 +1,5 @@ | |||
| import { | |||
| HalfFloatType, | |||
| IUniform, | |||
| NoColorSpace, | |||
| NoToneMapping, | |||
| @@ -10,9 +11,8 @@ import { | |||
| WebGLRenderer, | |||
| WebGLRenderTarget, | |||
| } from 'three' | |||
| import {IPassID, IPipelinePass, sortPasses} from '../postprocessing' | |||
| import {EffectComposer2, IPassID, IPipelinePass, sortPasses} from '../postprocessing' | |||
| import {IRenderTarget} from './RenderTarget' | |||
| import {EffectComposer2} from '../postprocessing/EffectComposer2' | |||
| import {RenderTargetManager} from './RenderTargetManager' | |||
| import {IShaderPropertiesUpdater} from '../materials' | |||
| import { | |||
| @@ -263,16 +263,29 @@ export class RenderManager extends RenderTargetManager<IRenderManagerEvent, IRen | |||
| } | |||
| } | |||
| /** | |||
| * 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 { | |||
| 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') | |||
| canvas.width = target.width | |||
| canvas.height = target.height | |||
| const ctx = canvas.getContext('2d') | |||
| 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}) | |||
| 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) | |||
| const string = canvas.toDataURL(mimeType, quality) | |||
| canvas.remove() | |||
| @@ -144,10 +144,15 @@ export abstract class RenderTargetManager<E extends BaseEvent = BaseEvent, ET ex | |||
| if (Array.isArray(this.texture)) { | |||
| this.texture.forEach(t => { | |||
| t.colorSpace = options.colorSpace | |||
| t.toJSON = () => ({}) | |||
| t.toJSON = () => { | |||
| console.warn('Multiple render target texture.toJSON not supported yet.') | |||
| return {} | |||
| } | |||
| }) | |||
| } 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 | |||
| } | |||
| } | |||