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
text/typescript
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
*/
('Custom BumpMap (MatExt)')
export class CustomBumpMapPlugin extends AViewerPluginSync {
static readonly PluginType = 'CustomBumpMapPlugin'
('Enabled', (that: CustomBumpMapPlugin)=>({onChange: that.setDirty}))
() enabled = true
('Bicubic', (that: CustomBumpMapPlugin)=>({onChange: that.setDirty}))
('CUSTOM_BUMP_MAP_BICUBIC', undefined, true, CustomBumpMapPlugin.prototype.setDirty)
() 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]