黑夜场景未完成

This commit is contained in:
2026-03-26 19:14:24 +08:00
parent 562d031171
commit 36965a7dc3
4 changed files with 373 additions and 10 deletions

View File

@@ -1 +1 @@
codex resume 019d2462-a1f1-7a72-947b-70470e482854
codex resume 019d284e-bc41-78f0-a961-32ce95bfa96a

View File

@@ -23,13 +23,18 @@ export class OceanScene {
this.controls = null;
this.water = null;
this.sky = null;
this.starField = null;
this.moonSprite = null;
this.galaxyBand = null;
this.sun = new THREE.Vector3();
this.terrain = null;
this.vegetation = null;
this.vegetationSystem = null;
this.pmremGenerator = null;
this.renderTarget = null;
this.ambientLight = null;
this.sunLight = null;
this.moonLight = null;
this.vegetationFillLight = null;
this.lightningLight = null;
this.lightningCloudGlow = null;
@@ -74,6 +79,8 @@ export class OceanScene {
this.initPostProcessing();
this.initAudio();
await this.initSky();
this.initStars();
this.initNightSky();
this.initClouds();
this.initFog();
await this.initWater();
@@ -136,8 +143,8 @@ export class OceanScene {
}
initLighting() {
const ambientLight = new THREE.AmbientLight(0x555555);
this.scene.add(ambientLight);
this.ambientLight = new THREE.AmbientLight(0x8ea0b7, 0.58);
this.scene.add(this.ambientLight);
this.sunLight = new THREE.DirectionalLight(0xffffff, 1.5);
this.sunLight.castShadow = true;
@@ -151,6 +158,10 @@ export class OceanScene {
this.sunLight.shadow.camera.bottom = -100;
this.scene.add(this.sunLight);
this.moonLight = new THREE.DirectionalLight(0xa9c7ff, 0);
this.moonLight.castShadow = false;
this.scene.add(this.moonLight);
this.vegetationFillLight = new THREE.DirectionalLight(0xffb06a, 0.95);
this.vegetationFillLight.castShadow = false;
this.scene.add(this.vegetationFillLight);
@@ -532,6 +543,193 @@ export class OceanScene {
this.pmremGenerator = new THREE.PMREMGenerator(this.renderer);
}
initStars() {
const starCount = 8000;
const positions = new Float32Array(starCount * 3);
const colors = new Float32Array(starCount * 3);
const sizes = new Float32Array(starCount);
const color = new THREE.Color();
for (let i = 0; i < starCount; i++) {
const radius = THREE.MathUtils.randFloat(2200, 4200);
const theta = Math.random() * Math.PI * 2.0;
const phi = THREE.MathUtils.randFloat(0.015, Math.PI * 0.49);
const sinPhi = Math.sin(phi);
const x = radius * sinPhi * Math.cos(theta);
const y = radius * Math.cos(phi);
const z = radius * sinPhi * Math.sin(theta);
positions[i * 3] = x;
positions[i * 3 + 1] = y;
positions[i * 3 + 2] = z;
color.setHSL(
THREE.MathUtils.randFloat(0.52, 0.64),
THREE.MathUtils.randFloat(0.15, 0.45),
THREE.MathUtils.randFloat(0.72, 0.96)
);
colors[i * 3] = color.r;
colors[i * 3 + 1] = color.g;
colors[i * 3 + 2] = color.b;
sizes[i] = Math.pow(Math.random(), 1.9);
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
geometry.setAttribute('sizeNoise', new THREE.BufferAttribute(sizes, 1));
const material = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
intensity: { value: this.params.starIntensity }
},
vertexShader: `
attribute float sizeNoise;
varying vec3 vColor;
varying float vPulse;
uniform float time;
uniform float intensity;
void main() {
vColor = color;
vPulse = fract(sizeNoise * 17.0 + time * (0.015 + sizeNoise * 0.035));
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * mvPosition;
float starSize = 2.8 + sizeNoise * 6.4;
float projectedSize = starSize * (6500.0 / -mvPosition.z) * (0.65 + intensity * 0.55);
gl_PointSize = max(1.6, projectedSize);
}
`,
fragmentShader: `
varying vec3 vColor;
varying float vPulse;
uniform float intensity;
void main() {
vec2 p = gl_PointCoord - vec2(0.5);
float d = length(p);
float core = smoothstep(0.26, 0.0, d);
float glow = smoothstep(0.58, 0.06, d);
float halo = smoothstep(0.82, 0.16, d);
float twinkle = 0.8 + 0.2 * sin(vPulse * 6.2831);
float alpha = (core * 1.2 + glow * 0.65 + halo * 0.18) * twinkle * intensity;
if (alpha <= 0.001) discard;
gl_FragColor = vec4(vColor, alpha);
}
`,
transparent: true,
depthTest: false,
depthWrite: false,
blending: THREE.AdditiveBlending,
vertexColors: true
});
this.starField = new THREE.Points(geometry, material);
this.starField.frustumCulled = false;
this.scene.add(this.starField);
this.updateStars();
}
initNightSky() {
const moonTexture = this.createMoonTexture();
const moonMaterial = new THREE.SpriteMaterial({
map: moonTexture,
color: 0xe9f0ff,
transparent: true,
depthWrite: false,
depthTest: false,
blending: THREE.AdditiveBlending
});
this.moonSprite = new THREE.Sprite(moonMaterial);
this.moonSprite.scale.setScalar(980);
this.scene.add(this.moonSprite);
const galaxyTexture = this.createGalaxyTexture();
const galaxyMaterial = new THREE.SpriteMaterial({
map: galaxyTexture,
color: 0xa7bedf,
transparent: true,
opacity: 0,
depthWrite: false,
depthTest: false,
blending: THREE.AdditiveBlending
});
this.galaxyBand = new THREE.Sprite(galaxyMaterial);
this.galaxyBand.scale.set(2600, 760, 1);
this.scene.add(this.galaxyBand);
}
createMoonTexture() {
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
const ctx = canvas.getContext('2d');
const glow = ctx.createRadialGradient(128, 128, 0, 128, 128, 128);
glow.addColorStop(0, 'rgba(255,255,255,0.98)');
glow.addColorStop(0.2, 'rgba(244,248,255,0.98)');
glow.addColorStop(0.34, 'rgba(228,238,255,0.96)');
glow.addColorStop(0.58, 'rgba(175,205,255,0.42)');
glow.addColorStop(0.82, 'rgba(126,164,235,0.12)');
glow.addColorStop(1, 'rgba(120,150,220,0)');
ctx.fillStyle = glow;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = 'multiply';
ctx.fillStyle = 'rgba(150,170,210,0.18)';
ctx.beginPath();
ctx.arc(152, 104, 34, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(98, 152, 20, 0, Math.PI * 2);
ctx.fill();
ctx.globalCompositeOperation = 'source-over';
const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace = THREE.SRGBColorSpace;
return texture;
}
createGalaxyTexture() {
const canvas = document.createElement('canvas');
canvas.width = 1024;
canvas.height = 256;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
const base = ctx.createLinearGradient(0, canvas.height * 0.5, canvas.width, canvas.height * 0.5);
base.addColorStop(0, 'rgba(0,0,0,0)');
base.addColorStop(0.16, 'rgba(110,130,170,0.04)');
base.addColorStop(0.35, 'rgba(165,180,215,0.1)');
base.addColorStop(0.5, 'rgba(235,235,255,0.16)');
base.addColorStop(0.68, 'rgba(165,180,215,0.1)');
base.addColorStop(0.84, 'rgba(110,130,170,0.04)');
base.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = base;
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < 1800; i++) {
const x = Math.random() * canvas.width;
const yCenter = canvas.height * 0.52 + Math.sin(x * 0.008) * 22.0;
const y = yCenter + (Math.random() - 0.5) * 120;
const alpha = Math.random() * Math.random() * 0.7;
const size = Math.random() < 0.06 ? 2.2 : 1.0;
ctx.fillStyle = `rgba(255,255,255,${alpha})`;
ctx.fillRect(x, y, size, size);
}
const blur = ctx.createRadialGradient(canvas.width * 0.5, canvas.height * 0.5, 10, canvas.width * 0.5, canvas.height * 0.5, canvas.width * 0.5);
blur.addColorStop(0, 'rgba(210,220,255,0.09)');
blur.addColorStop(0.5, 'rgba(150,170,220,0.04)');
blur.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = blur;
ctx.fillRect(0, 0, canvas.width, canvas.height);
const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace = THREE.SRGBColorSpace;
return texture;
}
initClouds() {
this.cloudGroup = new THREE.Group();
this.cloudGroup.position.y = 40;
@@ -1043,6 +1241,22 @@ export class OceanScene {
this.water.rotation.x = -Math.PI / 2;
this.water.position.y = -0.15;
this.setWaterColor(this.params.waterColor);
const baseWaterOnBeforeRender = this.water.onBeforeRender.bind(this.water);
this.water.onBeforeRender = (...args) => {
const starsWereVisible = this.starField?.visible ?? false;
if (this.starField) {
this.starField.visible = false;
}
try {
baseWaterOnBeforeRender(...args);
} finally {
if (this.starField) {
this.starField.visible = starsWereVisible;
}
}
};
this.scene.add(this.water);
}
@@ -1135,6 +1349,25 @@ export class OceanScene {
this.sun.y * sunDistance,
this.sun.z * sunDistance
);
const dayMix = THREE.MathUtils.clamp((this.sun.y + 0.06) / 0.52, 0, 1);
this.sunLight.intensity = THREE.MathUtils.lerp(0.0, 1.5, dayMix);
}
const nightMix = THREE.MathUtils.clamp((-this.sun.y + 0.02) / 0.72, 0, 1);
if (this.ambientLight) {
this.ambientLight.color.set(0x8ea0b7).lerp(new THREE.Color(0x425a77), nightMix);
this.ambientLight.intensity = THREE.MathUtils.lerp(0.66, 1.0, nightMix);
}
if (this.moonLight) {
const moonDistance = 115;
this.moonLight.position.set(
-this.sun.x * moonDistance,
Math.max(34, -this.sun.y * moonDistance * 0.5 + 42),
-this.sun.z * moonDistance
);
this.moonLight.intensity = 1.15 * nightMix;
}
if (this.vegetationFillLight) {
@@ -1144,7 +1377,8 @@ export class OceanScene {
Math.max(18, this.sun.y * fillDistance * 0.28 + 24),
-this.sun.z * fillDistance * 0.35 + 28
);
this.vegetationFillLight.intensity = THREE.MathUtils.lerp(0.7, 1.15, THREE.MathUtils.clamp((this.sun.y + 0.2) / 0.9, 0, 1));
const fillDayMix = THREE.MathUtils.clamp((this.sun.y + 0.2) / 0.9, 0, 1);
this.vegetationFillLight.intensity = THREE.MathUtils.lerp(0.5, 1.15, fillDayMix) + nightMix * 0.16;
}
if (this.renderTarget) {
@@ -1159,13 +1393,16 @@ export class OceanScene {
this.updateFog();
this.updateClouds();
this.updateStars();
}
getFogColor() {
const elevation = this.params.elevation;
if (elevation < 0) {
return 0x1a2a3a;
if (elevation < -4) {
return 0x121c2c;
} else if (elevation < 0) {
return 0x1d2b40;
} else if (elevation < 10) {
return 0x4a5a6a;
} else if (elevation < 20) {
@@ -1182,13 +1419,17 @@ export class OceanScene {
getAtmosphereColors() {
const sunMix = THREE.MathUtils.clamp((this.params.elevation + 10) / 100, 0, 1);
const fogColor = new THREE.Color(this.getFogColor());
const nightMix = THREE.MathUtils.clamp((-this.params.elevation + 1.0) / 12.0, 0, 1);
const warmHorizon = new THREE.Color(0xf0c7a3);
const coolHorizon = new THREE.Color(0xcfe0ee);
const horizonColor = warmHorizon.clone().lerp(coolHorizon, sunMix);
const nightHorizon = new THREE.Color(0x27415f);
const horizonColor = warmHorizon.clone().lerp(coolHorizon, sunMix).lerp(nightHorizon, nightMix);
const warmSkyBase = new THREE.Color(0xf6d7b8);
const coolSkyBase = new THREE.Color(0xbfd8eb);
const skyBaseColor = warmSkyBase.clone().lerp(coolSkyBase, sunMix * 0.92);
const skyBlendColor = skyBaseColor.clone().lerp(fogColor, 0.42);
const nightSkyBase = new THREE.Color(0x08111f);
const nightSkyBlend = new THREE.Color(0x182940);
const skyBaseColor = warmSkyBase.clone().lerp(coolSkyBase, sunMix * 0.92).lerp(nightSkyBase, nightMix);
const skyBlendColor = skyBaseColor.clone().lerp(fogColor, 0.42).lerp(nightSkyBlend, nightMix * 0.78);
return { sunMix, fogColor, horizonColor, skyBaseColor, skyBlendColor };
}
@@ -1376,6 +1617,16 @@ export class OceanScene {
}
}
setStarEnabled(value) {
this.params.starEnabled = value;
this.updateStars();
}
setStarIntensity(value) {
this.params.starIntensity = value;
this.updateStars();
}
updateRainAudioState() {
if (this.rainAudioPool.length === 0) return;
@@ -1485,6 +1736,8 @@ export class OceanScene {
this.setSnowIntensity(mergedParams.snowIntensity);
this.setSnowSpeed(mergedParams.snowSpeed);
this.setSnowEnabled(mergedParams.snowEnabled);
this.setStarIntensity(mergedParams.starIntensity);
this.setStarEnabled(mergedParams.starEnabled);
this.setLightningIntensity(mergedParams.lightningIntensity);
this.setLightningEnabled(mergedParams.lightningEnabled);
this.setRainEnabled(mergedParams.rainEnabled);
@@ -1686,6 +1939,46 @@ export class OceanScene {
});
}
updateStars() {
if (!this.starField) return;
const nightFactor = THREE.MathUtils.clamp((12.0 - this.params.elevation) / 16.0, 0.0, 1.0);
const weatherFade = this.params.rainEnabled ? 0.08 : this.params.snowEnabled ? 0.55 : 1.0;
const opacity = (this.params.starEnabled ? this.params.starIntensity : 0.0) * nightFactor * weatherFade;
this.starField.visible = opacity > 0.001;
this.starField.material.uniforms.intensity.value = opacity;
if (this.moonSprite) {
const moonAngle = THREE.MathUtils.degToRad(this.params.azimuth - 42);
const moonDistance = 3200;
this.moonSprite.position.set(
Math.cos(moonAngle) * moonDistance,
THREE.MathUtils.lerp(260, 520, nightFactor),
Math.sin(moonAngle) * moonDistance
);
const moonGlow = THREE.MathUtils.clamp((nightFactor - 0.18) / 0.72, 0, 1);
this.moonSprite.visible = moonGlow > 0.01;
this.moonSprite.material.opacity = THREE.MathUtils.lerp(0.0, 2.4, moonGlow);
const moonScale = THREE.MathUtils.lerp(820, 1460, moonGlow);
this.moonSprite.scale.setScalar(moonScale);
}
if (this.galaxyBand) {
const angle = THREE.MathUtils.degToRad(this.params.azimuth + 36);
const radius = 3000;
this.galaxyBand.position.set(
Math.cos(angle) * radius,
THREE.MathUtils.lerp(920, 1500, nightFactor),
Math.sin(angle) * radius
);
this.galaxyBand.material.rotation = THREE.MathUtils.degToRad(-24);
const galaxyFade = THREE.MathUtils.clamp((opacity - 0.18) / 1.1, 0, 1) * weatherFade;
this.galaxyBand.material.opacity = galaxyFade * 0.92;
this.galaxyBand.visible = this.galaxyBand.material.opacity > 0.01;
}
}
updateFog() {
const { sunMix, fogColor, horizonColor, skyBaseColor, skyBlendColor } = this.getAtmosphereColors();
const fogDensity = THREE.MathUtils.lerp(0.00015, 0.0018, this.params.fogDensity);
@@ -1745,6 +2038,9 @@ export class OceanScene {
this.skyHazeBand.material.opacity =
THREE.MathUtils.lerp(0.04, 0.18, this.params.fogDensity) *
THREE.MathUtils.lerp(0.78, 1.08, this.params.fogRange);
const nightHaze = THREE.MathUtils.clamp((-this.params.elevation + 1.0) / 12.0, 0, 1);
this.skyHazeBand.material.color.lerp(new THREE.Color(0x203754), nightHaze * 0.72);
this.skyHazeBand.material.opacity += nightHaze * 0.12;
this.skyHazeBand.position.y = THREE.MathUtils.lerp(340, 560, this.params.fogHeight);
this.skyHazeBand.scale.set(
THREE.MathUtils.lerp(0.98, 1.12, this.params.fogRange),
@@ -1774,6 +2070,10 @@ export class OceanScene {
});
}
if (this.starField) {
this.starField.material.uniforms.time.value = time;
}
if (this.fogGroup) {
this.fogLayers.forEach((layer, index) => {
layer.texture.offset.x = time * layer.speedX;
@@ -1795,6 +2095,7 @@ export class OceanScene {
if (this.lightningFlash > 0.001) {
this.updateClouds();
this.updateFog();
this.updateStars();
}
if (this.rainPass) {

View File

@@ -68,6 +68,8 @@ function setupControls(oceanScene) {
'snowEnabled',
'snowIntensity',
'snowSpeed',
'starEnabled',
'starIntensity',
'lightningEnabled',
'lightningIntensity'
];
@@ -96,6 +98,8 @@ function setupControls(oceanScene) {
snowEnabled: '是否启用降雪',
snowIntensity: '雪量',
snowSpeed: '降雪速度',
starEnabled: '是否启用星空',
starIntensity: '星空强度',
lightningEnabled: '是否启用雷闪',
lightningIntensity: '雷闪强度'
};
@@ -135,12 +139,25 @@ function setupControls(oceanScene) {
};
const refreshControllers = () => {
controllers.forEach((controller) => controller.updateDisplay());
updateStarControllerState();
};
const setControllerEnabled = (controller, enabled) => {
controller.domElement.style.opacity = enabled ? '1' : '0.45';
controller.domElement.style.pointerEvents = enabled ? 'auto' : 'none';
controller.enable?.();
if (!enabled) {
controller.disable?.();
}
controller.domElement.querySelectorAll('input, select, button').forEach((element) => {
element.disabled = !enabled;
});
};
const bindController = (controller, applyValue) => {
controllers.push(controller);
controller.onChange((value) => {
applyValue(value);
markPresetCustom();
updateStarControllerState();
});
return controller;
};
@@ -154,11 +171,23 @@ function setupControls(oceanScene) {
gui.add(exportActions, '导出预设');
const skyFolder = gui.addFolder('天空');
bindController(skyFolder.add(params, 'elevation', 0, 90, 0.1).name('太阳高度'), (value) => oceanScene.setSunElevation(value));
bindController(skyFolder.add(params, 'elevation', -12, 90, 0.1).name('太阳高度'), (value) => oceanScene.setSunElevation(value));
bindController(skyFolder.add(params, 'azimuth', -180, 180, 0.1).name('太阳方位'), (value) => oceanScene.setSunAzimuth(value));
bindController(skyFolder.add(params, 'exposure', 0, 1, 0.01).name('曝光度'), (value) => oceanScene.setExposure(value));
bindController(skyFolder.add(params, 'turbidity', 1, 20, 0.1).name('浑浊度'), (value) => oceanScene.setTurbidity(value));
bindController(skyFolder.add(params, 'rayleigh', 0, 4, 0.01).name('瑞利散射'), (value) => oceanScene.setRayleigh(value));
const starEnabledController = bindController(skyFolder.add(params, 'starEnabled').name('启用星空'), (value) => oceanScene.setStarEnabled(value));
const starIntensityController = bindController(skyFolder.add(params, 'starIntensity', 0, 1.5, 0.01).name('星空强度'), (value) => oceanScene.setStarIntensity(value));
const updateStarControllerState = () => {
const canUseStars = params.elevation < -1.0;
if (!canUseStars && params.starEnabled) {
oceanScene.setStarEnabled(false);
}
setControllerEnabled(starEnabledController, canUseStars);
setControllerEnabled(starIntensityController, canUseStars);
starEnabledController.updateDisplay();
starIntensityController.updateDisplay();
};
const bloomFolder = gui.addFolder('泛光');
bindController(bloomFolder.add(params, 'bloomStrength', 0, 1, 0.01).name('强度'), (value) => oceanScene.setBloomStrength(value));
@@ -194,6 +223,7 @@ function setupControls(oceanScene) {
bindController(snowFolder.add(params, 'snowSpeed', 0.2, 2.2, 0.01).name('速度'), (value) => oceanScene.setSnowSpeed(value));
gui.close();
updateStarControllerState();
}
main().catch(console.error);

View File

@@ -24,6 +24,8 @@ export const DEFAULT_SCENE_PARAMS = {
snowEnabled: false,
snowIntensity: 0.65,
snowSpeed: 0.85,
starEnabled: true,
starIntensity: 0.7,
lightningEnabled: false,
lightningIntensity: 0.75,
mieCoefficient: 0.005,
@@ -55,6 +57,7 @@ export const WEATHER_PRESETS = {
fogDensity: 0.12,
fogHeight: 0.58,
fogRange: 0.28,
starIntensity: 0.22,
lightningEnabled: false
}
},
@@ -76,6 +79,32 @@ export const WEATHER_PRESETS = {
fogDensity: 0.1,
fogHeight: 0.2,
fogRange: 0.24,
starIntensity: 0.0,
lightningEnabled: false
}
},
night: {
label: '黑夜',
params: {
...DEFAULT_SCENE_PARAMS,
elevation: -8,
azimuth: 205,
exposure: 0.2,
turbidity: 1.8,
rayleigh: 0.2,
bloomStrength: 0.12,
bloomRadius: 0.08,
waterColor: '#07131f',
cloudCoverage: 0.03,
cloudDensity: 0.08,
cloudElevation: 0.78,
fogDensity: 0.08,
fogHeight: 0.22,
fogRange: 0.2,
rainEnabled: false,
snowEnabled: false,
starEnabled: true,
starIntensity: 0.4,
lightningEnabled: false
}
},
@@ -97,6 +126,7 @@ export const WEATHER_PRESETS = {
fogDensity: 0.38,
fogHeight: 0.36,
fogRange: 0.62,
starIntensity: 0.04,
lightningEnabled: false
}
},
@@ -125,6 +155,7 @@ export const WEATHER_PRESETS = {
rainSpeed: 1.16,
rainAudioEnabled: true,
rainAudioVolume: 0.38,
starIntensity: 0.02,
lightningEnabled: true,
lightningIntensity: 0.68,
snowEnabled: false
@@ -148,6 +179,7 @@ export const WEATHER_PRESETS = {
fogDensity: 0.56,
fogHeight: 0.42,
fogRange: 0.74,
starIntensity: 0.16,
rainEnabled: false,
lightningEnabled: false,
snowEnabled: true,