// 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; }`;