Add files via upload
This commit is contained in:
parent
6d3d94931c
commit
4f2f610d05
|
|
@ -0,0 +1,330 @@
|
|||
// realistic_system.js - Global Realistic Lighting, Shadows, and Water Effects for ALL elements (including addons)
|
||||
// Features:
|
||||
// - Dynamic colored light propagation from fire, lights, hot pixels, etc.
|
||||
// - Realistic occlusion shadows (darker in crevices/caves), modulated by light intensity
|
||||
// - Wavy foam on ALL liquids (water, oils, mod liquids)
|
||||
// - Works with any mods/addons automatically (liquids get waves, all get lit/shadowed)
|
||||
// Performance: Low-res lightmap + cached shadows = smooth even on large views
|
||||
// Install: Save as realistic_system.js in mods folder, load via Mod Manager
|
||||
|
||||
// === LIGHTMAP SYSTEM (Dynamic Glow/Light Propagation) ===
|
||||
var lightmap = [];
|
||||
var nextLightmap = [];
|
||||
var lightmapScale = 4; // Higher = lower quality/faster (2-8 recommended)
|
||||
var lightSourceBoost = 3;
|
||||
var falloff = 0.85;
|
||||
|
||||
// Helper functions
|
||||
function rgbToArray(colorString) {
|
||||
if (typeof colorString !== "string") return [255,255,255];
|
||||
if (colorString.startsWith("rgb")) {
|
||||
return colorString.slice(4, -1).split(",").map(val => parseInt(val.trim()));
|
||||
} else if (colorString.startsWith("#")) {
|
||||
let hex = colorString.slice(1);
|
||||
if (hex.length === 3) hex = hex.split("").map(char => char + char).join("");
|
||||
let r = parseInt(hex.slice(0, 2), 16);
|
||||
let g = parseInt(hex.slice(2, 4), 16);
|
||||
let b = parseInt(hex.slice(4, 6), 16);
|
||||
return [r, g, b];
|
||||
}
|
||||
return [255,255,255];
|
||||
}
|
||||
|
||||
function scaleList(numbers, scale) {
|
||||
return numbers.map(number => number * scale);
|
||||
}
|
||||
|
||||
function rgbToHsv(r, g, b) {
|
||||
r /= 255; g /= 255; b /= 255;
|
||||
let max = Math.max(r, g, b), min = Math.min(r, g, b);
|
||||
let h, s, v = max;
|
||||
let d = max - min;
|
||||
s = max === 0 ? 0 : d / max;
|
||||
if (max === min) h = 0;
|
||||
else {
|
||||
switch (max) {
|
||||
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
|
||||
case g: h = (b - r) / d + 2; break;
|
||||
case b: h = (r - g) / d + 4; break;
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
return [h, s, v];
|
||||
}
|
||||
|
||||
function hsvToRgb(h, s, v) {
|
||||
let i = Math.floor(h * 6);
|
||||
let f = h * 6 - i;
|
||||
let p = v * (1 - s);
|
||||
let q = v * (1 - f * s);
|
||||
let t = v * (1 - (1 - f) * s);
|
||||
let r, g, b;
|
||||
switch (i % 6) {
|
||||
case 0: r = v; g = t; b = p; break;
|
||||
case 1: r = q; g = v; b = p; break;
|
||||
case 2: r = p; g = v; b = t; break;
|
||||
case 3: r = p; g = q; b = v; break;
|
||||
case 4: r = t; g = p; b = v; break;
|
||||
case 5: r = v; g = p; b = q; break;
|
||||
}
|
||||
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
||||
}
|
||||
|
||||
function initializeLightmap(w, h) {
|
||||
let lw = Math.ceil(w / lightmapScale) + 1;
|
||||
let lh = Math.ceil(h / lightmapScale) + 1;
|
||||
function createArray(width_, height_) {
|
||||
return Array.from({length: height_}, () => Array.from({length: width_}, () => ({color: [0, 0, 0]})));
|
||||
}
|
||||
lightmap = createArray(lw, lh);
|
||||
nextLightmap = createArray(lw, lh);
|
||||
}
|
||||
|
||||
function propagateLightmap() {
|
||||
if (!lightmap[0]) return;
|
||||
let width = lightmap[0].length;
|
||||
let height = lightmap.length;
|
||||
let neighbors = [{dx:1,dy:0},{dx:-1,dy:0},{dx:0,dy:1},{dx:0,dy:-1}];
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
let totalColor = [0,0,0];
|
||||
let neighborCount = 0;
|
||||
neighbors.forEach(n => {
|
||||
let nx = x + n.dx, ny = y + n.dy;
|
||||
if (nx >= 0 && ny >= 0 && nx < width && ny < height) {
|
||||
totalColor[0] += lightmap[ny][nx].color[0];
|
||||
totalColor[1] += lightmap[ny][nx].color[1];
|
||||
totalColor[2] += lightmap[ny][nx].color[2];
|
||||
neighborCount++;
|
||||
}
|
||||
});
|
||||
nextLightmap[y][x] = {
|
||||
color: [
|
||||
Math.min(765, Math.max(0, (totalColor[0] / neighborCount) * falloff)),
|
||||
Math.min(765, Math.max(0, (totalColor[1] / neighborCount) * falloff)),
|
||||
Math.min(765, Math.max(0, (totalColor[2] / neighborCount) * falloff))
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
// Copy next to current
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
lightmap[y][x] = {...nextLightmap[y][x]};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderLightmap(ctx) {
|
||||
if (!lightmap[0]) return;
|
||||
let lw = lightmap[0].length;
|
||||
let lh = lightmap.length;
|
||||
for (let y = 0; y < lh; y++) {
|
||||
for (let x = 0; x < lw; x++) {
|
||||
let color = lightmap[y][x].color;
|
||||
let r = color[0], g = color[1], b = color[2];
|
||||
if (r > 16 || g > 16 || b > 16) {
|
||||
let hsv = rgbToHsv(r, g, b);
|
||||
let newColor = hsvToRgb(hsv[0], hsv[1], 1);
|
||||
let alpha = hsv[2];
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillStyle = `rgba(${newColor[0]},${newColor[1]},${newColor[2]},${alpha * 0.4})`;
|
||||
ctx.fillRect(x * pixelSize * lightmapScale, y * pixelSize * lightmapScale, pixelSize * lightmapScale, pixelSize * lightmapScale);
|
||||
ctx.fillStyle = `rgba(${newColor[0]},${newColor[1]},${newColor[2]},${alpha * 0.25})`;
|
||||
ctx.fillRect((x * pixelSize - pixelSizeHalf) * lightmapScale, (y * pixelSize - pixelSizeHalf) * lightmapScale,
|
||||
pixelSize * lightmapScale * 2, pixelSize * lightmapScale * 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function glowItsOwnColor(pixel) {
|
||||
if (!pixel.color) return;
|
||||
let x = Math.floor(pixel.x / lightmapScale);
|
||||
let y = Math.floor(pixel.y / lightmapScale);
|
||||
if (x < 0 || y < 0 || x >= lightmap[0]?.length || y >= lightmap?.length) return;
|
||||
lightmap[y][x].color = scaleList(rgbToArray(pixel.color), lightSourceBoost);
|
||||
}
|
||||
|
||||
function glowPowered(pixel) {
|
||||
if (!pixel.charge || pixel.charge <= 0 || !pixel.color) return;
|
||||
glowItsOwnColor(pixel);
|
||||
}
|
||||
|
||||
// Override ticks for common light sources (works with addons if they use these elements)
|
||||
let lightEmitters = [
|
||||
"fire", "cold_fire", "plasma", "lava", "magma", "sun", "light", "liquid_light", "laser", "flash", "rainbow",
|
||||
"ember", "fw_ember", "explosion", "n_explosion", "supernova", "fireball", "blaster", "lightning", "electric",
|
||||
"positron", "neutron", "proton", "radiation", "fallout", "rad_cloud", "rad_steam", "uranium", "molten_uranium"
|
||||
];
|
||||
lightEmitters.forEach(elName => {
|
||||
let el = elements[elName];
|
||||
if (el && el.tick) {
|
||||
let origTick = el.tick;
|
||||
el.tick = function(pixel) {
|
||||
origTick(pixel);
|
||||
glowItsOwnColor(pixel);
|
||||
};
|
||||
}
|
||||
});
|
||||
["neon", "led", "light_bulb"].forEach(elName => {
|
||||
let el = elements[elName];
|
||||
if (el && el.tick) {
|
||||
let origTick = el.tick;
|
||||
el.tick = function(pixel) {
|
||||
origTick(pixel);
|
||||
glowPowered(pixel);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Temp-based glow for hot pixels (metals, etc.)
|
||||
function glowTemp(pixel) {
|
||||
let t = pixel.temp;
|
||||
if (t < 500) return;
|
||||
let intensity = Math.min(1, (t - 500) / 2000);
|
||||
let r = Math.min(255, 100 + 155 * intensity);
|
||||
let g = Math.min(255, 50 * intensity);
|
||||
let b = Math.min(255, 10 * intensity);
|
||||
let x = Math.floor(pixel.x / lightmapScale);
|
||||
let y = Math.floor(pixel.y / lightmapScale);
|
||||
if (x < 0 || y < 0 || x >= lightmap[0]?.length || y >= lightmap?.length) return;
|
||||
lightmap[y][x].color = scaleList([r, g, b], lightSourceBoost * intensity);
|
||||
}
|
||||
runPerPixel(glowTemp);
|
||||
|
||||
// Lightmap render hook
|
||||
renderPrePixel(function(ctx) {
|
||||
if (!paused) propagateLightmap();
|
||||
renderLightmap(ctx);
|
||||
});
|
||||
|
||||
// Init lightmap
|
||||
if (typeof runAfterReset !== 'undefined') {
|
||||
runAfterReset(() => initializeLightmap(width, height));
|
||||
} else {
|
||||
setTimeout(() => initializeLightmap(width, height), 100);
|
||||
}
|
||||
|
||||
// === SHADOWS / OCCLUSION (Realistic Darkness in Shadows) ===
|
||||
const DEFAULT_LIGHT_FACTOR = 0.8;
|
||||
const MIN_LIGHT_INTENSITY = 0.4;
|
||||
const MAX_DIRECT_NEIGHBORS = 4;
|
||||
const FOLLOWUP_COORDS_TO_CHECK = [
|
||||
[-1,-1],[-1,1],[1,-1],[1,1], [-2,0],[2,0],[0,-2],[0,2],
|
||||
[-3,0],[3,0],[0,-3],[0,3], [-4,0],[4,0],[0,-4],[0,4]
|
||||
];
|
||||
let transparentElements = [];
|
||||
function initTransparent() {
|
||||
transparentElements = [];
|
||||
Object.keys(elements).forEach(name => {
|
||||
let el = elements[name];
|
||||
if (el.state === "gas" || el.category === "special" || el.putInTransparentList) {
|
||||
transparentElements.push(name);
|
||||
}
|
||||
});
|
||||
// Add common transparents
|
||||
["glass","stained_glass","glass_shard","ice","led"].forEach(t => transparentElements.includes(t) || transparentElements.push(t));
|
||||
}
|
||||
initTransparent();
|
||||
|
||||
let frameCounter = 0;
|
||||
let pixelBrightnessCache = {};
|
||||
|
||||
function isOutOfBounds(x, y) {
|
||||
return x >= width || y >= height || x < 0 || y < 0;
|
||||
}
|
||||
|
||||
function getOutOfBoundsNeighbors(pixel) {
|
||||
let out = [];
|
||||
[[-1,0],[1,0],[0,-1],[0,1]].forEach(([dx,dy]) => {
|
||||
let nx = pixel.x + dx, ny = pixel.y + dy;
|
||||
if (isOutOfBounds(nx, ny)) out.push({x:nx,y:ny});
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function calculateBrightness(pixel) {
|
||||
let neighCount = getNeighbors(pixel).length + getOutOfBoundsNeighbors(pixel).length;
|
||||
if (neighCount >= MAX_DIRECT_NEIGHBORS) {
|
||||
let lightFactor = computeBrightnessFurther(pixel);
|
||||
return adjustBrightness(lightFactor);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
function computeBrightnessFurther(pixel) {
|
||||
let blockers = 0;
|
||||
FOLLOWUP_COORDS_TO_CHECK.forEach(([dx,dy]) => {
|
||||
let nx = pixel.x + dx, ny = pixel.y + dy;
|
||||
if (isOutOfBounds(nx, ny)) {
|
||||
blockers++;
|
||||
return;
|
||||
}
|
||||
let elName = pixelMap[nx]?.[ny]?.element;
|
||||
if (elName && !transparentElements.includes(elName)) blockers++;
|
||||
});
|
||||
return 1 - (blockers / FOLLOWUP_COORDS_TO_CHECK.length);
|
||||
}
|
||||
|
||||
function adjustBrightness(factor) {
|
||||
return factor * DEFAULT_LIGHT_FACTOR + MIN_LIGHT_INTENSITY;
|
||||
}
|
||||
|
||||
function applyShadows(ctx) {
|
||||
if (frameCounter % 2 === 0) {
|
||||
currentPixels.forEach(pixel => {
|
||||
let brightness = calculateBrightness(pixel);
|
||||
pixelBrightnessCache[`${pixel.x},${pixel.y}`] = brightness;
|
||||
});
|
||||
}
|
||||
currentPixels.forEach(pixel => {
|
||||
let brightness = pixelBrightnessCache[`${pixel.x},${pixel.y}`] || 1;
|
||||
// Sample local lightmap intensity
|
||||
let lx = Math.floor(pixel.x / lightmapScale);
|
||||
let ly = Math.floor(pixel.y / lightmapScale);
|
||||
let lightInt = 0;
|
||||
if (ly >= 0 && ly < lightmap?.length && lx >= 0 && lx < lightmap[0]?.length) {
|
||||
let lm = lightmap[ly][lx].color;
|
||||
lightInt = (lm[0] + lm[1] + lm[2]) / (255 * 3);
|
||||
}
|
||||
let shadeAlpha = (1 - brightness) * 0.7 * Math.max(0.2, 1 - lightInt * 0.8);
|
||||
ctx.globalAlpha = shadeAlpha;
|
||||
ctx.fillStyle = "#000";
|
||||
ctx.fillRect(pixel.x * pixelSize, pixel.y * pixelSize, pixelSize, pixelSize);
|
||||
});
|
||||
frameCounter++;
|
||||
}
|
||||
|
||||
renderPostPixel(applyShadows);
|
||||
|
||||
// === REALISTIC WATER / LIQUIDS (Waves & Foam on ALL Liquids) ===
|
||||
renderEachPixel(function(pixel, ctx) {
|
||||
let el = elements[pixel.element];
|
||||
if (el && el.state === "liquid") {
|
||||
// Wavy foam surface
|
||||
let time = (pixelTicks * 0.01 + pixel.x * 0.15 + pixel.y * 0.03) % (Math.PI * 2);
|
||||
let waveOffset = Math.sin(time) * 0.35 - 0.15;
|
||||
let foamY = Math.floor(pixel.y + waveOffset);
|
||||
let foamAlpha = 0.6 + Math.sin(time * 1.5) * 0.3;
|
||||
let foamColor = "#e8f4ff"; // Light blue-white foam
|
||||
drawSquare(ctx, foamColor, pixel.x, foamY, 1, foamAlpha * 0.4);
|
||||
// Subtle caustic glow if lit (bonus realism)
|
||||
let lx = Math.floor(pixel.x / lightmapScale);
|
||||
let ly = Math.floor(pixel.y / lightmapScale);
|
||||
if (ly >= 0 && ly < lightmap?.length && lx >= 0 && lx < lightmap[0]?.length) {
|
||||
let lmBright = (lightmap[ly][lx].color[0] + lightmap[ly][lx].color[1] + lightmap[ly][lx].color[2]) / (255 * 3);
|
||||
if (lmBright > 0.2) {
|
||||
let causticAlpha = lmBright * 0.3;
|
||||
let causticX = pixel.x + Math.sin(time * 0.7) * 0.2;
|
||||
let causticY = pixel.y + 0.5 + Math.cos(time * 1.2) * 0.15;
|
||||
drawSquare(ctx, "#00ff88", causticX, causticY, 0.8, causticAlpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Re-init transparent on element changes (for mods)
|
||||
if (typeof runEveryTick !== 'undefined') {
|
||||
runEveryTick(initTransparent);
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
// xVS_cal_wasm.c
|
||||
#include <emscripten.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define CLAMP(x, low, high) ((x) < (low) ? (low) : ((x) > (high) ? (high) : (x)))
|
||||
|
||||
// === LIGHT PROPAGATION ===
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
void propagate_lightmap_f32(
|
||||
const float* in_r, const float* in_g, const float* in_b,
|
||||
float* out_r, float* out_g, float* out_b,
|
||||
int width, int height, float falloff
|
||||
) {
|
||||
const int dx[4] = {1, -1, 0, 0};
|
||||
const int dy[4] = {0, 0, 1, -1};
|
||||
|
||||
for (int y = 0; y < height; ++y) {
|
||||
for (int x = 0; x < width; ++x) {
|
||||
float sum_r = 0, sum_g = 0, sum_b = 0;
|
||||
int count = 0;
|
||||
for (int d = 0; d < 4; ++d) {
|
||||
int nx = x + dx[d];
|
||||
int ny = y + dy[d];
|
||||
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
|
||||
int nidx = ny * width + nx;
|
||||
sum_r += in_r[nidx];
|
||||
sum_g += in_g[nidx];
|
||||
sum_b += in_b[nidx];
|
||||
count++;
|
||||
}
|
||||
}
|
||||
int idx = y * width + x;
|
||||
if (count > 0) {
|
||||
float factor = falloff / count;
|
||||
out_r[idx] = CLAMP(sum_r * factor, 0.0f, 765.0f);
|
||||
out_g[idx] = CLAMP(sum_g * factor, 0.0f, 765.0f);
|
||||
out_b[idx] = CLAMP(sum_b * factor, 0.0f, 765.0f);
|
||||
} else {
|
||||
out_r[idx] = out_g[idx] = out_b[idx] = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === SHADOW OCCLUSION (blocker count only) ===
|
||||
// Input: 1D array of element IDs (0 = empty, 1 = transparent, 2+ = opaque)
|
||||
// Output: blocker count per pixel
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
void compute_blockers_u8(
|
||||
const uint8_t* grid, // width × height
|
||||
uint8_t* blockers_out, // same size
|
||||
int width, int height,
|
||||
const int8_t* coords, // [dx0,dy0,dx1,dy1,...] length = 2*N
|
||||
int coord_count // N
|
||||
) {
|
||||
for (int y = 0; y < height; ++y) {
|
||||
for (int x = 0; x < width; ++x) {
|
||||
uint8_t blockers = 0;
|
||||
for (int i = 0; i < coord_count; ++i) {
|
||||
int nx = x + coords[i * 2];
|
||||
int ny = y + coords[i * 2 + 1];
|
||||
if (nx < 0 || nx >= width || ny < 0 || ny >= height) {
|
||||
blockers++;
|
||||
} else {
|
||||
uint8_t val = grid[ny * width + nx];
|
||||
if (val >= 2) blockers++; // opaque
|
||||
}
|
||||
}
|
||||
blockers_out[y * width + x] = blockers;
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
|
@ -0,0 +1,457 @@
|
|||
// realistic_system.js - Global Realistic Lighting, Shadows, and Water Effects for ALL elements (including addons)
|
||||
// Features:
|
||||
// - Dynamic colored light propagation from fire, lights, hot pixels, etc.
|
||||
// - Realistic occlusion shadows (darker in crevices/caves), modulated by light intensity
|
||||
// - Wavy foam on ALL liquids (water, oils, mod liquids)
|
||||
// - Works with any mods/addons automatically (liquids get waves, all get lit/shadowed)
|
||||
// Performance: Low-res lightmap + cached shadows = smooth even on large views
|
||||
// Install: Save as realistic_system.js in mods folder, load via Mod Manager
|
||||
|
||||
// ===== WASM LOADER (NON-MODULE VERSION) =====
|
||||
let wasmReady = false;
|
||||
|
||||
// Load embedded WASM script
|
||||
if (typeof Module === 'undefined') {
|
||||
const script = document.createElement('script');
|
||||
// Use absolute path if hosted on GitHub
|
||||
script.src = location.origin + location.pathname.replace(/[^/]*$/, '') + 'mods/xVS_cal_wasm.js';
|
||||
// Or if same folder: script.src = 'xVS_cal_wasm.js';
|
||||
script.onload = () => {
|
||||
Module.onRuntimeInitialized = () => {
|
||||
wasmReady = true;
|
||||
console.log("Realistic System: WASM ready (single-file)");
|
||||
};
|
||||
};
|
||||
script.onerror = () => {
|
||||
console.warn("WASM failed – using JS fallback");
|
||||
wasmReady = false;
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
// === LIGHTMAP SYSTEM ===
|
||||
var lightmap = [];
|
||||
var nextLightmap = [];
|
||||
var lightmapScale = 4;
|
||||
var lightSourceBoost = 3;
|
||||
var falloff = 0.85;
|
||||
|
||||
function rgbToArray(colorString) {
|
||||
if (typeof colorString !== "string") return [255,255,255];
|
||||
if (colorString.startsWith("rgb")) {
|
||||
return colorString.slice(4, -1).split(",").map(val => parseInt(val.trim()));
|
||||
} else if (colorString.startsWith("#")) {
|
||||
let hex = colorString.slice(1);
|
||||
if (hex.length === 3) hex = hex.split("").map(char => char + char).join("");
|
||||
let r = parseInt(hex.slice(0, 2), 16);
|
||||
let g = parseInt(hex.slice(2, 4), 16);
|
||||
let b = parseInt(hex.slice(4, 6), 16);
|
||||
return [r, g, b];
|
||||
}
|
||||
return [255,255,255];
|
||||
}
|
||||
|
||||
function scaleList(numbers, scale) {
|
||||
return numbers.map(number => number * scale);
|
||||
}
|
||||
|
||||
function rgbToHsv(r, g, b) {
|
||||
r /= 255; g /= 255; b /= 255;
|
||||
let max = Math.max(r, g, b), min = Math.min(r, g, b);
|
||||
let h, s, v = max;
|
||||
let d = max - min;
|
||||
s = max === 0 ? 0 : d / max;
|
||||
if (max === min) h = 0;
|
||||
else {
|
||||
switch (max) {
|
||||
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
|
||||
case g: h = (b - r) / d + 2; break;
|
||||
case b: h = (r - g) / d + 4; break;
|
||||
}
|
||||
h /= 6;
|
||||
}
|
||||
return [h, s, v];
|
||||
}
|
||||
|
||||
function hsvToRgb(h, s, v) {
|
||||
let i = Math.floor(h * 6);
|
||||
let f = h * 6 - i;
|
||||
let p = v * (1 - s);
|
||||
let q = v * (1 - f * s);
|
||||
let t = v * (1 - (1 - f) * s);
|
||||
let r, g, b;
|
||||
switch (i % 6) {
|
||||
case 0: r = v; g = t; b = p; break;
|
||||
case 1: r = q; g = v; b = p; break;
|
||||
case 2: r = p; g = v; b = t; break;
|
||||
case 3: r = p; g = q; b = v; break;
|
||||
case 4: r = t; g = p; b = v; break;
|
||||
case 5: r = v; g = p; b = q; break;
|
||||
}
|
||||
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
||||
}
|
||||
|
||||
function initializeLightmap(w, h) {
|
||||
let lw = Math.ceil(w / lightmapScale) + 1;
|
||||
let lh = Math.ceil(h / lightmapScale) + 1;
|
||||
function createArray(width_, height_) {
|
||||
return Array.from({length: height_}, () => Array.from({length: width_}, () => ({color: [0, 0, 0]})));
|
||||
}
|
||||
lightmap = createArray(lw, lh);
|
||||
nextLightmap = createArray(lw, lh);
|
||||
}
|
||||
|
||||
// === PROPAGATE LIGHTMAP (WASM OR JS) ===
|
||||
function propagateLightmap() {
|
||||
if (!lightmap[0]) return;
|
||||
const width = lightmap[0].length;
|
||||
const height = lightmap.length;
|
||||
const total = width * height;
|
||||
|
||||
if (wasmReady && Module) {
|
||||
try {
|
||||
const inR = new Float32Array(total);
|
||||
const inG = new Float32Array(total);
|
||||
const inB = new Float32Array(total);
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const idx = y * width + x;
|
||||
const c = lightmap[y][x].color;
|
||||
inR[idx] = c[0] || 0;
|
||||
inG[idx] = c[1] || 0;
|
||||
inB[idx] = c[2] || 0;
|
||||
}
|
||||
}
|
||||
|
||||
const outR = new Float32Array(total);
|
||||
const outG = new Float32Array(total);
|
||||
const outB = new Float32Array(total);
|
||||
|
||||
Module.ccall(
|
||||
'propagate_lightmap_f32',
|
||||
null,
|
||||
['number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number'],
|
||||
[
|
||||
Module.HEAPF32.subarray(inR.byteOffset / 4, inR.byteOffset / 4 + total),
|
||||
Module.HEAPF32.subarray(inG.byteOffset / 4, inG.byteOffset / 4 + total),
|
||||
Module.HEAPF32.subarray(inB.byteOffset / 4, inB.byteOffset / 4 + total),
|
||||
Module.HEAPF32.subarray(outR.byteOffset / 4, outR.byteOffset / 4 + total),
|
||||
Module.HEAPF32.subarray(outG.byteOffset / 4, outG.byteOffset / 4 + total),
|
||||
Module.HEAPF32.subarray(outB.byteOffset / 4, outB.byteOffset / 4 + total),
|
||||
width, height, falloff
|
||||
]
|
||||
);
|
||||
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const idx = y * width + x;
|
||||
nextLightmap[y][x].color = [outR[idx], outG[idx], outB[idx]];
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("WASM light propagation failed", e);
|
||||
wasmReady = false;
|
||||
return propagateLightmapJS();
|
||||
}
|
||||
} else {
|
||||
return propagateLightmapJS();
|
||||
}
|
||||
|
||||
// Copy next → current
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
lightmap[y][x] = {...nextLightmap[y][x]};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function propagateLightmapJS() {
|
||||
const width = lightmap[0].length;
|
||||
const height = lightmap.length;
|
||||
const neighbors = [{dx:1,dy:0},{dx:-1,dy:0},{dx:0,dy:1},{dx:0,dy:-1}];
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
let totalColor = [0,0,0];
|
||||
let neighborCount = 0;
|
||||
for (const n of neighbors) {
|
||||
const nx = x + n.dx;
|
||||
const ny = y + n.dy;
|
||||
if (nx >= 0 && ny >= 0 && nx < width && ny < height) {
|
||||
const c = lightmap[ny][nx].color;
|
||||
totalColor[0] += c[0];
|
||||
totalColor[1] += c[1];
|
||||
totalColor[2] += c[2];
|
||||
neighborCount++;
|
||||
}
|
||||
}
|
||||
const factor = neighborCount > 0 ? falloff / neighborCount : 0;
|
||||
nextLightmap[y][x].color = [
|
||||
Math.min(765, Math.max(0, totalColor[0] * factor)),
|
||||
Math.min(765, Math.max(0, totalColor[1] * factor)),
|
||||
Math.min(765, Math.max(0, totalColor[2] * factor))
|
||||
];
|
||||
}
|
||||
}
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
lightmap[y][x] = {...nextLightmap[y][x]};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderLightmap(ctx) {
|
||||
if (!lightmap[0]) return;
|
||||
let lw = lightmap[0].length;
|
||||
let lh = lightmap.length;
|
||||
for (let y = 0; y < lh; y++) {
|
||||
for (let x = 0; x < lw; x++) {
|
||||
let color = lightmap[y][x].color;
|
||||
let r = color[0], g = color[1], b = color[2];
|
||||
if (r > 16 || g > 16 || b > 16) {
|
||||
let hsv = rgbToHsv(r, g, b);
|
||||
let newColor = hsvToRgb(hsv[0], hsv[1], 1);
|
||||
let alpha = hsv[2];
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillStyle = `rgba(${newColor[0]},${newColor[1]},${newColor[2]},${alpha * 0.4})`;
|
||||
ctx.fillRect(x * pixelSize * lightmapScale, y * pixelSize * lightmapScale, pixelSize * lightmapScale, pixelSize * lightmapScale);
|
||||
ctx.fillStyle = `rgba(${newColor[0]},${newColor[1]},${newColor[2]},${alpha * 0.25})`;
|
||||
ctx.fillRect((x * pixelSize - pixelSizeHalf) * lightmapScale, (y * pixelSize - pixelSizeHalf) * lightmapScale,
|
||||
pixelSize * lightmapScale * 2, pixelSize * lightmapScale * 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function glowItsOwnColor(pixel) {
|
||||
if (!pixel.color) return;
|
||||
let x = Math.floor(pixel.x / lightmapScale);
|
||||
let y = Math.floor(pixel.y / lightmapScale);
|
||||
if (x < 0 || y < 0 || x >= lightmap[0]?.length || y >= lightmap?.length) return;
|
||||
lightmap[y][x].color = scaleList(rgbToArray(pixel.color), lightSourceBoost);
|
||||
}
|
||||
|
||||
function glowPowered(pixel) {
|
||||
if (!pixel.charge || pixel.charge <= 0 || !pixel.color) return;
|
||||
glowItsOwnColor(pixel);
|
||||
}
|
||||
|
||||
let lightEmitters = [
|
||||
"fire", "cold_fire", "plasma", "lava", "magma", "sun", "light", "liquid_light", "laser", "flash", "rainbow",
|
||||
"ember", "fw_ember", "explosion", "n_explosion", "supernova", "fireball", "blaster", "lightning", "electric",
|
||||
"positron", "neutron", "proton", "radiation", "fallout", "rad_cloud", "rad_steam", "uranium", "molten_uranium"
|
||||
];
|
||||
lightEmitters.forEach(elName => {
|
||||
let el = elements[elName];
|
||||
if (el && el.tick) {
|
||||
let origTick = el.tick;
|
||||
el.tick = function(pixel) {
|
||||
origTick(pixel);
|
||||
glowItsOwnColor(pixel);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
["neon", "led", "light_bulb"].forEach(elName => {
|
||||
let el = elements[elName];
|
||||
if (el && el.tick) {
|
||||
let origTick = el.tick;
|
||||
el.tick = function(pixel) {
|
||||
origTick(pixel);
|
||||
glowPowered(pixel);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function glowTemp(pixel) {
|
||||
let t = pixel.temp;
|
||||
if (t < 500) return;
|
||||
let intensity = Math.min(1, (t - 500) / 2000);
|
||||
let r = Math.min(255, 100 + 155 * intensity);
|
||||
let g = Math.min(255, 50 * intensity);
|
||||
let b = Math.min(255, 10 * intensity);
|
||||
let x = Math.floor(pixel.x / lightmapScale);
|
||||
let y = Math.floor(pixel.y / lightmapScale);
|
||||
if (x < 0 || y < 0 || x >= lightmap[0]?.length || y >= lightmap?.length) return;
|
||||
lightmap[y][x].color = scaleList([r, g, b], lightSourceBoost * intensity);
|
||||
}
|
||||
runPerPixel(glowTemp);
|
||||
|
||||
renderPrePixel(function(ctx) {
|
||||
if (!paused) propagateLightmap();
|
||||
renderLightmap(ctx);
|
||||
});
|
||||
|
||||
if (typeof runAfterReset !== 'undefined') {
|
||||
runAfterReset(() => initializeLightmap(width, height));
|
||||
} else {
|
||||
setTimeout(() => initializeLightmap(width, height), 100);
|
||||
}
|
||||
|
||||
// === SHADOWS (WASM ACCELERATED BLOCKER COUNT) ===
|
||||
const DEFAULT_LIGHT_FACTOR = 0.8;
|
||||
const MIN_LIGHT_INTENSITY = 0.4;
|
||||
const MAX_DIRECT_NEIGHBORS = 4;
|
||||
const FOLLOWUP_COORDS_TO_CHECK = [
|
||||
[-1,-1],[-1,1],[1,-1],[1,1],
|
||||
[-2,0],[2,0],[0,-2],[0,2],
|
||||
[-3,0],[3,0],[0,-3],[0,3],
|
||||
[-4,0],[4,0],[0,-4],[0,4]
|
||||
];
|
||||
|
||||
let transparentElements = [];
|
||||
function initTransparent() {
|
||||
transparentElements = [];
|
||||
Object.keys(elements).forEach(name => {
|
||||
let el = elements[name];
|
||||
if (el.state === "gas" || el.category === "special" || el.putInTransparentList) {
|
||||
transparentElements.push(name);
|
||||
}
|
||||
});
|
||||
["glass", "stained_glass", "glass_shard", "ice", "led"].forEach(t => {
|
||||
if (!transparentElements.includes(t)) transparentElements.push(t);
|
||||
});
|
||||
}
|
||||
initTransparent();
|
||||
|
||||
let frameCounter = 0;
|
||||
let pixelBrightnessCache = {};
|
||||
|
||||
function isOutOfBounds(x, y) {
|
||||
return x >= width || y >= height || x < 0 || y < 0;
|
||||
}
|
||||
|
||||
function calculateBrightness(pixel) {
|
||||
let directNeighbors = 0;
|
||||
[[-1,0],[1,0],[0,-1],[0,1]].forEach(([dx,dy]) => {
|
||||
if (!isOutOfBounds(pixel.x + dx, pixel.y + dy)) directNeighbors++;
|
||||
});
|
||||
let outOfBounds = 4 - directNeighbors;
|
||||
if (directNeighbors + outOfBounds >= MAX_DIRECT_NEIGHBORS) {
|
||||
return adjustBrightness(computeBrightnessFurther(pixel));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
function computeBrightnessFurther(pixel) {
|
||||
if (!wasmReady || !Module) return computeBrightnessFurtherJS(pixel);
|
||||
|
||||
// Build grid: 0=empty, 1=transparent, 2=opaque
|
||||
const lw = Math.min(1000, width); // limit for performance
|
||||
const lh = Math.min(1000, height);
|
||||
const grid = new Uint8Array(lw * lh);
|
||||
for (let y = 0; y < lh; y++) {
|
||||
for (let x = 0; x < lw; x++) {
|
||||
if (isOutOfBounds(x, y)) {
|
||||
grid[y * lw + x] = 2; // treat OOB as opaque
|
||||
} else {
|
||||
let elName = pixelMap[x]?.[y]?.element;
|
||||
if (!elName) grid[y * lw + x] = 0;
|
||||
else if (transparentElements.includes(elName)) grid[y * lw + x] = 1;
|
||||
else grid[y * lw + x] = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flatten coords
|
||||
const coordsFlat = new Int8Array(FOLLOWUP_COORDS_TO_CHECK.length * 2);
|
||||
for (let i = 0; i < FOLLOWUP_COORDS_TO_CHECK.length; i++) {
|
||||
coordsFlat[i * 2] = FOLLOWUP_COORDS_TO_CHECK[i][0];
|
||||
coordsFlat[i * 2 + 1] = FOLLOWUP_COORDS_TO_CHECK[i][1];
|
||||
}
|
||||
|
||||
const blockers = new Uint8Array(lw * lh);
|
||||
try {
|
||||
Module.ccall(
|
||||
'compute_blockers_u8',
|
||||
null,
|
||||
['number', 'number', 'number', 'number', 'number', 'number'],
|
||||
[
|
||||
Module.HEAPU8.subarray(grid.byteOffset, grid.byteOffset + grid.length),
|
||||
Module.HEAPU8.subarray(blockers.byteOffset, blockers.byteOffset + blockers.length),
|
||||
lw, lh,
|
||||
Module.HEAP8.subarray(coordsFlat.byteOffset, coordsFlat.byteOffset + coordsFlat.length),
|
||||
FOLLOWUP_COORDS_TO_CHECK.length
|
||||
]
|
||||
);
|
||||
let px = Math.min(pixel.x, lw - 1);
|
||||
let py = Math.min(pixel.y, lh - 1);
|
||||
let blockerCount = blockers[py * lw + px];
|
||||
return 1 - (blockerCount / FOLLOWUP_COORDS_TO_CHECK.length);
|
||||
} catch (e) {
|
||||
console.warn("WASM shadow failed", e);
|
||||
wasmReady = false;
|
||||
return computeBrightnessFurtherJS(pixel);
|
||||
}
|
||||
}
|
||||
|
||||
function computeBrightnessFurtherJS(pixel) {
|
||||
let blockers = 0;
|
||||
FOLLOWUP_COORDS_TO_CHECK.forEach(([dx,dy]) => {
|
||||
let nx = pixel.x + dx, ny = pixel.y + dy;
|
||||
if (isOutOfBounds(nx, ny)) {
|
||||
blockers++;
|
||||
return;
|
||||
}
|
||||
let elName = pixelMap[nx]?.[ny]?.element;
|
||||
if (elName && !transparentElements.includes(elName)) blockers++;
|
||||
});
|
||||
return 1 - (blockers / FOLLOWUP_COORDS_TO_CHECK.length);
|
||||
}
|
||||
|
||||
function adjustBrightness(factor) {
|
||||
return factor * DEFAULT_LIGHT_FACTOR + MIN_LIGHT_INTENSITY;
|
||||
}
|
||||
|
||||
function applyShadows(ctx) {
|
||||
if (frameCounter % 2 === 0) {
|
||||
currentPixels.forEach(pixel => {
|
||||
let brightness = calculateBrightness(pixel);
|
||||
pixelBrightnessCache[`${pixel.x},${pixel.y}`] = brightness;
|
||||
});
|
||||
}
|
||||
currentPixels.forEach(pixel => {
|
||||
let brightness = pixelBrightnessCache[`${pixel.x},${pixel.y}`] || 1;
|
||||
let lx = Math.floor(pixel.x / lightmapScale);
|
||||
let ly = Math.floor(pixel.y / lightmapScale);
|
||||
let lightInt = 0;
|
||||
if (ly >= 0 && ly < lightmap?.length && lx >= 0 && lx < lightmap[0]?.length) {
|
||||
let lm = lightmap[ly][lx].color;
|
||||
lightInt = (lm[0] + lm[1] + lm[2]) / (255 * 3);
|
||||
}
|
||||
let shadeAlpha = (1 - brightness) * 0.7 * Math.max(0.2, 1 - lightInt * 0.8);
|
||||
ctx.globalAlpha = shadeAlpha;
|
||||
ctx.fillStyle = "#000";
|
||||
ctx.fillRect(pixel.x * pixelSize, pixel.y * pixelSize, pixelSize, pixelSize);
|
||||
});
|
||||
frameCounter++;
|
||||
}
|
||||
renderPostPixel(applyShadows);
|
||||
|
||||
// === LIQUID WAVES (pure JS – too tied to rendering) ===
|
||||
renderEachPixel(function(pixel, ctx) {
|
||||
let el = elements[pixel.element];
|
||||
if (el && el.state === "liquid") {
|
||||
let time = (pixelTicks * 0.01 + pixel.x * 0.15 + pixel.y * 0.03) % (Math.PI * 2);
|
||||
let waveOffset = Math.sin(time) * 0.35 - 0.15;
|
||||
let foamY = Math.floor(pixel.y + waveOffset);
|
||||
let foamAlpha = 0.6 + Math.sin(time * 1.5) * 0.3;
|
||||
let foamColor = "#e8f4ff";
|
||||
drawSquare(ctx, foamColor, pixel.x, foamY, 1, foamAlpha * 0.4);
|
||||
|
||||
let lx = Math.floor(pixel.x / lightmapScale);
|
||||
let ly = Math.floor(pixel.y / lightmapScale);
|
||||
if (ly >= 0 && ly < lightmap?.length && lx >= 0 && lx < lightmap[0]?.length) {
|
||||
let lmBright = (lightmap[ly][lx].color[0] + lightmap[ly][lx].color[1] + lightmap[ly][lx].color[2]) / (255 * 3);
|
||||
if (lmBright > 0.2) {
|
||||
let causticAlpha = lmBright * 0.3;
|
||||
let causticX = pixel.x + Math.sin(time * 0.7) * 0.2;
|
||||
let causticY = pixel.y + 0.5 + Math.cos(time * 1.2) * 0.15;
|
||||
drawSquare(ctx, "#00ff88", causticX, causticY, 0.8, causticAlpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof runEveryTick !== 'undefined') {
|
||||
runEveryTick(initTransparent);
|
||||
}
|
||||
Loading…
Reference in New Issue