340 lines
11 KiB
JavaScript
340 lines
11 KiB
JavaScript
/**
|
|
* The shader definition which powers the TokenRing.
|
|
*/
|
|
class TokenRingSamplerShader extends PrimaryBaseSamplerShader {
|
|
|
|
/** @override */
|
|
static classPluginName = "tokenRingBatch";
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/** @override */
|
|
static pausable = false;
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/** @inheritdoc */
|
|
static batchGeometry = [
|
|
...(super.batchGeometry ?? []),
|
|
{id: "aRingTextureCoord", size: 2, normalized: false, type: PIXI.TYPES.FLOAT},
|
|
{id: "aBackgroundTextureCoord", size: 2, normalized: false, type: PIXI.TYPES.FLOAT},
|
|
{id: "aRingColor", size: 4, normalized: true, type: PIXI.TYPES.UNSIGNED_BYTE},
|
|
{id: "aBackgroundColor", size: 4, normalized: true, type: PIXI.TYPES.UNSIGNED_BYTE},
|
|
{id: "aStates", size: 1, normalized: false, type: PIXI.TYPES.FLOAT},
|
|
{id: "aScaleCorrection", size: 2, normalized: false, type: PIXI.TYPES.FLOAT},
|
|
{id: "aRingColorBand", size: 2, normalized: false, type: PIXI.TYPES.FLOAT},
|
|
{id: "aTextureScaleCorrection", size: 1, normalized: false, type: PIXI.TYPES.FLOAT}
|
|
];
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/** @inheritdoc */
|
|
static batchVertexSize = super.batchVertexSize + 12;
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/** @inheritdoc */
|
|
static reservedTextureUnits = super.reservedTextureUnits + 1;
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/**
|
|
* A null UVs array used for nulled texture position.
|
|
* @type {Float32Array}
|
|
*/
|
|
static nullUvs = new Float32Array([0, 0, 0, 0, 0, 0, 0, 0]);
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/** @inheritdoc */
|
|
static batchDefaultUniforms(maxTex) {
|
|
return {
|
|
...super.batchDefaultUniforms(maxTex),
|
|
tokenRingTexture: maxTex + super.reservedTextureUnits,
|
|
time: 0
|
|
};
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/** @override */
|
|
static _preRenderBatch(batchRenderer) {
|
|
super._preRenderBatch(batchRenderer);
|
|
batchRenderer.renderer.texture.bind(CONFIG.Token.ring.ringClass.baseTexture,
|
|
batchRenderer.uniforms.tokenRingTexture);
|
|
batchRenderer.uniforms.time = canvas.app.ticker.lastTime / 1000;
|
|
batchRenderer.uniforms.debugColorBands = CONFIG.Token.ring.debugColorBands;
|
|
}
|
|
|
|
/* ---------------------------------------- */
|
|
|
|
/** @inheritdoc */
|
|
static _packInterleavedGeometry(element, attributeBuffer, indexBuffer, aIndex, iIndex) {
|
|
super._packInterleavedGeometry(element, attributeBuffer, indexBuffer, aIndex, iIndex);
|
|
const {float32View, uint32View} = attributeBuffer;
|
|
|
|
// Prepare token ring attributes
|
|
const vertexData = element.vertexData;
|
|
const trConfig = CONFIG.Token.ringClass;
|
|
const object = element.object.object || {};
|
|
const ringColor = PIXI.Color.shared.setValue(object.ring?.ringColorLittleEndian ?? 0xFFFFFF).toNumber();
|
|
const bkgColor = PIXI.Color.shared.setValue(object.ring?.bkgColorLittleEndian ?? 0xFFFFFF).toNumber();
|
|
const ringUvsFloat = object.ring?.ringUVs ?? trConfig.tokenRingSamplerShader.nullUvs;
|
|
const bkgUvsFloat = object.ring?.bkgUVs ?? trConfig.tokenRingSamplerShader.nullUvs;
|
|
const states = (object.ring?.effects ?? 0) + 0.5;
|
|
const scaleCorrectionX = (object.ring?.scaleCorrection ?? 1) * (object.ring?.scaleAdjustmentX ?? 1);
|
|
const scaleCorrectionY = (object.ring?.scaleCorrection ?? 1) * (object.ring?.scaleAdjustmentY ?? 1);
|
|
const colorBandRadiusStart = object.ring?.colorBand.startRadius ?? 0;
|
|
const colorBandRadiusEnd = object.ring?.colorBand.endRadius ?? 0;
|
|
const textureScaleAdjustment = object.ring?.textureScaleAdjustment ?? 1;
|
|
|
|
// Write attributes into buffer
|
|
const vertexSize = this.vertexSize;
|
|
const attributeOffset = PrimaryBaseSamplerShader.batchVertexSize;
|
|
for ( let i = 0, j = attributeOffset; i < vertexData.length; i += 2, j += vertexSize ) {
|
|
let k = aIndex + j;
|
|
float32View[k++] = ringUvsFloat[i];
|
|
float32View[k++] = ringUvsFloat[i + 1];
|
|
float32View[k++] = bkgUvsFloat[i];
|
|
float32View[k++] = bkgUvsFloat[i + 1];
|
|
uint32View[k++] = ringColor;
|
|
uint32View[k++] = bkgColor;
|
|
float32View[k++] = states;
|
|
float32View[k++] = scaleCorrectionX;
|
|
float32View[k++] = scaleCorrectionY;
|
|
float32View[k++] = colorBandRadiusStart;
|
|
float32View[k++] = colorBandRadiusEnd;
|
|
float32View[k++] = textureScaleAdjustment;
|
|
}
|
|
}
|
|
|
|
/* ---------------------------------------- */
|
|
/* GLSL Shader Code */
|
|
/* ---------------------------------------- */
|
|
|
|
/**
|
|
* The fragment shader header.
|
|
* @type {string}
|
|
*/
|
|
static #FRAG_HEADER = `
|
|
const uint STATE_RING_PULSE = 0x02U;
|
|
const uint STATE_RING_GRADIENT = 0x04U;
|
|
const uint STATE_BKG_WAVE = 0x08U;
|
|
const uint STATE_INVISIBLE = 0x10U;
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
bool hasState(in uint state) {
|
|
return (vStates & state) == state;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
vec2 rotation(in vec2 uv, in float a) {
|
|
uv -= 0.5;
|
|
float s = sin(a);
|
|
float c = cos(a);
|
|
return uv * mat2(c, -s, s, c) + 0.5;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
float normalizedCos(in float val) {
|
|
return (cos(val) + 1.0) * 0.5;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
float wave(in float dist) {
|
|
float sinWave = 0.5 * (sin(-time * 4.0 + dist * 100.0) + 1.0);
|
|
return mix(1.0, 0.55 * sinWave + 0.8, clamp(1.0 - dist, 0.0, 1.0));
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
vec4 colorizeTokenRing(in vec4 tokenRing, in float dist) {
|
|
if ( tokenRing.a > 0.0 ) tokenRing.rgb /= tokenRing.a;
|
|
vec3 rcol = hasState(STATE_RING_PULSE)
|
|
? mix(tokenRing.rrr, tokenRing.rrr * 0.35, (cos(time * 2.0) + 1.0) * 0.5)
|
|
: tokenRing.rrr;
|
|
vec3 ccol = vRingColor * rcol;
|
|
vec3 gcol = hasState(STATE_RING_GRADIENT)
|
|
? mix(ccol, vBackgroundColor * tokenRing.r, smoothstep(0.0, 1.0, dot(rotation(vTextureCoord, time), vec2(0.5))))
|
|
: ccol;
|
|
vec3 col = mix(tokenRing.rgb, gcol, step(vRingColorBand.x, dist) - step(vRingColorBand.y, dist));
|
|
return vec4(col, 1.0) * tokenRing.a;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
vec4 colorizeTokenBackground(in vec4 tokenBackground, in float dist) {
|
|
if (tokenBackground.a > 0.0) tokenBackground.rgb /= tokenBackground.a;
|
|
|
|
float wave = hasState(STATE_BKG_WAVE) ? (0.5 + wave(dist) * 1.5) : 1.0;
|
|
vec3 bgColor = tokenBackground.rgb;
|
|
vec3 tintColor = vBackgroundColor.rgb;
|
|
vec3 resultColor;
|
|
|
|
// Overlay blend mode
|
|
if ( tintColor == vec3(1.0, 1.0, 1.0) ) {
|
|
// If tint color is pure white, keep the original background color
|
|
resultColor = bgColor;
|
|
} else {
|
|
// Overlay blend mode
|
|
for ( int i = 0; i < 3; i++ ) {
|
|
if ( bgColor[i] < 0.5 ) resultColor[i] = 2.0 * bgColor[i] * tintColor[i];
|
|
else resultColor[i] = 1.0 - 2.0 * (1.0 - bgColor[i]) * (1.0 - tintColor[i]);
|
|
}
|
|
}
|
|
return vec4(resultColor, 1.0) * tokenBackground.a * wave;
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
vec4 processTokenColor(in vec4 finalColor) {
|
|
if ( !hasState(STATE_INVISIBLE) ) return finalColor;
|
|
|
|
// Computing halo
|
|
float lum = perceivedBrightness(finalColor.rgb);
|
|
vec3 haloColor = vec3(lum) * vec3(0.5, 1.0, 1.0);
|
|
|
|
// Construct final image
|
|
return vec4(haloColor, 1.0) * finalColor.a
|
|
* (0.55 + normalizedCos(time * 2.0) * 0.25);
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
vec4 blend(vec4 src, vec4 dst) {
|
|
return src + (dst * (1.0 - src.a));
|
|
}
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
float getTokenTextureClip() {
|
|
return step(3.5,
|
|
step(0.0, vTextureCoord.x) +
|
|
step(0.0, vTextureCoord.y) +
|
|
step(vTextureCoord.x, 1.0) +
|
|
step(vTextureCoord.y, 1.0));
|
|
}
|
|
`;
|
|
|
|
/* ---------------------------------------- */
|
|
|
|
/**
|
|
* Fragment shader body.
|
|
* @type {string}
|
|
*/
|
|
static #FRAG_MAIN = `
|
|
vec4 color;
|
|
vec4 result;
|
|
|
|
%forloop%
|
|
|
|
if ( vStates == 0U ) result = color * vColor;
|
|
else {
|
|
// Compute distances
|
|
vec2 scaledDistVec = (vOrigTextureCoord - 0.5) * 2.0 * vScaleCorrection;
|
|
|
|
// Euclidean distance computation
|
|
float dist = length(scaledDistVec);
|
|
|
|
// Rectangular distance computation
|
|
vec2 absScaledDistVec = abs(scaledDistVec);
|
|
float rectangularDist = max(absScaledDistVec.x, absScaledDistVec.y);
|
|
|
|
// Clip token texture color (necessary when a mesh is padded on x and/or y axis)
|
|
color *= getTokenTextureClip();
|
|
|
|
// Blend token texture, token ring and token background
|
|
result = blend(
|
|
processTokenColor(color * (vColor / vColor.a)),
|
|
blend(
|
|
colorizeTokenRing(texture(tokenRingTexture, vRingTextureCoord), dist),
|
|
colorizeTokenBackground(texture(tokenRingTexture, vBackgroundTextureCoord), dist)
|
|
) * step(rectangularDist, 1.0)
|
|
) * vColor.a;
|
|
}
|
|
`;
|
|
|
|
/* ---------------------------------------- */
|
|
|
|
/**
|
|
* Fragment shader body for debug code.
|
|
* @type {string}
|
|
*/
|
|
static #FRAG_MAIN_DEBUG = `
|
|
if ( debugColorBands ) {
|
|
vec2 scaledDistVec = (vTextureCoord - 0.5) * 2.0 * vScaleCorrection;
|
|
float dist = length(scaledDistVec);
|
|
result.rgb += vec3(0.0, 0.5, 0.0) * (step(vRingColorBand.x, dist) - step(vRingColorBand.y, dist));
|
|
}
|
|
`;
|
|
|
|
/* ---------------------------------------- */
|
|
|
|
/** @override */
|
|
static _batchVertexShader = `
|
|
in vec2 aRingTextureCoord;
|
|
in vec2 aBackgroundTextureCoord;
|
|
in vec2 aScaleCorrection;
|
|
in vec2 aRingColorBand;
|
|
in vec4 aRingColor;
|
|
in vec4 aBackgroundColor;
|
|
in float aTextureScaleCorrection;
|
|
in float aStates;
|
|
|
|
out vec2 vRingTextureCoord;
|
|
out vec2 vBackgroundTextureCoord;
|
|
out vec2 vOrigTextureCoord;
|
|
flat out vec2 vRingColorBand;
|
|
flat out vec3 vRingColor;
|
|
flat out vec3 vBackgroundColor;
|
|
flat out vec2 vScaleCorrection;
|
|
flat out uint vStates;
|
|
|
|
void _main(out vec2 vertexPosition, out vec2 textureCoord, out vec4 color) {
|
|
vRingTextureCoord = aRingTextureCoord;
|
|
vBackgroundTextureCoord = aBackgroundTextureCoord;
|
|
vRingColor = aRingColor.rgb;
|
|
vBackgroundColor = aBackgroundColor.rgb;
|
|
vStates = uint(aStates);
|
|
vScaleCorrection = aScaleCorrection;
|
|
vRingColorBand = aRingColorBand;
|
|
vOrigTextureCoord = aTextureCoord;
|
|
vertexPosition = (translationMatrix * vec3(aVertexPosition, 1.0)).xy;
|
|
textureCoord = (aTextureCoord - 0.5) * aTextureScaleCorrection + 0.5;
|
|
color = aColor * tint;
|
|
}
|
|
`;
|
|
|
|
/* -------------------------------------------- */
|
|
|
|
/** @override */
|
|
static _batchFragmentShader = `
|
|
in vec2 vRingTextureCoord;
|
|
in vec2 vBackgroundTextureCoord;
|
|
in vec2 vOrigTextureCoord;
|
|
flat in vec3 vRingColor;
|
|
flat in vec3 vBackgroundColor;
|
|
flat in vec2 vScaleCorrection;
|
|
flat in vec2 vRingColorBand;
|
|
flat in uint vStates;
|
|
|
|
uniform sampler2D tokenRingTexture;
|
|
uniform float time;
|
|
uniform bool debugColorBands;
|
|
|
|
${this.CONSTANTS}
|
|
${this.PERCEIVED_BRIGHTNESS}
|
|
${TokenRingSamplerShader.#FRAG_HEADER}
|
|
|
|
vec4 _main() {
|
|
${TokenRingSamplerShader.#FRAG_MAIN}
|
|
${TokenRingSamplerShader.#FRAG_MAIN_DEBUG}
|
|
return result;
|
|
}
|
|
`;
|
|
}
|