2026-04-13 14:10:06 +02:00
|
|
|
import {
|
|
|
|
|
BackSide,
|
2026-04-22 12:55:08 +02:00
|
|
|
Camera,
|
2026-04-13 14:10:06 +02:00
|
|
|
Color,
|
|
|
|
|
Group,
|
|
|
|
|
Mesh,
|
|
|
|
|
MeshBasicMaterial,
|
2026-04-22 14:02:56 +02:00
|
|
|
PlaneGeometry,
|
2026-04-13 14:10:06 +02:00
|
|
|
Scene,
|
|
|
|
|
ShaderMaterial,
|
|
|
|
|
SphereGeometry,
|
2026-04-22 14:02:56 +02:00
|
|
|
Texture,
|
2026-04-22 15:18:26 +02:00
|
|
|
Vector2,
|
2026-04-22 14:02:56 +02:00
|
|
|
Vector3
|
2026-04-13 14:10:06 +02:00
|
|
|
} from "three";
|
|
|
|
|
|
2026-04-22 14:02:56 +02:00
|
|
|
import type { Vec3 } from "../core/vector";
|
|
|
|
|
import type {
|
|
|
|
|
WorldBackgroundSettings,
|
|
|
|
|
WorldSunLightSettings
|
|
|
|
|
} from "../document/world-settings";
|
2026-04-22 15:18:26 +02:00
|
|
|
import type { WorldShaderSkyRenderState } from "./world-shader-sky";
|
2026-04-13 14:10:06 +02:00
|
|
|
|
|
|
|
|
const BACKGROUND_SPHERE_RADIUS = 320;
|
|
|
|
|
const BACKGROUND_SPHERE_WIDTH_SEGMENTS = 48;
|
|
|
|
|
const BACKGROUND_SPHERE_HEIGHT_SEGMENTS = 24;
|
|
|
|
|
const DEFAULT_IMAGE_BACKGROUND_FALLBACK_COLOR = "#0d1116";
|
|
|
|
|
const NIGHT_BACKGROUND_EPSILON = 1e-4;
|
2026-04-22 14:02:56 +02:00
|
|
|
const MIN_CELESTIAL_BODY_INTENSITY = 1e-4;
|
|
|
|
|
const MIN_CELESTIAL_BODY_ALTITUDE = -0.02;
|
|
|
|
|
const CELESTIAL_BODY_DISTANCE = BACKGROUND_SPHERE_RADIUS - 6;
|
|
|
|
|
const SUN_CELESTIAL_BODY_SIZE = 28;
|
|
|
|
|
const MOON_CELESTIAL_BODY_SIZE = 20;
|
2026-04-13 14:10:06 +02:00
|
|
|
|
|
|
|
|
function clamp(value: number, min: number, max: number) {
|
|
|
|
|
return Math.min(Math.max(value, min), max);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function lerp(left: number, right: number, amount: number) {
|
|
|
|
|
return left + (right - left) * amount;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveGradientColors(background: WorldBackgroundSettings) {
|
|
|
|
|
if (background.mode === "solid") {
|
|
|
|
|
return {
|
|
|
|
|
topColorHex: background.colorHex,
|
|
|
|
|
bottomColorHex: background.colorHex
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (background.mode === "verticalGradient") {
|
|
|
|
|
return {
|
|
|
|
|
topColorHex: background.topColorHex,
|
|
|
|
|
bottomColorHex: background.bottomColorHex
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
topColorHex: DEFAULT_IMAGE_BACKGROUND_FALLBACK_COLOR,
|
|
|
|
|
bottomColorHex: DEFAULT_IMAGE_BACKGROUND_FALLBACK_COLOR
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface WorldBackgroundOverlayState {
|
|
|
|
|
texture: Texture | null;
|
|
|
|
|
opacity: number;
|
|
|
|
|
environmentIntensity: number;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:44:36 +02:00
|
|
|
export interface WorldEnvironmentBlendTextureResolver {
|
|
|
|
|
resolveBlendTexture(
|
|
|
|
|
baseTexture: Texture,
|
|
|
|
|
overlayTexture: Texture,
|
|
|
|
|
blendAmount: number
|
|
|
|
|
): Texture | null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 14:10:06 +02:00
|
|
|
export interface WorldEnvironmentState {
|
|
|
|
|
texture: Texture | null;
|
|
|
|
|
intensity: number;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 14:02:56 +02:00
|
|
|
export interface WorldCelestialBodyState {
|
|
|
|
|
colorHex: string;
|
|
|
|
|
direction: Vec3;
|
|
|
|
|
intensity: number;
|
|
|
|
|
size: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface WorldCelestialBodiesState {
|
|
|
|
|
sun: WorldCelestialBodyState | null;
|
|
|
|
|
moon: WorldCelestialBodyState | null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 14:10:06 +02:00
|
|
|
export function resolveWorldEnvironmentState(
|
|
|
|
|
background: WorldBackgroundSettings,
|
|
|
|
|
backgroundTexture: Texture | null,
|
2026-04-22 13:44:36 +02:00
|
|
|
overlay: WorldBackgroundOverlayState | null,
|
|
|
|
|
environmentBlendTextureResolver: WorldEnvironmentBlendTextureResolver | null = null
|
2026-04-13 14:10:06 +02:00
|
|
|
): WorldEnvironmentState {
|
|
|
|
|
const baseTexture = background.mode === "image" ? backgroundTexture : null;
|
|
|
|
|
const baseIntensity =
|
|
|
|
|
background.mode === "image" ? background.environmentIntensity : 0;
|
|
|
|
|
const overlayTexture = overlay?.texture ?? null;
|
|
|
|
|
const overlayOpacity = clamp(overlay?.opacity ?? 0, 0, 1);
|
|
|
|
|
const overlayIntensity = overlay?.environmentIntensity ?? 0;
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
baseTexture !== null &&
|
|
|
|
|
overlayTexture !== null &&
|
|
|
|
|
overlayOpacity > NIGHT_BACKGROUND_EPSILON &&
|
|
|
|
|
overlayOpacity < 1 - NIGHT_BACKGROUND_EPSILON
|
|
|
|
|
) {
|
2026-04-22 13:44:36 +02:00
|
|
|
const blendedTexture =
|
|
|
|
|
environmentBlendTextureResolver?.resolveBlendTexture(
|
|
|
|
|
baseTexture,
|
|
|
|
|
overlayTexture,
|
|
|
|
|
overlayOpacity
|
|
|
|
|
) ?? baseTexture;
|
|
|
|
|
|
2026-04-13 14:10:06 +02:00
|
|
|
return {
|
2026-04-22 13:44:36 +02:00
|
|
|
texture: blendedTexture,
|
2026-04-22 13:00:10 +02:00
|
|
|
intensity: lerp(baseIntensity, overlayIntensity, overlayOpacity)
|
2026-04-13 14:10:06 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (overlayTexture !== null && overlayOpacity > NIGHT_BACKGROUND_EPSILON) {
|
|
|
|
|
if (baseTexture === null) {
|
|
|
|
|
return {
|
|
|
|
|
texture: overlayTexture,
|
|
|
|
|
intensity: overlayIntensity * overlayOpacity
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (overlayOpacity >= 1 - NIGHT_BACKGROUND_EPSILON) {
|
|
|
|
|
return {
|
|
|
|
|
texture: overlayTexture,
|
|
|
|
|
intensity: overlayIntensity
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (baseTexture !== null) {
|
|
|
|
|
return {
|
|
|
|
|
texture: baseTexture,
|
|
|
|
|
intensity: baseIntensity
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (overlayTexture !== null && overlayOpacity > NIGHT_BACKGROUND_EPSILON) {
|
|
|
|
|
return {
|
|
|
|
|
texture: overlayTexture,
|
|
|
|
|
intensity: overlayIntensity * overlayOpacity
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
texture: null,
|
|
|
|
|
intensity: 1
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 14:02:56 +02:00
|
|
|
function normalizeDirection(direction: Vec3): Vec3 | null {
|
|
|
|
|
const length = Math.hypot(direction.x, direction.y, direction.z);
|
|
|
|
|
|
|
|
|
|
if (length <= 1e-6) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
x: direction.x / length,
|
|
|
|
|
y: direction.y / length,
|
|
|
|
|
z: direction.z / length
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveWorldCelestialBodyState(
|
|
|
|
|
light: WorldSunLightSettings | null,
|
|
|
|
|
size: number
|
|
|
|
|
): WorldCelestialBodyState | null {
|
|
|
|
|
if (light === null || light.intensity <= MIN_CELESTIAL_BODY_INTENSITY) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const direction = normalizeDirection(light.direction);
|
|
|
|
|
|
|
|
|
|
if (direction === null || direction.y < MIN_CELESTIAL_BODY_ALTITUDE) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
colorHex: light.colorHex,
|
|
|
|
|
direction,
|
|
|
|
|
intensity: light.intensity,
|
|
|
|
|
size
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function resolveWorldCelestialBodiesState(
|
|
|
|
|
showCelestialBodies: boolean,
|
|
|
|
|
sunLight: WorldSunLightSettings,
|
|
|
|
|
moonLight: WorldSunLightSettings | null
|
|
|
|
|
): WorldCelestialBodiesState {
|
|
|
|
|
if (!showCelestialBodies) {
|
|
|
|
|
return {
|
|
|
|
|
sun: null,
|
|
|
|
|
moon: null
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
sun: resolveWorldCelestialBodyState(sunLight, SUN_CELESTIAL_BODY_SIZE),
|
|
|
|
|
moon: resolveWorldCelestialBodyState(moonLight, MOON_CELESTIAL_BODY_SIZE)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 14:10:06 +02:00
|
|
|
const GRADIENT_VERTEX_SHADER = `
|
|
|
|
|
varying vec3 vWorldPosition;
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
|
|
|
|
|
vWorldPosition = worldPosition.xyz;
|
|
|
|
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const GRADIENT_FRAGMENT_SHADER = `
|
|
|
|
|
uniform vec3 uTopColor;
|
|
|
|
|
uniform vec3 uBottomColor;
|
|
|
|
|
varying vec3 vWorldPosition;
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
vec3 direction = normalize(vWorldPosition - cameraPosition);
|
|
|
|
|
float gradientAmount = clamp(direction.y * 0.5 + 0.5, 0.0, 1.0);
|
|
|
|
|
vec3 color = mix(uBottomColor, uTopColor, gradientAmount);
|
2026-04-22 14:02:56 +02:00
|
|
|
gl_FragColor = vec4(color, 1.0);
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
2026-04-22 15:19:17 +02:00
|
|
|
const DEFAULT_SKY_FRAGMENT_SHADER = `
|
|
|
|
|
uniform vec3 uSkyTopColor;
|
|
|
|
|
uniform vec3 uSkyBottomColor;
|
|
|
|
|
uniform vec3 uSunDirection;
|
|
|
|
|
uniform vec3 uSunColor;
|
|
|
|
|
uniform float uSunIntensity;
|
|
|
|
|
uniform float uSunDiscSizeDegrees;
|
|
|
|
|
uniform float uSunVisible;
|
|
|
|
|
uniform vec3 uMoonDirection;
|
|
|
|
|
uniform vec3 uMoonColor;
|
|
|
|
|
uniform float uMoonIntensity;
|
|
|
|
|
uniform float uMoonDiscSizeDegrees;
|
|
|
|
|
uniform float uMoonVisible;
|
|
|
|
|
uniform float uDaylightFactor;
|
|
|
|
|
uniform float uTwilightFactor;
|
|
|
|
|
uniform float uStarDensity;
|
|
|
|
|
uniform float uStarBrightness;
|
|
|
|
|
uniform float uStarVisibility;
|
|
|
|
|
uniform float uStarRotationRadians;
|
|
|
|
|
uniform float uCloudCoverage;
|
|
|
|
|
uniform float uCloudDensity;
|
|
|
|
|
uniform float uCloudSoftness;
|
|
|
|
|
uniform float uCloudScale;
|
|
|
|
|
uniform float uCloudHeight;
|
|
|
|
|
uniform float uCloudHeightVariation;
|
|
|
|
|
uniform vec3 uCloudTint;
|
|
|
|
|
uniform float uCloudOpacity;
|
|
|
|
|
uniform float uCloudOpacityRandomness;
|
|
|
|
|
uniform vec2 uCloudDriftOffset;
|
|
|
|
|
varying vec3 vWorldPosition;
|
|
|
|
|
|
|
|
|
|
const float PI = 3.1415926535897932384626433832795;
|
|
|
|
|
|
|
|
|
|
float hash12(vec2 point) {
|
|
|
|
|
return fract(sin(dot(point, vec2(127.1, 311.7))) * 43758.5453123);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float noise(vec2 point) {
|
|
|
|
|
vec2 cell = floor(point);
|
|
|
|
|
vec2 local = fract(point);
|
|
|
|
|
vec2 blend = local * local * (3.0 - 2.0 * local);
|
|
|
|
|
float a = hash12(cell);
|
|
|
|
|
float b = hash12(cell + vec2(1.0, 0.0));
|
|
|
|
|
float c = hash12(cell + vec2(0.0, 1.0));
|
|
|
|
|
float d = hash12(cell + vec2(1.0, 1.0));
|
|
|
|
|
|
|
|
|
|
return mix(mix(a, b, blend.x), mix(c, d, blend.x), blend.y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float fbm(vec2 point) {
|
|
|
|
|
float value = 0.0;
|
|
|
|
|
float amplitude = 0.5;
|
|
|
|
|
|
|
|
|
|
for (int octave = 0; octave < 5; octave++) {
|
|
|
|
|
value += noise(point) * amplitude;
|
|
|
|
|
point = point * 2.03 + vec2(19.7, 7.3);
|
|
|
|
|
amplitude *= 0.5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mat2 rotation2d(float radians) {
|
|
|
|
|
float sine = sin(radians);
|
|
|
|
|
float cosine = cos(radians);
|
|
|
|
|
|
|
|
|
|
return mat2(cosine, -sine, sine, cosine);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vec2 toSkyUv(vec3 direction) {
|
|
|
|
|
float longitude = atan(direction.z, direction.x);
|
|
|
|
|
float latitude = asin(clamp(direction.y, -1.0, 1.0));
|
|
|
|
|
|
|
|
|
|
return vec2(longitude / (2.0 * PI) + 0.5, latitude / PI + 0.5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float discMask(vec3 direction, vec3 lightDirection, float sizeDegrees, float featherScale) {
|
|
|
|
|
float sizeRadians = radians(max(sizeDegrees, 0.01));
|
|
|
|
|
float alignment = dot(direction, normalize(lightDirection));
|
|
|
|
|
float outerCos = cos(sizeRadians * 1.6);
|
|
|
|
|
float innerCos = cos(sizeRadians * max(featherScale, 0.18));
|
|
|
|
|
|
|
|
|
|
return smoothstep(outerCos, innerCos, alignment);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float glowMask(vec3 direction, vec3 lightDirection, float sizeDegrees, float radiusScale) {
|
|
|
|
|
float sizeRadians = radians(max(sizeDegrees, 0.01) * max(radiusScale, 1.0));
|
|
|
|
|
float alignment = dot(direction, normalize(lightDirection));
|
|
|
|
|
float outerCos = cos(sizeRadians * 1.8);
|
|
|
|
|
float innerCos = cos(sizeRadians * 0.55);
|
|
|
|
|
|
|
|
|
|
return smoothstep(outerCos, innerCos, alignment);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float starLayer(vec2 uv, float scale, float densityThreshold) {
|
|
|
|
|
vec2 scaledUv = uv * scale;
|
|
|
|
|
vec2 cell = floor(scaledUv);
|
|
|
|
|
vec2 local = fract(scaledUv) - 0.5;
|
|
|
|
|
float seed = hash12(cell);
|
|
|
|
|
float star = smoothstep(0.18, 0.0, length(local));
|
|
|
|
|
|
|
|
|
|
return step(densityThreshold, seed) * star * mix(0.45, 1.0, hash12(cell + 17.0));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
vec3 direction = normalize(vWorldPosition - cameraPosition);
|
|
|
|
|
float skyMix = clamp(direction.y * 0.5 + 0.5, 0.0, 1.0);
|
|
|
|
|
skyMix = pow(skyMix, 0.72);
|
|
|
|
|
|
|
|
|
|
vec3 skyColor = mix(uSkyBottomColor, uSkyTopColor, skyMix);
|
|
|
|
|
float horizonMask = pow(clamp(1.0 - abs(direction.y), 0.0, 1.0), 2.6);
|
|
|
|
|
skyColor += mix(uSkyBottomColor, vec3(1.0), 0.1 + uTwilightFactor * 0.18) * horizonMask * 0.04;
|
|
|
|
|
|
|
|
|
|
float sunDisc = uSunVisible * discMask(direction, uSunDirection, uSunDiscSizeDegrees, 0.42);
|
|
|
|
|
float sunGlow = uSunVisible * glowMask(direction, uSunDirection, uSunDiscSizeDegrees, 4.8);
|
|
|
|
|
float moonDisc = uMoonVisible * discMask(direction, uMoonDirection, uMoonDiscSizeDegrees, 0.5);
|
|
|
|
|
float moonGlow = uMoonVisible * glowMask(direction, uMoonDirection, uMoonDiscSizeDegrees, 5.6);
|
|
|
|
|
|
|
|
|
|
vec2 skyUv = toSkyUv(direction);
|
|
|
|
|
vec2 centeredStarUv = skyUv - 0.5;
|
|
|
|
|
centeredStarUv = rotation2d(uStarRotationRadians) * centeredStarUv;
|
|
|
|
|
vec2 starUv = centeredStarUv + 0.5;
|
|
|
|
|
float starDensity = clamp(uStarDensity, 0.0, 2.0);
|
|
|
|
|
float starLayerA = starLayer(starUv, mix(110.0, 360.0, clamp(starDensity * 0.65, 0.0, 1.0)), mix(0.994, 0.9, clamp(starDensity, 0.0, 1.0)));
|
|
|
|
|
float starLayerB = starLayer(starUv + vec2(13.4, 5.7), mix(220.0, 640.0, clamp(starDensity * 0.5, 0.0, 1.0)), mix(0.9985, 0.96, clamp(starDensity * 0.8, 0.0, 1.0)));
|
|
|
|
|
float starTwinkle = noise(starUv * 24.0 + vec2(uStarRotationRadians * 0.5, uTwilightFactor * 17.0));
|
|
|
|
|
float stars = (starLayerA * 0.75 + starLayerB * 1.15) * mix(0.8, 1.18, starTwinkle);
|
|
|
|
|
float starHorizonFade = smoothstep(-0.08, 0.12, direction.y);
|
|
|
|
|
skyColor += vec3(stars) * uStarBrightness * uStarVisibility * starHorizonFade;
|
|
|
|
|
|
|
|
|
|
vec2 cloudUv = skyUv;
|
|
|
|
|
cloudUv.x += uCloudDriftOffset.x;
|
|
|
|
|
cloudUv.y += uCloudDriftOffset.y;
|
|
|
|
|
float cloudScale = max(uCloudScale, 0.01);
|
|
|
|
|
float layerA = fbm(cloudUv * (0.9 + cloudScale * 1.4));
|
|
|
|
|
float layerB = fbm((cloudUv + vec2(5.1, 1.7)) * (1.8 + cloudScale * 2.1));
|
|
|
|
|
float layerC = noise((cloudUv - vec2(3.4, 7.2)) * (3.4 + cloudScale * 3.4));
|
|
|
|
|
float cloudDensity = clamp(uCloudDensity, 0.0, 2.0);
|
|
|
|
|
float cloudShape = mix(layerA, layerA * 0.58 + layerB * 0.42, clamp(cloudDensity / 1.35, 0.0, 1.0));
|
|
|
|
|
cloudShape = mix(cloudShape, cloudShape * 0.72 + layerC * 0.28, 0.35);
|
|
|
|
|
|
|
|
|
|
float bandCenter = mix(-0.15, 0.85, clamp(uCloudHeight, 0.0, 1.0));
|
|
|
|
|
float bandNoise = (noise(cloudUv * 0.45 + vec2(11.0, 23.0)) - 0.5) * 2.0 * clamp(uCloudHeightVariation, 0.0, 1.0);
|
|
|
|
|
float bandDistance = abs(direction.y - (bandCenter + bandNoise * 0.45));
|
|
|
|
|
float bandMask = 1.0 - smoothstep(0.22, 0.88, bandDistance + (1.0 - clamp(cloudDensity / 1.35, 0.0, 1.0)) * 0.15);
|
|
|
|
|
float coverageThreshold = mix(0.94, 0.12, clamp(uCloudCoverage, 0.0, 1.0));
|
|
|
|
|
float softness = mix(0.01, 0.22, clamp(uCloudSoftness, 0.0, 1.0));
|
|
|
|
|
float opacityNoise = mix(1.0, noise(cloudUv * 2.6 + vec2(19.0, 7.0)), clamp(uCloudOpacityRandomness, 0.0, 1.0));
|
|
|
|
|
float clouds = smoothstep(coverageThreshold, coverageThreshold - softness - 0.0001, cloudShape + bandMask * 0.22);
|
|
|
|
|
clouds *= bandMask;
|
|
|
|
|
clouds *= clamp(uCloudOpacity, 0.0, 1.0) * mix(0.82, 1.0, opacityNoise);
|
|
|
|
|
|
|
|
|
|
vec3 cloudLight = mix(mix(uSkyBottomColor, uSkyTopColor, 0.62), vec3(1.0), 0.12 + uDaylightFactor * 0.18 + uTwilightFactor * 0.08);
|
|
|
|
|
cloudLight += uSunColor * sunGlow * (0.16 + uTwilightFactor * 0.12);
|
|
|
|
|
vec3 cloudColor = mix(cloudLight, uCloudTint, 0.56);
|
|
|
|
|
skyColor = mix(skyColor, cloudColor, clamp(clouds, 0.0, 1.0));
|
|
|
|
|
|
|
|
|
|
skyColor += uSunColor * (sunGlow * (0.16 + uTwilightFactor * 0.32) + sunDisc * (0.75 + min(uSunIntensity, 3.5) * 0.25));
|
|
|
|
|
skyColor += uMoonColor * (moonGlow * 0.12 + moonDisc * 0.35 * (0.35 + min(uMoonIntensity, 2.0) * 0.65));
|
|
|
|
|
skyColor = clamp(skyColor, 0.0, 1.0);
|
|
|
|
|
|
|
|
|
|
gl_FragColor = vec4(skyColor, 1.0);
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
2026-04-22 14:02:56 +02:00
|
|
|
const CELESTIAL_BODY_VERTEX_SHADER = `
|
|
|
|
|
varying vec2 vUv;
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
vUv = uv;
|
|
|
|
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const SUN_FRAGMENT_SHADER = `
|
|
|
|
|
uniform vec3 uColor;
|
|
|
|
|
uniform float uIntensity;
|
|
|
|
|
varying vec2 vUv;
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
vec2 centeredUv = vUv * 2.0 - 1.0;
|
|
|
|
|
float distanceFromCenter = length(centeredUv);
|
|
|
|
|
float disc = smoothstep(0.52, 0.18, distanceFromCenter);
|
|
|
|
|
float core = smoothstep(0.26, 0.0, distanceFromCenter);
|
|
|
|
|
float halo = smoothstep(1.0, 0.28, distanceFromCenter);
|
|
|
|
|
float glow = clamp(0.18 + min(uIntensity, 4.0) * 0.18, 0.18, 0.95);
|
|
|
|
|
vec3 warmCore = mix(uColor, vec3(1.0, 0.97, 0.88), 0.45);
|
|
|
|
|
vec3 color =
|
|
|
|
|
warmCore * (disc * 1.15 + core * 0.8) +
|
|
|
|
|
uColor * halo * glow * 0.55;
|
|
|
|
|
float alpha = disc * 0.82 + core * 0.28 + halo * glow * 0.22;
|
|
|
|
|
|
|
|
|
|
if (alpha <= 0.001) {
|
|
|
|
|
discard;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
gl_FragColor = vec4(color, alpha);
|
|
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const MOON_FRAGMENT_SHADER = `
|
|
|
|
|
uniform vec3 uColor;
|
|
|
|
|
uniform float uIntensity;
|
|
|
|
|
varying vec2 vUv;
|
|
|
|
|
|
|
|
|
|
float hash(vec2 point) {
|
|
|
|
|
return fract(sin(dot(point, vec2(127.1, 311.7))) * 43758.5453123);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float noise(vec2 point) {
|
|
|
|
|
vec2 cell = floor(point);
|
|
|
|
|
vec2 local = fract(point);
|
|
|
|
|
vec2 blend = local * local * (3.0 - 2.0 * local);
|
|
|
|
|
|
|
|
|
|
float a = hash(cell);
|
|
|
|
|
float b = hash(cell + vec2(1.0, 0.0));
|
|
|
|
|
float c = hash(cell + vec2(0.0, 1.0));
|
|
|
|
|
float d = hash(cell + vec2(1.0, 1.0));
|
|
|
|
|
|
|
|
|
|
return mix(mix(a, b, blend.x), mix(c, d, blend.x), blend.y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
vec2 centeredUv = vUv * 2.0 - 1.0;
|
|
|
|
|
float distanceFromCenter = length(centeredUv);
|
|
|
|
|
float disc = smoothstep(0.52, 0.44, distanceFromCenter);
|
|
|
|
|
float body = smoothstep(0.48, 0.0, distanceFromCenter);
|
|
|
|
|
float halo = smoothstep(0.92, 0.32, distanceFromCenter);
|
|
|
|
|
float craterNoise = noise(centeredUv * 6.0 + vec2(17.0, 9.0));
|
|
|
|
|
float mariaNoise = noise(centeredUv * 3.0 + vec2(3.0, 5.0));
|
|
|
|
|
float surfaceVariation = mix(0.86, 1.04, craterNoise * 0.65 + mariaNoise * 0.35);
|
|
|
|
|
float glow = clamp(0.12 + min(uIntensity, 2.0) * 0.16, 0.12, 0.42);
|
|
|
|
|
vec3 moonColor = mix(uColor, vec3(1.0, 1.0, 1.0), 0.35) * surfaceVariation;
|
|
|
|
|
vec3 color = moonColor * (body * 1.08 + disc * 0.18) + uColor * halo * glow * 0.18;
|
|
|
|
|
float alpha = disc * 0.82 + halo * glow * 0.12;
|
|
|
|
|
|
|
|
|
|
if (alpha <= 0.001) {
|
|
|
|
|
discard;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
gl_FragColor = vec4(color, alpha);
|
2026-04-13 14:10:06 +02:00
|
|
|
}
|
|
|
|
|
`;
|
|
|
|
|
|
2026-04-22 14:02:56 +02:00
|
|
|
function createCelestialBodyMaterial(fragmentShader: string) {
|
|
|
|
|
return new ShaderMaterial({
|
|
|
|
|
uniforms: {
|
|
|
|
|
uColor: {
|
|
|
|
|
value: new Color("#ffffff")
|
|
|
|
|
},
|
|
|
|
|
uIntensity: {
|
|
|
|
|
value: 0
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
vertexShader: CELESTIAL_BODY_VERTEX_SHADER,
|
|
|
|
|
fragmentShader,
|
|
|
|
|
depthTest: false,
|
|
|
|
|
depthWrite: false,
|
|
|
|
|
fog: false,
|
|
|
|
|
transparent: true
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 14:10:06 +02:00
|
|
|
export class WorldBackgroundRenderer {
|
|
|
|
|
readonly scene = new Scene();
|
|
|
|
|
|
|
|
|
|
private readonly anchor = new Group();
|
|
|
|
|
private readonly geometry = new SphereGeometry(
|
|
|
|
|
BACKGROUND_SPHERE_RADIUS,
|
|
|
|
|
BACKGROUND_SPHERE_WIDTH_SEGMENTS,
|
|
|
|
|
BACKGROUND_SPHERE_HEIGHT_SEGMENTS
|
|
|
|
|
);
|
|
|
|
|
private readonly gradientMaterial = new ShaderMaterial({
|
|
|
|
|
uniforms: {
|
|
|
|
|
uTopColor: {
|
|
|
|
|
value: new Color(DEFAULT_IMAGE_BACKGROUND_FALLBACK_COLOR)
|
|
|
|
|
},
|
|
|
|
|
uBottomColor: {
|
|
|
|
|
value: new Color(DEFAULT_IMAGE_BACKGROUND_FALLBACK_COLOR)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
vertexShader: GRADIENT_VERTEX_SHADER,
|
|
|
|
|
fragmentShader: GRADIENT_FRAGMENT_SHADER,
|
|
|
|
|
side: BackSide,
|
|
|
|
|
depthTest: false,
|
|
|
|
|
depthWrite: false,
|
|
|
|
|
fog: false
|
|
|
|
|
});
|
2026-04-22 15:19:41 +02:00
|
|
|
private readonly shaderSkyMaterial = new ShaderMaterial({
|
|
|
|
|
uniforms: {
|
|
|
|
|
uSkyTopColor: {
|
|
|
|
|
value: new Color(DEFAULT_IMAGE_BACKGROUND_FALLBACK_COLOR)
|
|
|
|
|
},
|
|
|
|
|
uSkyBottomColor: {
|
|
|
|
|
value: new Color(DEFAULT_IMAGE_BACKGROUND_FALLBACK_COLOR)
|
|
|
|
|
},
|
|
|
|
|
uSunDirection: {
|
|
|
|
|
value: new Vector3(0, 1, 0)
|
|
|
|
|
},
|
|
|
|
|
uSunColor: {
|
|
|
|
|
value: new Color("#ffffff")
|
|
|
|
|
},
|
|
|
|
|
uSunIntensity: {
|
|
|
|
|
value: 0
|
|
|
|
|
},
|
|
|
|
|
uSunDiscSizeDegrees: {
|
|
|
|
|
value: 2.6
|
|
|
|
|
},
|
|
|
|
|
uSunVisible: {
|
|
|
|
|
value: 0
|
|
|
|
|
},
|
|
|
|
|
uMoonDirection: {
|
|
|
|
|
value: new Vector3(0, 1, 0)
|
|
|
|
|
},
|
|
|
|
|
uMoonColor: {
|
|
|
|
|
value: new Color("#ffffff")
|
|
|
|
|
},
|
|
|
|
|
uMoonIntensity: {
|
|
|
|
|
value: 0
|
|
|
|
|
},
|
|
|
|
|
uMoonDiscSizeDegrees: {
|
|
|
|
|
value: 1.8
|
|
|
|
|
},
|
|
|
|
|
uMoonVisible: {
|
|
|
|
|
value: 0
|
|
|
|
|
},
|
|
|
|
|
uDaylightFactor: {
|
|
|
|
|
value: 1
|
|
|
|
|
},
|
|
|
|
|
uTwilightFactor: {
|
|
|
|
|
value: 0
|
|
|
|
|
},
|
|
|
|
|
uStarDensity: {
|
|
|
|
|
value: 0.5
|
|
|
|
|
},
|
|
|
|
|
uStarBrightness: {
|
|
|
|
|
value: 0.75
|
|
|
|
|
},
|
|
|
|
|
uStarVisibility: {
|
|
|
|
|
value: 0
|
|
|
|
|
},
|
|
|
|
|
uStarRotationRadians: {
|
|
|
|
|
value: 0
|
|
|
|
|
},
|
|
|
|
|
uCloudCoverage: {
|
|
|
|
|
value: 0.55
|
|
|
|
|
},
|
|
|
|
|
uCloudDensity: {
|
|
|
|
|
value: 0.6
|
|
|
|
|
},
|
|
|
|
|
uCloudSoftness: {
|
|
|
|
|
value: 0.4
|
|
|
|
|
},
|
|
|
|
|
uCloudScale: {
|
|
|
|
|
value: 1.2
|
|
|
|
|
},
|
|
|
|
|
uCloudHeight: {
|
|
|
|
|
value: 0.6
|
|
|
|
|
},
|
|
|
|
|
uCloudHeightVariation: {
|
|
|
|
|
value: 0.2
|
|
|
|
|
},
|
|
|
|
|
uCloudTint: {
|
|
|
|
|
value: new Color("#ffffff")
|
|
|
|
|
},
|
|
|
|
|
uCloudOpacity: {
|
|
|
|
|
value: 0.65
|
|
|
|
|
},
|
|
|
|
|
uCloudOpacityRandomness: {
|
|
|
|
|
value: 0.2
|
|
|
|
|
},
|
|
|
|
|
uCloudDriftOffset: {
|
|
|
|
|
value: new Vector2(0, 0)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
vertexShader: GRADIENT_VERTEX_SHADER,
|
|
|
|
|
fragmentShader: DEFAULT_SKY_FRAGMENT_SHADER,
|
|
|
|
|
side: BackSide,
|
|
|
|
|
depthTest: false,
|
|
|
|
|
depthWrite: false,
|
|
|
|
|
fog: false
|
|
|
|
|
});
|
2026-04-13 14:10:06 +02:00
|
|
|
private readonly imageMaterial = new MeshBasicMaterial({
|
|
|
|
|
color: 0xffffff,
|
|
|
|
|
side: BackSide,
|
|
|
|
|
depthTest: false,
|
|
|
|
|
depthWrite: false,
|
|
|
|
|
fog: false
|
|
|
|
|
});
|
|
|
|
|
private readonly overlayMaterial = new MeshBasicMaterial({
|
|
|
|
|
color: 0xffffff,
|
|
|
|
|
side: BackSide,
|
|
|
|
|
depthTest: false,
|
|
|
|
|
depthWrite: false,
|
|
|
|
|
fog: false,
|
|
|
|
|
transparent: true,
|
|
|
|
|
opacity: 0
|
|
|
|
|
});
|
2026-04-22 14:02:56 +02:00
|
|
|
private readonly celestialGeometry = new PlaneGeometry(1, 1);
|
|
|
|
|
private readonly sunMaterial = createCelestialBodyMaterial(
|
|
|
|
|
SUN_FRAGMENT_SHADER
|
|
|
|
|
);
|
|
|
|
|
private readonly moonMaterial = createCelestialBodyMaterial(
|
|
|
|
|
MOON_FRAGMENT_SHADER
|
|
|
|
|
);
|
2026-04-22 15:19:41 +02:00
|
|
|
private readonly shaderMesh = new Mesh(this.geometry, this.shaderSkyMaterial);
|
2026-04-13 14:10:06 +02:00
|
|
|
private readonly gradientMesh = new Mesh(this.geometry, this.gradientMaterial);
|
|
|
|
|
private readonly imageMesh = new Mesh(this.geometry, this.imageMaterial);
|
|
|
|
|
private readonly overlayMesh = new Mesh(this.geometry, this.overlayMaterial);
|
2026-04-22 14:02:56 +02:00
|
|
|
private readonly sunMesh = new Mesh(this.celestialGeometry, this.sunMaterial);
|
|
|
|
|
private readonly moonMesh = new Mesh(this.celestialGeometry, this.moonMaterial);
|
|
|
|
|
private readonly celestialBodyPosition = new Vector3();
|
|
|
|
|
private sunState: WorldCelestialBodyState | null = null;
|
|
|
|
|
private moonState: WorldCelestialBodyState | null = null;
|
2026-04-13 14:10:06 +02:00
|
|
|
|
|
|
|
|
constructor() {
|
2026-04-22 15:19:52 +02:00
|
|
|
this.shaderMesh.renderOrder = -1003;
|
2026-04-13 14:10:06 +02:00
|
|
|
this.gradientMesh.renderOrder = -1002;
|
|
|
|
|
this.imageMesh.renderOrder = -1001;
|
|
|
|
|
this.overlayMesh.renderOrder = -1000;
|
2026-04-22 14:02:56 +02:00
|
|
|
this.moonMesh.renderOrder = -999;
|
|
|
|
|
this.sunMesh.renderOrder = -998;
|
|
|
|
|
|
|
|
|
|
for (const mesh of [
|
2026-04-22 15:19:52 +02:00
|
|
|
this.shaderMesh,
|
2026-04-22 14:02:56 +02:00
|
|
|
this.gradientMesh,
|
|
|
|
|
this.imageMesh,
|
|
|
|
|
this.overlayMesh,
|
|
|
|
|
this.sunMesh,
|
|
|
|
|
this.moonMesh
|
|
|
|
|
]) {
|
2026-04-13 14:10:06 +02:00
|
|
|
mesh.frustumCulled = false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 15:19:52 +02:00
|
|
|
this.anchor.add(this.shaderMesh);
|
2026-04-13 14:10:06 +02:00
|
|
|
this.anchor.add(this.gradientMesh);
|
|
|
|
|
this.anchor.add(this.imageMesh);
|
|
|
|
|
this.anchor.add(this.overlayMesh);
|
2026-04-22 14:02:56 +02:00
|
|
|
this.anchor.add(this.moonMesh);
|
|
|
|
|
this.anchor.add(this.sunMesh);
|
2026-04-13 14:10:06 +02:00
|
|
|
this.scene.add(this.anchor);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
update(
|
|
|
|
|
background: WorldBackgroundSettings,
|
|
|
|
|
backgroundTexture: Texture | null,
|
2026-04-22 14:02:56 +02:00
|
|
|
overlay: WorldBackgroundOverlayState | null,
|
2026-04-22 15:20:33 +02:00
|
|
|
celestialBodies: WorldCelestialBodiesState | null = null,
|
|
|
|
|
shaderSkyState: WorldShaderSkyRenderState | null = null
|
2026-04-13 14:10:06 +02:00
|
|
|
) {
|
|
|
|
|
const gradientColors = resolveGradientColors(background);
|
|
|
|
|
this.gradientMaterial.uniforms.uTopColor.value.set(
|
|
|
|
|
gradientColors.topColorHex
|
|
|
|
|
);
|
|
|
|
|
this.gradientMaterial.uniforms.uBottomColor.value.set(
|
|
|
|
|
gradientColors.bottomColorHex
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-22 15:20:33 +02:00
|
|
|
const showShaderBackground =
|
|
|
|
|
background.mode === "shader" && shaderSkyState !== null;
|
2026-04-13 14:10:06 +02:00
|
|
|
const showImageBackground =
|
2026-04-22 15:20:33 +02:00
|
|
|
!showShaderBackground &&
|
|
|
|
|
background.mode === "image" &&
|
|
|
|
|
backgroundTexture !== null;
|
2026-04-13 14:10:06 +02:00
|
|
|
|
|
|
|
|
if (this.imageMaterial.map !== backgroundTexture) {
|
|
|
|
|
this.imageMaterial.map = backgroundTexture;
|
|
|
|
|
this.imageMaterial.needsUpdate = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 15:20:33 +02:00
|
|
|
this.syncShaderSkyState(shaderSkyState);
|
|
|
|
|
this.shaderMesh.visible = showShaderBackground;
|
|
|
|
|
this.gradientMesh.visible = !showShaderBackground && !showImageBackground;
|
2026-04-13 14:10:06 +02:00
|
|
|
this.imageMesh.visible = showImageBackground;
|
|
|
|
|
|
|
|
|
|
const overlayTexture = overlay?.texture ?? null;
|
|
|
|
|
const overlayOpacity =
|
|
|
|
|
overlayTexture === null ? 0 : clamp(overlay?.opacity ?? 0, 0, 1);
|
|
|
|
|
|
|
|
|
|
if (this.overlayMaterial.map !== overlayTexture) {
|
|
|
|
|
this.overlayMaterial.map = overlayTexture;
|
|
|
|
|
this.overlayMaterial.needsUpdate = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.overlayMaterial.opacity = overlayOpacity;
|
2026-04-22 15:20:33 +02:00
|
|
|
this.overlayMesh.visible =
|
|
|
|
|
!showShaderBackground && overlayOpacity > NIGHT_BACKGROUND_EPSILON;
|
|
|
|
|
this.sunState = showShaderBackground ? null : celestialBodies?.sun ?? null;
|
|
|
|
|
this.moonState =
|
|
|
|
|
showShaderBackground ? null : celestialBodies?.moon ?? null;
|
2026-04-22 14:02:56 +02:00
|
|
|
this.syncCelestialBodyVisualState(
|
|
|
|
|
this.sunMesh,
|
|
|
|
|
this.sunMaterial,
|
|
|
|
|
this.sunState
|
|
|
|
|
);
|
|
|
|
|
this.syncCelestialBodyVisualState(
|
|
|
|
|
this.moonMesh,
|
|
|
|
|
this.moonMaterial,
|
|
|
|
|
this.moonState
|
|
|
|
|
);
|
2026-04-13 14:10:06 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 12:55:08 +02:00
|
|
|
syncToCamera(camera: Camera) {
|
2026-04-13 14:10:06 +02:00
|
|
|
this.anchor.position.copy(camera.position);
|
2026-04-22 14:02:56 +02:00
|
|
|
this.syncCelestialBodyPose(this.sunMesh, this.sunState, camera);
|
|
|
|
|
this.syncCelestialBodyPose(this.moonMesh, this.moonState, camera);
|
2026-04-13 14:10:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dispose() {
|
|
|
|
|
this.geometry.dispose();
|
2026-04-22 14:02:56 +02:00
|
|
|
this.celestialGeometry.dispose();
|
2026-04-13 14:10:06 +02:00
|
|
|
this.gradientMaterial.dispose();
|
2026-04-22 15:20:33 +02:00
|
|
|
this.shaderSkyMaterial.dispose();
|
2026-04-13 14:10:06 +02:00
|
|
|
this.imageMaterial.dispose();
|
|
|
|
|
this.overlayMaterial.dispose();
|
2026-04-22 14:02:56 +02:00
|
|
|
this.sunMaterial.dispose();
|
|
|
|
|
this.moonMaterial.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private syncCelestialBodyVisualState(
|
|
|
|
|
mesh: Mesh,
|
|
|
|
|
material: ShaderMaterial,
|
|
|
|
|
state: WorldCelestialBodyState | null
|
|
|
|
|
) {
|
|
|
|
|
if (state === null) {
|
|
|
|
|
mesh.visible = false;
|
|
|
|
|
material.uniforms.uIntensity.value = 0;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
material.uniforms.uColor.value.set(state.colorHex);
|
|
|
|
|
material.uniforms.uIntensity.value = state.intensity;
|
|
|
|
|
mesh.scale.setScalar(state.size);
|
|
|
|
|
mesh.visible = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 15:20:33 +02:00
|
|
|
private syncShaderSkyState(state: WorldShaderSkyRenderState | null) {
|
|
|
|
|
if (state === null) {
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uStarVisibility.value = 0;
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uSunVisible.value = 0;
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uMoonVisible.value = 0;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uSkyTopColor.value.set(
|
|
|
|
|
state.sky.topColorHex
|
|
|
|
|
);
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uSkyBottomColor.value.set(
|
|
|
|
|
state.sky.bottomColorHex
|
|
|
|
|
);
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uSunDirection.value.set(
|
|
|
|
|
state.celestial.sunDirection.x,
|
|
|
|
|
state.celestial.sunDirection.y,
|
|
|
|
|
state.celestial.sunDirection.z
|
|
|
|
|
);
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uSunColor.value.set(
|
|
|
|
|
state.celestial.sunColorHex
|
|
|
|
|
);
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uSunIntensity.value =
|
|
|
|
|
state.celestial.sunIntensity;
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uSunDiscSizeDegrees.value =
|
|
|
|
|
state.celestial.sunDiscSizeDegrees;
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uSunVisible.value = state.celestial.sunVisible
|
|
|
|
|
? 1
|
|
|
|
|
: 0;
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uMoonDirection.value.set(
|
|
|
|
|
state.celestial.moonDirection.x,
|
|
|
|
|
state.celestial.moonDirection.y,
|
|
|
|
|
state.celestial.moonDirection.z
|
|
|
|
|
);
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uMoonColor.value.set(
|
|
|
|
|
state.celestial.moonColorHex
|
|
|
|
|
);
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uMoonIntensity.value =
|
|
|
|
|
state.celestial.moonIntensity;
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uMoonDiscSizeDegrees.value =
|
|
|
|
|
state.celestial.moonDiscSizeDegrees;
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uMoonVisible.value = state.celestial.moonVisible
|
|
|
|
|
? 1
|
|
|
|
|
: 0;
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uDaylightFactor.value =
|
|
|
|
|
state.time.daylightFactor;
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uTwilightFactor.value =
|
|
|
|
|
state.time.twilightFactor;
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uStarDensity.value = state.stars.density;
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uStarBrightness.value =
|
|
|
|
|
state.stars.brightness;
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uStarVisibility.value =
|
|
|
|
|
state.stars.visibility;
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uStarRotationRadians.value =
|
|
|
|
|
state.stars.rotationRadians;
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uCloudCoverage.value =
|
|
|
|
|
state.clouds.coverage;
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uCloudDensity.value = state.clouds.density;
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uCloudSoftness.value =
|
|
|
|
|
state.clouds.softness;
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uCloudScale.value = state.clouds.scale;
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uCloudHeight.value = state.clouds.height;
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uCloudHeightVariation.value =
|
|
|
|
|
state.clouds.heightVariation;
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uCloudTint.value.set(state.clouds.tintHex);
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uCloudOpacity.value = state.clouds.opacity;
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uCloudOpacityRandomness.value =
|
|
|
|
|
state.clouds.opacityRandomness;
|
|
|
|
|
this.shaderSkyMaterial.uniforms.uCloudDriftOffset.value.set(
|
|
|
|
|
state.clouds.driftOffset.x,
|
|
|
|
|
state.clouds.driftOffset.y
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 14:02:56 +02:00
|
|
|
private syncCelestialBodyPose(
|
|
|
|
|
mesh: Mesh,
|
|
|
|
|
state: WorldCelestialBodyState | null,
|
|
|
|
|
camera: Camera
|
|
|
|
|
) {
|
|
|
|
|
if (state === null) {
|
|
|
|
|
mesh.visible = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.celestialBodyPosition
|
|
|
|
|
.set(state.direction.x, state.direction.y, state.direction.z)
|
|
|
|
|
.normalize()
|
|
|
|
|
.multiplyScalar(CELESTIAL_BODY_DISTANCE);
|
|
|
|
|
mesh.position.copy(this.celestialBodyPosition);
|
|
|
|
|
mesh.lookAt(camera.position);
|
2026-04-13 14:10:06 +02:00
|
|
|
}
|
2026-04-22 12:55:08 +02:00
|
|
|
}
|