| @@ -33,6 +33,10 @@ Code samples and demos covering various usecases and test are present in the [ex | |||
| Try them: https://threepipe.org/examples/ | |||
| View the source code by pressing the code button on the top left of the example page. | |||
| To make changes and run the example, click on the CodePen button on the top right of the source code. | |||
| ## Getting Started | |||
| @@ -75,6 +79,8 @@ The viewer initializes with a Scene, Camera, Camera controls(Orbit Controls), se | |||
| Check out the GLTF Load example to see it in action or to check the JS equivalent code: https://threepipe.org/examples/gltf-load/ | |||
| Check out the [Plugins](#plugins) section below to learn how to add additional functionality to the viewer. | |||
| ## License | |||
| The core framework([src](https://github.com/repalash/threepipe/tree/master/src), [dist](https://github.com/repalash/threepipe/tree/master/dist), [examples](https://github.com/repalash/threepipe/tree/master/examples) folders) and any [plugins](https://github.com/repalash/threepipe/tree/master/plugins) without a separate license are under the [Apache 2.0 license](https://github.com/repalash/threepipe/tree/master/LICENSE). | |||
| @@ -98,3 +104,115 @@ Check out WebGi - Premium Photo-realistic 3D rendering framework and tools for w | |||
| ## Contributing | |||
| Contributions to ThreePipe are welcome and encouraged! Feel free to open issues and pull requests on the GitHub repository. | |||
| ## File Formats | |||
| ThreePipe Asset Manager supports the import of following file formats out of the box: | |||
| * Models: | |||
| * gltf, glb | |||
| * obj, mtl | |||
| * fbx | |||
| * drc | |||
| * Materials | |||
| * mat, pmat, bmat (json based), registered material template slugs | |||
| * Images | |||
| * webp, png, jpeg, jpg, svg, ico | |||
| * hdr, exr | |||
| * ktx2, ktx, dds, pvr | |||
| * Misc | |||
| * json, vjson | |||
| * zip | |||
| * txt | |||
| Additional formats can be added by plugins: | |||
| * Models | |||
| * 3dm - Using [Rhino3dmLoadPlugin](#Rhino3dmLoadPlugin) | |||
| ## Plugins | |||
| ThreePipe has a simple plugin system that allows you to easily add new features to the viewer. Plugins can be added to the viewer using the `addPlugin` and `addPluginSync` methods. The plugin system is designed to be modular and extensible. Plugins can be added to the viewer at any time and can be removed using the `removePlugin` and `removePluginSync` methods. | |||
| ### DepthBufferPlugin | |||
| todo: image | |||
| Example: https://threepipe.org/examples/#depth-buffer-plugin/ | |||
| Source Code: [src/plugins/pipeline/DepthBufferPlugin.ts](./src/plugins/pipeline/DepthBufferPlugin.ts) | |||
| Depth Buffer Plugin adds a pre-render pass to the render manager and renders a depth buffer to a target. The render target can be accessed by other plugins throughout the rendering pipeline to create effects like depth of field, SSAO, SSR, etc. | |||
| ```typescript | |||
| import {ThreeViewer, DepthBufferPlugin} from 'threepipe' | |||
| const viewer = new ThreeViewer({...}) | |||
| const depthPlugin = viewer.addPluginSync(new DepthBufferPlugin(HalfFloatType)) | |||
| const depthTarget = depthPlugin.target; | |||
| // Use the depth target by accesing `depthTarget.texture`. | |||
| ``` | |||
| The depth values are based on camera near far values, which are controlled automatically by the viewer. To manually specify near, far values and limits, it can be set in the camera userData. Check the [example](https://threepipe.org/examples/#depth-buffer-plugin/) for more details. | |||
| ### NormalBufferPlugin | |||
| todo: image | |||
| Example: https://threepipe.org/examples/#normal-buffer-plugin/ | |||
| Source Code: [src/plugins/pipeline/NormalBufferPlugin.ts](./src/plugins/pipeline/NormalBufferPlugin.ts) | |||
| Normal Buffer Plugin adds a pre-render pass to the render manager and renders a normal buffer to a target. The render target can be accessed by other plugins throughout the rendering pipeline to create effects like SSAO, SSR, etc. | |||
| Note: Use [`DepthNormalBufferPlugin`](#DepthNormalBufferPlugin) if using both `DepthBufferPlugin` and `NormalBufferPlugin` to render both depth and normal buffers in a single pass. | |||
| ```typescript | |||
| import {ThreeViewer, NormalBufferPlugin} from 'threepipe' | |||
| const viewer = new ThreeViewer({...}) | |||
| const normalPlugin = viewer.addPluginSync(new NormalBufferPlugin()) | |||
| const normalTarget = normalPlugin.target; | |||
| // Use the normal target by accessing `normalTarget.texture`. | |||
| ``` | |||
| ### DepthNormalBufferPlugin | |||
| todo | |||
| ### RenderTargetPreviewPlugin | |||
| todo: image | |||
| Example: https://threepipe.org/examples/#render-target-preview/ | |||
| Source Code: [src/plugins/ui/RenderTargetPreviewPlugin.ts](./src/plugins/ui/RenderTargetPreviewPlugin.ts) | |||
| RenderTargetPreviewPlugin is a useful development and debugging plugin that renders any registered render-target to the screen in small collapsable panels. | |||
| ```typescript | |||
| import {ThreeViewer, RenderTargetPreviewPlugin, NormalBufferPlugin} from 'threepipe' | |||
| const viewer = new ThreeViewer({...}) | |||
| const normalPlugin = viewer.addPluginSync(new NormalBufferPlugin(HalfFloatType)) | |||
| const previewPlugin = viewer.addPluginSync(new RenderTargetPreviewPlugin()) | |||
| // Show the normal buffer in a panel | |||
| previewPlugin.addTarget(()=>normalPlugin.target, 'normal', false, false) | |||
| ``` | |||
| ### Rhino3dmLoadPlugin | |||
| Example: https://threepipe.org/examples/#rhino3dm-load/ | |||
| Source Code: [src/plugins/import/Rhino3dmLoadPlugin.ts](./src/plugins/import/Rhino3dmLoadPlugin.ts) | |||
| Adds support for loading .3dm files generated by [Rhino 3D](https://www.rhino3d.com/). This plugin includes some changes with how 3dm files are loaded in three.js. The changes are around loading layer and primitive properties when set as reference in the 3dm files. | |||
| @@ -69,8 +69,11 @@ export class AssetExporter extends EventDispatcher<BaseEvent, 'exporterCreate' | | |||
| this.dispatchEvent({type: 'exportFile', obj, state:'processing', exportOptions: options}) | |||
| const processed = await this.processBeforeExport(obj, options) | |||
| const ext = options.exportExt ?? processed?.typeExt ?? processed?.ext | |||
| if (!processed || !ext) throw new Error(`Unable to preprocess before export ${ext}`) | |||
| const ext = options.exportExt || processed?.typeExt || processed?.ext | |||
| if (!processed || !ext) { | |||
| console.error(processed, options, obj) | |||
| throw new Error(`Unable to preprocess before export ${ext}`) | |||
| } | |||
| if (processed.blob) res = processed.blob | |||
| else { | |||
| const parser = this._getParser(ext) | |||
| @@ -128,7 +131,9 @@ export class AssetExporter extends EventDispatcher<BaseEvent, 'exporterCreate' | | |||
| if (obj.isWebGLMultipleRenderTargets) console.error('AssetExporter: WebGLMultipleRenderTargets export not supported') | |||
| else if (!obj.renderManager) return {obj, ext: 'exr'} | |||
| else { | |||
| const blob = obj.renderManager.exportRenderTarget(obj as WebGLRenderTarget, 'auto') | |||
| const blob = obj.renderManager.exportRenderTarget(obj as WebGLRenderTarget, | |||
| (options.exportExt || '' !== '') && options.exportExt !== 'auto' ? | |||
| options.exportExt === 'exr' ? 'image/x-exr' : 'image/' + options.exportExt : 'auto') | |||
| return { | |||
| obj, ext: blob.ext, blob, | |||
| } | |||
| @@ -198,9 +198,15 @@ export class AssetManager extends EventDispatcher<BaseEvent&{data: ImportResult} | |||
| async loadImported<T extends ValOrArr<ImportResult|undefined> = ImportResult>(imported: T, {autoSetEnvironment = true, autoSetBackground = false, ...options}: AddAssetOptions = {}): Promise<T | never[]> { | |||
| const arr: (ImportResult|undefined)[] = Array.isArray(imported) ? imported : [imported] | |||
| let ret: T = Array.isArray(imported) ? [] : undefined as any | |||
| for (const obj of arr) { | |||
| if (!obj) continue | |||
| if (!obj) { | |||
| if (Array.isArray(ret)) ret.push(undefined) | |||
| continue | |||
| } | |||
| let r = obj | |||
| switch (obj.assetType) { | |||
| case 'material': | |||
| @@ -215,7 +221,7 @@ export class AssetManager extends EventDispatcher<BaseEvent&{data: ImportResult} | |||
| case 'model': | |||
| case 'light': | |||
| case 'camera': | |||
| await this.viewer.addSceneObject(<IObject3D|RootSceneImportResult>obj, options) // todo update references in scene update event | |||
| r = await this.viewer.addSceneObject(<IObject3D|RootSceneImportResult>obj, options) // todo update references in scene update event | |||
| break | |||
| case 'config': | |||
| if (options?.importConfig !== false) await this.viewer.importConfig(<ISerializedConfig>obj) | |||
| @@ -230,9 +236,11 @@ export class AssetManager extends EventDispatcher<BaseEvent&{data: ImportResult} | |||
| break | |||
| } | |||
| this.dispatchEvent({type: 'loadAsset', data: obj}) | |||
| if (Array.isArray(ret)) ret.push(r) | |||
| else ret = r as T | |||
| } | |||
| return imported || [] | |||
| return ret || [] | |||
| } | |||
| /** | |||
| @@ -84,6 +84,9 @@ export interface ICamera<E extends ICameraEvent = ICameraEvent, ET extends ICame | |||
| removeControlsCtor(key: string): void; | |||
| refreshCameraControls(setDirty?: boolean): void | |||
| updateProjectionMatrix(): void | |||
| fov?: number | |||
| // region inherited type fixes | |||
| // re-declaring from IObject3D because: https://github.com/microsoft/TypeScript/issues/16936 | |||
| @@ -1,4 +1,4 @@ | |||
| import type {Event, IUniform, Material, MaterialParameters, Shader} from 'three' | |||
| import type {Color, Event, IUniform, Material, MaterialParameters, Shader} from 'three' | |||
| import type {IDisposable, IJSONSerializable} from 'ts-browser-helpers' | |||
| import type {MaterialExtension} from '../materials' | |||
| import type {ChangeEvent, IUiConfigContainer} from 'uiconfig.js' | |||
| @@ -129,6 +129,9 @@ export interface IMaterial<E extends IMaterialEvent = IMaterialEvent, ET = IMate | |||
| transmissionMap?: ITexture | null | |||
| transmission?: number | |||
| color?: Color | |||
| wireframe?: boolean | |||
| isRawShaderMaterial?: boolean | |||
| isPhysicalMaterial?: boolean | |||
| @@ -213,7 +213,7 @@ export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements I | |||
| this.modelRoot.animations.push(animation) | |||
| } | |||
| } | |||
| obj.children.forEach(c=>this.addObject(c, options)) | |||
| return obj.children.map(c=>this.addObject(c, options)) | |||
| } | |||
| private _addObject3D(model: IObject3D|null, {autoCenter = false, autoScale = false, autoScaleRadius = 2., addToRoot = false, license}: AddObjectOptions = {}): void { | |||
| @@ -31,7 +31,7 @@ import { | |||
| IWebGLRenderer, | |||
| upgradeWebGLRenderer, | |||
| } from '../core' | |||
| import {base64ToArrayBuffer, Class, onChange2, serializable, serialize, ValOrArr} from 'ts-browser-helpers' | |||
| import {base64ToArrayBuffer, canvasFlipY, Class, onChange2, serializable, serialize, ValOrArr} from 'ts-browser-helpers' | |||
| import {uiConfig, uiFolderContainer, uiMonitor, uiSlider, uiToggle} from 'uiconfig.js' | |||
| import {generateUUID, textureDataToImageData} from '../three' | |||
| import {BlobExt, EXRExporter2} from '../assetmanager' | |||
| @@ -476,6 +476,7 @@ export class RenderManager extends RenderTargetManager<IRenderManagerEvent, IRen | |||
| /** | |||
| * Converts a render target to a png/jpeg data url string. | |||
| * Note: this will clamp the values to [0, 1] and converts to srgb for float and half-float render targets. | |||
| * @param target | |||
| * @param mimeType | |||
| * @param quality | |||
| @@ -496,7 +497,8 @@ export class RenderManager extends RenderTargetManager<IRenderManagerEvent, IRen | |||
| } | |||
| ctx.putImageData(imageData, 0, 0) | |||
| const string = canvas.toDataURL(mimeType, quality) | |||
| const string = (target.texture.flipY ? canvas : canvasFlipY(canvas)).toDataURL(mimeType, quality) // intentionally inverted ternary | |||
| canvas.remove() | |||
| return string | |||
| } | |||
| @@ -4,6 +4,6 @@ export {dataTextureFromColor, dataTextureFromVec4, halfFloatToRgbe} from './conv | |||
| export {uniform, matDefine} from './decorators' | |||
| export {getEncodingComponents, getTexelEncoding, getTexelDecoding, getTexelDecoding2, getTexelDecodingFunction, getTexelEncodingFunction, getTextureColorSpaceFromMap} from './encoding' | |||
| export {generateUUID, toIndexedGeometry} from './misc' | |||
| export {getTextureDataType, textureToCanvas, textureDataToImageData, textureToDataUrl, imageToCanvas} from './texture' | |||
| export {getTextureDataType, textureToCanvas, textureDataToImageData, textureToDataUrl, texImageToCanvas} from './texture' | |||
| // export {} from './constants' | |||
| @@ -11,7 +11,7 @@ import { | |||
| WebGLRenderer, | |||
| } from 'three' | |||
| import {TextureImageData} from 'three/src/textures/types' | |||
| import {LinearToSRGB} from 'ts-browser-helpers' | |||
| import {canvasFlipY, LinearToSRGB} from 'ts-browser-helpers' | |||
| export function getTextureDataType(renderer?: WebGLRenderer): TextureDataType { | |||
| if (!renderer) return UnsignedByteType | |||
| @@ -51,15 +51,15 @@ export function textureDataToImageData(imgData: TextureImageData | ImageData | { | |||
| * @param flipY | |||
| * @param canvas | |||
| */ | |||
| export function textureToCanvas(texture: Texture|DataTexture, maxWidth: number, flipY = false, canvas?: HTMLCanvasElement) { | |||
| export function textureToCanvas(texture: Texture|DataTexture, maxWidth: number, flipY = false) { | |||
| let img | |||
| if ((texture as DataTexture).isDataTexture) img = textureDataToImageData(texture.image, texture.colorSpace) | |||
| else img = texture.image | |||
| return imageToCanvas(img, maxWidth, flipY, canvas) | |||
| return texImageToCanvas(img, maxWidth, flipY) | |||
| } | |||
| export function imageToCanvas(image: TexImageSource, maxWidth: number, flipY = false, canvas?: HTMLCanvasElement) { | |||
| canvas = canvas || document.createElement('canvas') | |||
| export function texImageToCanvas(image: TexImageSource, maxWidth: number, flipY = false) { | |||
| const canvas = document.createElement('canvas') | |||
| // resize it to the size of our image | |||
| canvas.width = Math.min(maxWidth, image.width as number) | |||
| canvas.height = Math.floor(1.0 + canvas.width * (image.height as number) / (image.width as number)) | |||
| @@ -77,6 +77,7 @@ export function imageToCanvas(image: TexImageSource, maxWidth: number, flipY = f | |||
| } | |||
| let needsFlipY = false | |||
| if ((image as ImageData).data !== undefined) { // THREE.DataTexture | |||
| const imageData = image as ImageData | |||
| @@ -89,17 +90,18 @@ export function imageToCanvas(image: TexImageSource, maxWidth: number, flipY = f | |||
| console.error('textureToDataUrl: could not get temp canvas context') | |||
| ctx.putImageData(imageData, 0, 0) | |||
| } else { | |||
| tempCtx.putImageData(imageData, 0, 0) | |||
| tempCtx.putImageData(imageData, 0, 0) // for resize | |||
| ctx.drawImage(tempCanvas, 0, 0, canvas.width, canvas.height) | |||
| } | |||
| } else { | |||
| ctx.putImageData(imageData, 0, 0) | |||
| if (flipY) needsFlipY = true // because of putImageData | |||
| } | |||
| } else { | |||
| ctx.drawImage(image as any, 0, 0, canvas.width, canvas.height) | |||
| } | |||
| return canvas | |||
| return !needsFlipY ? canvas : canvasFlipY(canvas) | |||
| } | |||
| export function textureToDataUrl(texture: Texture|DataTexture, maxWidth: number, flipY: boolean, mimeType?: string, quality?: number) { | |||
| @@ -1 +1,28 @@ | |||
| export {downloadBlob, blobToDataURL} from 'ts-browser-helpers' | |||
| export type {IEvent, IEventDispatcher} from 'ts-browser-helpers' | |||
| export type {ImageCanvasOptions} from 'ts-browser-helpers' | |||
| export type {AnyFunction, AnyOptions, Class, IDisposable, IJSONSerializable, PartialPick, PartialRecord, StringKeyOf, Fof, ValOrFunc, ValOrArr, ValOrArrOp} from 'ts-browser-helpers' | |||
| export type {Serializer} from 'ts-browser-helpers' | |||
| export {PointerDragHelper} from 'ts-browser-helpers' | |||
| export {Damper} from 'ts-browser-helpers' | |||
| export {SimpleEventDispatcher} from 'ts-browser-helpers' | |||
| export {createCanvasElement, createDiv, createImage, createStyles, createScriptFromURL} from 'ts-browser-helpers' | |||
| export {TYPED_ARRAYS, arrayBufferToBase64, base64ToArrayBuffer, getTypedArray} from 'ts-browser-helpers' | |||
| export {escapeRegExp, getFilenameFromPath, parseFileExtension, replaceAll, toTitleCase, longestCommonPrefix} from 'ts-browser-helpers' | |||
| export {prettyScrollbar} from 'ts-browser-helpers' | |||
| export {blobToDataURL, downloadBlob, downloadFile, uploadFile, mobileAndTabletCheck} from 'ts-browser-helpers' | |||
| export {LinearToSRGB, SRGBToLinear, colorToDataUrl} from 'ts-browser-helpers' | |||
| export {onChange, onChange2, onChange3, serialize, serializable} from 'ts-browser-helpers' | |||
| export {aesGcmDecrypt, aesGcmEncrypt} from 'ts-browser-helpers' | |||
| export {verifyPermission, writeFile, getFileHandle, getNewFileHandle, readFile} from 'ts-browser-helpers' | |||
| export {embedUrlRefs, htmlToCanvas, htmlToPng, htmlToSvg} from 'ts-browser-helpers' | |||
| export {imageToCanvas, imageBitmapToBase64, imageUrlToImageData, imageDataToCanvas, isWebpExportSupported, canvasFlipY} from 'ts-browser-helpers' | |||
| export {absMax, clearBit, updateBit} from 'ts-browser-helpers' | |||
| export {includesAll} from 'ts-browser-helpers' | |||
| export {copyProps, getOrCall, getPropertyDescriptor, isPropertyWritable, safeSetProperty} from 'ts-browser-helpers' | |||
| export {deepAccessObject, getKeyByValue, objectHasOwn} from 'ts-browser-helpers' | |||
| export {makeColorSvg, makeTextSvg, makeColorSvgCircle, svgToCanvas, svgToPng} from 'ts-browser-helpers' | |||
| export {timeout, now} from 'ts-browser-helpers' | |||
| export {pathJoin, getUrlQueryParam, setUrlQueryParam, remoteWorkerURL} from 'ts-browser-helpers' | |||
| export {css, glsl, html, svgUrl} from 'ts-browser-helpers' | |||
| export {Serialization} from 'ts-browser-helpers' | |||
| @@ -7,4 +7,3 @@ export {ThreeSerialization, type SerializationMetaType, type SerializationResour | |||
| export {shaderReplaceString} from './shader-helpers' | |||
| export {makeGLBFile} from './gltf' | |||
| export {serialize, serializable, Serialization} from 'ts-browser-helpers' | |||
| @@ -538,16 +538,16 @@ export function convertStringsToArrayBuffersInMeta(meta: SerializationMetaType) | |||
| }) | |||
| } | |||
| export function getEmptyMeta(): SerializationMetaType { | |||
| export function getEmptyMeta(res?: Partial<SerializationResourcesType>): SerializationMetaType { | |||
| return { // see Object3D.js toJSON for more details | |||
| geometries: {}, | |||
| materials: {}, | |||
| textures: {}, | |||
| images: {}, | |||
| shapes: [], | |||
| skeletons: {}, | |||
| animations: [], | |||
| extras: {}, | |||
| geometries: {...res?.geometries}, | |||
| materials: {...res?.materials}, | |||
| textures: {...res?.textures}, | |||
| images: {...res?.images}, | |||
| shapes: {...res?.shapes}, | |||
| skeletons: {...res?.skeletons}, | |||
| animations: {...res?.animations}, | |||
| extras: {...res?.extras}, | |||
| _context: {}, | |||
| } | |||
| } | |||
| @@ -761,8 +761,8 @@ export function metaToResources(meta?: SerializationMetaType): Partial<Serializa | |||
| } | |||
| export function metaFromResources(resources?: Partial<SerializationResourcesType>, viewer?: ThreeViewer): SerializationMetaType { | |||
| return { | |||
| ...getEmptyMeta(), | |||
| ...resources, | |||
| ...getEmptyMeta(resources), | |||
| _context: { | |||
| assetManager: viewer?.assetManager, | |||
| assetImporter: viewer?.assetManager.importer, | |||
| @@ -79,7 +79,7 @@ export abstract class AViewerPlugin<T extends string = string, TViewer extends T | |||
| // } | |||
| get dirty(): boolean { | |||
| return this._dirty | |||
| return this.enabled && this._dirty | |||
| } | |||
| set dirty(value: boolean) { | |||
| @@ -955,7 +955,7 @@ export class ThreeViewer extends EventDispatcher<IViewerEvent, IViewerEventTypes | |||
| const obj = <RootSceneImportResult>imported | |||
| if (obj.importedViewerConfig && options?.importConfig !== false) await this.importConfig(obj.importedViewerConfig) | |||
| this._scene.loadModelRoot(obj, options) | |||
| return imported | |||
| return this._scene.modelRoot as T | |||
| } | |||
| this._scene.addObject(imported, options) | |||
| return imported | |||