@pmndrs/uikit
Version:
Build performant 3D user interfaces with Three.js and yoga.
367 lines (338 loc) • 15.6 kB
JavaScript
import { Color, FrontSide, MeshDepthMaterial, MeshDistanceMaterial, RGBADepthPacking, } from 'three';
import { setBorderRadius } from './utils.js';
import { computed } from '@preact/signals-core';
const noColor = new Color(-1, -1, -1);
const defaultDefaults = {
backgroundColor: noColor,
backgroundOpacity: -1,
borderColor: 0xffffff,
borderBottomLeftRadius: 0,
borderTopLeftRadius: 0,
borderBottomRightRadius: 0,
borderTopRightRadius: 0,
borderBend: 0,
borderOpacity: 1,
};
let defaultPanelMaterialConfig;
export function getDefaultPanelMaterialConfig() {
if (defaultPanelMaterialConfig == null) {
const defaultPanelMaterialKeys = {};
for (const key in defaultDefaults) {
defaultPanelMaterialKeys[key] = key;
}
defaultPanelMaterialConfig = createPanelMaterialConfig(defaultPanelMaterialKeys);
}
return defaultPanelMaterialConfig;
}
export function createPanelMaterialConfig(keys, overrideDefaults) {
const defaults = { ...defaultDefaults, ...overrideDefaults };
const setters = {};
for (const key in keys) {
const fn = materialSetters[key];
const defaultValue = defaults[key];
setters[keys[key]] = (data, offset, value, size, onUpdate) => fn(data, offset, (value ?? defaultValue), size, onUpdate);
}
const defaultData = new Float32Array(16); //filled with 0s by default
writeColor(defaultData, 4, defaults.backgroundColor, undefined);
writeColor(defaultData, 8, defaults.borderColor, undefined);
defaultData[11] = defaults.borderBend;
defaultData[12] = defaults.borderOpacity;
defaultData[15] = defaults.backgroundOpacity;
return {
hasProperty: (key) => key in setters,
defaultData,
setters,
computedIsVisibile: (propertiesSignal, borderInset, size, isVisible) => {
return computed(() => {
if (borderInset.value == null || size.value == null) {
return true;
}
const borderOpacity = keys.borderOpacity == null
? defaults.borderOpacity
: propertiesSignal.value.read(keys.borderOpacity, defaults.borderOpacity);
const backgroundOpacity = keys.backgroundOpacity == null
? defaults.backgroundOpacity
: propertiesSignal.value.read(keys.backgroundOpacity, defaults.backgroundOpacity);
const backgroundColor = keys.backgroundColor == null
? defaults.backgroundColor
: propertiesSignal.value.read(keys.backgroundColor, defaults.backgroundColor);
const borderVisible = borderInset.value.some((s) => s > 0) && borderOpacity > 0;
const [width, height] = size.value;
const backgroundVisible = width > 0 && height > 0 && (backgroundOpacity === -1 || backgroundOpacity > 0) && backgroundColor != noColor;
if (!backgroundVisible && !borderVisible) {
return false;
}
return isVisible.value;
});
},
};
}
const materialSetters = {
//0-3 = borderSizes
//4-6 = background color
backgroundColor: (d, o, p, _, u) => writeColor(d, o + 4, p, u),
//7 = border radiuses
borderBottomLeftRadius: (d, o, p, { value: s }, u) => s != null && writeBorderRadius(d, o + 7, 0, p, s[1], u),
borderBottomRightRadius: (d, o, p, { value: s }, u) => s != null && writeBorderRadius(d, o + 7, 1, p, s[1], u),
borderTopRightRadius: (d, o, p, { value: s }, u) => s != null && writeBorderRadius(d, o + 7, 2, p, s[1], u),
borderTopLeftRadius: (d, o, p, { value: s }, u) => s != null && writeBorderRadius(d, o + 7, 3, p, s[1], u),
//8 - 10 = border color
borderColor: (d, o, p, _, u) => writeColor(d, o + 8, p, u),
//11
borderBend: (d, o, p, _, u) => writeComponent(d, o + 11, p, u),
//12
borderOpacity: (d, o, p, _, u) => writeComponent(d, o + 12, p, u),
//13 = width
//14 = height
//15
backgroundOpacity: (d, o, p, _, u) => writeComponent(d, o + 15, p, u),
};
function writeBorderRadius(data, offset, indexInFloat, value, height, onUpdate) {
setBorderRadius(data, offset, indexInFloat, value, height);
onUpdate?.(offset, 1);
}
function writeComponent(data, offset, value, onUpdate) {
data[offset] = value;
onUpdate?.(offset, 1);
}
const colorHelper = new Color();
export function writeColor(target, offset, color, onUpdate) {
if (Array.isArray(color)) {
target.set(color, offset);
}
else {
colorHelper.set(color).toArray(target, offset);
}
onUpdate?.(offset, 3);
}
export function createPanelMaterial(MaterialClass, info) {
const material = new MaterialClass();
if (material.defines == null) {
material.defines = {};
}
material.side = FrontSide;
material.clipShadows = true;
material.transparent = true;
material.toneMapped = false;
material.shadowSide = FrontSide;
material.defines.USE_UV = '';
material.defines.USE_TANGENT = '';
const superOnBeforeCompile = material.onBeforeCompile;
material.onBeforeCompile = (parameters, renderer) => {
superOnBeforeCompile.call(material, parameters, renderer);
if (info.type === 'normal') {
parameters.uniforms.data = { value: info.data };
}
compilePanelMaterial(parameters, info.type === 'instanced');
};
return material;
}
export class PanelDistanceMaterial extends MeshDistanceMaterial {
info;
constructor(info) {
super();
this.info = info;
if (this.defines == null) {
this.defines = {};
}
this.defines.USE_UV = '';
this.clipShadows = true;
}
onBeforeCompile(parameters, renderer) {
super.onBeforeCompile(parameters, renderer);
if (this.info.type === 'normal') {
parameters.uniforms.data = { value: this.info.data };
}
compilePanelDepthMaterial(parameters, this.info.type === 'instanced');
}
}
export class PanelDepthMaterial extends MeshDepthMaterial {
info;
constructor(info) {
super({ depthPacking: RGBADepthPacking });
this.info = info;
if (this.defines == null) {
this.defines = {};
}
this.defines.USE_UV = '';
this.clipShadows = true;
}
onBeforeCompile(parameters, renderer) {
super.onBeforeCompile(parameters, renderer);
if (this.info.type === 'normal') {
parameters.uniforms.data = { value: this.info.data };
}
compilePanelDepthMaterial(parameters, this.info.type === 'instanced');
}
}
export const instancedPanelDepthMaterial = new PanelDepthMaterial({ type: 'instanced' });
export const instancedPanelDistanceMaterial = new PanelDistanceMaterial({ type: 'instanced' });
function compilePanelDepthMaterial(parameters, instanced) {
compilePanelClippingMaterial(parameters, instanced);
parameters.fragmentShader = parameters.fragmentShader.replace('#include <clipping_planes_fragment>', `#include <clipping_planes_fragment>
${getFargmentOpacityCode(instanced, undefined)}
`);
}
function compilePanelClippingMaterial(parameters, instanced) {
parameters.vertexShader = parameters.vertexShader.replace('#include <common>', ` #include <common>
out vec4 borderRadius;
${instanced ? '' : 'uniform highp mat4 data;'}`);
parameters.vertexShader = parameters.vertexShader.replace('#include <uv_vertex>', ` #include <uv_vertex>
highp int packedBorderRadius = int(data[1].w);
borderRadius = vec4(
packedBorderRadius / 125000 % 50,
packedBorderRadius / 2500 % 50,
packedBorderRadius / 50 % 50,
packedBorderRadius % 50
) * vec4(0.5 / 50.0);`);
if (instanced) {
parameters.vertexShader = parameters.vertexShader.replace('#include <common>', ` #include <common>
attribute highp mat4 aData;
attribute mat4 aClipping;
out mat4 data;
out mat4 clipping;
out vec3 localPosition;`);
parameters.vertexShader = parameters.vertexShader.replace('#include <uv_vertex>', ` #include <uv_vertex>
data = aData;
clipping = aClipping;
localPosition = (instanceMatrix * vec4(position, 1.0)).xyz;`);
}
parameters.fragmentShader =
`${instanced ? 'in' : 'uniform'} highp mat4 data;
in vec4 borderRadius;
${instanced
? `
in vec3 localPosition;
in mat4 clipping;
`
: ''}
float min4 (vec4 v) {
return min(min(min(v.x,v.y),v.z),v.w);
}
float max4 (vec4 v) {
return max(max(max(v.x,v.y),v.z),v.w);
}
vec2 radiusDistance(float radius, vec2 outside, vec2 border, vec2 borderSize) {
vec2 outerRadiusXX = vec2(radius, radius);
vec2 innerRadiusXX = outerRadiusXX - borderSize;
vec2 radiusWeightUnnormalized = abs(innerRadiusXX - border);
vec2 radiusWeight = radiusWeightUnnormalized / vec2(radiusWeightUnnormalized.x + radiusWeightUnnormalized.y);
return vec2(
radius - distance(outside, outerRadiusXX),
dot(radiusWeight, innerRadiusXX) - distance(border, innerRadiusXX)
);
}
` + parameters.fragmentShader;
parameters.fragmentShader = parameters.fragmentShader.replace('#include <clipping_planes_fragment>', ` ${instanced
? `
vec4 plane;
float distanceToPlane, distanceGradient;
float clipOpacity = 1.0;
for(int i = 0; i < 4; i++) {
plane = clipping[ i ];
distanceToPlane = - dot( -localPosition, plane.xyz ) + plane.w;
distanceGradient = fwidth( distanceToPlane ) / 2.0;
clipOpacity *= smoothstep( - distanceGradient, distanceGradient, distanceToPlane );
if ( clipOpacity < 0.01 ) discard;
}
`
: ''}
vec4 absoluteBorderSize = data[0];
vec3 backgroundColor = data[1].xyz;
vec3 borderColor = data[2].xyz;
float borderBend = data[2].w;
float borderOpacity = data[3].x;
float width = data[3].y;
float height = data[3].z;
float backgroundOpacity = data[3].w;
float ratio = width / height;
vec4 relative = vec4(height, height, height, height);
vec4 borderSize = absoluteBorderSize / relative;
vec4 v_outsideDistance = vec4(1.0 - vUv.y, (1.0 - vUv.x) * ratio, vUv.y, vUv.x * ratio);
vec4 v_borderDistance = v_outsideDistance - borderSize;
vec2 distance = vec2(min4(v_outsideDistance), min4(v_borderDistance));
vec4 negateBorderDistance = vec4(1.0) - v_borderDistance;
float maxWeight = max4(negateBorderDistance);
vec4 borderWeight = step(maxWeight, negateBorderDistance);
vec4 insideBorder;
if(all(lessThan(v_outsideDistance.xw, borderRadius.xx))) {
distance = radiusDistance(borderRadius.x, v_outsideDistance.xw, v_borderDistance.xw, borderSize.xw);
float tmp = borderRadius.x - borderSize.w;
vec2 xIntersection = vec2(tmp, tmp / ratio);
tmp = borderRadius.x - borderSize.x;
vec2 yIntersection = vec2(tmp * ratio, tmp);
vec2 lineIntersection = min(xIntersection, yIntersection);
insideBorder.yz = vec2(0.0);
insideBorder.xw = max(vec2(0.0), lineIntersection - v_borderDistance.xw);
} else if(all(lessThan(v_outsideDistance.xy, borderRadius.yy))) {
distance = radiusDistance(borderRadius.y, v_outsideDistance.xy, v_borderDistance.xy, borderSize.xy);
float tmp = borderRadius.y - borderSize.y;
vec2 xIntersection = vec2(tmp, tmp / ratio);
tmp = borderRadius.y - borderSize.x;
vec2 yIntersection = vec2(tmp * ratio, tmp);
vec2 lineIntersection = min(xIntersection, yIntersection);
insideBorder.zw = vec2(0.0);
insideBorder.xy = max(vec2(0.0), lineIntersection - v_borderDistance.xy);
} else if(all(lessThan(v_outsideDistance.zy, borderRadius.zz))) {
distance = radiusDistance(borderRadius.z, v_outsideDistance.zy, v_borderDistance.zy, borderSize.zy);
float tmp = borderRadius.z - borderSize.y;
vec2 xIntersection = vec2(tmp, tmp / ratio);
tmp = borderRadius.z - borderSize.z;
vec2 yIntersection = vec2(tmp * ratio, tmp);
vec2 lineIntersection = min(xIntersection, yIntersection);
insideBorder.xw = vec2(0.0);
insideBorder.zy =max(vec2(0.0), lineIntersection - v_borderDistance.zy);
} else if(all(lessThan(v_outsideDistance.zw, borderRadius.ww))) {
distance = radiusDistance(borderRadius.w, v_outsideDistance.zw, v_borderDistance.zw, borderSize.zw);
float tmp = borderRadius.w - borderSize.w;
vec2 xIntersection = vec2(tmp, tmp / ratio);
tmp = borderRadius.w - borderSize.z;
vec2 yIntersection = vec2(tmp * ratio, tmp);
vec2 lineIntersection = min(xIntersection, yIntersection);
insideBorder.xy = vec2(0.0);
insideBorder.zw = max(vec2(0.0), lineIntersection - v_borderDistance.zw);
}
if(insideBorder.x + insideBorder.y + insideBorder.z + insideBorder.w > 0.0) {
borderWeight = normalize(insideBorder);
}
#include <clipping_planes_fragment>`);
}
function getFargmentOpacityCode(instanced, existingOpacity) {
return `float ddx = fwidth(distance.x);
float outer = smoothstep(-ddx, ddx, distance.x);
float ddy = fwidth(distance.y);
float inner = smoothstep(-ddy, ddy, distance.y);
float transition = 1.0 - step(0.1, outer - inner) * (1.0 - inner);
if(backgroundColor.r < 0.0 && backgroundOpacity >= 0.0) {
backgroundColor = vec3(1.0);
}
if(backgroundOpacity < 0.0) {
backgroundOpacity = backgroundColor.r >= 0.0 ? 1.0 : 0.0;
}
if(backgroundOpacity < 0.0) {
backgroundOpacity = 0.0;
}
borderOpacity = min(backgroundOpacity + data[3].x, 1.0);
borderColor = mix(backgroundColor, data[2].xyz, data[3].x / borderOpacity);
float outOpacity = ${instanced ? 'clipOpacity * ' : ''} outer * mix(borderOpacity, ${existingOpacity == null ? '' : `${existingOpacity} *`} backgroundOpacity, transition);
if(outOpacity < 0.01) {
discard;
}`;
}
export function compilePanelMaterial(parameters, instanced) {
compilePanelClippingMaterial(parameters, instanced);
parameters.fragmentShader = parameters.fragmentShader.replace('#include <color_fragment>', ` #include <color_fragment>
${getFargmentOpacityCode(instanced, 'diffuseColor.a')}
diffuseColor.rgb = mix(borderColor, diffuseColor.rgb * backgroundColor, transition);
diffuseColor.a = outOpacity;
`);
parameters.fragmentShader = parameters.fragmentShader.replace('#include <normal_fragment_maps>', ` #include <normal_fragment_maps>
vec3 b = normalize(vBitangent);
vec3 t = normalize(vTangent);
mat4 directions = mat4(vec4(b, 1.0), vec4(t, 1.0), vec4(-b, 1.0), vec4(-t, 1.0));
float currentBorderSize = distance.x - distance.y;
float outsideNormalWeight = currentBorderSize < 0.00001 ? 0.0 : max(0.0, -distance.y / currentBorderSize) * borderBend;
vec3 outsideNormal = (borderWeight * transpose(directions)).xyz;
normal = normalize(outsideNormalWeight * outsideNormal + (1.0 - outsideNormalWeight) * normal);
`);
}