MVP checks / mvp (push) Waiting to run
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.
322 lines
13 KiB
JavaScript
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;
|
|
}`;
|