Files
shacraft-core/client/lighting-shaders.js
Emil c7e86663d8
MVP checks / mvp (push) Waiting to run
Expand voxel gameplay, lighting, full-height streaming and world imports
Add shared Rust/WASM physics, worker meshing and diagnostics, 64-chunk full-height streaming, atlas texture support, and baseline world import. Document the current implementation and include the supplied in-game lobby screenshot.
2026-09-17 02:10:53 +03:00

322 lines
13 KiB
JavaScript

// One direction drives visible sunlight, surface lighting, and the shadow camera.
const sun = [-0.5, 0.78, 0.37];
export const SUN_DIRECTION = Object.freeze(sun.map((v) => v / Math.hypot(...sun)));
const sunlight = `const vec3 SUN_DIRECTION = vec3(${SUN_DIRECTION.join(",")});`;
// Atlas cells are sampled as integer texels, so neighboring sprites never bleed.
const blockTextureSampling = `
uniform sampler2DArray uBlockTextures;
uniform vec2 uBlockAtlasGrid;
uniform vec2 uGrassOverlay;
vec4 blockPixelRaw(vec2 uv, float layer) {
if (uBlockAtlasGrid.x < 1.0) return texture(uBlockTextures, vec3(uv, layer));
ivec2 cellSize = textureSize(uBlockTextures, 0).xy / ivec2(uBlockAtlasGrid);
ivec2 cell = ivec2(mod(layer, uBlockAtlasGrid.x), floor(layer / uBlockAtlasGrid.x));
ivec2 pixel = min(ivec2(floor(fract(uv) * vec2(cellSize))), cellSize - 1);
return texelFetch(uBlockTextures, ivec3(cell * cellSize + pixel, 0), 0);
}
vec4 blockPixel(vec2 uv, float layer) {
vec4 pixel = blockPixelRaw(uv, layer);
if (uGrassOverlay.x >= 0.0 && abs(layer - uGrassOverlay.x) < 0.1) {
vec4 overlay = blockPixelRaw(uv, uGrassOverlay.y);
pixel.rgb = mix(pixel.rgb, overlay.rgb * vec3(0.58, 0.8, 0.34), overlay.a);
}
return pixel;
}`;
const colorSpace = `
vec3 toLinear(vec3 color) {
color = max(color, vec3(0.0));
return mix(color / 12.92, pow((color + 0.055) / 1.055, vec3(2.4)), step(vec3(0.04045), color));
}
vec3 toSRGB(vec3 color) {
color = max(color, vec3(0.0));
return mix(color * 12.92, 1.055 * pow(color, vec3(1.0 / 2.4)) - 0.055, step(vec3(0.0031308), color));
}`;
// Linear scene colors. The world fog and water reflection sample this same sky.
const atmosphere = `
${sunlight}
uniform float uDaylight;
vec3 daylightSky(vec3 ray) {
float altitude = clamp(ray.y, 0.0, 1.0);
vec3 horizon = vec3(0.67, 0.77, 0.80);
vec3 zenith = vec3(0.105, 0.315, 0.59);
vec3 sky = mix(horizon, zenith, pow(altitude, 0.48));
float sunAngle = max(dot(ray, SUN_DIRECTION), 0.0);
// Wide scattering is faint; the small solar disc is rendered separately.
sky += vec3(0.12, 0.075, 0.025) * pow(sunAngle, 24.0);
sky = mix(vec3(0.46, 0.51, 0.48), sky, smoothstep(-0.24, 0.015, ray.y));
vec3 nightHorizon = vec3(0.009, 0.015, 0.028);
vec3 nightZenith = vec3(0.0014, 0.0034, 0.011);
vec3 night = mix(nightHorizon, nightZenith, pow(altitude, 0.48));
night += vec3(0.0025, 0.0035, 0.006) * pow(sunAngle, 36.0);
night = mix(vec3(0.0035, 0.005, 0.009), night, smoothstep(-0.24, 0.015, ray.y));
return mix(night, sky, clamp(uDaylight, 0.0, 1.0));
}`;
export const worldVertex = `#version 300 es
precision highp float;
layout(location=0) in vec3 aPos;
layout(location=1) in vec3 aColor;
layout(location=2) in vec3 aNormal;
layout(location=3) in float aMaterial;
layout(location=4) in float aOpacity;
layout(location=5) in vec3 aBlockTexture;
layout(location=6) in vec2 aLight;
// RGB is linear block irradiance; alpha is the normalized skylight level.
layout(location=7) in vec4 aBlockLight;
uniform mat4 uVP;
uniform vec3 uOffset;
out vec3 vPos;
out vec3 vColor;
out vec3 vNormal;
flat out float vMaterial;
out float vOpacity;
out vec2 vBlockUV;
flat out float vBlockLayer;
out vec2 vLight;
out vec4 vBlockLight;
void main() {
vPos = aPos + uOffset;
vColor = aColor;
vNormal = aNormal;
vMaterial = aMaterial;
vOpacity = aOpacity;
vBlockUV = aBlockTexture.xy;
vBlockLayer = aBlockTexture.z;
vLight = aLight;
vBlockLight = aBlockLight;
gl_Position = uVP * vec4(vPos, 1.0);
}`;
export const worldFragment = `#version 300 es
precision highp float;
precision highp sampler2DArray;
in vec3 vPos;
in vec3 vColor;
in vec3 vNormal;
flat in float vMaterial;
in float vOpacity;
in vec2 vBlockUV;
flat in float vBlockLayer;
in vec2 vLight;
in vec4 vBlockLight;
uniform vec3 uEye;
uniform sampler2D uTexture;
uniform bool uTextured;
uniform sampler2D uEffectTexture;
uniform bool uEffectTextured;
${blockTextureSampling}
uniform float uPulse;
uniform float uFogDistance;
uniform float uViewDistance;
uniform mat4 uLightVP;
uniform sampler2D uShadowMap;
uniform bool uShadows;
uniform float uShadowTexel;
uniform vec3 uShadowCenter;
uniform float uShadowRadius;
out vec4 outColor;
${colorSpace}
${atmosphere}
float hash(vec3 p) {
return fract(sin(dot(p, vec3(127.1, 311.7, 74.7))) * 43758.5453);
}
vec3 shacraftBounceTint(vec3 color,float pulse){return color;}
float sunVisibility(vec3 normal, float incidence) {
if (!uShadows || incidence <= 0.0) return 1.0;
vec4 clip = uLightVP * vec4(vPos + normal * 0.025, 1.0);
vec3 projected = clip.xyz / clip.w * 0.5 + 0.5;
if (any(lessThan(projected, vec3(0.0))) || any(greaterThan(projected, vec3(1.0)))) return 1.0;
float bias = 0.00010 + 0.00028 * (1.0 - incidence);
float visible = 0.0;
// A compact, stable PCF kernel keeps the edges soft without moving grain.
for (int y = -1; y <= 1; ++y) {
for (int x = -1; x <= 1; ++x) {
float depth = texture(uShadowMap, projected.xy + vec2(float(x), float(y)) * uShadowTexel).r;
visible += step(projected.z - bias, depth);
}
}
float coverage = 1.0 - smoothstep(max(0.0, uShadowRadius - 6.0), uShadowRadius, distance(vPos, uShadowCenter));
vec2 border = min(projected.xy, 1.0 - projected.xy);
coverage *= smoothstep(0.0, 0.018, min(border.x, border.y));
return mix(1.0, visible / 9.0, coverage);
}
float textureAlpha = 1.0;
vec3 surfaceColor(vec3 normal) {
if (vBlockLayer >= 0.0) {
vec4 pixel = blockPixel(vBlockUV, vBlockLayer);
if (pixel.a < (uBlockAtlasGrid.x > 0.0 ? 0.01 : 0.5)) discard;
if (uBlockAtlasGrid.x > 0.0 && pixel.a < 0.99) textureAlpha = pixel.a;
return clamp(pixel.rgb * vColor, 0.0, 1.0);
}
vec2 uv = abs(normal.y) > 0.5 ? vPos.xz : (abs(normal.x) > 0.5 ? vPos.zy : vPos.xy);
vec3 p = floor((vPos + normal * 0.002) * 16.0);
float noise = hash(p) * 0.14 - 0.07;
float pattern = 1.0;
if (vMaterial > 1.5 && vMaterial < 2.5) {
pattern = 0.89 + 0.11 * step(0.07, fract(uv.y * 4.0));
pattern *= 0.9 + 0.1 * step(0.055, fract(uv.x * 0.5 + floor(uv.y * 4.0) * 0.5));
}
if (vMaterial > 2.5 && vMaterial < 3.5) {
pattern = 0.87 + 0.13 * step(0.06, fract(uv.y * 4.0));
pattern *= 0.84 + 0.16 * step(0.045, fract(uv.x * 2.0 + floor(uv.y * 4.0) * 0.5));
}
if (vMaterial > 3.5 && vMaterial < 4.5) {
pattern = 0.91 + 0.09 * sin(floor(uv.x * 16.0) * 1.73 + sin(floor(uv.y * 16.0) * 0.22));
}
float tex = uTextured ? mix(0.84, 1.12, texture(uTexture, uv).r) : 1.0;
if (vMaterial > 4.5 && vMaterial < 5.5 && uEffectTextured) {
tex = mix(0.75, 1.2, texture(uEffectTexture, uv).g);
}
vec3 color = clamp((vColor + noise) * pattern * tex, 0.0, 1.0);
if (vMaterial > 0.5 && vMaterial < 1.5 && normal.y < 0.5) {
color = mix(color, vec3(0.38, 0.29, 0.17), 0.65);
}
return color;
}
void main() {
vec3 normal = normalize(vNormal);
vec3 albedo = surfaceColor(normal);
if (vMaterial > 4.5 && vMaterial < 5.5) albedo = shacraftBounceTint(albedo, uPulse);
albedo = toLinear(albedo);
float occlusion = clamp(vLight.x, 0.0, 1.0);
float emission = clamp(vLight.y, 0.0, 1.0);
float skyLevel = clamp(vBlockLight.a, 0.0, 1.0);
float skyExposure = skyLevel * skyLevel;
vec3 blockLight = max(vBlockLight.rgb, vec3(0.0));
float incidence = max(dot(normal, SUN_DIRECTION), 0.0);
float sunlight = skyExposure > 0.0 ? sunVisibility(normal, incidence) * skyExposure : 0.0;
// The lower hemisphere has muted ground bounce, keeping undersides legible.
float hemisphere = normal.y * 0.5 + 0.5;
vec3 ambient = mix(vec3(0.15, 0.145, 0.13), vec3(0.29, 0.355, 0.44), hemisphere);
float daylight = clamp(uDaylight, 0.0, 1.0);
vec3 moonAmbient = mix(vec3(0.009, 0.011, 0.018), vec3(0.025, 0.036, 0.060), hemisphere);
ambient = mix(moonAmbient, ambient, daylight);
// Closed rooms receive a small visibility floor, not the outdoor hemisphere.
ambient *= mix(0.20, 1.0, occlusion) * mix(0.015, 1.0, skyExposure);
vec3 direct = mix(vec3(0.034, 0.050, 0.080), vec3(0.94, 0.865, 0.735), daylight) * incidence * sunlight;
// Voxel propagation already accounts for walls and distance to emitters.
// A weak contact term keeps corners grounded without swallowing torchlight.
vec3 localLight = blockLight * 1.1 * mix(0.65, 1.0, occlusion);
vec3 color = albedo * (ambient + direct + localLight);
color = mix(color, albedo * 1.14, emission);
vec3 eyeDelta = uEye - vPos;
vec3 toEye = eyeDelta / max(length(eyeDelta), 0.0001);
if (vMaterial > 5.5 && vMaterial < 6.5) {
// Flat, quiet voxel water: the silhouette and texture remain pixel aligned.
float fresnel = 0.035 + 0.58 * pow(1.0 - max(dot(normal, toEye), 0.0), 5.0);
vec3 reflection = daylightSky(reflect(-toEye, normal)) * 0.82 * skyExposure + blockLight * 0.08;
color = mix(color, reflection, fresnel);
vec3 halfVector = normalize(SUN_DIRECTION + toEye);
float glint = pow(max(dot(normal, halfVector), 0.0), 160.0);
color += mix(vec3(0.045, 0.060, 0.090), vec3(0.45, 0.38, 0.24), daylight) * glint * sunlight;
}
// Matching atmosphere avoids a separate tinted band along the horizon.
float fog = (1.0 - exp(-pow(length(eyeDelta) / uFogDistance, 2.0))) * 0.92;
// Fade completely before the prefetched ring can be unloaded.
float viewEdge = max(abs(eyeDelta.x), abs(eyeDelta.z));
fog = max(fog, smoothstep(uViewDistance * 0.82, uViewDistance, viewEdge));
color = mix(color, daylightSky(-toEye) * skyExposure, fog);
outColor = vec4(toSRGB(clamp(color, 0.0, 1.0)), textureAlpha < 0.99 ? textureAlpha : vOpacity);
}`;
export const skyVertex = `#version 300 es
precision highp float;
out vec2 vUV;
void main() {
vec2 p = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2);
vUV = p;
gl_Position = vec4(p * 2.0 - 1.0, 1.0, 1.0);
}`;
export const skyFragment = `#version 300 es
precision highp float;
in vec2 vUV;
uniform float uPitch;
uniform float uYaw;
uniform float uAspect;
uniform float uTanHalfFov;
uniform float uSkyExposure;
out vec4 outColor;
${colorSpace}
${atmosphere}
float starHash(vec2 cell) {
return fract(sin(dot(cell, vec2(127.1, 311.7))) * 43758.5453);
}
vec3 nightStars(vec3 ray) {
// A fixed world-space hemisphere avoids stars sliding with the camera.
vec2 skyPlane = ray.xz / max(ray.y + 1.15, 0.2) * 128.0;
vec2 cell = floor(skyPlane);
float seed = starHash(cell);
vec2 center = vec2(starHash(cell + 19.4), starHash(cell + 71.2)) * 0.6 + 0.2;
float radius = mix(0.065, 0.13, starHash(cell + 37.8));
float distanceToStar = length(fract(skyPlane) - center);
float edge = max(fwidth(distanceToStar), 0.01);
float star = 1.0 - smoothstep(max(0.0, radius - edge), radius + edge, distanceToStar);
star *= step(0.994, seed) * smoothstep(0.025, 0.20, ray.y);
vec3 tint = mix(vec3(0.48, 0.59, 0.80), vec3(0.82, 0.74, 0.60), starHash(cell + 113.0));
return tint * star * mix(0.32, 0.72, starHash(cell + 53.0));
}
void main() {
vec3 forward = vec3(sin(uYaw) * cos(uPitch), sin(uPitch), -cos(uYaw) * cos(uPitch));
vec3 right = vec3(cos(uYaw), 0.0, sin(uYaw));
vec3 up = vec3(-sin(uYaw) * sin(uPitch), cos(uPitch), cos(uYaw) * sin(uPitch));
vec2 screen = vUV * 2.0 - 1.0;
vec3 ray = normalize(forward + right * screen.x * uAspect * uTanHalfFov + up * screen.y * uTanHalfFov);
vec3 color = daylightSky(ray);
float daylight = clamp(uDaylight, 0.0, 1.0);
color += nightStars(ray) * (1.0 - daylight);
float sunAngle = dot(ray, SUN_DIRECTION);
float rim = max(fwidth(sunAngle), 0.000001);
float angularRadius = mix(0.0125, 0.0105, daylight);
float disc = smoothstep(cos(angularRadius) - rim, cos(angularRadius) + rim, sunAngle);
vec3 moonRight = normalize(cross(SUN_DIRECTION, vec3(0.0, 1.0, 0.0)));
vec3 moonUp = cross(moonRight, SUN_DIRECTION);
vec2 moonUV = vec2(dot(ray, moonRight), dot(ray, moonUp)) / sin(angularRadius);
vec2 craterA = moonUV - vec2(-0.24, 0.22), craterB = moonUV - vec2(0.28, -0.27);
float craters = 0.12 * exp(-dot(craterA, craterA) * 22.0) + 0.075 * exp(-dot(craterB, craterB) * 36.0);
float moonShade = 0.78 + 0.22 * sqrt(max(0.0, 1.0 - dot(moonUV, moonUV))) - craters;
vec3 moon = vec3(0.62, 0.70, 0.84) * moonShade;
color = mix(color, mix(moon, vec3(1.0, 0.965, 0.84), daylight), disc);
// Match the distant backdrop to cave fog when the streamed window ends
// underground; missing geometry must not reveal a bright surface sky.
outColor = vec4(toSRGB(clamp(color * uSkyExposure, 0.0, 1.0)), 1.0);
}`;
export const shadowVertex = `#version 300 es
precision highp float;
layout(location=0) in vec3 aPos;
layout(location=4) in float aOpacity;
layout(location=5) in vec3 aBlockTexture;
uniform mat4 uLightVP;
uniform vec3 uOffset;
out vec2 vBlockUV;
flat out float vBlockLayer;
out float vOpacity;
void main() {
vBlockUV = aBlockTexture.xy;
vBlockLayer = aBlockTexture.z;
vOpacity = aOpacity;
gl_Position = uLightVP * vec4(aPos + uOffset, 1.0);
}`;
export const shadowFragment = `#version 300 es
precision highp float;
precision highp sampler2DArray;
in vec2 vBlockUV;
flat in float vBlockLayer;
in float vOpacity;
${blockTextureSampling}
void main() {
if (vOpacity < 0.99) discard;
if (vBlockLayer >= 0.0 && blockPixel(vBlockUV, vBlockLayer).a < 0.5) discard;
}`;