UNPKG

threepipe

Version:

A modern 3D viewer framework built on top of three.js, written in TypeScript, designed to make creating high-quality, modular, and extensible 3D experiences on the web simple and enjoyable.

326 lines (289 loc) 13.8 kB
import {Matrix3, SRGBColorSpace} from 'three' import {AViewerPluginSync, ThreeViewer} from '../../viewer' import {uiFolderContainer, UiObjectConfig, uiToggle} from 'uiconfig.js' import {serialize} from 'ts-browser-helpers' import {IMaterial, IObject3D, ITexture, PhysicalMaterial} from '../../core' import {MaterialExtension, updateMaterialDefines} from '../../materials' import {isNonRelativeUrl, shaderReplaceString, ThreeSerialization} from '../../utils' import {AssetManager, GLTFWriter2} from '../../assetmanager' import type {GLTFLoaderPlugin, GLTFParser} from 'three/examples/jsm/loaders/GLTFLoader.js' import CustomBumpMapPluginShader from './shaders/CustomBumpMapPlugin.glsl' import {matDefine} from '../../three' import {makeSamplerUi} from '../../ui/image-ui' /** * Custom Bump Map Plugin * Adds a material extension to PhysicalMaterial to support custom bump maps. * A Custom bump map is similar to the built-in bump map, but allows using an extra bump map and scale to give a combined effect. * This plugin also has support for bicubic filtering of the custom bump map and is enabled by default. * It also adds a UI to the material to edit the settings. * It uses WEBGI_materials_custom_bump_map glTF extension to save the settings in glTF files. * @category Plugins */ @uiFolderContainer('Custom BumpMap (MatExt)') export class CustomBumpMapPlugin extends AViewerPluginSync { static readonly PluginType = 'CustomBumpMapPlugin' @uiToggle('Enabled', (that: CustomBumpMapPlugin)=>({onChange: that.setDirty})) @serialize() enabled = true @uiToggle('Bicubic', (that: CustomBumpMapPlugin)=>({onChange: that.setDirty})) @matDefine('CUSTOM_BUMP_MAP_BICUBIC', undefined, true, CustomBumpMapPlugin.prototype.setDirty) @serialize() bicubicFiltering = true private _defines: any = { ['CUSTOM_BUMP_MAP_DEBUG']: false, ['CUSTOM_BUMP_MAP_BICUBIC']: true, } private _uniforms: any = { customBumpUvTransform: {value: new Matrix3()}, customBumpScale: {value: 0.001}, customBumpMap: {value: null}, } public enableCustomBump(material: IMaterial, map?: ITexture, scale?: number): boolean { const ud = material?.userData if (!ud) return false if (ud._hasCustomBump === undefined) { const meshes = material.appliedMeshes let possible = true if (meshes) for (const {geometry} of meshes) { if (geometry && (!geometry.attributes.position || !geometry.attributes.normal || !geometry.attributes.uv)) { possible = false } // if (possible && !geometry.attributes.tangent) { // geometry.computeTangents() // } } if (!possible) { return false } } ud._hasCustomBump = true ud._customBumpScale = scale ?? ud._customBumpScale ?? 0.001 ud._customBumpMap = map ?? ud._customBumpMap ?? null if (material.setDirty) material.setDirty() return true } readonly materialExtension: MaterialExtension = { parsFragmentSnippet: (_, material: PhysicalMaterial)=>{ if (this.isDisabled() || !material?.userData._hasCustomBump) return '' return CustomBumpMapPluginShader }, shaderExtender: (shader, material: PhysicalMaterial) => { if (this.isDisabled() || !material?.userData._hasCustomBump) return const customBumpMap = material.userData._customBumpMap if (!customBumpMap) return shader.fragmentShader = shaderReplaceString(shader.fragmentShader, '#glMarker beforeAccumulation', ` #if defined(CUSTOM_BUMP_MAP_ENABLED) && CUSTOM_BUMP_MAP_ENABLED > 0 normal = perturbNormalArb( - vViewPosition, normal, dHdxy_fwd_cb(), faceDirection ); #endif `, {prepend: true} ) shader.vertexShader = shaderReplaceString(shader.vertexShader, '#include <uv_pars_vertex>', ` #if defined(CUSTOM_BUMP_MAP_ENABLED) && CUSTOM_BUMP_MAP_ENABLED > 0 varying vec2 vCustomBumpUv; uniform mat3 customBumpUvTransform; #endif `, {prepend: true}, ) shader.vertexShader = shaderReplaceString(shader.vertexShader, '#include <uv_vertex>', ` #if defined(CUSTOM_BUMP_MAP_ENABLED) && CUSTOM_BUMP_MAP_ENABLED > 0 vCustomBumpUv = ( customBumpUvTransform * vec3( uv, 1 ) ).xy; #endif `, {prepend: true}, ) shader.defines && (shader.defines.USE_UV = '') }, onObjectRender: (object: IObject3D, material) => { const userData = material.userData if (!userData?._hasCustomBump) return if (!object.isMesh || !object.geometry) return const tex = userData._customBumpMap?.isTexture ? userData._customBumpMap : null this._uniforms.customBumpMap.value = tex this._uniforms.customBumpScale.value = tex ? userData._customBumpScale ?? 0 : 0 if (tex) { tex.updateMatrix() this._uniforms.customBumpUvTransform.value.copy(tex.matrix) } updateMaterialDefines({ ...this._defines, ['CUSTOM_BUMP_MAP_ENABLED']: +this.enabled, }, material) }, extraUniforms: { // ...this._uniforms, // done in constructor }, computeCacheKey: (material1: PhysicalMaterial) => { return (this.enabled ? '1' : '0') + (material1.userData._hasCustomBump ? '1' : '0') + material1.userData?._customBumpMap?.uuid }, isCompatible: (material1: PhysicalMaterial) => material1.isPhysicalMaterial, getUiConfig: material => { // todo use uiConfigMaterialExtension const viewer = this._viewer! const enableCustomBump = this.enableCustomBump.bind(this) const state = material.userData const config: UiObjectConfig = { type: 'folder', label: 'CustomBumpMap', onChange: (ev)=>{ if (!ev.config) return this.setDirty() }, children: [ { type: 'checkbox', label: 'Enabled', get value() { return state._hasCustomBump || false }, set value(v) { if (v === state._hasCustomBump) return if (v) { if (!enableCustomBump(material)) viewer.dialog.alert('CustomBumpMapPlugin - Cannot add CustomBumpMap.') } else { state._hasCustomBump = false if (material.setDirty) material.setDirty() } config.uiRefresh?.(true, 'postFrame') }, }, { type: 'slider', label: 'Bump Scale', bounds: [-20, 20], stepSize: 0.001, hidden: () => !state._hasCustomBump, property: [state, '_customBumpScale'], // onChange: this.setDirty, }, { type: 'image', label: 'Bump Map', hidden: () => !state._hasCustomBump, property: [state, '_customBumpMap'], onChange: ()=>{ if (material.setDirty) material.setDirty() }, }, makeSamplerUi(state as any, '_customBumpMap', 'Sampler', ()=>!state._hasCustomBump, ()=>material.setDirty && material.setDirty()), ], } return config }, } setDirty = (): void => { this.materialExtension.setDirty?.() this._viewer?.setDirty() } constructor() { super() Object.assign(this.materialExtension.extraUniforms!, this._uniforms) } onAdded(v: ThreeViewer) { super.onAdded(v) // v.addEventListener('preRender', this._preRender) v.assetManager.materials.registerMaterialExtension(this.materialExtension) v.assetManager.registerGltfExtension(customBumpMapGLTFExtension) // v.getPlugin(GBufferPlugin)?.material?.registerMaterialExtensions([this.materialExtension]) } onRemove(v: ThreeViewer) { v.assetManager.materials?.unregisterMaterialExtension(this.materialExtension) v.assetManager.unregisterGltfExtension(customBumpMapGLTFExtension.name) // v.getPlugin(GBufferPlugin)?.material?.unregisterMaterialExtensions([this.materialExtension]) return super.onRemove(v) } /** * @deprecated use {@link customBumpMapGLTFExtension} */ public static readonly CUSTOM_BUMP_MAP_GLTF_EXTENSION = 'WEBGI_materials_custom_bump_map' } declare module '../../core/IMaterial' { interface IMaterialUserData { _hasCustomBump?: boolean _customBumpMap?: ITexture | null _customBumpScale?: number } } /** * FragmentClipping Materials Extension * * Specification: https://threepipe.org/docs/gltf-extensions/WEBGI_materials_fragment_clipping_extension.html */ class GLTFMaterialsCustomBumpMapImport implements GLTFLoaderPlugin { public name: string public parser: GLTFParser constructor(parser: GLTFParser, private _viewer?: ThreeViewer) { this.parser = parser this.name = customBumpMapGLTFExtension.name } async extendMaterialParams(materialIndex: number, materialParams: any) { const parser = this.parser const materialDef = parser.json.materials[materialIndex] if (!materialDef.extensions || !materialDef.extensions[this.name]) return const extension = materialDef.extensions[this.name] if (!materialParams.userData) materialParams.userData = {} materialParams.userData._hasCustomBump = true // single _ so that its saved when cloning but not when saving materialParams.userData._customBumpScale = extension.customBumpScale ?? 0.0 const resources = extension.resources ? await this._viewer?.loadConfigResources(extension.resources) : undefined const pending = [] const tex = extension.customBumpMap if (tex) { if (tex && tex.resource && typeof tex.resource === 'string') { materialParams.userData._customBumpMap = ThreeSerialization.Deserialize(tex, null, resources, false) } else if (tex && tex.index !== undefined) { pending.push(parser.assignTexture(materialParams.userData, '_customBumpMap', tex).then((t: any) => { // t.format = RGBFormat t.colorSpace = SRGBColorSpace })) } else { console.warn('CustomBumpMapPlugin: Invalid Texture Map in extension', tex, materialDef.name) materialParams.userData._customBumpMap = null } } return Promise.all(pending) } // do any mesh or geometry processing here // afterRoot(result: GLTF): Promise<void> | null { // result.scene.traverse((object: any) => { // const mat = object.material?.userData?._hasCustomBump // if (!mat) return // const geom = object.geometry // if (!geom.attributes.tangent) { // geom.computeTangents() // geom.attributes.tangent.needsUpdate = true // } // }) // return null // } } const glTFMaterialsCustomBumpMapExport = (w: GLTFWriter2)=> ({ writeMaterial: (material: any, materialDef: any) => { if (!material.isMeshStandardMaterial || !material.userData._hasCustomBump) return if ((material.userData._customBumpScale || 0) < 0.001) return // todo: is this correct? materialDef.extensions = materialDef.extensions || {} const meta = {images: {}, textures: {}} const extensionDef: any = {} extensionDef.customBumpScale = material.userData._customBumpScale || 1.0 const rootPath = material.userData._customBumpMap?.userData.rootPath // this is required because gltf transform doesnt support data uris or external urls if (rootPath && isNonRelativeUrl(rootPath)) { extensionDef.customBumpMap = ThreeSerialization.Serialize(material.userData._customBumpMap, meta, false) } if (w.checkEmptyMap(material.userData._customBumpMap) && extensionDef.customBumpMap === undefined) { const customBumpMapDef = {index: w.processTexture(material.userData._customBumpMap)} w.applyTextureTransform(customBumpMapDef, material.userData._customBumpMap) extensionDef.customBumpMap = customBumpMapDef } if (Object.keys(meta.textures).length || Object.keys(meta.images).length) extensionDef.resources = meta materialDef.extensions[ customBumpMapGLTFExtension.name ] = extensionDef w.extensionsUsed[ customBumpMapGLTFExtension.name ] = true }, }) export const customBumpMapGLTFExtension = { name: 'WEBGI_materials_custom_bump_map', import: (p, v) => new GLTFMaterialsCustomBumpMapImport(p, v), export: glTFMaterialsCustomBumpMapExport, textures: { customBumpMap: 'RGB', }, } satisfies AssetManager['gltfExtensions'][number]