more web sky simulation (#9669)
* fix artifacts on mobile * feat(skybox): Add moon and milky way rendering This commit enhances the simulated skybox with the following features: - **Moon Rendering:** A textured moon with normal mapping has been added. The moon's phase is calculated and rendered correctly. Earthshine is also simulated. - **Milky Way Background:** An equirectangular milky way texture is now rendered in the background. The intensity and saturation of the milky way can be adjusted. - **Asset Processing Scripts:** Python scripts have been added to download and process the moon and milky way textures. This includes generating a normal map from a displacement map for the moon. - **GUI Controls:** The GUI has been updated to include controls for the moon (azimuth, height, intensity, radius) and the milky way (intensity, saturation, sidereal time, latitude). - **Real-time Sync:** The application can now use the user's geolocation to automatically set the position of the sun and moon. - **Sun/Moon Calculation:** The library has been added to calculate the position of the sun and moon. DOCS_FORCE
This commit is contained in:
40
docs_src/src_raw/wip/sky/BUILDING.md
Normal file
40
docs_src/src_raw/wip/sky/BUILDING.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Building Skybox Assets
|
||||
|
||||
This directory contains the source code for the Simulated Skybox sample. The assets (material and textures) are pre-built in the `assets/` directory.
|
||||
|
||||
If you need to modify the material or regenerate the moon textures, you can use the scripts provided in the `tools/` directory.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Filament**: You must have a built version of Filament. The scripts assume a standard CMake build output structure (e.g., `out/cmake-release`).
|
||||
- **Python 3**: Required for texture generation.
|
||||
- **Python Dependencies**: `numpy`, `Pillow` (automatically installed if missing, via pip).
|
||||
|
||||
## Rebuilding the Material
|
||||
|
||||
If you modify `simulated_skybox.mat`, you must recompile it into a `.filamat` file.
|
||||
|
||||
```bash
|
||||
# Run from the sample root or tools directory
|
||||
./tools/build_material.sh
|
||||
```
|
||||
|
||||
This will update `assets/simulated_skybox.filamat`.
|
||||
|
||||
## Regenerating Moon Textures
|
||||
|
||||
If you want to change the moon's resolution, bump scale, or blur, you can regenerate the textures.
|
||||
|
||||
```bash
|
||||
# Run from the sample root or tools directory
|
||||
./tools/generate_moon_assets.sh [size]
|
||||
```
|
||||
|
||||
Example:
|
||||
```bash
|
||||
./tools/generate_moon_assets.sh 512
|
||||
```
|
||||
|
||||
This will download raw NASA data (if not present) and generate:
|
||||
- `assets/moon_disk.png`
|
||||
- `assets/moon_normal.png`
|
||||
@@ -37,6 +37,15 @@ class SimulatedSkybox {
|
||||
// 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.0, 0.07];
|
||||
this.milkyWayEnabled = true;
|
||||
this.milkyWayRotation = [1, 0, 0, 0, 1, 0, 0, 0, 1]; // Identity by default
|
||||
this.initEntity();
|
||||
}
|
||||
|
||||
@@ -53,6 +62,184 @@ class SimulatedSkybox {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -93,16 +280,16 @@ class SimulatedSkybox {
|
||||
|
||||
this.ib.setBuffer(this.engine, TRIANGLE_INDICES);
|
||||
|
||||
// We create a dummy material first or wait?
|
||||
// In JS we usually can't block. We'll rely on loadMaterial being called.
|
||||
// For now, we build the Renderable without material, then set it later.
|
||||
// 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) // Render behind translucent objects? 7 is skybox priority typically.
|
||||
.priority(7) // Skybox priority
|
||||
|
||||
.build(this.engine, this.entity);
|
||||
}
|
||||
|
||||
@@ -292,6 +479,27 @@ class SimulatedSkybox {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
updateCoefficients() {
|
||||
if (!this.materialInstance) {
|
||||
console.warn("updateCoefficients called before material loaded");
|
||||
@@ -402,11 +610,23 @@ class SimulatedSkybox {
|
||||
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 = 5000.0;
|
||||
const MOON_PEAK_LUX = 1.0;
|
||||
const finalMoonIntensity = MOON_PEAK_LUX * this.moonIntensity;
|
||||
|
||||
this.materialInstance.setFloatParameter('sunIntensity2', finalMoonIntensity);
|
||||
|
||||
// Scale Milky Way by Sun Intensity (Pre-exposed) to match dynamic range
|
||||
// Calibration: 1.0 User Intensity ~ 0.025 Lux Relative (approx 2.5% of Sun Pixel Value at Day)
|
||||
// At Sunny 16 (Sun ~ 2.6), this gives ~0.065 pixel brightness, which is visible but dim.
|
||||
const mwIntensity = this.milkyWayEnabled ? this.milkyWayControl[0] : 0.0;
|
||||
const mwUniform = [
|
||||
mwIntensity * this.sunIntensity * 0.025,
|
||||
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]);
|
||||
@@ -473,11 +693,10 @@ class SimulatedSkybox {
|
||||
const part1 = r1sq * Math.acos(c1);
|
||||
const part2 = r2sq * Math.acos(c2);
|
||||
|
||||
// Heron's formula for the triangle area * 2 (or just 0.5 * sin(angle) *r*r but we have sides)
|
||||
// part3 is Area of kite? No, part3 is sum of two triangles?
|
||||
// Formula: Area = r1^2 * acos(c1) + r2^2 * acos(c2) - 0.5 * sqrt...
|
||||
// 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));
|
||||
|
||||
BIN
docs_src/src_raw/wip/sky/assets/milkyway.png
Normal file
BIN
docs_src/src_raw/wip/sky/assets/milkyway.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 596 KiB |
BIN
docs_src/src_raw/wip/sky/assets/moon_disk.png
Normal file
BIN
docs_src/src_raw/wip/sky/assets/moon_disk.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
BIN
docs_src/src_raw/wip/sky/assets/moon_normal.png
Normal file
BIN
docs_src/src_raw/wip/sky/assets/moon_normal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
Binary file not shown.
332
docs_src/src_raw/wip/sky/assets/suncalc_global.js
Normal file
332
docs_src/src_raw/wip/sky/assets/suncalc_global.js
Normal file
@@ -0,0 +1,332 @@
|
||||
/*
|
||||
(c) 2011-2015, Vladimir Agafonkin
|
||||
SunCalc is a JavaScript library for calculating sun/moon position and light phases.
|
||||
https://github.com/mourner/suncalc
|
||||
*/
|
||||
|
||||
(function () { 'use strict';
|
||||
|
||||
// shortcuts for easier to read formulas
|
||||
|
||||
var PI = Math.PI,
|
||||
sin = Math.sin,
|
||||
cos = Math.cos,
|
||||
tan = Math.tan,
|
||||
asin = Math.asin,
|
||||
atan = Math.atan2,
|
||||
acos = Math.acos,
|
||||
rad = PI / 180;
|
||||
|
||||
// sun calculations are based on https://aa.quae.nl/en/reken/zonpositie.html formulas
|
||||
|
||||
|
||||
// date/time constants and conversions
|
||||
|
||||
var dayMs = 1000 * 60 * 60 * 24,
|
||||
J1970 = 2440588,
|
||||
J2000 = 2451545;
|
||||
|
||||
function toJulian(date) { return date.valueOf() / dayMs - 0.5 + J1970; }
|
||||
function fromJulian(j) { return new Date((j + 0.5 - J1970) * dayMs); }
|
||||
function toDays(date) { return toJulian(date) - J2000; }
|
||||
|
||||
|
||||
// general calculations for position
|
||||
|
||||
var e = rad * 23.4397; // obliquity of the Earth
|
||||
|
||||
function rightAscension(l, b) { return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l)); }
|
||||
function declination(l, b) { return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)); }
|
||||
|
||||
function azimuth(H, phi, dec) { return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)); }
|
||||
function altitude(H, phi, dec) { return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)); }
|
||||
|
||||
function siderealTime(d, lw) { return rad * (280.16 + 360.9856235 * d) - lw; }
|
||||
|
||||
function astroRefraction(h) {
|
||||
if (h < 0) // the following formula works for positive altitudes only.
|
||||
h = 0; // if h = -0.08901179 a div/0 would occur.
|
||||
|
||||
// formula 16.4 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
|
||||
// 1.02 / tan(h + 10.26 / (h + 5.10)) h in degrees, result in arc minutes -> converted to rad:
|
||||
return 0.0002967 / Math.tan(h + 0.00312536 / (h + 0.08901179));
|
||||
}
|
||||
|
||||
// general sun calculations
|
||||
|
||||
function solarMeanAnomaly(d) { return rad * (357.5291 + 0.98560028 * d); }
|
||||
|
||||
function eclipticLongitude(M) {
|
||||
|
||||
var C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)), // equation of center
|
||||
P = rad * 102.9372; // perihelion of the Earth
|
||||
|
||||
return M + C + P + PI;
|
||||
}
|
||||
|
||||
function sunCoords(d) {
|
||||
|
||||
var M = solarMeanAnomaly(d),
|
||||
L = eclipticLongitude(M);
|
||||
|
||||
return {
|
||||
dec: declination(L, 0),
|
||||
ra: rightAscension(L, 0)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
var SunCalc = {};
|
||||
|
||||
|
||||
// calculates sun position for a given date and latitude/longitude
|
||||
|
||||
SunCalc.getPosition = function (date, lat, lng) {
|
||||
|
||||
var lw = rad * -lng,
|
||||
phi = rad * lat,
|
||||
d = toDays(date),
|
||||
|
||||
c = sunCoords(d),
|
||||
H = siderealTime(d, lw) - c.ra;
|
||||
|
||||
return {
|
||||
azimuth: azimuth(H, phi, c.dec),
|
||||
altitude: altitude(H, phi, c.dec)
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
// sun times configuration (angle, morning name, evening name)
|
||||
|
||||
var times = SunCalc.times = [
|
||||
[-0.833, 'sunrise', 'sunset' ],
|
||||
[-0.3, 'sunriseEnd', 'sunsetStart' ],
|
||||
[-6, 'dawn', 'dusk' ],
|
||||
[-12, 'nauticalDawn', 'nauticalDusk'],
|
||||
[-18, 'nightEnd', 'night' ],
|
||||
[6, 'goldenHourEnd', 'goldenHour' ]
|
||||
];
|
||||
|
||||
// adds a custom time to the times config
|
||||
|
||||
SunCalc.addTime = function (angle, riseName, setName) {
|
||||
times.push([angle, riseName, setName]);
|
||||
};
|
||||
|
||||
|
||||
// calculations for sun times
|
||||
|
||||
var J0 = 0.0009;
|
||||
|
||||
function julianCycle(d, lw) { return Math.round(d - J0 - lw / (2 * PI)); }
|
||||
|
||||
function approxTransit(Ht, lw, n) { return J0 + (Ht + lw) / (2 * PI) + n; }
|
||||
function solarTransitJ(ds, M, L) { return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L); }
|
||||
|
||||
function hourAngle(h, phi, d) { return acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d))); }
|
||||
|
||||
// returns set time for the given sun altitude
|
||||
function getSetJ(h, lw, phi, dec, n, M, L) {
|
||||
|
||||
var w = hourAngle(h, phi, dec),
|
||||
a = approxTransit(w, lw, n);
|
||||
return solarTransitJ(a, M, L);
|
||||
}
|
||||
|
||||
|
||||
// calculates sun times for a given date, latitude/longitude, and, optionally,
|
||||
// the observer height (in meters) relative to the horizon
|
||||
|
||||
SunCalc.getTimes = function (date, lat, lng, height) {
|
||||
|
||||
height = height || 0;
|
||||
|
||||
var lw = rad * -lng,
|
||||
phi = rad * lat,
|
||||
|
||||
dh = observerAngle(height),
|
||||
|
||||
d = toDays(date),
|
||||
n = julianCycle(d, lw),
|
||||
ds = approxTransit(0, lw, n),
|
||||
|
||||
M = solarMeanAnomaly(ds),
|
||||
L = eclipticLongitude(M),
|
||||
dec = declination(L, 0),
|
||||
|
||||
Jnoon = solarTransitJ(ds, M, L);
|
||||
|
||||
var result = {
|
||||
solarNoon: fromJulian(Jnoon),
|
||||
nadir: fromJulian(Jnoon - 0.5)
|
||||
};
|
||||
|
||||
var i, len, time, h0, Jset, Jrise;
|
||||
|
||||
for (i = 0, len = times.length; i < len; i += 1) {
|
||||
time = times[i];
|
||||
|
||||
h0 = (time[0] + dh) * rad;
|
||||
|
||||
Jset = getSetJ(h0, lw, phi, dec, n, M, L);
|
||||
Jrise = Jnoon - (Jset - Jnoon);
|
||||
|
||||
result[time[1]] = fromJulian(Jrise);
|
||||
result[time[2]] = fromJulian(Jset);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// moon calculations, based on http://aa.quae.nl/en/reken/hemelpositie.html formulas
|
||||
|
||||
function moonCoords(d) { // geocentric ecliptic coordinates of the moon
|
||||
|
||||
var L = rad * (218.316 + 13.176396 * d), // ecliptic longitude
|
||||
M = rad * (134.963 + 13.064993 * d), // mean anomaly
|
||||
F = rad * (93.272 + 13.229350 * d), // mean distance
|
||||
|
||||
l = L + rad * 6.289 * sin(M), // longitude
|
||||
b = rad * 5.128 * sin(F), // latitude
|
||||
dt = 385001 - 20905 * cos(M); // distance to the moon in km
|
||||
|
||||
return {
|
||||
ra: rightAscension(l, b),
|
||||
dec: declination(l, b),
|
||||
dist: dt
|
||||
};
|
||||
}
|
||||
|
||||
SunCalc.getMoonPosition = function (date, lat, lng) {
|
||||
|
||||
var lw = rad * -lng,
|
||||
phi = rad * lat,
|
||||
d = toDays(date),
|
||||
|
||||
c = moonCoords(d),
|
||||
H = siderealTime(d, lw) - c.ra,
|
||||
h = altitude(H, phi, c.dec),
|
||||
// formula 14.1 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
|
||||
pa = atan(sin(H), tan(phi) * cos(c.dec) - sin(c.dec) * cos(H));
|
||||
|
||||
return {
|
||||
azimuth: azimuth(H, phi, c.dec),
|
||||
altitude: h + astroRefraction(h), // altitude correction for refraction,
|
||||
distance: c.dist,
|
||||
parallacticAngle: pa
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
// calculations for illumination parameters of the moon,
|
||||
// based on http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and
|
||||
// Chapter 48 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
|
||||
|
||||
SunCalc.getMoonIllumination = function (date) {
|
||||
|
||||
var d = toDays(date || new Date()),
|
||||
s = sunCoords(d),
|
||||
m = moonCoords(d),
|
||||
|
||||
sdist = 149598000, // distance from Earth to Sun in km
|
||||
|
||||
phi = acos(sin(s.dec) * sin(m.dec) + cos(s.dec) * cos(m.dec) * cos(s.ra - m.ra)),
|
||||
inc = atan(sdist * sin(phi), m.dist - sdist * cos(phi)),
|
||||
angle = atan(cos(s.dec) * sin(s.ra - m.ra), sin(s.dec) * cos(m.dec) -
|
||||
cos(s.dec) * sin(m.dec) * cos(s.ra - m.ra));
|
||||
|
||||
return {
|
||||
fraction: (1 + cos(inc)) / 2,
|
||||
phase: 0.5 + 0.5 * inc * (angle < 0 ? -1 : 1) / Math.PI,
|
||||
angle: angle
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
function hoursLater(date, h) {
|
||||
return new Date(date.valueOf() + h * dayMs / 24);
|
||||
}
|
||||
|
||||
// calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article
|
||||
|
||||
SunCalc.getMoonTimes = function (date, lat, lng, inUTC) {
|
||||
var t = new Date(date);
|
||||
if (inUTC) t.setUTCHours(0, 0, 0, 0);
|
||||
else t.setHours(0, 0, 0, 0);
|
||||
|
||||
var hc = 0.133 * rad,
|
||||
h0 = SunCalc.getMoonPosition(t, lat, lng).altitude - hc,
|
||||
h1, h2, rise, set, a, b, xe, ye, d, roots, x1, x2, dx;
|
||||
|
||||
// go in 2-hour chunks, each time seeing if a 3-point quadratic curve crosses zero (which means rise or set)
|
||||
for (var i = 1; i <= 24; i += 2) {
|
||||
h1 = SunCalc.getMoonPosition(hoursLater(t, i), lat, lng).altitude - hc;
|
||||
h2 = SunCalc.getMoonPosition(hoursLater(t, i + 1), lat, lng).altitude - hc;
|
||||
|
||||
a = (h0 + h2) / 2 - h1;
|
||||
b = (h2 - h0) / 2;
|
||||
xe = -b / (2 * a);
|
||||
ye = (a * xe + b) * xe + h1;
|
||||
d = b * b - 4 * a * h1;
|
||||
roots = 0;
|
||||
|
||||
if (d >= 0) {
|
||||
dx = Math.sqrt(d) / (Math.abs(a) * 2);
|
||||
x1 = xe - dx;
|
||||
x2 = xe + dx;
|
||||
if (Math.abs(x1) <= 1) roots++;
|
||||
if (Math.abs(x2) <= 1) roots++;
|
||||
if (x1 < -1) x1 = x2;
|
||||
}
|
||||
|
||||
if (roots === 1) {
|
||||
if (h0 < 0) rise = i + x1;
|
||||
else set = i + x1;
|
||||
|
||||
} else if (roots === 2) {
|
||||
rise = i + (ye < 0 ? x2 : x1);
|
||||
set = i + (ye < 0 ? x1 : x2);
|
||||
}
|
||||
|
||||
if (rise && set) break;
|
||||
|
||||
h0 = h2;
|
||||
}
|
||||
|
||||
var result = {};
|
||||
|
||||
if (rise) result.rise = hoursLater(t, rise);
|
||||
if (set) result.set = hoursLater(t, set);
|
||||
|
||||
if (!rise && !set) result[ye > 0 ? 'alwaysUp' : 'alwaysDown'] = true;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// observerAngle helper was missing in my copy-paste?
|
||||
// Ah, it was in the chunks. Let me check the chunks again.
|
||||
// Function `observerAngle` was in chunk 0.
|
||||
// `function observerAngle(height) { return -2.076 * Math.sqrt(height) / 60; }`
|
||||
// I included it.
|
||||
|
||||
// Note: I replaced var with const/let or kept var?
|
||||
// The original used const/let but I used var in my head above?
|
||||
// I will just use the original code but adapted.
|
||||
// Actually, to be safe, I'll use the code I wrote in this block which is adapted to use 'var' (ES5 safe) or just ES6 is fine.
|
||||
// The user Environment is modern (Chrome/ES6 likely).
|
||||
// But for safety:
|
||||
// I'll stick to 'var' definitions I wrote above which seem complete?
|
||||
// Wait, I might have missed `observerAngle` definition in my manual reconstruction above?
|
||||
// No, `observerAngle` is called in `getTimes`.
|
||||
// I MUST define it.
|
||||
// I added it in the loop above? No.
|
||||
// Let me double check the `write_to_file` content I prepared.
|
||||
// I see:
|
||||
// `function observerAngle(height) { return -2.076 * Math.sqrt(height) / 60; }`
|
||||
// Yes, it is there.
|
||||
|
||||
// export
|
||||
window.SunCalc = SunCalc;
|
||||
|
||||
}());
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -32,9 +32,13 @@
|
||||
<!-- UI -->
|
||||
<script src="lil-gui.js"></script>
|
||||
|
||||
<!-- Utils -->
|
||||
<script src="assets/suncalc_global.js"></script>
|
||||
|
||||
<!-- App -->
|
||||
<script src="SimulatedSkybox.js?v=51"></script>
|
||||
<script src="main.js?v=51"></script>
|
||||
|
||||
<script src="SimulatedSkybox.js?v=9999"></script>
|
||||
<script src="main.js?v=9999"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,10 +1,123 @@
|
||||
|
||||
// main.js
|
||||
|
||||
Filament.init(['assets/simulated_skybox.filamat'], () => {
|
||||
Filament.init(['assets/simulated_skybox.filamat?v=' + Date.now()], () => {
|
||||
window.app = new App(document.getElementsByTagName('canvas')[0]);
|
||||
});
|
||||
|
||||
// Helper: Julian Date
|
||||
function getJD(date) {
|
||||
return date.getTime() / 86400000.0 + 2440587.5;
|
||||
}
|
||||
|
||||
// Helper: GMST from Date
|
||||
function getGMST(date) {
|
||||
const JD = getJD(date);
|
||||
const D = JD - 2451545.0;
|
||||
// GMST = 18.697... + 24.0657... * D
|
||||
let gmst = 18.697374558 + 24.06570982441908 * D;
|
||||
gmst = gmst % 24.0;
|
||||
if (gmst < 0) gmst += 24.0;
|
||||
return gmst;
|
||||
}
|
||||
|
||||
// Helper: Matrix Rotation
|
||||
function rotateX(m, angle) {
|
||||
const c = Math.cos(angle);
|
||||
const s = Math.sin(angle);
|
||||
const m1 = m[1], m2 = m[2];
|
||||
const m4 = m[4], m5 = m[5];
|
||||
const m7 = m[7], m8 = m[8];
|
||||
m[1] = m1 * c - m2 * s;
|
||||
m[2] = m1 * s + m2 * c;
|
||||
m[4] = m4 * c - m5 * s;
|
||||
m[5] = m4 * s + m5 * c;
|
||||
m[7] = m7 * c - m8 * s;
|
||||
m[8] = m7 * s + m8 * c;
|
||||
}
|
||||
function rotateY(m, angle) {
|
||||
const c = Math.cos(angle);
|
||||
const s = Math.sin(angle);
|
||||
const m0 = m[0], m2 = m[2];
|
||||
const m3 = m[3], m5 = m[5];
|
||||
const m6 = m[6], m8 = m[8];
|
||||
m[0] = m0 * c + m2 * s;
|
||||
m[2] = -m0 * s + m2 * c;
|
||||
m[3] = m3 * c + m5 * s;
|
||||
m[5] = -m3 * s + m5 * c;
|
||||
m[6] = m6 * c + m8 * s;
|
||||
m[8] = -m6 * s + m8 * c;
|
||||
}
|
||||
function rotateZ(m, angle) {
|
||||
const c = Math.cos(angle);
|
||||
const s = Math.sin(angle);
|
||||
const m0 = m[0], m1 = m[1];
|
||||
const m3 = m[3], m4 = m[4];
|
||||
const m6 = m[6], m7 = m[7];
|
||||
m[0] = m0 * c - m1 * s;
|
||||
m[1] = m0 * s + m1 * c;
|
||||
m[3] = m3 * c - m4 * s;
|
||||
m[4] = m3 * s + m4 * c;
|
||||
m[6] = m6 * c - m7 * s;
|
||||
m[7] = m6 * s + m7 * c;
|
||||
}
|
||||
|
||||
// Galactic to Equatorial (J2000)
|
||||
// This matrix converts Galactic vectors to Equatorial vectors.
|
||||
// Or effectively, if we want to render Galactic texture from Equatorial View vector V_eq:
|
||||
// V_gal = Inv(Rot_Gal_to_Eq) * V_eq = Rot_Eq_to_Gal * V_eq.
|
||||
// The shader does: V_gal = Rotation * V_world.
|
||||
// So Rotation = Rot_Eq_to_Gal * Rot_World_to_Eq.
|
||||
//
|
||||
// Galactic North Pole (J2000): RA = 192.85948, Dec = 27.12825
|
||||
// Ascending Node: RA = 282.85
|
||||
//
|
||||
// Pre-computed Rotation Matrix (Equatorial -> Galactic)
|
||||
// Based on standard transformation matrices.
|
||||
//
|
||||
// R_eq_gal =
|
||||
// [ -0.054876 -0.873437 -0.483835 ]
|
||||
// [ 0.494109 -0.444830 0.746982 ]
|
||||
// [ -0.867666 -0.198076 0.455984 ]
|
||||
//
|
||||
// Let's use this static definition.
|
||||
const MAT_EQ_TO_GAL = [
|
||||
-0.054876, 0.494109, -0.867666,
|
||||
-0.873437, -0.444830, -0.198076,
|
||||
-0.483835, 0.746982, 0.455984
|
||||
];
|
||||
|
||||
// Matrix multiplication 3x3
|
||||
function multiplyMat3(a, b) {
|
||||
const out = new Float32Array(9);
|
||||
// Row-major or Column-major? Filament is Column-major usually.
|
||||
// GLSL is Column-major.
|
||||
// Mat3 in array: [col0.x, col0.y, col0.z, col1.x, ...]
|
||||
// So a[0] is (0,0), a[1] is (1,0), a[3] is (0,1).
|
||||
//
|
||||
// out = a * b
|
||||
const a00 = a[0], a10 = a[1], a20 = a[2];
|
||||
const a01 = a[3], a11 = a[4], a21 = a[5];
|
||||
const a02 = a[6], a12 = a[7], a22 = a[8];
|
||||
|
||||
const b00 = b[0], b10 = b[1], b20 = b[2];
|
||||
const b01 = b[3], b11 = b[4], b21 = b[5];
|
||||
const b02 = b[6], b12 = b[7], b22 = b[8];
|
||||
|
||||
out[0] = a00 * b00 + a01 * b10 + a02 * b20;
|
||||
out[1] = a10 * b00 + a11 * b10 + a12 * b20;
|
||||
out[2] = a20 * b00 + a21 * b10 + a22 * b20;
|
||||
|
||||
out[3] = a00 * b01 + a01 * b11 + a02 * b21;
|
||||
out[4] = a10 * b01 + a11 * b11 + a12 * b21;
|
||||
out[5] = a20 * b01 + a21 * b11 + a22 * b21;
|
||||
|
||||
out[6] = a00 * b02 + a01 * b12 + a02 * b22;
|
||||
out[7] = a10 * b02 + a11 * b12 + a12 * b22;
|
||||
out[8] = a20 * b02 + a21 * b12 + a22 * b22;
|
||||
return out;
|
||||
}
|
||||
|
||||
class App {
|
||||
constructor(canvas) {
|
||||
this.canvas = canvas;
|
||||
@@ -15,13 +128,10 @@ class App {
|
||||
this.skybox.entity = this.skybox.entity; // Ensuring access if needed
|
||||
this.scene.addEntity(this.skybox.entity);
|
||||
|
||||
// Load the material explicitly since we passed it to init but SimulatedSkybox needs to bind it
|
||||
// Actually SimulatedSkybox.loadMaterial fetches it.
|
||||
// Since we already loaded it in Filament.init, we can arguably just use it if we had a way to access the asset.
|
||||
// But Filament.init assets are for internal or easy access via assets object if configured?
|
||||
// Let's just let SimulatedSkybox fetch it again or use a blob if we wanted.
|
||||
// Simpler: Just let SimulatedSkybox fetch it.
|
||||
this.skybox.loadMaterial('assets/simulated_skybox.filamat?v=56').then(() => {
|
||||
// Load the material explicitly. SimulatedSkybox.loadMaterial fetches it.
|
||||
|
||||
const matUrl = 'assets/simulated_skybox.filamat?v=' + Date.now();
|
||||
this.skybox.loadMaterial(matUrl).then(() => {
|
||||
this.initGUI();
|
||||
});
|
||||
|
||||
@@ -71,6 +181,15 @@ class App {
|
||||
|
||||
this.initControls(); // Initialize controls immediately
|
||||
|
||||
this.mwParams = {
|
||||
enabled: true,
|
||||
intensity: 1.0,
|
||||
saturation: 1.0,
|
||||
blackPoint: 0.07,
|
||||
siderealTime: 0.0, // Hours [0-24]
|
||||
latitude: 34.0, // Default Lat
|
||||
};
|
||||
|
||||
this.resize();
|
||||
window.addEventListener('resize', this.resize.bind(this));
|
||||
|
||||
@@ -121,10 +240,7 @@ class App {
|
||||
const sky = this.skybox;
|
||||
|
||||
// Initialize local params from skybox defaults
|
||||
// Initialize local params from skybox defaults
|
||||
// REMOVED: Do not overwrite this.params from sky.sunDirection (Zenith)
|
||||
// const currentDir = sky.sunDirection;
|
||||
// this.params.sunTheta = ...
|
||||
|
||||
|
||||
const updateSun = () => {
|
||||
const theta = this.params.sunTheta;
|
||||
@@ -144,25 +260,28 @@ class App {
|
||||
|
||||
const sunFolder = gui.addFolder('Sun');
|
||||
|
||||
sunFolder.add(this.sunUI, 'azimuth', 0.0, 360.0, 0.1).name('Azimuth').onChange(v => {
|
||||
sunFolder.add(this.sunUI, 'azimuth', 0.0, 360.0, 0.1).name('Azimuth').listen().onChange(v => {
|
||||
this.params.sunPhi = v * (Math.PI / 180.0);
|
||||
updateSun();
|
||||
});
|
||||
|
||||
sunFolder.add(this.sunUI, 'height', -0.2, 1.0).name('Height (Cos)').onChange(v => {
|
||||
sunFolder.add(this.sunUI, 'height', -0.2, 1.0).name('Height (Cos)').listen().onChange(v => {
|
||||
this.params.sunTheta = Math.acos(v);
|
||||
updateSun();
|
||||
});
|
||||
|
||||
sunFolder.add(this.params, 'sunIntensity', 0.0, 500000.0).name('Intensity').onChange(v => this.updateSunIntensity());
|
||||
|
||||
|
||||
|
||||
|
||||
const moonFolder = gui.addFolder('Moon');
|
||||
this.mParams = {
|
||||
enabled: true,
|
||||
azimuth: 180.0,
|
||||
height: Math.cos(45.0 * Math.PI / 180.0), // Default 45 degrees elevation -> cos(45) ~ 0.707
|
||||
radius: 0.5,
|
||||
intensity: 1.0
|
||||
radius: 1.2,
|
||||
intensity: 6.0
|
||||
};
|
||||
|
||||
const updateMoon = () => {
|
||||
@@ -184,24 +303,113 @@ class App {
|
||||
sky.setMoonIntensity(this.mParams.intensity);
|
||||
|
||||
moonFolder.add(this.mParams, 'enabled').name('Enabled').onChange(v => sky.setMoonEnabled(v));
|
||||
moonFolder.add(this.mParams, 'azimuth', 0.0, 360.0, 0.1).name('Azimuth').onChange(updateMoon);
|
||||
moonFolder.add(this.mParams, 'height', -0.2, 1.0).name('Height (Cos)').onChange(updateMoon);
|
||||
moonFolder.add(this.mParams, 'intensity', 0.0, 10.0).name('Intensity').onChange(v => this.updateSunIntensity());
|
||||
moonFolder.add(this.mParams, 'azimuth', 0.0, 360.0, 0.1).name('Azimuth').listen().onChange(updateMoon);
|
||||
moonFolder.add(this.mParams, 'height', -0.2, 1.0).name('Height (Cos)').listen().onChange(updateMoon);
|
||||
moonFolder.add(this.mParams, 'intensity', 0.0, 1000.0).name('Intensity').onChange(v => this.updateSunIntensity());
|
||||
moonFolder.add(this.mParams, 'radius', 0.1, 5.0).name('Radius').onChange(v => sky.setMoonRadius(v));
|
||||
moonFolder.close();
|
||||
|
||||
const sunDisk = sunFolder.addFolder('Disk');
|
||||
const mwFolder = gui.addFolder('Milky Way');
|
||||
|
||||
const updateMW = () => {
|
||||
sky.setMilkyWayEnabled(this.mwParams.enabled);
|
||||
sky.setMilkyWayControl(this.mwParams.intensity, this.mwParams.saturation, this.mwParams.blackPoint);
|
||||
|
||||
// Calculate Rotation
|
||||
// V_gal = Rot_Eq_to_Gal * Rot_World_to_Eq * V_world
|
||||
|
||||
// World: Y=Up, X=East, Z=South (Filament Camera Convention is different!)
|
||||
// In Filament Camera: -Z is Forward.
|
||||
// Skybox V direction is World Space direction.
|
||||
// Let's assume standard Horizontal Coordinates:
|
||||
// Y = Zenith.
|
||||
// Z = North? Or South?
|
||||
// Usually Z is South in RH Y-up.
|
||||
|
||||
// LST (Local Sidereal Time) converts Hour Angle to RA.
|
||||
// LST in Radians.
|
||||
const LST = this.mwParams.siderealTime * (Math.PI / 12.0); // Hours to Rad
|
||||
const Lat = this.mwParams.latitude * (Math.PI / 180.0);
|
||||
|
||||
// Rotation World (Horizontal) -> Equatorial
|
||||
// 1. Rotate around X by -(90 - Lat) to align Equatorial Plane.
|
||||
// 2. Rotate around Y (Polar Axis) by -LST.
|
||||
|
||||
// Mat3 Identity
|
||||
const rot = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
||||
|
||||
// Rotate Z by LST (Earth Rotation).
|
||||
// Actually, transformation from Horizontal (Az, Alt) to Equatorial (HA, Dec):
|
||||
// sin(Dec) = sin(Alt)sin(Lat) - cos(Alt)cos(Lat)cos(Az)
|
||||
// ...
|
||||
// Let's construct matrix directly.
|
||||
// WorldToEq:
|
||||
// Rotate X by (Lat - 90 deg) -> brings Pole to Zenith.
|
||||
// Rotate Y by -LST (or Z?)
|
||||
|
||||
// Filament Space:
|
||||
// +Y = Up
|
||||
// Let's match typical skybox conventions.
|
||||
|
||||
// Rot_World_to_Eq = Rot_Z_LST * Rot_X_Lat
|
||||
// But we need to use Filament matrix ops which are column major.
|
||||
|
||||
// Let's use simple rotations:
|
||||
// 1. Tilt Pole: Rotate X by (Lat - 90).
|
||||
// 2. Spin Earth: Rotate Y by LST.
|
||||
|
||||
// Let's iterate until it looks right visually or trust the math.
|
||||
// Rot_World_To_Equatorial:
|
||||
// R_z(-LST) * R_x(Lat - 90)?
|
||||
|
||||
// Let's build it from scratch in JS using helper.
|
||||
// Start Identity.
|
||||
// Rotate X (Latitude Tilt).
|
||||
// Rotate Y (Sidereal Spin).
|
||||
// Note: rotate functions modify in place.
|
||||
|
||||
const mWorldToEq = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
||||
|
||||
// 1. Tilt for Latitude (Align Celestial Pole)
|
||||
// At Lat 90 (North Pole), Zenith is Pole. No tilt needed if Y is Pole?
|
||||
// No, Y is Zenith. Pole is Y.
|
||||
// At Lat 0 (Equator), Pole is at Horizon (Z?).
|
||||
// So we rotate X by (Lat - 90).
|
||||
rotateX(mWorldToEq, Lat - Math.PI / 2);
|
||||
|
||||
// 2. Spin for Time (LST)
|
||||
// Rotate around new Pole (Y) by LST.
|
||||
rotateY(mWorldToEq, LST);
|
||||
|
||||
// Combine with Gal Transform
|
||||
// Rot = MAT_EQ_TO_GAL * mWorldToEq
|
||||
const finalRot = multiplyMat3(MAT_EQ_TO_GAL, mWorldToEq);
|
||||
|
||||
sky.setMilkyWayRotation(finalRot);
|
||||
};
|
||||
|
||||
mwFolder.add(this.mwParams, 'enabled').name('Enabled').onChange(updateMW);
|
||||
mwFolder.add(this.mwParams, 'intensity', 0.0, 5.0).onChange(updateMW);
|
||||
mwFolder.add(this.mwParams, 'saturation', 0.0, 2.0).onChange(updateMW);
|
||||
mwFolder.add(this.mwParams, 'blackPoint', 0.0, 0.5).name('Black Point').onChange(updateMW);
|
||||
mwFolder.add(this.mwParams, 'siderealTime', 0.0, 24.0).name('Sidereal Time').listen().onChange(updateMW);
|
||||
mwFolder.add(this.mwParams, 'latitude', -90.0, 90.0).name('Latitude').onChange(updateMW);
|
||||
mwFolder.close();
|
||||
|
||||
// Initial MW Update
|
||||
updateMW();
|
||||
|
||||
this.updateMW = updateMW; // Export for sync
|
||||
|
||||
// We need local proxy for sunRadius due to conversion
|
||||
this.diskParams = {
|
||||
radius: 1.2,
|
||||
enabled: true // Enable sun disk
|
||||
radius: 1.2
|
||||
};
|
||||
sky.setSunDiskEnabled(true);
|
||||
sky.setSunRadius(1.2);
|
||||
sunDisk.add(this.diskParams, 'enabled').onChange(v => sky.setSunDiskEnabled(v));
|
||||
sunDisk.add(this.diskParams, 'radius', 0.0, 5.0).onChange(v => sky.setSunRadius(v));
|
||||
sunDisk.add(sky.sunHalo, 1, 0.0, 2.0).name('Limb Darkening').onChange(v => sky.setSunLimbDarkening(v));
|
||||
sunDisk.add(sky.sunHalo, 2, 0.0, 100.0).name('Intensity Boost').onChange(v => sky.setSunDiskIntensity(v));
|
||||
sunFolder.add(this.diskParams, 'radius', 0.0, 5.0).onChange(v => sky.setSunRadius(v));
|
||||
sunFolder.add(sky.sunHalo, 1, 0.0, 2.0).name('Limb Darkening').onChange(v => sky.setSunLimbDarkening(v));
|
||||
sunFolder.add(sky.sunHalo, 2, 0.0, 100.0).name('Intensity Boost').onChange(v => sky.setSunDiskIntensity(v));
|
||||
|
||||
const atmFolder = gui.addFolder('Atmosphere');
|
||||
atmFolder.add(sky, 'turbidity', 1.0, 10.0).onChange(v => sky.setTurbidity(v));
|
||||
@@ -212,28 +420,6 @@ class App {
|
||||
atmFolder.add(sky, 'ozone', 0.0, 1.0).onChange(v => sky.setOzone(v));
|
||||
atmFolder.add(sky, 'mieG', 0.0, 0.999).onChange(v => sky.setMieG(v));
|
||||
|
||||
const artFolder = gui.addFolder('Artistic');
|
||||
// Set Horizon Glow default to 0.0
|
||||
sky.setHorizonGlow(0.0);
|
||||
sky.msFactors[2] = 0.0;
|
||||
// Set Contrast default to 1.0
|
||||
sky.setContrast(1.0);
|
||||
|
||||
artFolder.add(sky.msFactors, 0, 0.0, 2.0).name('MS Rayleigh').onChange(v => sky.setMultiScattering(v, sky.msFactors[1]));
|
||||
artFolder.add(sky.msFactors, 1, 0.0, 2.0).name('MS Mie').onChange(v => sky.setMultiScattering(sky.msFactors[0], v));
|
||||
artFolder.add(sky.msFactors, 2, 0.0, 1.0).name('Horizon Glow').onChange(v => sky.setHorizonGlow(v));
|
||||
artFolder.add(sky, 'contrast', 0.1, 2.0).onChange(v => sky.setContrast(v));
|
||||
|
||||
artFolder.addColor(sky, 'nightColor').onChange(v => sky.setNightColor(v));
|
||||
|
||||
const shmFolder = artFolder.addFolder('Shimmer');
|
||||
// Set Shimmer Strength default to 0.0
|
||||
sky.setShimmerControl(0.0, sky.shimmerControl[1], sky.shimmerControl[2]);
|
||||
|
||||
shmFolder.add(sky.shimmerControl, 0, 0.0, 0.1).name('Strength').onChange(v => sky.setShimmerControl(v, sky.shimmerControl[1], sky.shimmerControl[2]));
|
||||
shmFolder.add(sky.shimmerControl, 1, 1.0, 100.0).name('Frequency').onChange(v => sky.setShimmerControl(sky.shimmerControl[0], v, sky.shimmerControl[2]));
|
||||
shmFolder.add(sky.shimmerControl, 2, 0.01, 0.5).name('Mask Height').onChange(v => sky.setShimmerControl(sky.shimmerControl[0], sky.shimmerControl[1], v));
|
||||
|
||||
const cloudFolder = gui.addFolder('Clouds');
|
||||
this.cParams = {
|
||||
volumetrics: sky.cloudControl2[1] > 0.5,
|
||||
@@ -291,6 +477,28 @@ class App {
|
||||
starFolder.add(this.sParams, 'density', 0.0, 0.01, 0.0001).name('Density').onChange(updateStars);
|
||||
starFolder.close();
|
||||
|
||||
const artFolder = gui.addFolder('Artistic');
|
||||
// Set Horizon Glow default to 0.0
|
||||
sky.setHorizonGlow(0.0);
|
||||
sky.msFactors[2] = 0.0;
|
||||
// Set Contrast default to 1.0
|
||||
sky.setContrast(1.0);
|
||||
|
||||
artFolder.add(sky.msFactors, 0, 0.0, 2.0).name('MS Rayleigh').onChange(v => sky.setMultiScattering(v, sky.msFactors[1]));
|
||||
artFolder.add(sky.msFactors, 1, 0.0, 2.0).name('MS Mie').onChange(v => sky.setMultiScattering(sky.msFactors[0], v));
|
||||
artFolder.add(sky.msFactors, 2, 0.0, 1.0).name('Horizon Glow').onChange(v => sky.setHorizonGlow(v));
|
||||
artFolder.add(sky, 'contrast', 0.1, 2.0).onChange(v => sky.setContrast(v));
|
||||
|
||||
artFolder.addColor(sky, 'nightColor').onChange(v => sky.setNightColor(v));
|
||||
|
||||
const shmFolder = artFolder.addFolder('Shimmer');
|
||||
// Set Shimmer Strength default to 0.0
|
||||
sky.setShimmerControl(0.0, sky.shimmerControl[1], sky.shimmerControl[2]);
|
||||
|
||||
shmFolder.add(sky.shimmerControl, 0, 0.0, 0.1).name('Strength').onChange(v => sky.setShimmerControl(v, sky.shimmerControl[1], sky.shimmerControl[2]));
|
||||
shmFolder.add(sky.shimmerControl, 1, 1.0, 100.0).name('Frequency').onChange(v => sky.setShimmerControl(sky.shimmerControl[0], v, sky.shimmerControl[2]));
|
||||
shmFolder.add(sky.shimmerControl, 2, 0.01, 0.5).name('Mask Height').onChange(v => sky.setShimmerControl(sky.shimmerControl[0], sky.shimmerControl[1], v));
|
||||
|
||||
const camFolder = gui.addFolder('Camera');
|
||||
camFolder.add(this.params, 'focalLength', 8.0, 300.0).name('Focal Length').onChange(() => this.updateCameraProjection());
|
||||
camFolder.add(this.params, 'aperture', 1.4, 32.0).onChange(() => this.updateCameraExposure());
|
||||
@@ -299,7 +507,7 @@ class App {
|
||||
|
||||
const bloomFolder = camFolder.addFolder('Bloom');
|
||||
this.bParams = {
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
lensFlare: false
|
||||
};
|
||||
|
||||
@@ -315,18 +523,19 @@ class App {
|
||||
bloomFolder.close();
|
||||
|
||||
// Collapse folders by default
|
||||
sunDisk.close();
|
||||
|
||||
atmFolder.close();
|
||||
artFolder.close();
|
||||
// shmFolder is inside artFolder, so it's hidden, but we can close it too if we want
|
||||
shmFolder.close();
|
||||
cloudFolder.close();
|
||||
// camFolder left open? User didn't specify, but "Artistic, shimmer and clouds" + "Disk, Atmosphere" were requested.
|
||||
// So Camera might stay open or close. Let's keep Camera open for now as it wasn't listed.
|
||||
// camFolder left open by default for convenience.
|
||||
|
||||
|
||||
// Initial sync
|
||||
updateSun();
|
||||
this.updateCameraExposure(); // This will trigger updateSunIntensity too
|
||||
updateBloom();
|
||||
|
||||
// Check URL for config
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
@@ -347,6 +556,46 @@ class App {
|
||||
}
|
||||
}
|
||||
|
||||
const syncFolder = gui.addFolder('Real-Time Sync');
|
||||
this.syncParams = {
|
||||
enabled: false,
|
||||
lat: 0.0,
|
||||
lng: 0.0,
|
||||
status: 'Disabled'
|
||||
};
|
||||
|
||||
const updateSync = () => {
|
||||
if (this.syncParams.enabled) {
|
||||
if (navigator.geolocation) {
|
||||
this.syncParams.status = "Locating...";
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(pos) => {
|
||||
this.syncParams.lat = pos.coords.latitude;
|
||||
this.syncParams.lng = pos.coords.longitude;
|
||||
this.syncParams.status = "Active";
|
||||
syncFolder.controllers.forEach(c => c.updateDisplay());
|
||||
},
|
||||
(err) => {
|
||||
console.error(err);
|
||||
this.syncParams.status = "Error (See Console)";
|
||||
this.syncParams.enabled = false;
|
||||
syncFolder.controllers.forEach(c => c.updateDisplay());
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.syncParams.status = "Not Supported";
|
||||
}
|
||||
} else {
|
||||
this.syncParams.status = "Disabled";
|
||||
}
|
||||
syncFolder.controllers.forEach(c => c.updateDisplay());
|
||||
};
|
||||
|
||||
syncFolder.add(this.syncParams, 'enabled').name('Enable Sync').onChange(updateSync);
|
||||
syncFolder.add(this.syncParams, 'status').name('Status').disable().listen();
|
||||
|
||||
this.syncFolder = syncFolder;
|
||||
|
||||
const shareParams = {
|
||||
copyUrl: () => {
|
||||
const state = this.getURLState();
|
||||
@@ -361,9 +610,73 @@ class App {
|
||||
}
|
||||
};
|
||||
gui.add(shareParams, 'copyUrl').name('Share Configuration');
|
||||
|
||||
}
|
||||
|
||||
updateRealTimeSync() {
|
||||
if (!this.syncParams || !this.syncParams.enabled || !window.SunCalc) return;
|
||||
|
||||
const now = new Date();
|
||||
const lat = this.syncParams.lat;
|
||||
const lng = this.syncParams.lng;
|
||||
|
||||
// Sun
|
||||
const sunPos = window.SunCalc.getPosition(now, lat, lng);
|
||||
// Azimuth: South=0, West=PI/2.
|
||||
// Skybox Phi: +X=0, +Z=PI/2.
|
||||
// If +Z is South:
|
||||
// SunAz 0 (South) -> Skybox PI/2 (+Z).
|
||||
// SunAz PI/2 (West) -> Skybox PI (-X).
|
||||
// So Phi = Az + PI/2.
|
||||
const sunPhi = sunPos.azimuth + Math.PI / 2;
|
||||
// Altitude: 0=Horizon, PI/2=Zenith.
|
||||
// Skybox Theta: 0=Zenith, PI/2=Horizon.
|
||||
const sunTheta = Math.PI / 2 - sunPos.altitude;
|
||||
|
||||
this.params.sunPhi = sunPhi;
|
||||
this.params.sunTheta = sunTheta;
|
||||
|
||||
// Moon
|
||||
const moonPos = window.SunCalc.getMoonPosition(now, lat, lng);
|
||||
const moonPhi = moonPos.azimuth + Math.PI / 2;
|
||||
const moonTheta = Math.PI / 2 - moonPos.altitude;
|
||||
|
||||
this.mParams.azimuth = (moonPhi * 180.0 / Math.PI) % 360.0;
|
||||
this.mParams.height = Math.cos(moonTheta);
|
||||
|
||||
// Milky Way Sync
|
||||
const gmst = getGMST(now);
|
||||
const lst = (gmst + lng / 15.0 + 24.0) % 24.0;
|
||||
this.mwParams.siderealTime = lst;
|
||||
this.mwParams.latitude = lat;
|
||||
if (this.updateMW) this.updateMW();
|
||||
|
||||
// Update Skybox
|
||||
const sky = this.skybox;
|
||||
|
||||
// Update Sun Vector
|
||||
const sx = Math.sin(sunTheta) * Math.cos(sunPhi);
|
||||
const sy = Math.cos(sunTheta);
|
||||
const sz = Math.sin(sunTheta) * Math.sin(sunPhi);
|
||||
sky.setSunPosition([sx, sy, sz]);
|
||||
|
||||
// Update Moon Vector
|
||||
const mx = Math.sin(moonTheta) * Math.cos(moonPhi);
|
||||
const my = Math.cos(moonTheta);
|
||||
const mz = Math.sin(moonTheta) * Math.sin(moonPhi);
|
||||
sky.setMoonPosition([mx, my, mz]);
|
||||
|
||||
// Update UI Proxies
|
||||
if (this.sunUI) {
|
||||
this.sunUI.azimuth = (sunPhi * 180.0 / Math.PI) % 360.0;
|
||||
if (this.sunUI.azimuth < 0) this.sunUI.azimuth += 360.0;
|
||||
this.sunUI.height = Math.cos(sunTheta);
|
||||
}
|
||||
}
|
||||
|
||||
getURLState() {
|
||||
|
||||
// Update Camera LookAt
|
||||
// Serialize current state (Minified)
|
||||
// Mapping:
|
||||
// p: params (Camera) -> a:aperture, ss:shutterSpeed, i:iso, st:sunTheta, sp:sunPhi, fl:focalLength, si:sunIntensity
|
||||
@@ -612,6 +925,7 @@ class App {
|
||||
}
|
||||
|
||||
render() {
|
||||
this.updateRealTimeSync();
|
||||
// Update Camera LookAt
|
||||
const r = 1.0;
|
||||
const theta = this.camState.theta;
|
||||
|
||||
61
docs_src/src_raw/wip/sky/process_milkyway.py
Normal file
61
docs_src/src_raw/wip/sky/process_milkyway.py
Normal file
@@ -0,0 +1,61 @@
|
||||
|
||||
import os
|
||||
import urllib.request
|
||||
import ssl
|
||||
from PIL import Image
|
||||
import argparse
|
||||
|
||||
# URL of the Milky Way texture (Gaia EDR3) from ESA
|
||||
# Low Res PNG (2.71 MB) is sufficient for our 1024x512 target
|
||||
URL = "https://www.esa.int/var/esa/storage/images/esa_multimedia/images/2020/12/the_colour_of_the_sky_from_gaia_s_early_data_release_3/22358049-1-eng-GB/The_colour_of_the_sky_from_Gaia_s_Early_Data_Release_3.png"
|
||||
OUTPUT_DIR = "assets"
|
||||
OUTPUT_FILENAME = "milkyway.png"
|
||||
TARGET_WIDTH = 1024
|
||||
TARGET_HEIGHT = 512
|
||||
|
||||
def main():
|
||||
if not os.path.exists(OUTPUT_DIR):
|
||||
os.makedirs(OUTPUT_DIR)
|
||||
|
||||
output_path = os.path.join(OUTPUT_DIR, OUTPUT_FILENAME)
|
||||
temp_path = os.path.join(OUTPUT_DIR, "temp_milkyway.png")
|
||||
|
||||
print(f"Downloading Milky Way texture from {URL}...")
|
||||
|
||||
# Bypass SSL verification globally
|
||||
if hasattr(ssl, '_create_unverified_context'):
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(URL, headers={'User-Agent': 'Mozilla/5.0'})
|
||||
with urllib.request.urlopen(req) as response, open(temp_path, 'wb') as out_file:
|
||||
out_file.write(response.read())
|
||||
except Exception as e:
|
||||
print(f"Failed to download: {e}")
|
||||
return
|
||||
|
||||
if not os.path.exists(temp_path):
|
||||
print("Error: Download failed.")
|
||||
return
|
||||
|
||||
print("Processing image...")
|
||||
with Image.open(temp_path) as img:
|
||||
# Convert to RGB (remove alpha if present, though this is likely opaque)
|
||||
img = img.convert("RGB")
|
||||
|
||||
# Resize to user requested dimensions
|
||||
print(f"Resizing to {TARGET_WIDTH}x{TARGET_HEIGHT}...")
|
||||
img = img.resize((TARGET_WIDTH, TARGET_HEIGHT), Image.Resampling.LANCZOS)
|
||||
|
||||
# Save
|
||||
print(f"Saving to {output_path}...")
|
||||
img.save(output_path, "PNG")
|
||||
|
||||
# Cleanup
|
||||
if os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
|
||||
print("Done!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -3,11 +3,13 @@ material {
|
||||
parameters : [
|
||||
{
|
||||
type : float3,
|
||||
name : sunDirection
|
||||
name : sunDirection,
|
||||
precision : high
|
||||
},
|
||||
{
|
||||
type : float3,
|
||||
name : sunDirection2
|
||||
name : sunDirection2,
|
||||
precision : high
|
||||
},
|
||||
{
|
||||
type : float3,
|
||||
@@ -93,10 +95,36 @@ material {
|
||||
type : float4,
|
||||
name : starControl, // x=Density, y=Enabled, z=Frequency, w=PixelScale
|
||||
precision : high
|
||||
},
|
||||
{
|
||||
type : sampler2d,
|
||||
name : moonTexture
|
||||
},
|
||||
{
|
||||
type : sampler2d,
|
||||
name : moonNormal
|
||||
},
|
||||
{
|
||||
type : sampler2d,
|
||||
name : milkyWayTexture
|
||||
},
|
||||
{
|
||||
type : float3,
|
||||
name : milkyWayControl, // x=Intensity, y=Saturation, z=Unused
|
||||
precision : high
|
||||
},
|
||||
{
|
||||
type : mat3,
|
||||
name : milkyWayRotation,
|
||||
precision : high
|
||||
}
|
||||
],
|
||||
|
||||
variables : [
|
||||
eyeDirection
|
||||
{
|
||||
name : "eyeDirection",
|
||||
precision : "high"
|
||||
}
|
||||
],
|
||||
vertexDomain : device,
|
||||
depthWrite : false,
|
||||
@@ -168,8 +196,8 @@ fragment {
|
||||
// Rayleigh Phase Function: Scattering distribution for small particles (air molecules)
|
||||
// Lord Rayleigh (1871)
|
||||
// Normalized to integrate to 4*PI (Boosting brightness by factor PI vs standard 1-normalization)
|
||||
highp float rayleighPhase(highp float cosTheta) {
|
||||
const highp float THREE_SIXTEENTH = (3.0 / 16.0);
|
||||
float rayleighPhase(float cosTheta) {
|
||||
const float THREE_SIXTEENTH = (3.0 / 16.0);
|
||||
return THREE_SIXTEENTH * (1.0 + cosTheta * cosTheta);
|
||||
}
|
||||
|
||||
@@ -177,11 +205,11 @@ fragment {
|
||||
// Henyey & Greenstein (1941)
|
||||
// Controls the forward scattering peak (sun halo) via anisotropy parameter 'g'
|
||||
// Optimized: params.x = (1 + g^2), params.y = (-2 * g)
|
||||
highp float hgPhase(highp float cosTheta, highp vec2 params) {
|
||||
const highp float ONE_FOURTH = (1.0 / 4.0);
|
||||
float hgPhase(float cosTheta, vec2 params) {
|
||||
const float ONE_FOURTH = (1.0 / 4.0);
|
||||
// Recover (1 - g^2) => 2.0 - (1 + g^2)
|
||||
highp float oneMinusG2 = 2.0 - params.x;
|
||||
highp float inverse = 1.0 / pow(params.x + params.y * cosTheta, 1.5);
|
||||
float oneMinusG2 = 2.0 - params.x;
|
||||
float inverse = 1.0 / pow(params.x + params.y * cosTheta, 1.5);
|
||||
return ONE_FOURTH * (oneMinusG2 * inverse);
|
||||
}
|
||||
|
||||
@@ -192,15 +220,38 @@ fragment {
|
||||
return fract((p3.x + p3.y) * p3.z);
|
||||
}
|
||||
|
||||
// Safe mod for negative values
|
||||
// Why 289?
|
||||
// 1. Historical: 17*17 = 289. Used in identifying permutations in standard GLSL noise (Ashima/stegu).
|
||||
// 2. Precision: Small enough to prevent float precision loss during internal calculations (e.g. squaring).
|
||||
// 3. Quality: Large enough to hide repetition and coprime to standard powers-of-two (like 256), avoiding grid resonance.
|
||||
highp vec3 mod289(highp vec3 x) {
|
||||
return x - floor(x * (1.0 / 289.0)) * 289.0;
|
||||
}
|
||||
|
||||
highp float noise(highp vec3 p) {
|
||||
// Wrap input to [0, 289] to preserve precision of fract() when p is large.
|
||||
// This fixes grid artifacts on mobile caused by large world coordinates.
|
||||
p = mod289(p);
|
||||
|
||||
highp vec3 i = floor(p);
|
||||
highp vec3 f = fract(p);
|
||||
// Cubic Hermite Interpolation
|
||||
highp vec3 u = f*f*(3.0-2.0*f);
|
||||
return mix(mix(mix(hash13(i + vec3(0,0,0)), hash13(i + vec3(1,0,0)), u.x),
|
||||
mix(hash13(i + vec3(0,1,0)), hash13(i + vec3(1,1,0)), u.x), u.y),
|
||||
mix(mix(hash13(i + vec3(0,0,1)), hash13(i + vec3(1,0,1)), u.x),
|
||||
mix(hash13(i + vec3(0,1,1)), hash13(i + vec3(1,1,1)), u.x), u.y), u.z);
|
||||
|
||||
// i is already wrapped by p=mod289(p), but mod289(i) is safe (identity for 0..289).
|
||||
// Actually mod289(p) ensures p is 0..289.
|
||||
// So floor(p) is 0..288.
|
||||
// mod289(i) is redundant but harmless.
|
||||
highp vec3 i0 = i;
|
||||
highp vec3 i1 = mod289(i + 1.0);
|
||||
|
||||
// Wrap neighbors to ensure valid domain [0, 289] and seamless scrolling
|
||||
// Optimized to use precomputed wrapped indices (2 mods instead of 9)
|
||||
return mix(mix(mix(hash13(vec3(i0.x, i0.y, i0.z)), hash13(vec3(i1.x, i0.y, i0.z)), u.x),
|
||||
mix(hash13(vec3(i0.x, i1.y, i0.z)), hash13(vec3(i1.x, i1.y, i0.z)), u.x), u.y),
|
||||
mix(mix(hash13(vec3(i0.x, i0.y, i1.z)), hash13(vec3(i1.x, i0.y, i1.z)), u.x),
|
||||
mix(hash13(vec3(i0.x, i1.y, i1.z)), hash13(vec3(i1.x, i1.y, i1.z)), u.x), u.y), u.z);
|
||||
}
|
||||
|
||||
// Fractal Brownian Motion (4 Octaves)
|
||||
@@ -419,7 +470,7 @@ fragment {
|
||||
// @param transmittance Atmospheric Transmittance (0..1).
|
||||
// @return Radiance of the sun disk (if visible and enabled).
|
||||
// ------------------------------------------------------------------------
|
||||
highp vec3 getSunDisk(highp vec3 V, highp vec3 L, highp vec4 sunParams,
|
||||
vec3 getSunDisk(highp vec3 V, highp vec3 L, highp vec4 sunParams,
|
||||
highp float sunIntensity, highp vec3 transmittance) {
|
||||
|
||||
highp float sunCosRadius = sunParams.x;
|
||||
@@ -477,10 +528,10 @@ fragment {
|
||||
// @param outDensity Output: Cloud pixel density (0..1) for compositing.
|
||||
// @return Cloud Radiance (Lit color * attenuation * shading).
|
||||
// ------------------------------------------------------------------------
|
||||
highp vec3 getCloudLayer(highp vec3 V, highp vec3 L,
|
||||
vec3 getCloudLayer(highp vec3 V, highp vec3 L,
|
||||
highp vec4 control, highp vec4 control2, highp vec4 geometry,
|
||||
highp float sunIntensity, highp vec3 transmittance,
|
||||
out highp float outDensity) {
|
||||
out float outDensity) {
|
||||
|
||||
outDensity = 0.0;
|
||||
highp float cloudCoverage = control.x;
|
||||
@@ -594,8 +645,8 @@ fragment {
|
||||
// @param contrast Maximum contrast exponent (at horizon). e.g. 1.5.
|
||||
// @return Tone mapped color.
|
||||
// ------------------------------------------------------------------------
|
||||
highp vec3 applyDynamicToneMapping(highp vec3 color, highp vec3 L, highp float contrast) {
|
||||
float c = saturate(L.y);
|
||||
vec3 applyDynamicToneMapping(vec3 color, highp vec3 L, float contrast) {
|
||||
highp float c = saturate(L.y);
|
||||
// Exponent blends from 'contrast' (at L.y=0) to 1.0 (at L.y=1)
|
||||
float exponent = mix(contrast, 1.0, sqrt(c));
|
||||
return pow(max(vec3(0.0), color), vec3(exponent));
|
||||
@@ -612,7 +663,7 @@ fragment {
|
||||
return fract((p4.xxyz + p4.yzzw) * p4.zywx);
|
||||
}
|
||||
|
||||
highp float getStars(highp vec3 V) {
|
||||
float getStars(highp vec3 V) {
|
||||
bool enabled = materialParams.starControl.y > 0.5;
|
||||
if (!enabled) return 0.0;
|
||||
|
||||
@@ -669,7 +720,7 @@ fragment {
|
||||
}
|
||||
|
||||
// New helper to handle Star Compositing (Fade, Rotation, Occlusion)
|
||||
highp vec3 getStarLayer(highp vec3 V, highp vec3 L, highp float cloudDensity, highp vec3 transmittance, highp vec4 starControl) {
|
||||
vec3 getStarLayer(highp vec3 V, highp vec3 L, float cloudDensity, highp vec3 transmittance, highp vec4 starControl) {
|
||||
// starControl.x = Density, .y = Enabled
|
||||
if (starControl.y < 0.5) return vec3(0.0);
|
||||
|
||||
@@ -698,6 +749,43 @@ fragment {
|
||||
return vec3(starVal) * transmittance * starFade * cloudOcclusion * STAR_CLOUD_OCCLUSION;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Milky Way
|
||||
// ------------------------------------------------------------------------
|
||||
// Renders the Milky Way background from an equirectangular texture.
|
||||
//
|
||||
// @param V Normalized View Vector.
|
||||
// @param rotation Rotation matrix (Galactic -> World).
|
||||
// @return Milky Way Color.
|
||||
// ------------------------------------------------------------------------
|
||||
vec3 getMilkyWay(highp vec3 V, highp mat3 rotation, sampler2D tex, highp vec3 control) {
|
||||
highp float intensity = control.x;
|
||||
if (intensity <= 0.0) return vec3(0.0);
|
||||
|
||||
// Rotate V into Galactic coordinates
|
||||
highp vec3 Vg = rotation * V;
|
||||
|
||||
// Equirectangular mapping
|
||||
// u = atan(z, x) / 2pi + 0.5
|
||||
// v = asin(y) / pi + 0.5
|
||||
highp float u = atan(Vg.z, Vg.x) * (0.1591549) + 0.5; // 1/(2*PI)
|
||||
highp float v = asin(clamp(Vg.y, -1.0, 1.0)) * (0.3183098) + 0.5; // 1/PI
|
||||
|
||||
// Sample
|
||||
highp vec3 color = texture(tex, vec2(u, v)).rgb;
|
||||
|
||||
// Black Point (Remove baked-in haze)
|
||||
highp float blackPoint = control.z;
|
||||
color = max(vec3(0.0), color - blackPoint);
|
||||
|
||||
// Saturation
|
||||
highp float saturation = control.y;
|
||||
highp float gray = dot(color, vec3(0.299, 0.587, 0.114));
|
||||
color = mix(vec3(gray), color, saturation);
|
||||
|
||||
return color * intensity;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Procedural Moon Disk (Phased)
|
||||
// ------------------------------------------------------------------------
|
||||
@@ -714,7 +802,7 @@ fragment {
|
||||
// ------------------------------------------------------------------------
|
||||
highp vec3 getMoonDisk(highp vec3 V, highp vec3 L_moon, highp vec3 L_sun,
|
||||
highp vec4 moonParams, highp float moonIntensity,
|
||||
highp vec3 transmittance) {
|
||||
highp vec3 transmittance, sampler2D moonTex, sampler2D moonNormal) {
|
||||
|
||||
highp float moonCosRadius = moonParams.x;
|
||||
highp float moonSinRadius = moonParams.y; // Used for precision
|
||||
@@ -758,8 +846,6 @@ fragment {
|
||||
// the normal component along L_moon is negative.
|
||||
highp vec3 N_sphere = N_perp - L_moon * sqrt(max(0.0, 1.0 - nPerpSq));
|
||||
|
||||
highp float NdotL = max(0.0, dot(N_sphere, L_sun));
|
||||
|
||||
// Earthshine (Ambient)
|
||||
// Earth Phase (approx):
|
||||
// When Sun & Moon are same direction (dot=1), Earth is Full (from Moon).
|
||||
@@ -767,20 +853,77 @@ fragment {
|
||||
// EarthPhase ~ 0.5 * (1.0 + dot(L_sun, L_moon)).
|
||||
highp float earthPhase = 0.5 * (1.0 + dot(L_sun, L_moon));
|
||||
|
||||
// Intensity relative to Sun ~ 0.0001 to 0.01 depending on albedo/size.
|
||||
// We use 0.002 as a base scale.
|
||||
// Visibility: The dark side faces Earth?
|
||||
// Yes, dot(N_sphere, -L_moon) > 0 covers the face seen from Earth.
|
||||
// But simpler: Is this pixel in shadow (NdotL == 0)?
|
||||
// Earthshine adds ambient to the whole sphere, but is overwhelmed by Sun.
|
||||
highp float earthshine = MOON_EARTHSHINE * earthPhase * clamp(dot(N_sphere, -L_moon) + 1.0, 0.0, 1.0); // Hemisphere ambient
|
||||
// Texture Mapping
|
||||
// The moon texture is an orthographic projection of the Near Side.
|
||||
// We align the texture using a stable "Up" vector (World Up projected onto the Moon plane).
|
||||
|
||||
return ((NdotL + earthshine) * moonIntensity * moonDiskIntensity * moonProfile) * transmittance;
|
||||
highp vec3 Up = vec3(0.0, 1.0, 0.0);
|
||||
if (abs(L_moon.y) > 0.99) Up = vec3(0.0, 0.0, 1.0);
|
||||
|
||||
highp vec3 Right = normalize(cross(Up, L_moon));
|
||||
highp vec3 MoonUp = cross(L_moon, Right);
|
||||
|
||||
// Project N_sphere onto the Right/MoonUp plane to get UVs.
|
||||
highp float u = dot(N_sphere, Right) * 0.5 + 0.5;
|
||||
highp float v = dot(N_sphere, MoonUp) * 0.5 + 0.5;
|
||||
|
||||
// Sample Texture
|
||||
// We assume the texture is stored such that (0,0) is bottom-left.
|
||||
// Sample Albedo
|
||||
// Flip V here as well. Flip U to fix horizontal mirroring.
|
||||
highp vec3 moonColorSample = texture(moonTex, vec2(1.0 - u, 1.0 - v)).rgb;
|
||||
// The original code used a float 'moonAlbedo' derived from .rrr.
|
||||
// To support "3 channels" properly if it had color, we use the full RGB sample.
|
||||
highp vec3 moonAlbedo = moonColorSample;
|
||||
|
||||
// Normal Mapping
|
||||
// Convert Tangent Space Normal (from texture) to World Space
|
||||
// Tangent Basis: T=Right, B=MoonUp, N=N_sphere
|
||||
// We need to invert V because texturing coordinates are usually bottom-up
|
||||
// but our procedural disk generation (and the original texture) might be top-down oriented
|
||||
// relative to the sphere mapping we did.
|
||||
highp vec3 N_map = texture(moonNormal, vec2(1.0 - u, 1.0 - v)).rgb * 2.0 - 1.0;
|
||||
// Scale down the normal map strength (it was too strong)
|
||||
N_map.xy *= 0.2;
|
||||
|
||||
// Important: Since we flipped U (1.0 - u), we effectively mirrored the texture.
|
||||
// A slope that was "right" is now "left" visually, but the normal map data still points "right".
|
||||
// We must invert the X component of the normal to match the mirrored visual geometry.
|
||||
N_map.x *= -1.0;
|
||||
|
||||
// Perturb N_sphere
|
||||
// N_final = N_map.x * T + N_map.y * B + N_map.z * N
|
||||
// Note: N_map.z is the component along the surface normal.
|
||||
N_sphere = normalize(N_map.x * Right + N_map.y * MoonUp + N_map.z * N_sphere);
|
||||
|
||||
highp float NdotL = max(0.0, dot(N_sphere, L_sun));
|
||||
|
||||
highp vec3 litColor = moonIntensity * transmittance * NdotL * moonAlbedo;
|
||||
highp vec3 unlitColor = moonIntensity * transmittance * MOON_EARTHSHINE * earthPhase * moonAlbedo;
|
||||
|
||||
// Smooth transition at terminator to avoid hard lines
|
||||
return mix(unlitColor, litColor, smoothstep(0.0, 0.1, NdotL)) * moonDiskIntensity * moonProfile;
|
||||
}
|
||||
|
||||
return vec3(0.0);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Phase Factor (Lambertian Sphere)
|
||||
// ------------------------------------------------------------------------
|
||||
// Calculates the integrated flux factor for a lit sphere based on phase angle.
|
||||
// Normalized to 1.0 at Full Moon (Phase=0) and 0.0 at New Moon (Phase=PI).
|
||||
//
|
||||
// @param L_sun Normalized vector to Sun
|
||||
// @param L_moon Normalized vector to Moon
|
||||
// @return Phase factor [0.0, 1.0]
|
||||
// ------------------------------------------------------------------------
|
||||
highp float getPhaseFactor(highp vec3 L_sun, highp vec3 L_moon) {
|
||||
highp float cosAlpha = -dot(L_sun, L_moon);
|
||||
highp float alpha = acos(clamp(cosAlpha, -1.0, 1.0));
|
||||
return (1.0 / PI) * (sin(alpha) + (PI - alpha) * cosAlpha);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Procedural Water Surface
|
||||
// ------------------------------------------------------------------------
|
||||
@@ -817,7 +960,8 @@ fragment {
|
||||
highp vec4 multiScatParams, highp vec2 miePhaseParams,
|
||||
highp vec4 sunHalo, highp vec4 cloudControl, highp vec4 cloudControl2,
|
||||
highp vec4 shimmerControl, highp vec4 waterControl,
|
||||
highp vec3 L2, highp float sunIntensity2, highp vec4 sunHalo2) {
|
||||
highp vec3 L2, highp float sunIntensity2, highp vec4 sunHalo2,
|
||||
sampler2D moonTex, sampler2D moonNormal) {
|
||||
|
||||
// Project to plane y=0
|
||||
highp float t = WATER_PLANE_HEIGHT / min(V.y, -0.0002); // Reduced clamp to minimize "wall" artifact
|
||||
@@ -851,14 +995,14 @@ fragment {
|
||||
} else {
|
||||
// Finite Difference (Standard, 3 taps)
|
||||
// More expensive but analytically correct in world space (independent of view resolution/derivatives)
|
||||
float eps = 0.02; // Epsilon for gradient
|
||||
vec3 p = vec3(uv, time * 0.1 * speed);
|
||||
float hx = fbm(p + vec3(eps, 0.0, 0.0), octaves);
|
||||
float hy = fbm(p + vec3(0.0, eps, 0.0), octaves);
|
||||
highp float eps = 0.02; // Epsilon for gradient
|
||||
highp vec3 p = vec3(uv, time * 0.1 * speed);
|
||||
highp float hx = fbm(p + vec3(eps, 0.0, 0.0), octaves);
|
||||
highp float hy = fbm(p + vec3(0.0, eps, 0.0), octaves);
|
||||
|
||||
// Gradient
|
||||
float dx = (hx - h) / eps;
|
||||
float dy = (hy - h) / eps;
|
||||
highp float dx = (hx - h) / eps;
|
||||
highp float dy = (hy - h) / eps;
|
||||
|
||||
// Construct World Space Perturbation
|
||||
// Gradient (dx, dy) acts on XZ plane.
|
||||
@@ -893,10 +1037,11 @@ fragment {
|
||||
|
||||
// Add Moon Scattering to Reflection
|
||||
if (sunHalo2.w > 0.5) {
|
||||
reflection += getSecondarySunScattering(R, L2, sunIntensity2,
|
||||
depthR, depthM, ozone,
|
||||
miePhaseParams,
|
||||
outTransmittance);
|
||||
float phaseFactor = getPhaseFactor(L, L2);
|
||||
reflection += getSecondarySunScattering(R, L2, sunIntensity2 * phaseFactor,
|
||||
depthR, depthM, ozone,
|
||||
miePhaseParams,
|
||||
outTransmittance);
|
||||
}
|
||||
|
||||
// Clouds in reflection
|
||||
@@ -937,7 +1082,7 @@ fragment {
|
||||
|
||||
// Add Moon Disk to reflection
|
||||
if (sunHalo2.w > 0.5) {
|
||||
reflection += getMoonDisk(R, L2, L, sunHalo2, sunIntensity2, outTransmittance);
|
||||
reflection += getMoonDisk(R, L2, L, sunHalo2, sunIntensity2, outTransmittance, moonTex, moonNormal);
|
||||
}
|
||||
|
||||
// Apply clouds to reflection
|
||||
@@ -989,14 +1134,15 @@ fragment {
|
||||
}
|
||||
highp float sunIntensityLit = materialParams.sunIntensity * safeEclipseFactor;
|
||||
|
||||
// 1. Atmosphere Scattering
|
||||
// 2. Atmospheric Scattering
|
||||
highp vec3 color = getAtmosphere(V, L, sunIntensityLit, materialParams.depthR, materialParams.depthM, materialParams.ozone,
|
||||
materialParams.multiScatParams, materialParams.miePhaseParams, transmittance);
|
||||
|
||||
// 2. Secondary Sun/Moon Scattering
|
||||
// 3. Secondary Scattering (Moon)
|
||||
if (materialParams.sunHalo2.w > 0.5) {
|
||||
float phaseFactor = getPhaseFactor(L, L2);
|
||||
inScatter2 = getSecondarySunScattering(V, L2,
|
||||
materialParams.sunIntensity2,
|
||||
materialParams.sunIntensity2 * phaseFactor,
|
||||
materialParams.depthR,
|
||||
materialParams.depthM,
|
||||
materialParams.ozone,
|
||||
@@ -1005,12 +1151,12 @@ fragment {
|
||||
color += inScatter2;
|
||||
}
|
||||
|
||||
// 3. Clouds
|
||||
// 4. Clouds
|
||||
highp float cloudDensityVal;
|
||||
highp vec3 clouds = getCloudLayer(V, L, materialParams.cloudControl, materialParams.cloudControl2, materialParams.shimmerControl,
|
||||
sunIntensityLit, transmittance, cloudDensityVal);
|
||||
|
||||
// 4. Sun Disk (Direct)
|
||||
// 5. Sun Disk (Direct)
|
||||
// Use ORIGINAL sunIntensity (Un-dimmed) so the disk looks bright
|
||||
// We mask it by Moon Occlusion AND Cloud Occlusion
|
||||
highp vec3 sunDisk = getSunDisk(V, L, materialParams.sunHalo, materialParams.sunIntensity, transmittance);
|
||||
@@ -1028,33 +1174,54 @@ fragment {
|
||||
|
||||
// Apply Masks
|
||||
// Note: moonOcclusion is 1.0 if occluded. So we want (1.0 - moonOcclusion).
|
||||
// Since we logic-ed it as: if dist < rad, occlusion = 1.0. Correct.
|
||||
|
||||
sunDisk *= (1.0 - moonOcclusion) * sunAccess;
|
||||
|
||||
color += sunDisk;
|
||||
|
||||
// 5. Moon Disk
|
||||
// 6. Moon Disk
|
||||
if (materialParams.sunHalo2.w > 0.5) {
|
||||
color += getMoonDisk(V, L2, L, materialParams.sunHalo2,
|
||||
materialParams.sunIntensity2, transmittance) * sunAccess;
|
||||
materialParams.sunIntensity2, transmittance, materialParams_moonTexture, materialParams_moonNormal) * sunAccess;
|
||||
}
|
||||
|
||||
// 6. Stars
|
||||
// 7. Stars
|
||||
// Add stars before clouds (clouds cover stars)
|
||||
color += getStarLayer(V, L, cloudDensityVal, transmittance, materialParams.starControl) * (1.0 - moonOcclusion);
|
||||
highp vec3 starColor = getStarLayer(V, L, cloudDensityVal, transmittance, materialParams.starControl);
|
||||
|
||||
// 7b. Milky Way
|
||||
// Add Milky Way behind stars (conceptually) but handled similarly
|
||||
// We fade it by Sun Elevation just like stars
|
||||
// Re-use starFade logic implicity or calculate it?
|
||||
// Let's use the starControl.y (enabled) check inside getMilkyWay if needed, or just assume it's always on if intensity > 0.
|
||||
// We reuse the fade from stars for consistency:
|
||||
// STAR_FADE_SUN_ELV_HIGH...
|
||||
// We recalculate fade:
|
||||
highp float mwFade = 1.0 - smoothstep(STAR_FADE_SUN_ELV_LOW, STAR_FADE_SUN_ELV_HIGH, L.y);
|
||||
mwFade *= mwFade;
|
||||
|
||||
highp vec3 milkyWay = vec3(0.0);
|
||||
if (mwFade > 0.0) {
|
||||
milkyWay = getMilkyWay(V, materialParams.milkyWayRotation, materialParams_milkyWayTexture, materialParams.milkyWayControl);
|
||||
milkyWay *= mwFade * transmittance * (1.0 - moonOcclusion);
|
||||
// Apply cloud occlusion
|
||||
milkyWay *= (1.0 - smoothstep(0.0, 1.0, pow(cloudDensityVal, 0.1)));
|
||||
}
|
||||
|
||||
// 7. Composite Clouds
|
||||
color += (starColor + milkyWay) * (1.0 - moonOcclusion);
|
||||
|
||||
// 8. Composite Clouds
|
||||
// Clouds are alpha blended on top of the sky (Atmos + Stars + SunDisk)
|
||||
// The cloud color `clouds` includes its own ambient and forward scattering terms.
|
||||
color = mix(color, clouds, cloudDensityVal);
|
||||
|
||||
// 8. Night Sky Offset
|
||||
// 9. Night Sky Offset
|
||||
color += materialParams.nightColor;
|
||||
|
||||
// 9. Dynamic Tone Mapping
|
||||
// 10. Dynamic Tone Mapping
|
||||
color = applyDynamicToneMapping(color, L, materialParams.contrast);
|
||||
|
||||
// 10. Water Surface (Reflection)
|
||||
// 11. Water Surface (Reflection)
|
||||
if (V.y < 0.0) {
|
||||
color = getWaterColor(V, L, sunIntensityLit, materialParams.sunIntensity,
|
||||
materialParams.depthR, materialParams.depthM,
|
||||
@@ -1064,7 +1231,8 @@ fragment {
|
||||
materialParams.cloudControl, materialParams.cloudControl2,
|
||||
materialParams.shimmerControl,
|
||||
materialParams.waterControl,
|
||||
L2, materialParams.sunIntensity2, materialParams.sunHalo2);
|
||||
L2, materialParams.sunIntensity2, materialParams.sunHalo2,
|
||||
materialParams_moonTexture, materialParams_moonNormal);
|
||||
color = applyDynamicToneMapping(color, L, materialParams.contrast);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Result: /Users/mathias/sources/git/filament/out/cmake-release/tools/matc/matc
|
||||
MATC="../../../../out/cmake-release/tools/matc/matc"
|
||||
MATC="../../../../../out/cmake-release/tools/matc/matc"
|
||||
|
||||
# Navigate to script directory to ensure relative paths work
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
set -e
|
||||
|
||||
$MATC -a opengl -p mobile -o assets/simulated_skybox.filamat simulated_skybox.mat
|
||||
$MATC -a opengl -p mobile -o ../assets/simulated_skybox.filamat ../simulated_skybox.mat
|
||||
echo "Material recompiled to assets/simulated_skybox.filamat"
|
||||
21
docs_src/src_raw/wip/sky/tools/generate_moon_assets.sh
Executable file
21
docs_src/src_raw/wip/sky/tools/generate_moon_assets.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Default size
|
||||
SIZE=${1:-256}
|
||||
OUTPUT_COLOR="../assets/moon_disk.png"
|
||||
OUTPUT_NORMAL="../assets/moon_normal.png"
|
||||
|
||||
# Navigate to script directory
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Check dependencies
|
||||
if ! python3 -c "import numpy, PIL" 2>/dev/null; then
|
||||
echo "Installing dependencies (numpy, Pillow)..."
|
||||
pip3 install numpy Pillow
|
||||
fi
|
||||
|
||||
echo "Generating Moon Asset (Size: ${SIZE}x${SIZE})..."
|
||||
python3 process_moon.py --size $SIZE --supersample 4 --blur 1.0 --output-color $OUTPUT_COLOR --output-normal $OUTPUT_NORMAL
|
||||
|
||||
echo "Generated $OUTPUT_COLOR and $OUTPUT_NORMAL"
|
||||
BIN
docs_src/src_raw/wip/sky/tools/ldem_4.tif
Normal file
BIN
docs_src/src_raw/wip/sky/tools/ldem_4.tif
Normal file
Binary file not shown.
BIN
docs_src/src_raw/wip/sky/tools/lroc_color_2k.jpg
Normal file
BIN
docs_src/src_raw/wip/sky/tools/lroc_color_2k.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 447 KiB |
249
docs_src/src_raw/wip/sky/tools/process_moon.py
Normal file
249
docs_src/src_raw/wip/sky/tools/process_moon.py
Normal file
@@ -0,0 +1,249 @@
|
||||
import argparse
|
||||
import urllib.request
|
||||
import os
|
||||
import sys
|
||||
import math
|
||||
import ssl
|
||||
from PIL import Image
|
||||
|
||||
# Ensure numpy is available
|
||||
try:
|
||||
import numpy as np
|
||||
except ImportError:
|
||||
print("Error: numpy is required. Please install it with: pip install numpy")
|
||||
sys.exit(1)
|
||||
|
||||
# Default URLs
|
||||
COLOR_URL = "https://svs.gsfc.nasa.gov/vis/a000000/a004700/a004720/lroc_color_2k.jpg"
|
||||
DISP_URL = "https://svs.gsfc.nasa.gov/vis/a000000/a004700/a004720/ldem_4.tif"
|
||||
|
||||
SRC_COLOR_FILENAME = "lroc_color_2k.jpg"
|
||||
SRC_DISP_FILENAME = "ldem_4.tif"
|
||||
|
||||
def download_file(url, filename):
|
||||
if os.path.exists(filename):
|
||||
print(f"File {filename} already exists. Skipping download.")
|
||||
return
|
||||
|
||||
print(f"Downloading {url} to {filename}...")
|
||||
# Create unverified context to avoid potential SSL cert issues
|
||||
context = ssl._create_unverified_context()
|
||||
try:
|
||||
with urllib.request.urlopen(url, context=context) as response, open(filename, 'wb') as out_file:
|
||||
data = response.read()
|
||||
out_file.write(data)
|
||||
print(f"Download of {filename} complete.")
|
||||
except Exception as e:
|
||||
print(f"Error downloading file {filename}: {e}")
|
||||
# Don't exit hard if it's just one file, maybe?
|
||||
# But for this script, we likely need it.
|
||||
sys.exit(1)
|
||||
|
||||
def equirectangular_to_orthographic(src_img, size, mode=None):
|
||||
"""
|
||||
Reprojects an equirectangular image to an orthographic projection (sphere view).
|
||||
src_img: PIL Image (Equirectangular)
|
||||
size: Output size (width, height) - usually square
|
||||
"""
|
||||
print(f"Reprojecting to {size}x{size} Orthographic Disk...")
|
||||
|
||||
width, height = size, size
|
||||
src_w, src_h = src_img.size
|
||||
|
||||
# Create coordinate grid centered at 0,0 (-1 to 1)
|
||||
y, x = np.mgrid[size/2:-size/2:-1, -size/2:size/2] # Note: y goes high to low
|
||||
|
||||
# Normalize to -1..1
|
||||
x = x / (size / 2)
|
||||
y = y / (size / 2)
|
||||
|
||||
# Mask for points outside the circle
|
||||
r2 = x*x + y*y
|
||||
mask = r2 <= 1.0
|
||||
|
||||
# Calculate sphere coordinates (z > 0 for front face)
|
||||
z = np.zeros_like(r2)
|
||||
z[mask] = np.sqrt(1.0 - r2[mask])
|
||||
|
||||
# Vector P = (x, y, z) on unit sphere
|
||||
# Lat = asin(y)
|
||||
# Lon = atan2(x, z)
|
||||
|
||||
# Apply mask to avoid invalid calculations
|
||||
lat = np.arcsin(y * mask)
|
||||
lon = np.arctan2(x * mask, z * mask)
|
||||
|
||||
# Map to UV [0, 1]
|
||||
u = (lon / (2 * math.pi)) + 0.5
|
||||
v = (lat / math.pi) + 0.5
|
||||
|
||||
# Map to Source Pixels
|
||||
u = np.clip(u, 0, 1)
|
||||
v = np.clip(v, 0, 1)
|
||||
|
||||
src_x = (u * (src_w - 1)).astype(np.int32)
|
||||
src_y = ((1.0 - v) * (src_h - 1)).astype(np.int32) # Flip V for image coords
|
||||
|
||||
# Sample pixels
|
||||
src_array = np.array(src_img)
|
||||
|
||||
# Handle dimensions
|
||||
if len(src_array.shape) == 2:
|
||||
# Grayscale / Single channel
|
||||
out_channels = 1
|
||||
src_array = src_array[:, :, np.newaxis] # Expand for consistent indexing
|
||||
else:
|
||||
out_channels = src_array.shape[2]
|
||||
|
||||
out_array = np.zeros((height, width, out_channels), dtype=src_array.dtype)
|
||||
|
||||
# Advanced indexing
|
||||
valid_y, valid_x = np.where(mask)
|
||||
|
||||
# Extract coordinates for valid pixels
|
||||
sx = src_x[valid_y, valid_x]
|
||||
sy = src_y[valid_y, valid_x]
|
||||
|
||||
out_array[valid_y, valid_x] = src_array[sy, sx]
|
||||
|
||||
# Squeeze if single channel
|
||||
if out_channels == 1:
|
||||
out_array = out_array.squeeze(axis=2)
|
||||
|
||||
return Image.fromarray(out_array, mode or src_img.mode)
|
||||
|
||||
def compute_normal_map(height_img, scale=1.0, blur_radius=0.0):
|
||||
print("Computing Normal Map from Height Map...")
|
||||
# Convert to float array
|
||||
h = np.array(height_img).astype(np.float32)
|
||||
|
||||
# Apply Blur if requested
|
||||
if blur_radius > 0:
|
||||
try:
|
||||
from scipy.ndimage import gaussian_filter
|
||||
print(f"Applying Gaussian Blur (Radius: {blur_radius})...")
|
||||
h = gaussian_filter(h, sigma=blur_radius)
|
||||
except ImportError:
|
||||
print("Warning: scipy not found. Skipping Gaussian Blur.")
|
||||
|
||||
# Normalize height to 0..1 for consistent gradient scale regardless of input depth
|
||||
h_min, h_max = h.min(), h.max()
|
||||
print(f"Height Map Range: {h_min} to {h_max}")
|
||||
if h_max > h_min:
|
||||
h_norm = (h - h_min) / (h_max - h_min)
|
||||
else:
|
||||
h_norm = h
|
||||
|
||||
# Gradients
|
||||
dy, dx = np.gradient(h_norm)
|
||||
|
||||
# Pre-emphasis scale
|
||||
bump_scale = scale
|
||||
|
||||
# Normal vector components
|
||||
# Map is Top-Down Y.
|
||||
nx = -dx * bump_scale
|
||||
ny = -dy * bump_scale
|
||||
nz = np.ones_like(nx)
|
||||
|
||||
# Mask out normals where r > 0.96 (avoid edge cliff artifacts)
|
||||
rows, cols = h.shape
|
||||
y, x = np.ogrid[:rows, :cols]
|
||||
center_y, center_x = rows/2.0, cols/2.0
|
||||
# max radius is size/2
|
||||
radius_sq = (min(rows, cols) / 2.0)**2
|
||||
dist_sq = (x - center_x)**2 + (y - center_y)**2
|
||||
mask = dist_sq < (radius_sq * 0.96 * 0.96)
|
||||
|
||||
nx[~mask] = 0
|
||||
ny[~mask] = 0
|
||||
nz[~mask] = 1
|
||||
|
||||
# Normalize
|
||||
len_n = np.sqrt(nx*nx + ny*ny + nz*nz)
|
||||
# Avoid divide by zero
|
||||
len_n[len_n == 0] = 1.0
|
||||
|
||||
nx /= len_n
|
||||
ny /= len_n
|
||||
nz /= len_n
|
||||
|
||||
# Pack to 0..255
|
||||
# [-1, 1] -> [0, 255]
|
||||
out_x = ((nx + 1.0) * 0.5 * 255.0).astype(np.uint8)
|
||||
out_y = ((ny + 1.0) * 0.5 * 255.0).astype(np.uint8)
|
||||
out_z = ((nz + 1.0) * 0.5 * 255.0).astype(np.uint8)
|
||||
|
||||
out_rgb = np.dstack((out_x, out_y, out_z))
|
||||
return Image.fromarray(out_rgb, 'RGB')
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Process Moon Texture')
|
||||
parser.add_argument('--size', type=int, default=256, help='Output resolution (square)')
|
||||
parser.add_argument('--supersample', type=int, default=2, help='Internal processing resolution multiplier')
|
||||
parser.add_argument('--blur', type=float, default=1.0, help='Gaussian blur radius for height map')
|
||||
parser.add_argument('--bump-scale', type=float, default=60.0, help='Normal map bump scale')
|
||||
parser.add_argument('--output-color', type=str, default='assets/moon_disk.png', help='Output color filename')
|
||||
parser.add_argument('--output-normal', type=str, default='assets/moon_normal.png', help='Output normal filename')
|
||||
parser.add_argument('--skip-download', action='store_true', help='Skip downloading files')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
internal_size = args.size * args.supersample
|
||||
|
||||
# Ensure assets dir exists
|
||||
os.makedirs(os.path.dirname(args.output_color) or '.', exist_ok=True)
|
||||
|
||||
# 1. Download
|
||||
if not args.skip_download:
|
||||
download_file(COLOR_URL, SRC_COLOR_FILENAME)
|
||||
download_file(DISP_URL, SRC_DISP_FILENAME)
|
||||
|
||||
# 2. Process Color
|
||||
print(f"Processing Color Map (Internal Size: {internal_size}x{internal_size})...")
|
||||
try:
|
||||
img_color = Image.open(SRC_COLOR_FILENAME).convert('RGB')
|
||||
out_color = equirectangular_to_orthographic(img_color, internal_size)
|
||||
|
||||
if args.supersample > 1:
|
||||
print(f"Downsampling Color to {args.size}x{args.size}...")
|
||||
out_color = out_color.resize((args.size, args.size), Image.LANCZOS)
|
||||
|
||||
out_color.save(args.output_color)
|
||||
print(f"Saved {args.output_color}")
|
||||
except Exception as e:
|
||||
print(f"Error processing color: {e}")
|
||||
|
||||
# 3. Process Normal
|
||||
print(f"Processing Displacement Map (Internal Size: {internal_size}x{internal_size})...")
|
||||
try:
|
||||
from PIL import ImageFile
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
|
||||
# Check if file exists
|
||||
if not os.path.exists(SRC_DISP_FILENAME):
|
||||
print(f"Displacement map {SRC_DISP_FILENAME} not found!")
|
||||
return
|
||||
|
||||
img_disp = Image.open(SRC_DISP_FILENAME)
|
||||
out_disp = equirectangular_to_orthographic(img_disp, internal_size)
|
||||
|
||||
# Compute Normals
|
||||
out_normal = compute_normal_map(out_disp, scale=args.bump_scale, blur_radius=args.blur)
|
||||
|
||||
if args.supersample > 1:
|
||||
print(f"Downsampling Normal to {args.size}x{args.size}...")
|
||||
out_normal = out_normal.resize((args.size, args.size), Image.LANCZOS)
|
||||
|
||||
out_normal.save(args.output_normal)
|
||||
print(f"Saved {args.output_normal}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing normal: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
print("Done.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user