500 lines
24 KiB
JavaScript
500 lines
24 KiB
JavaScript
const gravityWebGlRenderers = new WeakMap();
|
|
const GRAVITY_SPLAT_STRIDE_FLOATS = 20;
|
|
export function drawGravityFieldWebGl(ctx, props, gravityDocument, bodies, bounds, width, height, renderSplatsDocument) {
|
|
const renderer = resolveGravityWebGlRenderer(ctx.canvas);
|
|
if (!renderer)
|
|
return false;
|
|
const gl = renderer.gl;
|
|
if (!ensureGravityWebGlTarget(renderer, width, height)) {
|
|
gravityWebGlRenderers.set(ctx.canvas, null);
|
|
return false;
|
|
}
|
|
const splatBuffers = buildGravitySplatBuffers(props, gravityDocument, bodies, bounds, renderSplatsDocument);
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, renderer.framebuffer);
|
|
gl.viewport(0, 0, width, height);
|
|
gl.disable(gl.DEPTH_TEST);
|
|
gl.disable(gl.CULL_FACE);
|
|
gl.enable(gl.BLEND);
|
|
gl.blendFunc(gl.ONE, gl.ONE);
|
|
gl.clearColor(0, 0, 0, 1);
|
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
if (splatBuffers.gravity.length > 0) {
|
|
gl.useProgram(renderer.splatProgram);
|
|
gl.uniform4f(gl.getUniformLocation(renderer.splatProgram, "u_viewport"), bounds.minX, bounds.minY, bounds.maxX, bounds.maxY);
|
|
gl.uniform1f(gl.getUniformLocation(renderer.splatProgram, "u_time"), numberProp(props.simulationTimeSeconds, 0));
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, renderer.splatBuffer);
|
|
gl.bufferData(gl.ARRAY_BUFFER, splatBuffers.gravity, gl.DYNAMIC_DRAW);
|
|
bindGravitySplatAttributes(gl, renderer.splatProgram);
|
|
gl.drawArrays(gl.TRIANGLES, 0, splatBuffers.gravity.length / GRAVITY_SPLAT_STRIDE_FLOATS);
|
|
}
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, renderer.tintFramebuffer);
|
|
gl.clearColor(0, 0, 0, 0);
|
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
if (splatBuffers.tint.length > 0) {
|
|
gl.useProgram(renderer.splatProgram);
|
|
gl.uniform4f(gl.getUniformLocation(renderer.splatProgram, "u_viewport"), bounds.minX, bounds.minY, bounds.maxX, bounds.maxY);
|
|
gl.uniform1f(gl.getUniformLocation(renderer.splatProgram, "u_time"), numberProp(props.simulationTimeSeconds, 0));
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, renderer.splatBuffer);
|
|
gl.bufferData(gl.ARRAY_BUFFER, splatBuffers.tint, gl.DYNAMIC_DRAW);
|
|
bindGravitySplatAttributes(gl, renderer.splatProgram);
|
|
gl.drawArrays(gl.TRIANGLES, 0, splatBuffers.tint.length / GRAVITY_SPLAT_STRIDE_FLOATS);
|
|
}
|
|
gl.disable(gl.BLEND);
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
gl.viewport(0, 0, width, height);
|
|
gl.useProgram(renderer.shadeProgram);
|
|
gl.activeTexture(gl.TEXTURE0);
|
|
gl.bindTexture(gl.TEXTURE_2D, renderer.fieldTexture);
|
|
gl.uniform1i(gl.getUniformLocation(renderer.shadeProgram, "u_field"), 0);
|
|
gl.activeTexture(gl.TEXTURE1);
|
|
gl.bindTexture(gl.TEXTURE_2D, renderer.tintTexture);
|
|
gl.uniform1i(gl.getUniformLocation(renderer.shadeProgram, "u_tint"), 1);
|
|
gl.uniform2f(gl.getUniformLocation(renderer.shadeProgram, "u_resolution"), width, height);
|
|
gl.uniform1f(gl.getUniformLocation(renderer.shadeProgram, "u_time"), numberProp(gravityDocument.simulationTimeSeconds, numberProp(props.simulationTimeSeconds, 0)));
|
|
gl.uniform1f(gl.getUniformLocation(renderer.shadeProgram, "u_lineInterval"), numberProp(props.lineInterval, numberProp(props.scalarFieldLineInterval, numberProp(props.depthRange, 228.79605) / 20)));
|
|
gl.uniform1f(gl.getUniformLocation(renderer.shadeProgram, "u_lineOffset"), numberProp(props.lineOffset, numberProp(props.startDepth, 2.310401)));
|
|
gl.uniform1f(gl.getUniformLocation(renderer.shadeProgram, "u_lineWidth"), numberProp(props.lineWidth, 0.3));
|
|
gl.uniform1f(gl.getUniformLocation(renderer.shadeProgram, "u_lineFade"), numberProp(props.lineFade, 5));
|
|
gl.uniform1f(gl.getUniformLocation(renderer.shadeProgram, "u_angleWidth"), numberProp(props.angleWidth, 0.05));
|
|
gl.uniform1f(gl.getUniformLocation(renderer.shadeProgram, "u_angleFade"), numberProp(props.angleFade, 1.15));
|
|
gl.uniform1f(gl.getUniformLocation(renderer.shadeProgram, "u_dangerSteepness"), numberProp(props.dangerSteepness, 1.5));
|
|
gl.uniform1f(gl.getUniformLocation(renderer.shadeProgram, "u_scale"), numberProp(props.isolineScale, 2.6924083));
|
|
uniform3(gl, renderer.shadeProgram, "u_baseColor", vector3Prop(props.scalarFieldBaseColor, [0.002, 0.006, 0.012]));
|
|
uniform3(gl, renderer.shadeProgram, "u_fieldGlowColor", vector3Prop(props.scalarFieldGlowColor, [0.018, 0.050, 0.075]));
|
|
uniform3(gl, renderer.shadeProgram, "u_lineLowColor", vector3Prop(props.scalarFieldLowLineColor, [0.0, 0.34, 0.52]));
|
|
uniform3(gl, renderer.shadeProgram, "u_lineHighColor", vector3Prop(props.scalarFieldHighLineColor, [1.45, 0.30, 0.05]));
|
|
uniform3(gl, renderer.shadeProgram, "u_angleLowColor", vector3Prop(props.scalarFieldLowAngleColor, [0.06, 0.16, 0.24]));
|
|
uniform3(gl, renderer.shadeProgram, "u_angleHighColor", vector3Prop(props.scalarFieldHighAngleColor, [1.10, 0.24, 0.04]));
|
|
gl.uniform1f(gl.getUniformLocation(renderer.shadeProgram, "u_tintScale"), numberProp(props.vectorFieldTintScale, 0.45));
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, renderer.quadBuffer);
|
|
const position = gl.getAttribLocation(renderer.shadeProgram, "a_position");
|
|
gl.enableVertexAttribArray(position);
|
|
gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
|
|
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
ctx.drawImage(renderer.canvas, 0, 0, width, height);
|
|
return true;
|
|
}
|
|
function buildGravitySplatsFromDocument(document) {
|
|
const splats = objectProps(document?.splats);
|
|
const layers = Array.isArray(document?.layers) ? document.layers : [];
|
|
const gravityLayerIndex = layers.findIndex(layer => stringProp(layer?.layerKey, "") === "gravity.height");
|
|
if (gravityLayerIndex < 0)
|
|
return undefined;
|
|
const count = Math.max(0, positiveInt(splats.count, 0));
|
|
const centerX = numberArray(splats.centerX);
|
|
const centerY = numberArray(splats.centerY);
|
|
const halfExtentX = numberArray(splats.halfExtentX);
|
|
const halfExtentY = numberArray(splats.halfExtentY);
|
|
const rotationCos = numberArray(splats.rotationCos);
|
|
const rotationSin = numberArray(splats.rotationSin);
|
|
const channel = numberArray(splats.channel);
|
|
const falloff = numberArray(splats.falloff);
|
|
const valueR = numberArray(splats.valueR);
|
|
const valueG = numberArray(splats.valueG);
|
|
const sourceKind = numberArray(splats.sourceKind);
|
|
const frequencyX = numberArray(splats.frequencyX);
|
|
const animationSpeed = numberArray(splats.animationSpeed);
|
|
const sourceFlags = numberArray(splats.sourceFlags);
|
|
const layerIndex = numberArray(splats.layerIndex);
|
|
const rows = [];
|
|
const corners = [-1, -1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1];
|
|
for (let index = 0; index < count; index += 1) {
|
|
if ((layerIndex[index] ?? -1) !== gravityLayerIndex)
|
|
continue;
|
|
for (let corner = 0; corner < corners.length; corner += 2) {
|
|
rows.push(corners[corner], corners[corner + 1], centerX[index] ?? 0, centerY[index] ?? 0, Math.max(0.0001, halfExtentX[index] ?? 1), Math.max(0.0001, halfExtentY[index] ?? 1), rotationCos[index] ?? 1, rotationSin[index] ?? 0, channel[index] ?? 1, falloff[index] ?? 0, valueR[index] ?? 0, 0, sourceKind[index] ?? 0, frequencyX[index] ?? 1, animationSpeed[index] ?? 0, sourceFlags[index] ?? 0);
|
|
}
|
|
}
|
|
return rows.length > 0 ? new Float32Array(rows) : undefined;
|
|
}
|
|
function buildGravitySplatBuffers(props, gravityDocument, bodies, bounds, renderSplatsDocument) {
|
|
const gravityRows = [];
|
|
const tintRows = [];
|
|
appendUnityGravitySplats(gravityRows, props, gravityDocument, bodies, bounds);
|
|
appendTintSplatsFromDocument(tintRows, props, renderSplatsDocument);
|
|
return {
|
|
gravity: new Float32Array(gravityRows),
|
|
tint: new Float32Array(tintRows),
|
|
};
|
|
}
|
|
function appendUnityGravitySplats(rows, props, gravityDocument, bodies, bounds) {
|
|
const terrainRadius = numberProp(gravityDocument.terrainRadius, numberProp(props.terrainRadius, 1200));
|
|
const terrainDepth = numberProp(gravityDocument.terrainDepth, numberProp(props.terrainDepth, -8));
|
|
if (terrainDepth !== 0) {
|
|
appendGravitySplat(rows, 0, 0, Math.max(1, terrainRadius * 2), Math.max(1, terrainRadius * 2), -terrainDepth, 0, 0, 1, Math.max(0.0001, numberProp(gravityDocument.terrainDepthExponent, numberProp(props.terrainDepthExponent, 1.2))), 0, 1, 0, 0);
|
|
}
|
|
for (const body of bodies) {
|
|
appendGravitySplat(rows, body.x, body.y, Math.max(1, body.radius), Math.max(1, body.radius), -body.depth, 0, 0, 1, Math.max(0.0001, body.exponent), 0, 1, 0, 0);
|
|
if (body.waveRadius > 0 && body.waveDepth !== 0) {
|
|
appendGravitySplat(rows, body.x, body.y, body.waveRadius, body.waveRadius, -body.waveDepth, 0, 0, 1, 8.0, 4, numberProp(gravityDocument.terrainWaveFrequency, numberProp(props.terrainWaveFrequency, 0.6)), body.waveSpeed, 0);
|
|
}
|
|
}
|
|
void bounds;
|
|
}
|
|
function appendTintSplatsFromDocument(rows, props, document) {
|
|
const splats = objectProps(document?.splats);
|
|
const layers = Array.isArray(document?.layers) ? document.layers : [];
|
|
const tintLayerIndices = new Set();
|
|
const selectedLayer = firstString(props.vectorFieldLayer, props.tintFieldLayer, "fog.tint");
|
|
layers.forEach((layer, index) => {
|
|
const layerKey = stringProp(layer?.layerKey, "");
|
|
const channel = numberProp(layer?.channel, -1);
|
|
if (channel === 4 && layerKey === selectedLayer)
|
|
tintLayerIndices.add(index);
|
|
});
|
|
const count = Math.max(0, positiveInt(splats.count, 0));
|
|
if (count <= 0 || tintLayerIndices.size === 0)
|
|
return;
|
|
const centerX = numberArray(splats.centerX);
|
|
const centerY = numberArray(splats.centerY);
|
|
const halfExtentX = numberArray(splats.halfExtentX);
|
|
const halfExtentY = numberArray(splats.halfExtentY);
|
|
const rotationCos = numberArray(splats.rotationCos);
|
|
const rotationSin = numberArray(splats.rotationSin);
|
|
const channel = numberArray(splats.channel);
|
|
const falloff = numberArray(splats.falloff);
|
|
const valueR = numberArray(splats.valueR);
|
|
const valueG = numberArray(splats.valueG);
|
|
const valueB = numberArray(splats.valueB);
|
|
const valueA = numberArray(splats.valueA);
|
|
const sourceKind = numberArray(splats.sourceKind);
|
|
const frequencyX = numberArray(splats.frequencyX);
|
|
const animationSpeed = numberArray(splats.animationSpeed);
|
|
const sourceFlags = numberArray(splats.sourceFlags);
|
|
const layerIndex = numberArray(splats.layerIndex);
|
|
for (let index = 0; index < count; index += 1) {
|
|
if (!tintLayerIndices.has(layerIndex[index] ?? -1) || (channel[index] ?? -1) !== 4)
|
|
continue;
|
|
const power = (falloff[index] ?? 0) === 0 ? 0.0001 : 1.25;
|
|
appendGravitySplat(rows, centerX[index] ?? 0, centerY[index] ?? 0, Math.max(0.0001, halfExtentX[index] ?? 1), Math.max(0.0001, halfExtentY[index] ?? 1), valueR[index] ?? 0, valueG[index] ?? 0, valueB[index] ?? 0, valueA[index] ?? 1, power, sourceKind[index] ?? 0, frequencyX[index] ?? 1, animationSpeed[index] ?? 0, sourceFlags[index] ?? 0, rotationCos[index] ?? 1, rotationSin[index] ?? 0);
|
|
}
|
|
}
|
|
function resolveGravityWebGlRenderer(canvas) {
|
|
if (gravityWebGlRenderers.has(canvas))
|
|
return gravityWebGlRenderers.get(canvas) || null;
|
|
const output = document.createElement("canvas");
|
|
const gl = output.getContext("webgl2", { alpha: false, antialias: false, premultipliedAlpha: false });
|
|
if (!gl) {
|
|
gravityWebGlRenderers.set(canvas, null);
|
|
return null;
|
|
}
|
|
const colorBufferFloat = gl.getExtension("EXT_color_buffer_float");
|
|
if (!colorBufferFloat) {
|
|
gravityWebGlRenderers.set(canvas, null);
|
|
return null;
|
|
}
|
|
const splatProgram = createGravityProgram(gl, GRAVITY_SPLAT_VERTEX_SHADER, GRAVITY_SPLAT_FRAGMENT_SHADER);
|
|
const shadeProgram = createGravityProgram(gl, GRAVITY_SHADE_VERTEX_SHADER, GRAVITY_SHADE_FRAGMENT_SHADER);
|
|
const splatBuffer = gl.createBuffer();
|
|
const quadBuffer = gl.createBuffer();
|
|
if (!splatProgram || !shadeProgram || !splatBuffer || !quadBuffer) {
|
|
gravityWebGlRenderers.set(canvas, null);
|
|
return null;
|
|
}
|
|
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuffer);
|
|
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
|
|
-1, -1, 1, -1, 1, 1,
|
|
-1, -1, 1, 1, -1, 1,
|
|
]), gl.STATIC_DRAW);
|
|
const renderer = {
|
|
canvas: output,
|
|
gl,
|
|
fieldTexture: null,
|
|
tintTexture: null,
|
|
framebuffer: null,
|
|
tintFramebuffer: null,
|
|
splatProgram,
|
|
shadeProgram,
|
|
splatBuffer,
|
|
quadBuffer,
|
|
width: 0,
|
|
height: 0,
|
|
};
|
|
gravityWebGlRenderers.set(canvas, renderer);
|
|
return renderer;
|
|
}
|
|
function ensureGravityWebGlTarget(renderer, width, height) {
|
|
if (renderer.width === width && renderer.height === height && renderer.fieldTexture && renderer.tintTexture && renderer.framebuffer && renderer.tintFramebuffer)
|
|
return true;
|
|
const gl = renderer.gl;
|
|
renderer.width = width;
|
|
renderer.height = height;
|
|
renderer.canvas.width = width;
|
|
renderer.canvas.height = height;
|
|
if (renderer.fieldTexture)
|
|
gl.deleteTexture(renderer.fieldTexture);
|
|
if (renderer.tintTexture)
|
|
gl.deleteTexture(renderer.tintTexture);
|
|
if (renderer.framebuffer)
|
|
gl.deleteFramebuffer(renderer.framebuffer);
|
|
if (renderer.tintFramebuffer)
|
|
gl.deleteFramebuffer(renderer.tintFramebuffer);
|
|
renderer.fieldTexture = gl.createTexture();
|
|
renderer.tintTexture = gl.createTexture();
|
|
renderer.framebuffer = gl.createFramebuffer();
|
|
renderer.tintFramebuffer = gl.createFramebuffer();
|
|
if (!renderer.fieldTexture || !renderer.tintTexture || !renderer.framebuffer || !renderer.tintFramebuffer)
|
|
return false;
|
|
configureGravityTexture(gl, renderer.fieldTexture, width, height);
|
|
configureGravityTexture(gl, renderer.tintTexture, width, height);
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, renderer.framebuffer);
|
|
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, renderer.fieldTexture, 0);
|
|
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE)
|
|
return false;
|
|
gl.bindFramebuffer(gl.FRAMEBUFFER, renderer.tintFramebuffer);
|
|
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, renderer.tintTexture, 0);
|
|
return gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE;
|
|
}
|
|
function configureGravityTexture(gl, texture, width, height) {
|
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, width, height, 0, gl.RGBA, gl.HALF_FLOAT, null);
|
|
}
|
|
function buildGravitySplats(props, gravityDocument, bodies, bounds) {
|
|
const rows = [];
|
|
void bounds;
|
|
const terrainRadius = numberProp(gravityDocument.terrainRadius, numberProp(props.terrainRadius, 1200));
|
|
const terrainDepth = numberProp(gravityDocument.terrainDepth, numberProp(props.terrainDepth, -8));
|
|
if (terrainDepth !== 0) {
|
|
const terrainHalfExtent = Math.max(1, terrainRadius * 2);
|
|
appendGravitySplat(rows, 0, 0, terrainHalfExtent, terrainHalfExtent, -terrainDepth, 0, 0, 1, Math.max(0.0001, numberProp(gravityDocument.terrainDepthExponent, numberProp(props.terrainDepthExponent, 1.2))), 0, 1, 0, 0);
|
|
}
|
|
for (const body of bodies) {
|
|
appendGravitySplat(rows, body.x, body.y, Math.max(1, body.radius), Math.max(1, body.radius), -body.depth, 0, 0, 1, Math.max(0.0001, body.exponent), 0, 1, 0, 0);
|
|
if (body.waveRadius > 0 && body.waveDepth !== 0) {
|
|
appendGravitySplat(rows, body.x, body.y, body.waveRadius, body.waveRadius, -body.waveDepth, 0, 0, 1, 8.0, 4, numberProp(gravityDocument.terrainWaveFrequency, numberProp(props.terrainWaveFrequency, 0.6)), body.waveSpeed, 0);
|
|
}
|
|
}
|
|
return new Float32Array(rows);
|
|
}
|
|
function appendGravitySplat(rows, centerX, centerY, halfX, halfY, valueR, valueG, valueB, valueA, power, sourceKind, frequencyX, animationSpeed, sourceFlags, rotationCos = 1, rotationSin = 0) {
|
|
const corners = [-1, -1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1];
|
|
for (let i = 0; i < corners.length; i += 2) {
|
|
rows.push(corners[i], corners[i + 1], centerX, centerY, halfX, halfY, rotationCos, rotationSin, power, 0, 0, 0, sourceKind, frequencyX, animationSpeed, sourceFlags, valueR, valueG, valueB, valueA);
|
|
}
|
|
}
|
|
function bindGravitySplatAttributes(gl, program) {
|
|
bindGravitySplatAttribute(gl, program, "a_corner", 2, 0);
|
|
bindGravitySplatAttribute(gl, program, "a_center", 2, 2);
|
|
bindGravitySplatAttribute(gl, program, "a_half", 2, 4);
|
|
bindGravitySplatAttribute(gl, program, "a_rot", 2, 6);
|
|
bindGravitySplatAttribute(gl, program, "a_meta", 4, 8);
|
|
bindGravitySplatAttribute(gl, program, "a_source", 4, 12);
|
|
bindGravitySplatAttribute(gl, program, "a_value", 4, 16);
|
|
}
|
|
function bindGravitySplatAttribute(gl, program, name, size, offsetFloats) {
|
|
const location = gl.getAttribLocation(program, name);
|
|
if (location < 0)
|
|
return;
|
|
gl.enableVertexAttribArray(location);
|
|
gl.vertexAttribPointer(location, size, gl.FLOAT, false, GRAVITY_SPLAT_STRIDE_FLOATS * 4, offsetFloats * 4);
|
|
}
|
|
function createGravityProgram(gl, vertexSource, fragmentSource) {
|
|
const vertex = compileGravityShader(gl, gl.VERTEX_SHADER, vertexSource);
|
|
const fragment = compileGravityShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
|
|
if (!vertex || !fragment)
|
|
return null;
|
|
const program = gl.createProgram();
|
|
if (!program)
|
|
return null;
|
|
gl.attachShader(program, vertex);
|
|
gl.attachShader(program, fragment);
|
|
gl.linkProgram(program);
|
|
if (!gl.getProgramParameter(program, gl.LINK_STATUS))
|
|
return null;
|
|
return program;
|
|
}
|
|
function compileGravityShader(gl, type, source) {
|
|
const shader = gl.createShader(type);
|
|
if (!shader)
|
|
return null;
|
|
gl.shaderSource(shader, source);
|
|
gl.compileShader(shader);
|
|
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS))
|
|
return null;
|
|
return shader;
|
|
}
|
|
function numberProp(value, fallback) {
|
|
const numeric = Number(value);
|
|
return Number.isFinite(numeric) ? numeric : fallback;
|
|
}
|
|
function vector3Prop(value, fallback) {
|
|
if (Array.isArray(value)) {
|
|
const values = value.map(entry => Number(entry));
|
|
if (values.length >= 3 && values.slice(0, 3).every(Number.isFinite))
|
|
return [values[0], values[1], values[2]];
|
|
}
|
|
if (typeof value === "string") {
|
|
const values = value.split(/[,\s]+/).map(entry => Number(entry)).filter(Number.isFinite);
|
|
if (values.length >= 3)
|
|
return [values[0], values[1], values[2]];
|
|
}
|
|
return fallback;
|
|
}
|
|
function uniform3(gl, program, name, value) {
|
|
gl.uniform3f(gl.getUniformLocation(program, name), value[0], value[1], value[2]);
|
|
}
|
|
function numberArray(value) {
|
|
if (!Array.isArray(value))
|
|
return [];
|
|
return value.map(entry => numberProp(entry, 0));
|
|
}
|
|
function objectProps(value) {
|
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
}
|
|
function stringProp(value, fallback) {
|
|
return value === null || value === undefined ? fallback : String(value);
|
|
}
|
|
function firstString(...values) {
|
|
for (const value of values) {
|
|
if (typeof value === "string" && value.trim())
|
|
return value;
|
|
if (typeof value === "number" && Number.isFinite(value))
|
|
return String(value);
|
|
}
|
|
return "";
|
|
}
|
|
function positiveInt(value, fallback) {
|
|
const numeric = Number(value);
|
|
return Number.isFinite(numeric) ? Math.max(0, Math.floor(numeric)) : fallback;
|
|
}
|
|
const GRAVITY_SPLAT_VERTEX_SHADER = `#version 300 es
|
|
in vec2 a_corner;
|
|
in vec2 a_center;
|
|
in vec2 a_half;
|
|
in vec2 a_rot;
|
|
in vec4 a_meta;
|
|
in vec4 a_source;
|
|
in vec4 a_value;
|
|
uniform vec4 u_viewport;
|
|
out vec2 v_local;
|
|
out vec4 v_meta;
|
|
out vec4 v_source;
|
|
out vec4 v_value;
|
|
void main() {
|
|
vec2 scaled = a_corner * a_half;
|
|
vec2 world = a_center + vec2(
|
|
scaled.x * a_rot.x - scaled.y * a_rot.y,
|
|
scaled.x * a_rot.y + scaled.y * a_rot.x
|
|
);
|
|
vec2 clip = vec2(
|
|
((world.x - u_viewport.x) / max(0.0001, u_viewport.z - u_viewport.x)) * 2.0 - 1.0,
|
|
((world.y - u_viewport.y) / max(0.0001, u_viewport.w - u_viewport.y)) * 2.0 - 1.0
|
|
);
|
|
gl_Position = vec4(clip, 0.0, 1.0);
|
|
v_local = a_corner;
|
|
v_meta = a_meta;
|
|
v_source = a_source;
|
|
v_value = a_value;
|
|
}`;
|
|
const GRAVITY_SPLAT_FRAGMENT_SHADER = `#version 300 es
|
|
precision highp float;
|
|
in vec2 v_local;
|
|
in vec4 v_meta;
|
|
in vec4 v_source;
|
|
in vec4 v_value;
|
|
uniform float u_time;
|
|
out vec4 outColor;
|
|
float hash21(vec2 p) {
|
|
p = fract(p * vec2(123.34, 456.21));
|
|
p += dot(p, p + 45.32);
|
|
return fract(p.x * p.y);
|
|
}
|
|
float valueNoise(vec2 p) {
|
|
vec2 i = floor(p);
|
|
vec2 f = fract(p);
|
|
float a = hash21(i);
|
|
float b = hash21(i + vec2(1.0, 0.0));
|
|
float c = hash21(i + vec2(0.0, 1.0));
|
|
float d = hash21(i + vec2(1.0, 1.0));
|
|
vec2 u = f * f * (3.0 - 2.0 * f);
|
|
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y) * 2.0 - 1.0;
|
|
}
|
|
float powerPulse(float x, float power) {
|
|
x = clamp(abs(x), 0.0, 1.0);
|
|
return pow((x + 1.0) * (1.0 - x), max(0.0001, power));
|
|
}
|
|
void main() {
|
|
float d = length(v_local);
|
|
if (d > 1.0) discard;
|
|
float alpha = powerPulse(d, v_meta.x);
|
|
if (alpha <= 0.0001) discard;
|
|
float source = 1.0;
|
|
if (v_source.x > 0.5 && v_source.x < 2.5) {
|
|
float timeOffset = v_source.x > 1.5 ? u_time * v_source.z : 0.0;
|
|
source = valueNoise(v_local * v_source.y + timeOffset);
|
|
if (v_source.w != 0.0) source = abs(source);
|
|
} else if (v_source.x > 3.5) {
|
|
source = cos(pow(d * 2.0, 1.25) * v_source.y + u_time * v_source.z);
|
|
}
|
|
outColor = v_value * alpha * source;
|
|
}`;
|
|
const GRAVITY_SHADE_VERTEX_SHADER = `#version 300 es
|
|
in vec2 a_position;
|
|
out vec2 v_uv;
|
|
void main() {
|
|
v_uv = a_position * 0.5 + 0.5;
|
|
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
}`;
|
|
const GRAVITY_SHADE_FRAGMENT_SHADER = `#version 300 es
|
|
precision highp float;
|
|
uniform sampler2D u_field;
|
|
uniform sampler2D u_tint;
|
|
uniform vec2 u_resolution;
|
|
uniform float u_time;
|
|
uniform float u_lineInterval;
|
|
uniform float u_lineOffset;
|
|
uniform float u_lineWidth;
|
|
uniform float u_lineFade;
|
|
uniform float u_angleWidth;
|
|
uniform float u_angleFade;
|
|
uniform float u_dangerSteepness;
|
|
uniform float u_scale;
|
|
uniform vec3 u_baseColor;
|
|
uniform vec3 u_fieldGlowColor;
|
|
uniform vec3 u_lineLowColor;
|
|
uniform vec3 u_lineHighColor;
|
|
uniform vec3 u_angleLowColor;
|
|
uniform vec3 u_angleHighColor;
|
|
uniform float u_tintScale;
|
|
in vec2 v_uv;
|
|
out vec4 outColor;
|
|
vec2 calcGrad(vec2 uv, float me) {
|
|
vec2 texel = 1.0 / max(vec2(1.0), u_resolution);
|
|
float n = -texture(u_field, vec2(uv.x, uv.y + texel.y)).r;
|
|
float e = -texture(u_field, vec2(uv.x + texel.x, uv.y)).r;
|
|
return vec2(e - me, n - me);
|
|
}
|
|
void main() {
|
|
float height = -texture(u_field, v_uv).r;
|
|
vec4 tint = texture(u_tint, v_uv);
|
|
vec2 plan = calcGrad(v_uv, height);
|
|
float slope = max(length(plan), 0.00001);
|
|
vec3 color = u_baseColor;
|
|
float fieldGlow = smoothstep(0.0, u_lineInterval * 7.0, abs(height));
|
|
color += u_fieldGlowColor * fieldGlow;
|
|
color += tint.rgb * clamp(tint.a, 0.0, 1.0) * u_tintScale;
|
|
|
|
float dangerBlend = smoothstep(0.0, u_dangerSteepness, pow(slope / max(0.0001, u_scale), 2.0));
|
|
vec3 baseLine = mix(u_lineLowColor, u_lineHighColor, dangerBlend);
|
|
vec3 angleLine = mix(u_angleLowColor, u_angleHighColor, dangerBlend) * clamp(slope / max(0.0001, u_scale), 0.0, 1.0);
|
|
|
|
vec3 lines = vec3(0.0);
|
|
float interval = max(0.0001, abs(u_lineInterval));
|
|
float nearestLine = abs(fract((height + u_lineOffset) / interval) - 0.5) * interval;
|
|
float lineDistance = nearestLine / slope;
|
|
float line = 1.0 - smoothstep(u_lineWidth, u_lineWidth * u_lineFade, lineDistance);
|
|
lines += line * baseLine;
|
|
|
|
float angle = atan(plan.y, plan.x) / 3.1415926536 + 1.0;
|
|
float nearestAngle = abs(fract(angle * 6.0) - 0.5) / 6.0;
|
|
float angleMask = smoothstep(interval * 0.25, interval * 1.5, abs(height));
|
|
float angleLineAmount = 1.0 - smoothstep(u_angleWidth, u_angleWidth * u_angleFade, nearestAngle);
|
|
lines += angleLineAmount * angleMask * angleLine;
|
|
|
|
color += clamp(lines, 0.0, 1.7);
|
|
outColor = vec4(color, 1.0);
|
|
}`;
|