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.
329 lines (305 loc) • 14.3 kB
text/typescript
import {Vector2, Vector2Tuple, Vector3, Vector3Tuple, Vector4, Vector4Tuple} from 'three'
import {AViewerPluginSync, ThreeViewer} from '../../viewer'
import {uiFolderContainer, UiObjectConfig, uiToggle} from 'uiconfig.js'
import {serialize} from 'ts-browser-helpers'
import {IMaterial, IMaterialUserData, IObject3D, PhysicalMaterial} from '../../core'
import {MaterialExtension, updateMaterialDefines} from '../../materials'
import {shaderReplaceString, ThreeSerialization} from '../../utils'
import {AssetManager, GLTFWriter2} from '../../assetmanager'
import type {GLTFLoaderPlugin, GLTFParser} from 'three/examples/jsm/loaders/GLTFLoader.js'
import NoiseBumpMaterialPluginPars from './shaders/NoiseBumpMaterialPlugin.pars.glsl'
import NoiseBumpMaterialPluginPatch from './shaders/NoiseBumpMaterialPlugin.patch.glsl'
/**
* NoiseBump Materials Extension
* Adds a material extension to PhysicalMaterial to add support for sparkle bump / noise bump by creating procedural bump map from noise to simulate sparkle flakes.
* It uses voronoise function from blender along with several additions to generate the noise for the generation.
* It also adds a UI to the material to edit the settings.
* It uses WEBGI_materials_noise_bump glTF extension to save the settings in glTF files.
* @category Plugins
*/
('Noise/Sparkle Bump (MatExt)')
export class NoiseBumpMaterialPlugin extends AViewerPluginSync {
static readonly PluginType = 'NoiseBumpMaterialPlugin'
('Enabled', (that: NoiseBumpMaterialPlugin)=>({onChange: that.setDirty}))
() enabled = true
// private _defines: any = {
// }
private _uniforms: any = {
noiseBumpParams: {value: new Vector2()}, // u scale, v scale,
noiseBumpScale: {value: 0.05},
noiseBumpFlakeScale: {value: 1000.0},
noiseFlakeClamp: {value: 1.0},
noiseFlakeRadius: {value: 0.5},
flakeParams: {value: new Vector4(0, 1, 3, 0)},
flakeFallOffParams: {value: new Vector3(0, 1, 0)},
useColorFlakes: {value: false},
}
public static AddNoiseBumpMaterial(material: IMaterial, params?: IMaterialUserData['_noiseBumpMat']): boolean {
const ud = material?.userData
if (!ud) return false
if (!ud._noiseBumpMat) {
ud._noiseBumpMat = {}
}
const tf = ud._noiseBumpMat
tf.hasBump = true
if (tf.bumpNoiseParams === undefined) tf.bumpNoiseParams = new Vector2(0.5, 0.5)
if (tf.bumpScale === undefined) tf.bumpScale = 0.05
if (tf.flakeScale === undefined) tf.flakeScale = 0.05
if (tf.flakeClamp === undefined) tf.flakeClamp = 1
if (tf.flakeRadius === undefined) tf.flakeRadius = 0.3
if (tf.useColorFlakes === undefined) tf.useColorFlakes = false
if (tf.flakeParams === undefined) tf.flakeParams = new Vector4(0, 1, 3, 0)
if (tf.flakeFallOffParams === undefined) tf.flakeFallOffParams = new Vector3(0, 1, 0)
params && Object.assign(tf, params)
if (material.setDirty) material.setDirty()
return true
}
readonly materialExtension: MaterialExtension = {
parsFragmentSnippet: (_, material: PhysicalMaterial)=>{
if (this.isDisabled() || !material?.userData._noiseBumpMat?.hasBump) return ''
return NoiseBumpMaterialPluginPars
},
shaderExtender: (shader, material: PhysicalMaterial) => {
if (this.isDisabled() || !material?.userData._noiseBumpMat?.hasBump) return
shader.fragmentShader = shaderReplaceString(shader.fragmentShader, '#glMarker beforeAccumulation', NoiseBumpMaterialPluginPatch, {prepend: true})
shader.defines && (shader.defines.USE_UV = '')
// @ts-expect-error this is not req anymore actually.
shader.extensionDerivatives = true
},
onObjectRender: (_: IObject3D, material) => {
const tfUd = material.userData._noiseBumpMat
if (!tfUd?.hasBump) return
if (Array.isArray(tfUd.bumpNoiseParams)) this._uniforms.noiseBumpParams.value.fromArray(tfUd.bumpNoiseParams)
else this._uniforms.noiseBumpParams.value.copy(tfUd.bumpNoiseParams)
this._uniforms.noiseBumpScale.value = tfUd.bumpScale
this._uniforms.noiseBumpFlakeScale.value = tfUd.flakeScale
this._uniforms.noiseFlakeClamp.value = tfUd.flakeClamp
this._uniforms.noiseFlakeRadius.value = tfUd.flakeRadius
if (Array.isArray(tfUd.flakeParams)) this._uniforms.flakeParams.value.fromArray(tfUd.flakeParams)
else this._uniforms.flakeParams.value.copy(tfUd.flakeParams)
if (Array.isArray(tfUd.flakeFallOffParams)) this._uniforms.flakeFallOffParams.value.fromArray(tfUd.flakeFallOffParams)
else this._uniforms.flakeFallOffParams.value.copy(tfUd.flakeFallOffParams)
this._uniforms.useColorFlakes.value = tfUd.useColorFlakes
updateMaterialDefines({
// ...this._defines,
['NOISE_BUMP_MATERIAL_ENABLED']: +!this.isDisabled(),
}, material)
},
extraUniforms: {
// ...this._uniforms, // done in constructor
},
computeCacheKey: (material1: PhysicalMaterial) => {
return (this.isDisabled() ? '0' : '1') + (material1.userData._noiseBumpMat?.hasBump ? '1' : '0')
},
isCompatible: (material1: PhysicalMaterial) => material1.isPhysicalMaterial,
getUiConfig: material => { // todo use uiConfigMaterialExtension
const viewer = this._viewer!
if (material.userData._noiseBumpMat === undefined) material.userData._noiseBumpMat = {}
const state = material.userData._noiseBumpMat
const config: UiObjectConfig = {
type: 'folder',
label: 'SparkleBump (NoiseBump)',
onChange: (ev)=>{
if (!ev.config) return
this.setDirty()
},
children: [
{
type: 'checkbox',
label: 'Enabled',
get value() {
return state.hasBump || false
},
set value(v) {
if (v === state.hasBump) return
if (v) {
if (!NoiseBumpMaterialPlugin.AddNoiseBumpMaterial(material))
viewer.dialog.alert('Cannot add NoiseBumpMaterial.')
} else {
state.hasBump = false
if (material.setDirty) material.setDirty()
}
config.uiRefresh?.(true, 'postFrame')
},
},
{
type: 'vec4',
label: 'Bump Noise Params',
bounds: [0, 1],
hidden: () => !state.hasBump,
property: [state, 'bumpNoiseParams'],
},
{
type: 'slider',
label: 'Bump Scale',
bounds: [0, 0.001],
stepSize: 0.00001,
hidden: () => !state.hasBump,
property: [state, 'bumpScale'],
},
{
type: 'slider',
label: 'Flake Scale',
bounds: [100, 10000],
stepSize: 0.0001,
hidden: () => !state.hasBump,
property: [state, 'flakeScale'],
},
{
type: 'slider',
label: 'Flake Clamp',
bounds: [0, 1],
stepSize: 1,
hidden: () => !state.hasBump,
property: [state, 'flakeClamp'],
},
{
type: 'slider',
label: 'Flake Radius',
bounds: [0.01, 1],
stepSize: 0.001,
hidden: () => !state.hasBump,
property: [state, 'flakeRadius'],
},
{
type: 'slider',
label: 'Flake Roughness',
bounds: [0., 1],
stepSize: 0.01,
hidden: () => !state.hasBump,
property: [state.flakeParams, 'x'],
},
{
type: 'slider',
label: 'Flake Metalness',
bounds: [0., 1],
stepSize: 0.01,
hidden: () => !state.hasBump,
property: [state.flakeParams, 'y'],
},
{
type: 'slider',
label: 'Flake Strength',
bounds: [0.0, 100],
stepSize: 0.001,
hidden: () => !state.hasBump,
property: [state.flakeParams, 'z'],
},
{
type: 'slider',
label: 'Flake Threshold',
bounds: [0.1, 10],
stepSize: 0.001,
hidden: () => !state.hasBump,
property: [state.flakeParams, 'w'],
},
{
type: 'slider',
label: 'Falloff',
stepSize: 1,
bounds: [0, 1],
hidden: () => !state.hasBump,
property: [state.flakeFallOffParams, 'x'],
},
{
type: 'slider',
label: 'Linear falloff factor',
bounds: [0., 10],
stepSize: 0.001,
hidden: () => !state.hasBump,
property: [state.flakeFallOffParams, 'y'],
},
{
type: 'slider',
label: 'Quadratic falloff factor',
bounds: [0., 10],
stepSize: 0.001,
hidden: () => !state.hasBump,
property: [state.flakeFallOffParams, 'z'],
},
{
type: 'checkbox',
label: 'Colored Flakes',
hidden: () => !state.hasBump,
property: [state, 'useColorFlakes'],
},
],
}
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.assetManager.materials.registerMaterialExtension(this.materialExtension)
v.assetManager.registerGltfExtension(noiseBumpMaterialGLTFExtension)
}
onRemove(v: ThreeViewer) {
v.assetManager.materials?.unregisterMaterialExtension(this.materialExtension)
v.assetManager.unregisterGltfExtension(noiseBumpMaterialGLTFExtension.name)
return super.onRemove(v)
}
/**
* @deprecated - use {@link noiseBumpMaterialGLTFExtension}
*/
public static readonly NOISE_BUMP_MATERIAL_GLTF_EXTENSION = 'WEBGI_materials_noise_bump'
}
declare module '../../core/IMaterial' {
interface IMaterialUserData {
_noiseBumpMat?: {
hasBump?: boolean
bumpNoiseParams?: Vector2Tuple | Vector2
bumpScale?: number
flakeScale?: number
flakeClamp?: number
flakeRadius?: number
useColorFlakes?: boolean
flakeParams?: Vector4Tuple | Vector4
flakeFallOffParams?: Vector3Tuple | Vector3
}
}
}
/**
* FragmentClipping Materials Extension
*
* Specification: https://threepipe.org/docs/gltf-extensions/WEBGI_materials_fragment_clipping_extension.html (todo - fix link)
*/
class GLTFMaterialsNoiseBumpMaterialImport implements GLTFLoaderPlugin {
public name: string
public parser: GLTFParser
constructor(parser: GLTFParser) {
this.parser = parser
this.name = noiseBumpMaterialGLTFExtension.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 = {}
NoiseBumpMaterialPlugin.AddNoiseBumpMaterial(materialParams)
ThreeSerialization.Deserialize(extension, materialParams.userData._noiseBumpMat)
}
}
const glTFMaterialsNoiseBumpMaterialExport = (w: GLTFWriter2)=> ({
writeMaterial: (material: any, materialDef: any) => {
if (!material.isMeshStandardMaterial || !material.userData._noiseBumpMat?.hasBump) return
materialDef.extensions = materialDef.extensions || {}
const extensionDef: any = ThreeSerialization.Serialize(material.userData._noiseBumpMat)
materialDef.extensions[ noiseBumpMaterialGLTFExtension.name ] = extensionDef
w.extensionsUsed[ noiseBumpMaterialGLTFExtension.name ] = true
},
})
export const noiseBumpMaterialGLTFExtension = {
name: 'WEBGI_materials_noise_bump',
import: (p) => new GLTFMaterialsNoiseBumpMaterialImport(p),
export: glTFMaterialsNoiseBumpMaterialExport,
textures: undefined,
} satisfies AssetManager['gltfExtensions'][number]