723 lines
24 KiB
JavaScript
723 lines
24 KiB
JavaScript
|
|
// SimulatedSkybox.js
|
|
// Ported from samples/utils/SimulatedSkybox.cpp
|
|
|
|
class SimulatedSkybox {
|
|
constructor(engine) {
|
|
this.engine = engine;
|
|
|
|
// Default Parameters
|
|
this.sunDirection = [0, 1, 0];
|
|
this.sunIntensity = 100000.0;
|
|
this.turbidity = 2.0;
|
|
this.rayleigh = 1.0;
|
|
this.mieCoefficient = 1.0;
|
|
this.mieG = 0.8;
|
|
this.ozone = 0.0;
|
|
this.msFactors = [0.1, 0.5, 0.0];
|
|
this.contrast = 1.0;
|
|
this.nightColor = [0.0, 3.0e-9, 7.5e-9];
|
|
this.shimmerControl = [0.0, 20.0, 0.1];
|
|
this.cloudControl = [0.0, 0.1, 8000.0, 0.0];
|
|
this.cloudControl2 = [0.0, 0.0, 0.0, 0.0];
|
|
this.waterControl = [50.0, 1.0, 1.0, 4.0]; // x=Strength, y=Speed, z=DerivativeTrick, w=Octaves
|
|
this.starControl = [0.001, 1.0, 350.0, 0.01]; // x=Density, y=Enabled, z=Frequency, w=PixelScale
|
|
this.starIntensity = 1.0;
|
|
this.focalLength = 24.0;
|
|
this.height = 1000.0;
|
|
this.planetRadius = 6360.0;
|
|
|
|
// Sun Halo
|
|
// x=cos(rad), y=limbDarkening, z=intensity, w=enabled
|
|
// Sun Halo
|
|
this.sunHalo = [Math.cos(0.5 * Math.PI / 180.0), 0.5, 1.0, 1.0];
|
|
|
|
// Moon Parameters (Mapped to Secondary Sun)
|
|
this.moonDirection = [-0.2, 0.8, -0.2]; // Default Moon Pos
|
|
this.moonIntensity = 1.0; // Scale Factor (1.0 = Physical Peak)
|
|
// x=cos(rad), y=sin(rad) [Precision Fix], z=intensity, w=enabled
|
|
this.moonHalo = [Math.cos(0.5 * Math.PI / 180.0), Math.sin(0.5 * Math.PI / 180.0), 1.0, 0.0]; // Disabled by default
|
|
|
|
this.moonTextureObj = null;
|
|
this.moonNormalObj = null;
|
|
this.milkyWayTextureObj = null;
|
|
|
|
// Milky Way Parameters
|
|
// x=Intensity, y=Saturation, z=Unused
|
|
this.milkyWayControl = [1.0, 1.2, 0.05];
|
|
this.milkyWayEnabled = true;
|
|
this.milkyWayRotation = [1, 0, 0, 0, 1, 0, 0, 0, 1]; // Identity by default
|
|
this.initEntity();
|
|
}
|
|
|
|
async loadMaterial(url) {
|
|
console.log("Loading material from:", url);
|
|
const response = await fetch(url);
|
|
const buffer = await response.arrayBuffer();
|
|
this.material = this.engine.createMaterial(new Uint8Array(buffer));
|
|
this.materialInstance = this.material.createInstance();
|
|
|
|
// Re-bind the entity with the loaded material
|
|
const rcm = this.engine.getRenderableManager();
|
|
const instance = rcm.getInstance(this.entity);
|
|
rcm.setMaterialInstanceAt(instance, 0, this.materialInstance);
|
|
|
|
console.log("Material loaded and bound.");
|
|
|
|
// Load Moon Texture
|
|
try {
|
|
const texUrl = 'assets/moon_disk.png';
|
|
const Texture = Filament.Texture;
|
|
const TextureSampler = Filament.TextureSampler;
|
|
const PixelDataFormat = Filament.PixelDataFormat;
|
|
const PixelDataType = Filament.PixelDataType;
|
|
const TextureUsage = Filament.Texture$Usage;
|
|
const TextureFormat = Filament.Texture$InternalFormat;
|
|
const MinFilter = Filament.MinFilter;
|
|
const MagFilter = Filament.MagFilter;
|
|
const WrapMode = Filament.WrapMode;
|
|
const texResponse = await fetch(texUrl);
|
|
const texBlob = await texResponse.blob();
|
|
const bitmap = await createImageBitmap(texBlob);
|
|
|
|
const width = bitmap.width;
|
|
const height = bitmap.height;
|
|
|
|
this.moonTextureObj = Texture.Builder()
|
|
.width(width)
|
|
.height(height)
|
|
.levels(0xff)
|
|
.format(TextureFormat.SRGB8_A8)
|
|
.usage(TextureUsage.SAMPLEABLE.value | TextureUsage.UPLOADABLE.value | TextureUsage.GEN_MIPMAPPABLE.value)
|
|
.build(this.engine);
|
|
|
|
// Extract Data using a Canvas
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.drawImage(bitmap, 0, 0);
|
|
const imageData = ctx.getImageData(0, 0, width, height);
|
|
|
|
// Use RGBA data directly (Uint8ClampedArray)
|
|
// Filament supports RGBA upload for RGB/SRGB internal formats (drops alpha).
|
|
// This matches Canvas layout (Top-Down, RGBA) and is 4-byte aligned.
|
|
const pbd = Filament.PixelBuffer(
|
|
new Uint8Array(imageData.data.buffer), // Wrap buffer to ensure Uint8Array
|
|
PixelDataFormat.RGBA,
|
|
PixelDataType.UBYTE
|
|
);
|
|
|
|
this.moonTextureObj.setImage(this.engine, 0, pbd);
|
|
this.moonTextureObj.generateMipmaps(this.engine);
|
|
|
|
const sampler = new TextureSampler(
|
|
MinFilter.LINEAR,
|
|
MagFilter.LINEAR,
|
|
WrapMode.CLAMP_TO_EDGE
|
|
);
|
|
|
|
this.materialInstance.setTextureParameter('moonTexture', this.moonTextureObj, sampler);
|
|
|
|
} catch (e) {
|
|
console.error("Failed to load moon texture:", e);
|
|
}
|
|
|
|
// Load Moon Normal
|
|
try {
|
|
const texUrl = 'assets/moon_normal.png';
|
|
const Texture = Filament.Texture;
|
|
const TextureSampler = Filament.TextureSampler;
|
|
const PixelDataFormat = Filament.PixelDataFormat;
|
|
const PixelDataType = Filament.PixelDataType;
|
|
const TextureUsage = Filament.Texture$Usage;
|
|
const TextureFormat = Filament.Texture$InternalFormat;
|
|
const MinFilter = Filament.MinFilter;
|
|
const MagFilter = Filament.MagFilter;
|
|
const WrapMode = Filament.WrapMode;
|
|
const texResponse = await fetch(texUrl);
|
|
const texBlob = await texResponse.blob();
|
|
const bitmap = await createImageBitmap(texBlob);
|
|
|
|
const width = bitmap.width;
|
|
const height = bitmap.height;
|
|
|
|
this.moonNormalObj = Texture.Builder()
|
|
.width(width)
|
|
.height(height)
|
|
.levels(0xff)
|
|
.format(TextureFormat.RGBA8)
|
|
.usage(TextureUsage.SAMPLEABLE.value | TextureUsage.UPLOADABLE.value | TextureUsage.GEN_MIPMAPPABLE.value)
|
|
.build(this.engine);
|
|
|
|
// Extract Data using a Canvas
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.drawImage(bitmap, 0, 0);
|
|
const imageData = ctx.getImageData(0, 0, width, height);
|
|
|
|
// Use RGBA data directly
|
|
const pbd = Filament.PixelBuffer(
|
|
new Uint8Array(imageData.data.buffer),
|
|
PixelDataFormat.RGBA,
|
|
PixelDataType.UBYTE
|
|
);
|
|
|
|
this.moonNormalObj.setImage(this.engine, 0, pbd);
|
|
this.moonNormalObj.generateMipmaps(this.engine);
|
|
|
|
const sampler = new TextureSampler(
|
|
MinFilter.LINEAR_MIPMAP_LINEAR,
|
|
MagFilter.LINEAR,
|
|
WrapMode.CLAMP_TO_EDGE
|
|
);
|
|
|
|
this.materialInstance.setTextureParameter('moonNormal', this.moonNormalObj, sampler);
|
|
|
|
} catch (e) {
|
|
console.error("Failed to load moon normal:", e);
|
|
}
|
|
|
|
// Load Milky Way Texture
|
|
try {
|
|
const texUrl = 'assets/milkyway.png';
|
|
const Texture = Filament.Texture;
|
|
const TextureSampler = Filament.TextureSampler;
|
|
const PixelDataFormat = Filament.PixelDataFormat;
|
|
const PixelDataType = Filament.PixelDataType;
|
|
const TextureUsage = Filament.Texture$Usage;
|
|
const TextureFormat = Filament.Texture$InternalFormat;
|
|
const MinFilter = Filament.MinFilter;
|
|
const MagFilter = Filament.MagFilter;
|
|
const WrapMode = Filament.WrapMode;
|
|
const texResponse = await fetch(texUrl);
|
|
const texBlob = await texResponse.blob();
|
|
const bitmap = await createImageBitmap(texBlob);
|
|
|
|
const width = bitmap.width;
|
|
const height = bitmap.height;
|
|
|
|
this.milkyWayTextureObj = Texture.Builder()
|
|
.width(width)
|
|
.height(height)
|
|
.levels(0xff)
|
|
.format(TextureFormat.SRGB8_A8)
|
|
.usage(TextureUsage.SAMPLEABLE.value | TextureUsage.UPLOADABLE.value | TextureUsage.GEN_MIPMAPPABLE.value)
|
|
.build(this.engine);
|
|
|
|
// Extract Data using a Canvas
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.drawImage(bitmap, 0, 0);
|
|
try {
|
|
const imageData = ctx.getImageData(0, 0, width, height);
|
|
|
|
// Use RGBA data directly
|
|
const pbd = Filament.PixelBuffer(
|
|
new Uint8Array(imageData.data.buffer),
|
|
PixelDataFormat.RGBA,
|
|
PixelDataType.UBYTE
|
|
);
|
|
|
|
this.milkyWayTextureObj.setImage(this.engine, 0, pbd);
|
|
this.milkyWayTextureObj.generateMipmaps(this.engine);
|
|
|
|
const sampler = new TextureSampler(
|
|
MinFilter.LINEAR_MIPMAP_LINEAR,
|
|
MagFilter.LINEAR,
|
|
WrapMode.REPEAT // Equirectangular wraps horizontally
|
|
);
|
|
|
|
this.materialInstance.setTextureParameter('milkyWayTexture', this.milkyWayTextureObj, sampler);
|
|
} catch (err) {
|
|
console.warn("Milky Way texture data access failed (CORS?):", err);
|
|
}
|
|
|
|
} catch (e) {
|
|
console.error("Failed to load milky way texture:", e);
|
|
}
|
|
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
initEntity() {
|
|
const EntityManager = Filament.EntityManager;
|
|
const RenderableManager = Filament.RenderableManager;
|
|
const VertexBuffer = Filament.VertexBuffer;
|
|
const IndexBuffer = Filament.IndexBuffer;
|
|
const AttributeType = Filament.VertexBuffer$AttributeType;
|
|
const VertexAttribute = Filament.VertexAttribute;
|
|
const PrimitiveType = Filament.RenderableManager$PrimitiveType;
|
|
const IndexType = Filament.IndexBuffer$IndexType;
|
|
|
|
this.entity = EntityManager.get().create();
|
|
|
|
// 3 vertices for full screen triangle
|
|
// coords: -1,-1 to 3,-1 to -1,3
|
|
const TRIANGLE_VERTICES = new Float32Array([
|
|
-1.0, -1.0,
|
|
3.0, -1.0,
|
|
-1.0, 3.0
|
|
]);
|
|
|
|
const TRIANGLE_INDICES = new Uint16Array([0, 1, 2]);
|
|
|
|
this.vb = VertexBuffer.Builder()
|
|
.vertexCount(3)
|
|
.bufferCount(1)
|
|
.attribute(VertexAttribute.POSITION, 0, AttributeType.FLOAT2, 0, 8)
|
|
.build(this.engine);
|
|
|
|
this.vb.setBufferAt(this.engine, 0, TRIANGLE_VERTICES);
|
|
|
|
this.ib = IndexBuffer.Builder()
|
|
.indexCount(3)
|
|
.bufferType(IndexType.USHORT)
|
|
.build(this.engine);
|
|
|
|
this.ib.setBuffer(this.engine, TRIANGLE_INDICES);
|
|
|
|
// Build the Renderable without material; it will be set later by loadMaterial.
|
|
|
|
|
|
RenderableManager.Builder(1)
|
|
.geometry(0, PrimitiveType.TRIANGLES, this.vb, this.ib)
|
|
.culling(false)
|
|
.castShadows(false)
|
|
.receiveShadows(false)
|
|
.priority(7) // Skybox priority
|
|
|
|
.build(this.engine, this.entity);
|
|
}
|
|
|
|
setSunPosition(direction) {
|
|
// normalize
|
|
const len = Math.hypot(direction[0], direction[1], direction[2]);
|
|
if (len > 0) {
|
|
this.sunDirection = [direction[0] / len, direction[1] / len, direction[2] / len];
|
|
} else {
|
|
this.sunDirection = [0, 1, 0];
|
|
}
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setSunIntensity(intensity) {
|
|
this.sunIntensity = Math.max(0.0, intensity);
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setTurbidity(turbidity) {
|
|
this.turbidity = Math.max(0.0, turbidity);
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setRayleigh(rayleigh) {
|
|
this.rayleigh = Math.max(0.0, rayleigh);
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setMieCoefficient(mie) {
|
|
this.mieCoefficient = Math.max(0.0, mie);
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setMieG(g) {
|
|
this.mieG = Math.max(0.0, g);
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setOzone(strength) {
|
|
this.ozone = Math.max(0.0, strength);
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setMultiScattering(r, m) {
|
|
this.msFactors[0] = Math.max(0.0, Math.min(2.0, r));
|
|
this.msFactors[1] = Math.max(0.0, Math.min(2.0, m));
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setHorizonGlow(strength) {
|
|
this.msFactors[2] = Math.max(0.0, Math.min(1.0, strength));
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setContrast(contrast) {
|
|
this.contrast = contrast;
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setNightColor(color) {
|
|
this.nightColor = color;
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setSunRadius(degrees) {
|
|
const rad = degrees * (Math.PI / 180.0);
|
|
this.sunHalo[0] = Math.cos(rad);
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setSunDiskIntensity(intensity) {
|
|
this.sunHalo[2] = Math.max(0.0, intensity);
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setSunLimbDarkening(strength) {
|
|
this.sunHalo[1] = Math.max(0.0, strength);
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setSunDiskEnabled(enabled) {
|
|
this.sunHalo[3] = enabled ? 1.0 : 0.0;
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setShimmerControl(strength, frequency, maskHeight) {
|
|
this.shimmerControl[0] = Math.max(0.0, strength);
|
|
this.shimmerControl[1] = Math.max(0.0, frequency);
|
|
this.shimmerControl[2] = Math.max(0.001, maskHeight);
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setCloudControl(coverage, density, height, speed) {
|
|
this.cloudControl[0] = Math.max(0.0, Math.min(1.0, coverage));
|
|
this.cloudControl[1] = Math.max(0.0, density);
|
|
this.cloudControl[2] = Math.max(1000.0, height);
|
|
// JS speed adjustment logic matches C++: speed * (0.05 / 72.0)
|
|
this.cloudControl[3] = speed * (0.05 / 72.0);
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setCloudShapeEvolution(speed) {
|
|
this.cloudControl2[0] = speed;
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setCloudVolumetricLighting(enabled) {
|
|
this.cloudControl2[1] = enabled ? 1.0 : 0.0;
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setWaterControl(strength, speed, derivativeTrick, octaves) {
|
|
this.waterControl[0] = Math.max(0.0, strength);
|
|
this.waterControl[1] = Math.max(0.0, speed);
|
|
this.waterControl[2] = derivativeTrick;
|
|
this.waterControl[3] = Math.max(1.0, Math.min(8.0, octaves));
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setStarControl(density, enabled) {
|
|
// Compensate for grid frequency reduction (350 -> 100)
|
|
// Fewer cells = fewer stars, so we increase density threshold.
|
|
// Factor ~ (350/100)^2 = 12.25
|
|
const compensatedDensity = density * 12.0;
|
|
this.starControl[0] = Math.max(0.0, Math.min(1.0, compensatedDensity));
|
|
this.starControl[1] = enabled ? 1.0 : 0.0;
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setFocalLength(mm) {
|
|
this.focalLength = Math.max(1.0, mm);
|
|
this.updateStarFrequency();
|
|
}
|
|
|
|
setResolution(height) {
|
|
this.height = Math.max(1.0, height);
|
|
this.updateStarFrequency();
|
|
}
|
|
|
|
updateStarFrequency() {
|
|
// World-Anchored Stars
|
|
// z = Fixed Frequency (World Space Grid)
|
|
// w = Pixel Scale (Screen Space Radius)
|
|
|
|
// Fixed Frequency: Defines the "Universe" coordinate system.
|
|
// Reduced to 100.0 to allow larger stars without clipping (square artifacts).
|
|
this.starControl[2] = 100.0;
|
|
|
|
// Pixel Scale in Radians
|
|
// We use linear scaling (24/f) instead of atan(fov) to ensure star size remains
|
|
// constant in pixels across all focal lengths (Perspective Projection).
|
|
const fovFactor = 24.0 / this.focalLength;
|
|
const pixelScale = (1.0 / this.height) * fovFactor;
|
|
|
|
// Pass to shader (w component)
|
|
// Target radius: 1.3 pixels (Diameter 2.6 pixels)
|
|
// Visible but sharp.
|
|
this.starControl[3] = pixelScale * 1.3;
|
|
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setStarIntensity(intensity) {
|
|
this.starIntensity = Math.max(0.0, intensity);
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setMoonPosition(direction) {
|
|
// normalize
|
|
const len = Math.hypot(direction[0], direction[1], direction[2]);
|
|
if (len > 0) {
|
|
this.moonDirection = [direction[0] / len, direction[1] / len, direction[2] / len];
|
|
}
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setMoonIntensity(intensity) {
|
|
this.moonIntensity = Math.max(0.0, intensity);
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setMoonRadius(degrees) {
|
|
const rad = degrees * (Math.PI / 180.0);
|
|
this.moonHalo[0] = Math.cos(rad);
|
|
this.moonHalo[1] = Math.sin(rad);
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setMoonEnabled(enabled) {
|
|
this.moonHalo[3] = enabled ? 1.0 : 0.0;
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setMilkyWayControl(intensity, saturation, blackPoint) {
|
|
this.milkyWayControl[0] = Math.max(0.0, intensity);
|
|
this.milkyWayControl[1] = Math.max(0.0, saturation);
|
|
if (blackPoint !== undefined) {
|
|
this.milkyWayControl[2] = Math.max(0.0, blackPoint);
|
|
}
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setMilkyWayEnabled(enabled) {
|
|
this.milkyWayEnabled = !!enabled;
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
setMilkyWayRotation(rotationMatrix) {
|
|
if (rotationMatrix && rotationMatrix.length === 9) {
|
|
this.milkyWayRotation = rotationMatrix;
|
|
this.updateCoefficients();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
setExposure(exposure) {
|
|
this.exposure = exposure;
|
|
this.updateCoefficients();
|
|
}
|
|
|
|
updateCoefficients() {
|
|
if (!this.materialInstance) {
|
|
console.warn("updateCoefficients called before material loaded");
|
|
return;
|
|
}
|
|
|
|
|
|
// 1. Rayleigh Coefficients
|
|
const F_PI = Math.PI;
|
|
const lambda = [680e-9, 550e-9, 440e-9];
|
|
const n = 1.0003;
|
|
const N = 2.545e25;
|
|
const term = (8.0 * Math.pow(F_PI, 3.0) * Math.pow(n * n - 1.0, 2.0)) / (3.0 * N);
|
|
|
|
const depthR = [
|
|
term / Math.pow(lambda[0], 4.0),
|
|
term / Math.pow(lambda[1], 4.0),
|
|
term / Math.pow(lambda[2], 4.0)
|
|
].map(v => v * 8000.0 * this.rayleigh);
|
|
|
|
// 2. Mie Coefficients
|
|
const mieAlpha = 1.3;
|
|
const mieBase = 2.0e-5 * this.turbidity;
|
|
const depthM = [
|
|
mieBase * Math.pow(550e-9 / lambda[0], mieAlpha),
|
|
mieBase * Math.pow(550e-9 / lambda[1], mieAlpha),
|
|
mieBase * Math.pow(550e-9 / lambda[2], mieAlpha)
|
|
].map(v => v * 1200.0 * this.mieCoefficient);
|
|
|
|
// Fake Ozone
|
|
const ozone = [0.0, this.ozone * 0.1, 0.0];
|
|
|
|
// Sun Fade (Horizon)
|
|
const cutoffAngle = 96.0 * (F_PI / 180.0);
|
|
const steepness = 1.5;
|
|
const zenithFade = 1.0 - Math.exp(-(cutoffAngle / steepness));
|
|
const zenithAngle = Math.acos(Math.max(-1.0, Math.min(1.0, this.sunDirection[1])));
|
|
const sunFade = Math.max(0.0, 1.0 - Math.exp(-((cutoffAngle - zenithAngle) / steepness))) / zenithFade;
|
|
|
|
const physicalSunIntensity = this.sunIntensity * sunFade;
|
|
|
|
// Radiance Conversion for Sun Halo
|
|
// Solid Angle = 2 * PI * (1 - cos(angularRadius))
|
|
const solidAngle = 2.0 * F_PI * (1.0 - this.sunHalo[0]);
|
|
const radianceConversion = 1.0 / Math.max(1e-9, solidAngle);
|
|
const sunHaloUpload = [...this.sunHalo];
|
|
sunHaloUpload[2] *= radianceConversion;
|
|
|
|
// Cloud Intersection
|
|
const r = this.planetRadius;
|
|
const h = this.cloudControl[2] * 0.001; // m -> km
|
|
const intersectC = r * r - (r + h) * (r + h);
|
|
const cloudUniform = [...this.cloudControl];
|
|
cloudUniform[2] = intersectC;
|
|
|
|
// Shimmer Uniform
|
|
const shimmerUniform = [...this.shimmerControl, r];
|
|
|
|
// Multi-Scattering Vector
|
|
const isotropicPhase = 0.25;
|
|
const msVector = depthR.map((v, i) => (v * this.msFactors[0] + depthM[i] * this.msFactors[1]) * isotropicPhase);
|
|
|
|
// Upload
|
|
this.materialInstance.setFloat3Parameter('sunDirection', new Float32Array(this.sunDirection));
|
|
this.materialInstance.setFloat3Parameter('depthR', new Float32Array(depthR));
|
|
this.materialInstance.setFloat3Parameter('depthM', new Float32Array(depthM));
|
|
this.materialInstance.setFloat3Parameter('ozone', new Float32Array(ozone));
|
|
this.materialInstance.setFloat4Parameter('sunHalo', new Float32Array(sunHaloUpload));
|
|
|
|
this.materialInstance.setFloat4Parameter('multiScatParams', new Float32Array([...msVector, this.msFactors[2]]));
|
|
|
|
// Mie Phase
|
|
const g2 = this.mieG * this.mieG;
|
|
this.materialInstance.setFloat2Parameter('miePhaseParams', new Float32Array([1.0 + g2, -2.0 * this.mieG]));
|
|
|
|
this.materialInstance.setFloatParameter('contrast', this.contrast);
|
|
|
|
const nightColorScaled = this.nightColor.map(v => v * this.sunIntensity); // Lux scaling
|
|
this.materialInstance.setFloat3Parameter('nightColor', new Float32Array(nightColorScaled));
|
|
|
|
this.materialInstance.setFloat4Parameter('shimmerControl', new Float32Array(shimmerUniform));
|
|
this.materialInstance.setFloat4Parameter('cloudControl', new Float32Array(cloudUniform));
|
|
this.materialInstance.setFloat4Parameter('cloudControl2', new Float32Array(this.cloudControl2));
|
|
this.materialInstance.setFloat4Parameter('waterControl', new Float32Array(this.waterControl));
|
|
this.materialInstance.setFloat4Parameter('starControl', new Float32Array(this.starControl));
|
|
this.materialInstance.setFloatParameter('starIntensity', this.starIntensity);
|
|
|
|
this.materialInstance.setFloatParameter('sunIntensity', physicalSunIntensity);
|
|
|
|
// Moon Upload (Secondary Sun)
|
|
this.materialInstance.setFloat3Parameter('sunDirection2', new Float32Array(this.moonDirection));
|
|
|
|
// Calculate Moon Phase Factor (Lambertian Sphere)
|
|
// We model the moon as a Lambertian sphere to calculate its integrated brightness (illuminance)
|
|
// based on the phase angle (angle between Sun-Moon and Observer-Moon vectors).
|
|
//
|
|
// Phase Angle (alpha):
|
|
// For a distant observer (Earth), the phase angle can be approximated as the angle between
|
|
// the vector to the Sun and the vector to the Earth (from the Moon).
|
|
// cos(alpha) = -dot(L_moon, L_sun)
|
|
//
|
|
// Lambertian Phase Law:
|
|
// The integrated flux of a lit sphere varies as:
|
|
// Phi(alpha) = (1/PI) * (sin(alpha) + (PI - alpha) * cos(alpha))
|
|
// This gives 1.0 at Full Moon (alpha=0) and 0.0 at New Moon (alpha=PI).
|
|
|
|
const dotSM = this.sunDirection[0] * this.moonDirection[0] +
|
|
this.sunDirection[1] * this.moonDirection[1] +
|
|
this.sunDirection[2] * this.moonDirection[2];
|
|
|
|
// Final Intensity = Peak * Scale (No Phase Factor - Phase is handled in Shader via N.L)
|
|
const MOON_PEAK_LUX = 1.0;
|
|
const finalMoonIntensity = MOON_PEAK_LUX * this.moonIntensity;
|
|
|
|
this.materialInstance.setFloatParameter('sunIntensity2', finalMoonIntensity);
|
|
|
|
// Scale Milky Way by Sun Intensity
|
|
// Calibration: 1.0 User Intensity = 1.5e-3 cd/m^2 (Nits) approx.
|
|
// Target: 1.5e-3 Nits. SunIntensity = 100,000.
|
|
// Scale = 1.5e-3 / 1.0e5 = 1.5e-8.
|
|
const mwIntensity = this.milkyWayEnabled ? this.milkyWayControl[0] : 0.0;
|
|
const mwUniform = [
|
|
mwIntensity * this.sunIntensity * 1.5e-8,
|
|
this.milkyWayControl[1],
|
|
this.milkyWayControl[2]
|
|
];
|
|
this.materialInstance.setFloat3Parameter('milkyWayControl', new Float32Array(mwUniform));
|
|
this.materialInstance.setMat3Parameter('milkyWayRotation', new Float32Array(this.milkyWayRotation));
|
|
|
|
// Moon Halo Upload (Disk Visualization)
|
|
// Multiplier = 1.0 / SolidAngle
|
|
const moonSolidAngle = 2.0 * F_PI * (1.0 - this.moonHalo[0]);
|
|
const moonRadConv = 1.0 / Math.max(1e-9, moonSolidAngle);
|
|
const moonHaloUpload = [...this.moonHalo];
|
|
moonHaloUpload[2] *= moonRadConv;
|
|
this.materialInstance.setFloat4Parameter('sunHalo2', new Float32Array(moonHaloUpload));
|
|
this.materialInstance.setFloatParameter('exposure', this.exposure !== undefined ? this.exposure : 1.0);
|
|
|
|
// Solar Eclipse (CPU Calculation)
|
|
const sunRadius = Math.acos(this.sunHalo[0]);
|
|
const moonRadius = Math.acos(this.moonHalo[0]);
|
|
// Dot product of Sun and Moon directions
|
|
const dot = this.sunDirection[0] * this.moonDirection[0] +
|
|
this.sunDirection[1] * this.moonDirection[1] +
|
|
this.sunDirection[2] * this.moonDirection[2];
|
|
const separation = Math.acos(Math.max(-1.0, Math.min(1.0, dot)));
|
|
|
|
let eclipseFactor = 1.0;
|
|
// Only calculate if moon is enabled
|
|
if (this.moonHalo[3] > 0.5) {
|
|
const overlap = this.areaIntersection(sunRadius, moonRadius, separation);
|
|
const sunArea = Math.PI * sunRadius * sunRadius;
|
|
// Ensure we don't divide by zero and clamp result
|
|
const ratio = overlap / Math.max(1e-9, sunArea);
|
|
eclipseFactor = 1.0 - Math.max(0.0, Math.min(1.0, ratio));
|
|
|
|
}
|
|
|
|
// Safety check for NaN
|
|
if (isNaN(eclipseFactor)) {
|
|
console.warn("SimulatedSkybox: eclipseFactor is NaN, resetting to 1.0");
|
|
eclipseFactor = 1.0;
|
|
}
|
|
|
|
|
|
|
|
this.materialInstance.setFloatParameter('eclipseFactor', eclipseFactor);
|
|
}
|
|
|
|
areaIntersection(r1, r2, d) {
|
|
// Circle intersection area
|
|
// r1, r2: radii
|
|
// d: distance between centers
|
|
|
|
// Case 1: Too far apart
|
|
if (d >= r1 + r2) {
|
|
return 0.0;
|
|
}
|
|
// Case 2: One inside another
|
|
if (d <= Math.abs(r1 - r2)) {
|
|
return Math.PI * Math.min(r1, r2) * Math.min(r1, r2);
|
|
}
|
|
|
|
const r1sq = r1 * r1;
|
|
const r2sq = r2 * r2;
|
|
|
|
// Law of Cosines for sector angles
|
|
// c1 = (d^2 + r1^2 - r2^2) / (2 * d * r1)
|
|
// c2 = (d^2 + r2^2 - r1^2) / (2 * d * r2)
|
|
// We clamp to [-1, 1] to avoid NaN from floating point errors
|
|
const c1 = Math.max(-1.0, Math.min(1.0, (d * d + r1sq - r2sq) / (2.0 * d * r1)));
|
|
const c2 = Math.max(-1.0, Math.min(1.0, (d * d + r2sq - r1sq) / (2.0 * d * r2)));
|
|
|
|
const part1 = r1sq * Math.acos(c1);
|
|
const part2 = r2sq * Math.acos(c2);
|
|
|
|
// Heron's formula for the triangle area * 2
|
|
// The sqrt term represents the area of the two triangles formed by the chord and centers.
|
|
|
|
|
|
// Robust sqrt
|
|
const val = (-d + r1 + r2) * (d + r1 - r2) * (d - r1 + r2) * (d + r1 + r2);
|
|
const part3 = 0.5 * Math.sqrt(Math.max(0.0, val));
|
|
|
|
return part1 + part2 - part3;
|
|
}
|
|
}
|