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.
242 lines (214 loc) • 10.1 kB
text/typescript
import {Color} from 'three'
import {AViewerPluginSync, ThreeViewer} from '../../viewer'
import {uiFolderContainer, UiObjectConfig, uiToggle} from 'uiconfig.js'
import {glsl, serialize} from 'ts-browser-helpers'
import {IMaterialUserData, 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'
/**
* Clearcoat Tint Plugin
* Adds a material extension to PhysicalMaterial which adds tint and thickness to the built-in clearcoat properties.
* It also adds a UI to the material to edit the settings.
* It uses WEBGI_materials_clearcoat_tint glTF extension to save the settings in glTF files.
* @category Plugins
*/
('Clearcoat Tint (MatExt)')
export class ClearcoatTintPlugin extends AViewerPluginSync {
static readonly PluginType = 'ClearcoatTintPlugin'
('Enabled', (that: ClearcoatTintPlugin)=>({onChange: that.setDirty}))
() enabled = true
// private _defines: any = {
// // eslint-disable-next-line @typescript-eslint/naming-convention
// CLEARCOAT_TINT_DEBUG: false,
// }
private _uniforms: any = {
ccTintColor: {value: new Color()},
ccThickness: {value: 0.},
ccIor: {value: 0.},
}
static AddClearcoatTint(material: PhysicalMaterial, params?: IMaterialUserData['_clearcoatTint']): IMaterialUserData['_clearcoatTint']|null {
const ud = material?.userData
if (!ud) return null
if (!ud._clearcoatTint) ud._clearcoatTint = {}
const tf = ud._clearcoatTint!
tf.enableTint = true
if (tf.tintColor === undefined) tf.tintColor = '#ffffff'
if (tf.thickness === undefined) tf.thickness = 0.1
if (tf.ior === undefined) tf.ior = 1.5
params && Object.assign(tf, params)
if (material.setDirty) material.setDirty()
return tf
}
// private _multiplyPass?: MultiplyPass
readonly materialExtension: MaterialExtension = {
parsFragmentSnippet: (_, material: PhysicalMaterial)=>{
if (this.isDisabled() || !material?.userData._clearcoatTint?.enableTint || !(material.clearcoat > 0)) return ''
return glsl`
uniform vec3 ccTintColor;
uniform float ccThickness;
uniform float ccIor;
vec3 clearcoatTint(const in float dotNV, const in float dotNL, const in float clearcoat) {
vec3 tint = ( ccThickness > 0. ? 1. - ccTintColor : ccTintColor); // Set thickness < 0 for glow.
tint = exp(tint * -(ccThickness * ((dotNL + dotNV) / max(dotNL * dotNV, 1e-3)))); // beer's law
return mix(vec3(1.0), tint, clearcoat);
}
`
},
shaderExtender: (shader, material: PhysicalMaterial) => {
if (this.isDisabled() || !material?.userData._clearcoatTint?.enableTint || !(material.clearcoat > 0)) return
// Note: clearcoat only considers specular, not diffuse
shader.fragmentShader = shaderReplaceString(shader.fragmentShader,
'float dotNVcc = saturate( dot( geometryClearcoatNormal, geometryViewDir ) );',
'float dotNVcc = saturate( dot( geometryClearcoatNormal, -refract(geometryViewDir, geometryClearcoatNormal, 1./ccIor) ) );')
// todo: we are considering all light is coming from env map, but we should consider light coming from light sources by seperating light and env map attenuation
shader.fragmentShader = shaderReplaceString(shader.fragmentShader,
'outgoingLight = outgoingLight * ( 1.0 - material.clearcoat * Fcc ) + clearcoatSpecular * material.clearcoat;',
'outgoingLight *= clearcoatTint(dotNVcc, dotNVcc, material.clearcoat);\n', {prepend: true})
shader.defines && (shader.defines.USE_UV = '')
},
onObjectRender: (_, material) => {
const tfUd = material.userData._clearcoatTint
if (!tfUd?.enableTint) return
this._uniforms.ccTintColor.value.set(tfUd.tintColor) // could be number or string also, apart from Color
this._uniforms.ccThickness.value = tfUd.thickness
this._uniforms.ccIor.value = tfUd.ior
updateMaterialDefines({
// ...this._defines,
['CLEARCOAT_TINT_ENABLED']: +!this.isDisabled(),
}, material)
},
extraUniforms: {
// ...this._uniforms, // done in constructor
},
computeCacheKey: (material1: PhysicalMaterial) => {
return (this.isDisabled() ? '0' : '1') + (material1.userData._clearcoatTint?.enableTint ? '1' : '0') + (material1.clearcoat > 0 ? '1' : '0')
},
isCompatible: (material1: PhysicalMaterial) => {
return material1.isPhysicalMaterial
},
getUiConfig: (material: PhysicalMaterial) => { // todo use uiConfigMaterialExtension
const viewer = this._viewer!
if (material.userData._clearcoatTint === undefined) material.userData._clearcoatTint = {}
const state = material.userData._clearcoatTint
const config: UiObjectConfig = {
type: 'folder',
label: 'Clearcoat Tint',
onChange: (ev)=>{
if (!ev.config) return
this.setDirty()
},
children: [
{
type: 'checkbox',
label: 'Enabled',
get value() {
return state.enableTint || false
},
set value(v) {
if (v === state.enableTint) return
if (v) {
if (!ClearcoatTintPlugin.AddClearcoatTint(material))
viewer.dialog.alert('Cannot add clearcoat tint.')
} else {
state.enableTint = false
if (material.setDirty) material.setDirty()
}
config.uiRefresh?.(true, 'postFrame')
},
},
{
type: 'color',
label: 'Tint color',
hidden: () => !state.enableTint,
property: [state, 'tintColor'],
},
{
type: 'input',
label: 'Thickness',
hidden: () => !state.enableTint,
property: [state, 'thickness'],
},
{
type: 'slider',
bounds: [0.8, 2.5],
label: 'IOR',
hidden: () => !state.enableTint,
property: [state, 'ior'],
},
],
}
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(clearCoatTintGLTFExtension)
}
onRemove(v: ThreeViewer) {
v.assetManager.materials?.unregisterMaterialExtension(this.materialExtension)
v.assetManager.unregisterGltfExtension(clearCoatTintGLTFExtension.name)
return super.onRemove(v)
}
/**
* @deprecated - use {@link clearCoatTintGLTFExtension}
*/
public static readonly CLEARCOAT_TINT_GLTF_EXTENSION = 'WEBGI_materials_clearcoat_tint'
}
declare module '../../core/IMaterial' {
interface IMaterialUserData {
_clearcoatTint?: {
enableTint?: boolean
tintColor?: Color|number|string
thickness?: number
ior?: number
}
}
}
/**
* ClearcoatTint Materials Extension
*
* Specification: https://threepipe.org/docs/gltf-extensions/WEBGI_materials_clearcoat_tint.html (todo - fix link)
*/
class GLTFMaterialsClearcoatTintExtensionImport implements GLTFLoaderPlugin {
public name: string
public parser: GLTFParser
constructor(parser: GLTFParser) {
this.parser = parser
this.name = clearCoatTintGLTFExtension.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 = {}
ClearcoatTintPlugin.AddClearcoatTint(materialParams)
ThreeSerialization.Deserialize(extension, materialParams.userData._clearcoatTint)
}
}
const glTFMaterialsClearcoatTintExtensionExport = (w: GLTFWriter2)=> ({
writeMaterial: (material: any, materialDef: any) => {
if (!material.isMeshStandardMaterial || !material.userData._clearcoatTint?.enableTint) return
materialDef.extensions = materialDef.extensions || {}
const extensionDef: any = ThreeSerialization.Serialize(material.userData._clearcoatTint)
materialDef.extensions[ clearCoatTintGLTFExtension.name ] = extensionDef
w.extensionsUsed[ clearCoatTintGLTFExtension.name ] = true
},
})
export const clearCoatTintGLTFExtension = {
name: 'WEBGI_materials_clearcoat_tint',
import: (p) => new GLTFMaterialsClearcoatTintExtensionImport(p),
export: glTFMaterialsClearcoatTintExtensionExport,
textures: undefined,
} satisfies AssetManager['gltfExtensions'][number]