sandboxels/mods/occlusion.js

150 lines
4.5 KiB
JavaScript

const DEFAULT_LIGHT_FACTOR = 0.8;
const MIN_LIGHT_INTENSITY = 0.6; // If pixel is completely obscured, it will still have this amount of light
const MAX_DIRECT_NEIGHBORS = 4; // getNeighbors() returns max 4 pixels
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]
];
// Pre-initialize the list of transparent elements
let transparentElementsTmp = "glass,stained_glass,glass_shard,solid_diamond,ice,led".split(",");
let transparentElements = [];
// Function to create the list of transparent elements based on their properties
function initializeTransparentElements() {
Object.keys(elements).forEach(elementName => {
const element = elements[elementName];
// Check if the element is in a gas or liquid state
if (element.state === "gas") {
transparentElements.push(elementName);
}
// Check if the element's category is "special"
if (element.category === "special") {
transparentElements.push(elementName);
}
// Check if the element has a custom flag for transparency
if (element.putInTransparentList) {
transparentElements.push(elementName);
}
});
}
// Call the function once at startup to populate the transparentElements list
initializeTransparentElements();
// Customizable frame interval for recalculating brightness
const calculateEveryNFrames = 2;
let frameCounter = 0;
// Cache for storing pixel brightnesses
let pixelBrightnessCache = {};
// scaleList should only be defined once
if (typeof scaleList === 'undefined') {
function scaleList(numbers, scale) {
return numbers.map(number => number * scale);
}
}
function isOutOfBounds(x, y) {
return x >= width || y >= height || x < 0 || y < 0;
}
function getOutOfBoundsNeighbors(pixel) {
const outOfBoundsNeighbors = [];
// Define the 4 direct neighbors: left, right, top, bottom
const neighborsToCheck = [
{ x: pixel.x - 1, y: pixel.y },
{ x: pixel.x + 1, y: pixel.y },
{ x: pixel.x, y: pixel.y - 1 },
{ x: pixel.x, y: pixel.y + 1 }
];
// Check each of the neighbors to see if they are out of bounds
neighborsToCheck.forEach(neighbor => {
if (isOutOfBounds(neighbor.x, neighbor.y)) {
outOfBoundsNeighbors.push(neighbor);
}
});
return outOfBoundsNeighbors;
}
// Iterate over each pixel and either calculate or draw occlusion lighting
function applyLightingToPixels(ctx) {
// Recalculate pixel brightnesses every `n` frames
if (frameCounter % calculateEveryNFrames === 0) {
currentPixels.forEach(pixel => {
const brightness = calculateBrightness(pixel);
pixelBrightnessCache[`${pixel.x},${pixel.y}`] = brightness; // Cache the brightness
});
}
// Draw pixels based on cached brightness
currentPixels.forEach(pixel => {
const brightness = pixelBrightnessCache[`${pixel.x},${pixel.y}`] || 1; // Default to full brightness if not calculated yet
drawPixelShade(ctx, pixel, brightness);
});
// Increment the frame counter
frameCounter++;
}
// Darken a pixel based on brightness
function drawPixelShade(ctx, pixel, brightness) {
ctx.globalAlpha = 1.0;
ctx.fillStyle = `rgba(0, 0, 0, ${1 - brightness})`;
ctx.fillRect(pixel.x * pixelSize, pixel.y * pixelSize, pixelSize, pixelSize);
}
// Compute brightness for a given pixel
function calculateBrightness(pixel) {
const neighboringPixelsCount = getNeighbors(pixel).length + getOutOfBoundsNeighbors(pixel).length;
// If the pixel has enough light-blocking neighbors, perform a deeper search
if (neighboringPixelsCount >= MAX_DIRECT_NEIGHBORS) {
const lightFactor = computeBrightnessFurther(pixel);
return adjustBrightness(lightFactor);
}
return 1; // Full brightness
}
// Compute brightness based on further pixels that block light
function computeBrightnessFurther(pixel) {
let lightBlockers = 0;
FOLLOWUP_COORDS_TO_CHECK.forEach(offset => {
const [dx, dy] = offset;
const xCoord = pixel.x + dx;
const yCoord = pixel.y + dy;
if (isOutOfBounds(xCoord, yCoord)) {
lightBlockers++;
return;
}
// Check if the element is transparent
const element = pixelMap[xCoord][yCoord];
if (element != undefined && !transparentElements.includes(element.element)) {
lightBlockers++;
}
});
return 1 - (lightBlockers / FOLLOWUP_COORDS_TO_CHECK.length);
}
// Adjust brightness based on light factor
function adjustBrightness(lightFactor) {
return lightFactor * DEFAULT_LIGHT_FACTOR + MIN_LIGHT_INTENSITY;
}
renderPostPixel(applyLightingToPixels);