feat: 电影级真实感重构 — 真实月面/银河/泛光/撕裂破碎

按用户参考图(破碎月亮)对齐的整套真实感升级:
- 月球: SSS/NASA 2K真实纹理+运行时亮度转凹凸, 顶点撕裂伤口(不规则锯齿轮廓)+发光裂缝(emissive放射纹+断面代码)+伤口辉光脉动
- 破碎: 2块大碎片(顶点扰动)+220块Instanced碎石量级谱(伤口切线拖带,各自漂移翻滚)+5团青灰尘埃云(烟尘流动)
- 太空: 真实星野天球(SSS银河2K)替换程序点星, 保留近景尘埃视差层
- 太阳: 亮度>1的小烈核心(推过泛光阈值)+克制光晕+变形宽银幕横向光纹
- 渲染: ACES电影色调映射+UnrealBloomPass泛光(three.module+8个后处理文件本地化,importmap加载)
- 移动端降级: 关泛光+减粒子
- 页脚: NASA/SSS纹理CC-BY署名
This commit is contained in:
leefer
2026-08-02 13:11:16 +08:00
parent 40dd2fffff
commit d5d4623aa9
16 changed files with 54631 additions and 284 deletions
+6 -2
View File
@@ -247,13 +247,17 @@
</div> </div>
<div class="footer-bottom"> <div class="footer-bottom">
<span>&copy; 2026 郑州晟算科技有限公司</span> <span>&copy; 2026 郑州晟算科技有限公司</span>
<span style="opacity:0.45;font-size:11px">Moon &amp; Stars Imagery: NASA · Solar System Scope (CC BY 4.0)</span>
<span>ground_control@shengsuan:~$ repairing_moon --progress 85%</span> <span>ground_control@shengsuan:~$ repairing_moon --progress 85%</span>
</div> </div>
</div> </div>
</footer> </footer>
<script src="js/lib/three.min.js"></script> <script type="importmap">
<script src="js/space.js"></script> {"imports":{"three":"./js/lib/three.module.js","three/addons/":"./js/lib/addons/"}}
</script>
<script>window.SPACE_MODE=document.body.getAttribute("data-space")||"orbit";</script>
<script type="module" src="js/space.js"></script>
<script src="js/main.js"></script> <script src="js/main.js"></script>
</body> </body>
</html> </html>
+6 -2
View File
@@ -237,6 +237,7 @@
</div> </div>
<div class="footer-bottom"> <div class="footer-bottom">
<span>&copy; 2026 郑州晟算科技有限公司</span> <span>&copy; 2026 郑州晟算科技有限公司</span>
<span style="opacity:0.45;font-size:11px">Moon &amp; Stars Imagery: NASA · Solar System Scope (CC BY 4.0)</span>
<span>ground_control@shengsuan:~$ repairing_moon --progress 85%</span> <span>ground_control@shengsuan:~$ repairing_moon --progress 85%</span>
</div> </div>
</div> </div>
@@ -245,8 +246,11 @@
<style> <style>
@keyframes radarSweep { to { transform: rotate(360deg); } } @keyframes radarSweep { to { transform: rotate(360deg); } }
</style> </style>
<script src="js/lib/three.min.js"></script> <script type="importmap">
<script src="js/space.js"></script> {"imports":{"three":"./js/lib/three.module.js","three/addons/":"./js/lib/addons/"}}
</script>
<script>window.SPACE_MODE=document.body.getAttribute("data-space")||"orbit";</script>
<script type="module" src="js/space.js"></script>
<script src="js/main.js"></script> <script src="js/main.js"></script>
</body> </body>
</html> </html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

+6 -2
View File
@@ -411,13 +411,17 @@
</div> </div>
<div class="footer-bottom"> <div class="footer-bottom">
<span>&copy; 2026 郑州晟算科技有限公司</span> <span>&copy; 2026 郑州晟算科技有限公司</span>
<span style="opacity:0.45;font-size:11px">Moon &amp; Stars Imagery: NASA · Solar System Scope (CC BY 4.0)</span>
<span>ground_control@shengsuan:~$ repairing_moon --progress 85%</span> <span>ground_control@shengsuan:~$ repairing_moon --progress 85%</span>
</div> </div>
</div> </div>
</footer> </footer>
<script src="js/lib/three.min.js"></script> <script type="importmap">
<script src="js/space.js"></script> {"imports":{"three":"./js/lib/three.module.js","three/addons/":"./js/lib/addons/"}}
</script>
<script>window.SPACE_MODE=document.body.getAttribute("data-space")||"journey";</script>
<script type="module" src="js/space.js"></script>
<script src="js/main.js"></script> <script src="js/main.js"></script>
</body> </body>
</html> </html>
@@ -0,0 +1,231 @@
import {
Clock,
HalfFloatType,
NoBlending,
Vector2,
WebGLRenderTarget
} from 'three';
import { CopyShader } from '../shaders/CopyShader.js';
import { ShaderPass } from './ShaderPass.js';
import { MaskPass } from './MaskPass.js';
import { ClearMaskPass } from './MaskPass.js';
class EffectComposer {
constructor( renderer, renderTarget ) {
this.renderer = renderer;
this._pixelRatio = renderer.getPixelRatio();
if ( renderTarget === undefined ) {
const size = renderer.getSize( new Vector2() );
this._width = size.width;
this._height = size.height;
renderTarget = new WebGLRenderTarget( this._width * this._pixelRatio, this._height * this._pixelRatio, { type: HalfFloatType } );
renderTarget.texture.name = 'EffectComposer.rt1';
} else {
this._width = renderTarget.width;
this._height = renderTarget.height;
}
this.renderTarget1 = renderTarget;
this.renderTarget2 = renderTarget.clone();
this.renderTarget2.texture.name = 'EffectComposer.rt2';
this.writeBuffer = this.renderTarget1;
this.readBuffer = this.renderTarget2;
this.renderToScreen = true;
this.passes = [];
this.copyPass = new ShaderPass( CopyShader );
this.copyPass.material.blending = NoBlending;
this.clock = new Clock();
}
swapBuffers() {
const tmp = this.readBuffer;
this.readBuffer = this.writeBuffer;
this.writeBuffer = tmp;
}
addPass( pass ) {
this.passes.push( pass );
pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
}
insertPass( pass, index ) {
this.passes.splice( index, 0, pass );
pass.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
}
removePass( pass ) {
const index = this.passes.indexOf( pass );
if ( index !== - 1 ) {
this.passes.splice( index, 1 );
}
}
isLastEnabledPass( passIndex ) {
for ( let i = passIndex + 1; i < this.passes.length; i ++ ) {
if ( this.passes[ i ].enabled ) {
return false;
}
}
return true;
}
render( deltaTime ) {
// deltaTime value is in seconds
if ( deltaTime === undefined ) {
deltaTime = this.clock.getDelta();
}
const currentRenderTarget = this.renderer.getRenderTarget();
let maskActive = false;
for ( let i = 0, il = this.passes.length; i < il; i ++ ) {
const pass = this.passes[ i ];
if ( pass.enabled === false ) continue;
pass.renderToScreen = ( this.renderToScreen && this.isLastEnabledPass( i ) );
pass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime, maskActive );
if ( pass.needsSwap ) {
if ( maskActive ) {
const context = this.renderer.getContext();
const stencil = this.renderer.state.buffers.stencil;
//context.stencilFunc( context.NOTEQUAL, 1, 0xffffffff );
stencil.setFunc( context.NOTEQUAL, 1, 0xffffffff );
this.copyPass.render( this.renderer, this.writeBuffer, this.readBuffer, deltaTime );
//context.stencilFunc( context.EQUAL, 1, 0xffffffff );
stencil.setFunc( context.EQUAL, 1, 0xffffffff );
}
this.swapBuffers();
}
if ( MaskPass !== undefined ) {
if ( pass instanceof MaskPass ) {
maskActive = true;
} else if ( pass instanceof ClearMaskPass ) {
maskActive = false;
}
}
}
this.renderer.setRenderTarget( currentRenderTarget );
}
reset( renderTarget ) {
if ( renderTarget === undefined ) {
const size = this.renderer.getSize( new Vector2() );
this._pixelRatio = this.renderer.getPixelRatio();
this._width = size.width;
this._height = size.height;
renderTarget = this.renderTarget1.clone();
renderTarget.setSize( this._width * this._pixelRatio, this._height * this._pixelRatio );
}
this.renderTarget1.dispose();
this.renderTarget2.dispose();
this.renderTarget1 = renderTarget;
this.renderTarget2 = renderTarget.clone();
this.writeBuffer = this.renderTarget1;
this.readBuffer = this.renderTarget2;
}
setSize( width, height ) {
this._width = width;
this._height = height;
const effectiveWidth = this._width * this._pixelRatio;
const effectiveHeight = this._height * this._pixelRatio;
this.renderTarget1.setSize( effectiveWidth, effectiveHeight );
this.renderTarget2.setSize( effectiveWidth, effectiveHeight );
for ( let i = 0; i < this.passes.length; i ++ ) {
this.passes[ i ].setSize( effectiveWidth, effectiveHeight );
}
}
setPixelRatio( pixelRatio ) {
this._pixelRatio = pixelRatio;
this.setSize( this._width, this._height );
}
dispose() {
this.renderTarget1.dispose();
this.renderTarget2.dispose();
this.copyPass.dispose();
}
}
export { EffectComposer };
+104
View File
@@ -0,0 +1,104 @@
import { Pass } from './Pass.js';
class MaskPass extends Pass {
constructor( scene, camera ) {
super();
this.scene = scene;
this.camera = camera;
this.clear = true;
this.needsSwap = false;
this.inverse = false;
}
render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
const context = renderer.getContext();
const state = renderer.state;
// don't update color or depth
state.buffers.color.setMask( false );
state.buffers.depth.setMask( false );
// lock buffers
state.buffers.color.setLocked( true );
state.buffers.depth.setLocked( true );
// set up stencil
let writeValue, clearValue;
if ( this.inverse ) {
writeValue = 0;
clearValue = 1;
} else {
writeValue = 1;
clearValue = 0;
}
state.buffers.stencil.setTest( true );
state.buffers.stencil.setOp( context.REPLACE, context.REPLACE, context.REPLACE );
state.buffers.stencil.setFunc( context.ALWAYS, writeValue, 0xffffffff );
state.buffers.stencil.setClear( clearValue );
state.buffers.stencil.setLocked( true );
// draw into the stencil buffer
renderer.setRenderTarget( readBuffer );
if ( this.clear ) renderer.clear();
renderer.render( this.scene, this.camera );
renderer.setRenderTarget( writeBuffer );
if ( this.clear ) renderer.clear();
renderer.render( this.scene, this.camera );
// unlock color and depth buffer and make them writable for subsequent rendering/clearing
state.buffers.color.setLocked( false );
state.buffers.depth.setLocked( false );
state.buffers.color.setMask( true );
state.buffers.depth.setMask( true );
// only render where stencil is set to 1
state.buffers.stencil.setLocked( false );
state.buffers.stencil.setFunc( context.EQUAL, 1, 0xffffffff ); // draw if == 1
state.buffers.stencil.setOp( context.KEEP, context.KEEP, context.KEEP );
state.buffers.stencil.setLocked( true );
}
}
class ClearMaskPass extends Pass {
constructor() {
super();
this.needsSwap = false;
}
render( renderer /*, writeBuffer, readBuffer, deltaTime, maskActive */ ) {
renderer.state.buffers.stencil.setLocked( false );
renderer.state.buffers.stencil.setTest( false );
}
}
export { MaskPass, ClearMaskPass };
+95
View File
@@ -0,0 +1,95 @@
import {
BufferGeometry,
Float32BufferAttribute,
OrthographicCamera,
Mesh
} from 'three';
class Pass {
constructor() {
this.isPass = true;
// if set to true, the pass is processed by the composer
this.enabled = true;
// if set to true, the pass indicates to swap read and write buffer after rendering
this.needsSwap = true;
// if set to true, the pass clears its buffer before rendering
this.clear = false;
// if set to true, the result of the pass is rendered to screen. This is set automatically by EffectComposer.
this.renderToScreen = false;
}
setSize( /* width, height */ ) {}
render( /* renderer, writeBuffer, readBuffer, deltaTime, maskActive */ ) {
console.error( 'THREE.Pass: .render() must be implemented in derived pass.' );
}
dispose() {}
}
// Helper for passes that need to fill the viewport with a single quad.
const _camera = new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
// https://github.com/mrdoob/three.js/pull/21358
class FullscreenTriangleGeometry extends BufferGeometry {
constructor() {
super();
this.setAttribute( 'position', new Float32BufferAttribute( [ - 1, 3, 0, - 1, - 1, 0, 3, - 1, 0 ], 3 ) );
this.setAttribute( 'uv', new Float32BufferAttribute( [ 0, 2, 0, 0, 2, 0 ], 2 ) );
}
}
const _geometry = new FullscreenTriangleGeometry();
class FullScreenQuad {
constructor( material ) {
this._mesh = new Mesh( _geometry, material );
}
dispose() {
this._mesh.geometry.dispose();
}
render( renderer ) {
renderer.render( this._mesh, _camera );
}
get material() {
return this._mesh.material;
}
set material( value ) {
this._mesh.material = value;
}
}
export { Pass, FullScreenQuad };
@@ -0,0 +1,99 @@
import {
Color
} from 'three';
import { Pass } from './Pass.js';
class RenderPass extends Pass {
constructor( scene, camera, overrideMaterial = null, clearColor = null, clearAlpha = null ) {
super();
this.scene = scene;
this.camera = camera;
this.overrideMaterial = overrideMaterial;
this.clearColor = clearColor;
this.clearAlpha = clearAlpha;
this.clear = true;
this.clearDepth = false;
this.needsSwap = false;
this._oldClearColor = new Color();
}
render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
const oldAutoClear = renderer.autoClear;
renderer.autoClear = false;
let oldClearAlpha, oldOverrideMaterial;
if ( this.overrideMaterial !== null ) {
oldOverrideMaterial = this.scene.overrideMaterial;
this.scene.overrideMaterial = this.overrideMaterial;
}
if ( this.clearColor !== null ) {
renderer.getClearColor( this._oldClearColor );
renderer.setClearColor( this.clearColor );
}
if ( this.clearAlpha !== null ) {
oldClearAlpha = renderer.getClearAlpha();
renderer.setClearAlpha( this.clearAlpha );
}
if ( this.clearDepth == true ) {
renderer.clearDepth();
}
renderer.setRenderTarget( this.renderToScreen ? null : readBuffer );
if ( this.clear === true ) {
// TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600
renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
}
renderer.render( this.scene, this.camera );
// restore
if ( this.clearColor !== null ) {
renderer.setClearColor( this._oldClearColor );
}
if ( this.clearAlpha !== null ) {
renderer.setClearAlpha( oldClearAlpha );
}
if ( this.overrideMaterial !== null ) {
this.scene.overrideMaterial = oldOverrideMaterial;
}
renderer.autoClear = oldAutoClear;
}
}
export { RenderPass };
@@ -0,0 +1,77 @@
import {
ShaderMaterial,
UniformsUtils
} from 'three';
import { Pass, FullScreenQuad } from './Pass.js';
class ShaderPass extends Pass {
constructor( shader, textureID ) {
super();
this.textureID = ( textureID !== undefined ) ? textureID : 'tDiffuse';
if ( shader instanceof ShaderMaterial ) {
this.uniforms = shader.uniforms;
this.material = shader;
} else if ( shader ) {
this.uniforms = UniformsUtils.clone( shader.uniforms );
this.material = new ShaderMaterial( {
name: ( shader.name !== undefined ) ? shader.name : 'unspecified',
defines: Object.assign( {}, shader.defines ),
uniforms: this.uniforms,
vertexShader: shader.vertexShader,
fragmentShader: shader.fragmentShader
} );
}
this.fsQuad = new FullScreenQuad( this.material );
}
render( renderer, writeBuffer, readBuffer /*, deltaTime, maskActive */ ) {
if ( this.uniforms[ this.textureID ] ) {
this.uniforms[ this.textureID ].value = readBuffer.texture;
}
this.fsQuad.material = this.material;
if ( this.renderToScreen ) {
renderer.setRenderTarget( null );
this.fsQuad.render( renderer );
} else {
renderer.setRenderTarget( writeBuffer );
// TODO: Avoid using autoClear properties, see https://github.com/mrdoob/three.js/pull/15571#issuecomment-465669600
if ( this.clear ) renderer.clear( renderer.autoClearColor, renderer.autoClearDepth, renderer.autoClearStencil );
this.fsQuad.render( renderer );
}
}
dispose() {
this.material.dispose();
this.fsQuad.dispose();
}
}
export { ShaderPass };
@@ -0,0 +1,415 @@
import {
AdditiveBlending,
Color,
HalfFloatType,
MeshBasicMaterial,
ShaderMaterial,
UniformsUtils,
Vector2,
Vector3,
WebGLRenderTarget
} from 'three';
import { Pass, FullScreenQuad } from './Pass.js';
import { CopyShader } from '../shaders/CopyShader.js';
import { LuminosityHighPassShader } from '../shaders/LuminosityHighPassShader.js';
/**
* UnrealBloomPass is inspired by the bloom pass of Unreal Engine. It creates a
* mip map chain of bloom textures and blurs them with different radii. Because
* of the weighted combination of mips, and because larger blurs are done on
* higher mips, this effect provides good quality and performance.
*
* Reference:
* - https://docs.unrealengine.com/latest/INT/Engine/Rendering/PostProcessEffects/Bloom/
*/
class UnrealBloomPass extends Pass {
constructor( resolution, strength, radius, threshold ) {
super();
this.strength = ( strength !== undefined ) ? strength : 1;
this.radius = radius;
this.threshold = threshold;
this.resolution = ( resolution !== undefined ) ? new Vector2( resolution.x, resolution.y ) : new Vector2( 256, 256 );
// create color only once here, reuse it later inside the render function
this.clearColor = new Color( 0, 0, 0 );
// render targets
this.renderTargetsHorizontal = [];
this.renderTargetsVertical = [];
this.nMips = 5;
let resx = Math.round( this.resolution.x / 2 );
let resy = Math.round( this.resolution.y / 2 );
this.renderTargetBright = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
this.renderTargetBright.texture.name = 'UnrealBloomPass.bright';
this.renderTargetBright.texture.generateMipmaps = false;
for ( let i = 0; i < this.nMips; i ++ ) {
const renderTargetHorizonal = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
renderTargetHorizonal.texture.name = 'UnrealBloomPass.h' + i;
renderTargetHorizonal.texture.generateMipmaps = false;
this.renderTargetsHorizontal.push( renderTargetHorizonal );
const renderTargetVertical = new WebGLRenderTarget( resx, resy, { type: HalfFloatType } );
renderTargetVertical.texture.name = 'UnrealBloomPass.v' + i;
renderTargetVertical.texture.generateMipmaps = false;
this.renderTargetsVertical.push( renderTargetVertical );
resx = Math.round( resx / 2 );
resy = Math.round( resy / 2 );
}
// luminosity high pass material
const highPassShader = LuminosityHighPassShader;
this.highPassUniforms = UniformsUtils.clone( highPassShader.uniforms );
this.highPassUniforms[ 'luminosityThreshold' ].value = threshold;
this.highPassUniforms[ 'smoothWidth' ].value = 0.01;
this.materialHighPassFilter = new ShaderMaterial( {
uniforms: this.highPassUniforms,
vertexShader: highPassShader.vertexShader,
fragmentShader: highPassShader.fragmentShader
} );
// gaussian blur materials
this.separableBlurMaterials = [];
const kernelSizeArray = [ 3, 5, 7, 9, 11 ];
resx = Math.round( this.resolution.x / 2 );
resy = Math.round( this.resolution.y / 2 );
for ( let i = 0; i < this.nMips; i ++ ) {
this.separableBlurMaterials.push( this.getSeperableBlurMaterial( kernelSizeArray[ i ] ) );
this.separableBlurMaterials[ i ].uniforms[ 'invSize' ].value = new Vector2( 1 / resx, 1 / resy );
resx = Math.round( resx / 2 );
resy = Math.round( resy / 2 );
}
// composite material
this.compositeMaterial = this.getCompositeMaterial( this.nMips );
this.compositeMaterial.uniforms[ 'blurTexture1' ].value = this.renderTargetsVertical[ 0 ].texture;
this.compositeMaterial.uniforms[ 'blurTexture2' ].value = this.renderTargetsVertical[ 1 ].texture;
this.compositeMaterial.uniforms[ 'blurTexture3' ].value = this.renderTargetsVertical[ 2 ].texture;
this.compositeMaterial.uniforms[ 'blurTexture4' ].value = this.renderTargetsVertical[ 3 ].texture;
this.compositeMaterial.uniforms[ 'blurTexture5' ].value = this.renderTargetsVertical[ 4 ].texture;
this.compositeMaterial.uniforms[ 'bloomStrength' ].value = strength;
this.compositeMaterial.uniforms[ 'bloomRadius' ].value = 0.1;
const bloomFactors = [ 1.0, 0.8, 0.6, 0.4, 0.2 ];
this.compositeMaterial.uniforms[ 'bloomFactors' ].value = bloomFactors;
this.bloomTintColors = [ new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ), new Vector3( 1, 1, 1 ) ];
this.compositeMaterial.uniforms[ 'bloomTintColors' ].value = this.bloomTintColors;
// blend material
const copyShader = CopyShader;
this.copyUniforms = UniformsUtils.clone( copyShader.uniforms );
this.blendMaterial = new ShaderMaterial( {
uniforms: this.copyUniforms,
vertexShader: copyShader.vertexShader,
fragmentShader: copyShader.fragmentShader,
blending: AdditiveBlending,
depthTest: false,
depthWrite: false,
transparent: true
} );
this.enabled = true;
this.needsSwap = false;
this._oldClearColor = new Color();
this.oldClearAlpha = 1;
this.basic = new MeshBasicMaterial();
this.fsQuad = new FullScreenQuad( null );
}
dispose() {
for ( let i = 0; i < this.renderTargetsHorizontal.length; i ++ ) {
this.renderTargetsHorizontal[ i ].dispose();
}
for ( let i = 0; i < this.renderTargetsVertical.length; i ++ ) {
this.renderTargetsVertical[ i ].dispose();
}
this.renderTargetBright.dispose();
//
for ( let i = 0; i < this.separableBlurMaterials.length; i ++ ) {
this.separableBlurMaterials[ i ].dispose();
}
this.compositeMaterial.dispose();
this.blendMaterial.dispose();
this.basic.dispose();
//
this.fsQuad.dispose();
}
setSize( width, height ) {
let resx = Math.round( width / 2 );
let resy = Math.round( height / 2 );
this.renderTargetBright.setSize( resx, resy );
for ( let i = 0; i < this.nMips; i ++ ) {
this.renderTargetsHorizontal[ i ].setSize( resx, resy );
this.renderTargetsVertical[ i ].setSize( resx, resy );
this.separableBlurMaterials[ i ].uniforms[ 'invSize' ].value = new Vector2( 1 / resx, 1 / resy );
resx = Math.round( resx / 2 );
resy = Math.round( resy / 2 );
}
}
render( renderer, writeBuffer, readBuffer, deltaTime, maskActive ) {
renderer.getClearColor( this._oldClearColor );
this.oldClearAlpha = renderer.getClearAlpha();
const oldAutoClear = renderer.autoClear;
renderer.autoClear = false;
renderer.setClearColor( this.clearColor, 0 );
if ( maskActive ) renderer.state.buffers.stencil.setTest( false );
// Render input to screen
if ( this.renderToScreen ) {
this.fsQuad.material = this.basic;
this.basic.map = readBuffer.texture;
renderer.setRenderTarget( null );
renderer.clear();
this.fsQuad.render( renderer );
}
// 1. Extract Bright Areas
this.highPassUniforms[ 'tDiffuse' ].value = readBuffer.texture;
this.highPassUniforms[ 'luminosityThreshold' ].value = this.threshold;
this.fsQuad.material = this.materialHighPassFilter;
renderer.setRenderTarget( this.renderTargetBright );
renderer.clear();
this.fsQuad.render( renderer );
// 2. Blur All the mips progressively
let inputRenderTarget = this.renderTargetBright;
for ( let i = 0; i < this.nMips; i ++ ) {
this.fsQuad.material = this.separableBlurMaterials[ i ];
this.separableBlurMaterials[ i ].uniforms[ 'colorTexture' ].value = inputRenderTarget.texture;
this.separableBlurMaterials[ i ].uniforms[ 'direction' ].value = UnrealBloomPass.BlurDirectionX;
renderer.setRenderTarget( this.renderTargetsHorizontal[ i ] );
renderer.clear();
this.fsQuad.render( renderer );
this.separableBlurMaterials[ i ].uniforms[ 'colorTexture' ].value = this.renderTargetsHorizontal[ i ].texture;
this.separableBlurMaterials[ i ].uniforms[ 'direction' ].value = UnrealBloomPass.BlurDirectionY;
renderer.setRenderTarget( this.renderTargetsVertical[ i ] );
renderer.clear();
this.fsQuad.render( renderer );
inputRenderTarget = this.renderTargetsVertical[ i ];
}
// Composite All the mips
this.fsQuad.material = this.compositeMaterial;
this.compositeMaterial.uniforms[ 'bloomStrength' ].value = this.strength;
this.compositeMaterial.uniforms[ 'bloomRadius' ].value = this.radius;
this.compositeMaterial.uniforms[ 'bloomTintColors' ].value = this.bloomTintColors;
renderer.setRenderTarget( this.renderTargetsHorizontal[ 0 ] );
renderer.clear();
this.fsQuad.render( renderer );
// Blend it additively over the input texture
this.fsQuad.material = this.blendMaterial;
this.copyUniforms[ 'tDiffuse' ].value = this.renderTargetsHorizontal[ 0 ].texture;
if ( maskActive ) renderer.state.buffers.stencil.setTest( true );
if ( this.renderToScreen ) {
renderer.setRenderTarget( null );
this.fsQuad.render( renderer );
} else {
renderer.setRenderTarget( readBuffer );
this.fsQuad.render( renderer );
}
// Restore renderer settings
renderer.setClearColor( this._oldClearColor, this.oldClearAlpha );
renderer.autoClear = oldAutoClear;
}
getSeperableBlurMaterial( kernelRadius ) {
const coefficients = [];
for ( let i = 0; i < kernelRadius; i ++ ) {
coefficients.push( 0.39894 * Math.exp( - 0.5 * i * i / ( kernelRadius * kernelRadius ) ) / kernelRadius );
}
return new ShaderMaterial( {
defines: {
'KERNEL_RADIUS': kernelRadius
},
uniforms: {
'colorTexture': { value: null },
'invSize': { value: new Vector2( 0.5, 0.5 ) }, // inverse texture size
'direction': { value: new Vector2( 0.5, 0.5 ) },
'gaussianCoefficients': { value: coefficients } // precomputed Gaussian coefficients
},
vertexShader:
`varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader:
`#include <common>
varying vec2 vUv;
uniform sampler2D colorTexture;
uniform vec2 invSize;
uniform vec2 direction;
uniform float gaussianCoefficients[KERNEL_RADIUS];
void main() {
float weightSum = gaussianCoefficients[0];
vec3 diffuseSum = texture2D( colorTexture, vUv ).rgb * weightSum;
for( int i = 1; i < KERNEL_RADIUS; i ++ ) {
float x = float(i);
float w = gaussianCoefficients[i];
vec2 uvOffset = direction * invSize * x;
vec3 sample1 = texture2D( colorTexture, vUv + uvOffset ).rgb;
vec3 sample2 = texture2D( colorTexture, vUv - uvOffset ).rgb;
diffuseSum += (sample1 + sample2) * w;
weightSum += 2.0 * w;
}
gl_FragColor = vec4(diffuseSum/weightSum, 1.0);
}`
} );
}
getCompositeMaterial( nMips ) {
return new ShaderMaterial( {
defines: {
'NUM_MIPS': nMips
},
uniforms: {
'blurTexture1': { value: null },
'blurTexture2': { value: null },
'blurTexture3': { value: null },
'blurTexture4': { value: null },
'blurTexture5': { value: null },
'bloomStrength': { value: 1.0 },
'bloomFactors': { value: null },
'bloomTintColors': { value: null },
'bloomRadius': { value: 0.0 }
},
vertexShader:
`varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader:
`varying vec2 vUv;
uniform sampler2D blurTexture1;
uniform sampler2D blurTexture2;
uniform sampler2D blurTexture3;
uniform sampler2D blurTexture4;
uniform sampler2D blurTexture5;
uniform float bloomStrength;
uniform float bloomRadius;
uniform float bloomFactors[NUM_MIPS];
uniform vec3 bloomTintColors[NUM_MIPS];
float lerpBloomFactor(const in float factor) {
float mirrorFactor = 1.2 - factor;
return mix(factor, mirrorFactor, bloomRadius);
}
void main() {
gl_FragColor = bloomStrength * ( lerpBloomFactor(bloomFactors[0]) * vec4(bloomTintColors[0], 1.0) * texture2D(blurTexture1, vUv) +
lerpBloomFactor(bloomFactors[1]) * vec4(bloomTintColors[1], 1.0) * texture2D(blurTexture2, vUv) +
lerpBloomFactor(bloomFactors[2]) * vec4(bloomTintColors[2], 1.0) * texture2D(blurTexture3, vUv) +
lerpBloomFactor(bloomFactors[3]) * vec4(bloomTintColors[3], 1.0) * texture2D(blurTexture4, vUv) +
lerpBloomFactor(bloomFactors[4]) * vec4(bloomTintColors[4], 1.0) * texture2D(blurTexture5, vUv) );
}`
} );
}
}
UnrealBloomPass.BlurDirectionX = new Vector2( 1.0, 0.0 );
UnrealBloomPass.BlurDirectionY = new Vector2( 0.0, 1.0 );
export { UnrealBloomPass };
+45
View File
@@ -0,0 +1,45 @@
/**
* Full-screen textured quad shader
*/
const CopyShader = {
name: 'CopyShader',
uniforms: {
'tDiffuse': { value: null },
'opacity': { value: 1.0 }
},
vertexShader: /* glsl */`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader: /* glsl */`
uniform float opacity;
uniform sampler2D tDiffuse;
varying vec2 vUv;
void main() {
vec4 texel = texture2D( tDiffuse, vUv );
gl_FragColor = opacity * texel;
}`
};
export { CopyShader };
@@ -0,0 +1,66 @@
import {
Color
} from 'three';
/**
* Luminosity
* http://en.wikipedia.org/wiki/Luminosity
*/
const LuminosityHighPassShader = {
name: 'LuminosityHighPassShader',
shaderID: 'luminosityHighPass',
uniforms: {
'tDiffuse': { value: null },
'luminosityThreshold': { value: 1.0 },
'smoothWidth': { value: 1.0 },
'defaultColor': { value: new Color( 0x000000 ) },
'defaultOpacity': { value: 0.0 }
},
vertexShader: /* glsl */`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`,
fragmentShader: /* glsl */`
uniform sampler2D tDiffuse;
uniform vec3 defaultColor;
uniform float defaultOpacity;
uniform float luminosityThreshold;
uniform float smoothWidth;
varying vec2 vUv;
void main() {
vec4 texel = texture2D( tDiffuse, vUv );
vec3 luma = vec3( 0.299, 0.587, 0.114 );
float v = dot( texel.xyz, luma );
vec4 outputColor = vec4( defaultColor.rgb, defaultOpacity );
float alpha = smoothstep( luminosityThreshold, luminosityThreshold + smoothWidth, v );
gl_FragColor = mix( outputColor, texel, alpha );
}`
};
export { LuminosityHighPassShader };
+53044
View File
File diff suppressed because one or more lines are too long
+431 -276
View File
@@ -1,10 +1,18 @@
/* ============================================================ /* ============================================================
晟算科技 · 太空场景引擎 晟算科技 · 太空场景引擎 v3 —— 电影级真实感
破碎的月亮 = 失控的软件世界 | 修复进度 85% 停住 真实月面(NASA/SSS) + 真实星野(银河) + ACES调色 + 泛光
模式: journey(首页滚动驱动) / orbit(子页面轨道展示) 撕裂伤口 + 碎石量级谱 + 尘埃云
模块加载(import map), 模式: journey / orbit
============================================================ */ ============================================================ */
window.SPACE_MODE = document.body.getAttribute("data-space") || "journey"; import * as THREE from "three";
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js";
if (!window.SPACE_MODE) {
window.SPACE_MODE = document.body.getAttribute("data-space") || "journey";
}
(function () { (function () {
"use strict"; "use strict";
@@ -13,155 +21,54 @@ window.SPACE_MODE = document.body.getAttribute("data-space") || "journey";
var reducedMotion = window.matchMedia("(prefers-reduced-motion: reduced)").matches; var reducedMotion = window.matchMedia("(prefers-reduced-motion: reduced)").matches;
var bootEl = document.getElementById("spaceBoot"); var bootEl = document.getElementById("spaceBoot");
var canvasHost = document.getElementById("spaceCanvas"); var canvasHost = document.getElementById("spaceCanvas");
var isJourney = window.SPACE_MODE === "journey";
/* ---------- WebGL 不可用时的兜底 ---------- */
function fallback() { function fallback() {
if (bootEl) bootEl.remove(); if (bootEl) bootEl.remove();
document.body.classList.add("space-fallback"); document.body.classList.add("space-fallback", "space-ready");
// 退化为2D星空(CSS实现,见space.css)
} }
if (!window.THREE) { fallback(); return; } var testC = document.createElement("canvas");
var testCanvas = document.createElement("canvas"); if (!(testC.getContext("webgl") || testC.getContext("experimental-webgl"))) { fallback(); return; }
var gl = testCanvas.getContext("webgl") || testCanvas.getContext("experimental-webgl");
if (!gl) { fallback(); return; }
/* ============================================================ /* ============================================================
程序化纹理生成 工具
============================================================ */ ============================================================ */
function glowTexture(inner, mid, size) {
// 径向光晕纹理(太阳辉光/镜头光斑)
function makeGlowTexture(inner, outer, size) {
var c = document.createElement("canvas"); var c = document.createElement("canvas");
c.width = c.height = size; c.width = c.height = size;
var ctx = c.getContext("2d"); var x = c.getContext("2d");
var g = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2); var g = x.createRadialGradient(size/2, size/2, 0, size/2, size/2, size/2);
g.addColorStop(0, inner); g.addColorStop(0, inner);
g.addColorStop(0.35, inner.replace(/[\d.]+\)$/, "0.35)")); g.addColorStop(0.25, inner.replace(/[\d.]+\)$/, "0.5)"));
g.addColorStop(1, outer); g.addColorStop(0.55, mid.replace(/[\d.]+\)$/, "0.18)"));
ctx.fillStyle = g; g.addColorStop(0.8, mid.replace(/[\d.]+\)$/, "0.05)"));
ctx.fillRect(0, 0, size, size); g.addColorStop(1, "rgba(0,0,0,0)");
var t = new THREE.CanvasTexture(c); x.fillStyle = g;
return t; x.fillRect(0, 0, size, size);
}
// 月球纹理: 写实环形山 + 代码字符混合
var crackPath = []; // 裂缝路径点(供粒子泄漏/光缝定位)
function makeMoonTexture() {
var W = 1024, H = 512;
var c = document.createElement("canvas");
c.width = W; c.height = H;
var ctx = c.getContext("2d");
// 基底灰
var base = ctx.createLinearGradient(0, 0, W, H);
base.addColorStop(0, "#8d8d8f");
base.addColorStop(0.5, "#7c7c80");
base.addColorStop(1, "#6e6e73");
ctx.fillStyle = base;
ctx.fillRect(0, 0, W, H);
// 月海(大块暗斑)
for (var m = 0; m < 7; m++) {
var mx = Math.random() * W, my = Math.random() * H, mr = 60 + Math.random() * 130;
var mg = ctx.createRadialGradient(mx, my, 0, mx, my, mr);
mg.addColorStop(0, "rgba(70,70,76,0.5)");
mg.addColorStop(1, "rgba(70,70,76,0)");
ctx.fillStyle = mg;
ctx.beginPath(); ctx.arc(mx, my, mr, 0, Math.PI * 2); ctx.fill();
}
// 环形山
for (var i = 0; i < 220; i++) {
var x = Math.random() * W, y = Math.random() * H;
var r = 2 + Math.random() * 18;
ctx.fillStyle = "rgba(50,50,56," + (0.15 + Math.random() * 0.25) + ")";
ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();
// 亮缘
ctx.strokeStyle = "rgba(200,200,205," + (0.1 + Math.random() * 0.2) + ")";
ctx.lineWidth = 1;
ctx.beginPath(); ctx.arc(x, y, r, -0.6, 1.8); ctx.stroke();
}
// 表面噪点
for (var n = 0; n < 2600; n++) {
var nx = Math.random() * W, ny = Math.random() * H;
ctx.fillStyle = Math.random() > 0.5
? "rgba(255,255,255," + Math.random() * 0.06 + ")"
: "rgba(0,0,0," + Math.random() * 0.08 + ")";
ctx.fillRect(nx, ny, 1.5, 1.5);
}
// 代码字符纹理(远看是月面,近看全是代码)
var glyphs = "01{}[]<>/=;+*#$&%@!?~:fnrax";
ctx.textBaseline = "top";
for (var g2 = 0; g2 < 520; g2++) {
var gx = Math.random() * W, gy = Math.random() * H;
var fs = 6 + Math.random() * 8;
ctx.font = fs + "px monospace";
var greenish = Math.random() > 0.72;
ctx.fillStyle = greenish
? "rgba(90,220,150," + (0.06 + Math.random() * 0.1) + ")"
: "rgba(230,230,235," + (0.04 + Math.random() * 0.08) + ")";
ctx.fillText(glyphs[Math.floor(Math.random() * glyphs.length)], gx, gy);
}
// 裂缝(锯齿状,贯穿一侧) —— 同时记录路径
ctx.strokeStyle = "rgba(18,18,22,0.9)";
ctx.lineCap = "round";
var cx = W * 0.62, cy = H * 0.16;
ctx.lineWidth = 7;
ctx.beginPath(); ctx.moveTo(cx, cy);
crackPath.push([cx, cy]);
while (cy < H * 0.9) {
cx += (Math.random() - 0.42) * 90;
cy += 26 + Math.random() * 30;
ctx.lineTo(cx, cy);
crackPath.push([cx, cy]);
ctx.lineWidth = Math.max(2, ctx.lineWidth * 0.94);
}
ctx.stroke();
// 裂缝两侧的次级裂纹
for (var b = 0; b < 8; b++) {
var bp = crackPath[Math.floor(Math.random() * crackPath.length)];
ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(bp[0], bp[1]);
var bx = bp[0], by = bp[1];
for (var s = 0; s < 4; s++) {
bx += (Math.random() - 0.5) * 60;
by += Math.random() * 26;
ctx.lineTo(bx, by);
}
ctx.stroke();
}
var tex = new THREE.CanvasTexture(c);
tex.colorSpace = THREE.SRGBColorSpace;
return tex;
}
// 代码字符精灵纹理
function makeCharSprite(ch, color) {
var s = 48;
var c = document.createElement("canvas");
c.width = c.height = s;
var ctx = c.getContext("2d");
ctx.font = "bold 30px monospace";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.shadowColor = color;
ctx.shadowBlur = 8;
ctx.fillStyle = color;
ctx.fillText(ch, s / 2, s / 2);
return new THREE.CanvasTexture(c); return new THREE.CanvasTexture(c);
} }
// Three.js SphereGeometry 的标准 UV 映射
function uvToSphere(u, v, r) {
var phi = 2 * Math.PI * u;
var theta = Math.PI * v;
return new THREE.Vector3(
-r * Math.cos(phi) * Math.sin(theta),
r * Math.cos(theta),
r * Math.sin(phi) * Math.sin(theta)
);
}
// 冲击点: 朝向 +x+z (首页相机可见侧)
var IMPACT = { u: 0.334, v: 0.47 };
/* ============================================================ /* ============================================================
场景搭建 场景与渲染器(ACES电影调色)
============================================================ */ ============================================================ */
var scene = new THREE.Scene(); var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 2000); var camera = new THREE.PerspectiveCamera(55, innerWidth / innerHeight, 0.1, 3000);
var rig = new THREE.Group(); // 相机吊舱(负责呼吸式漂移) var rig = new THREE.Group();
rig.add(camera); rig.add(camera);
scene.add(rig); scene.add(rig);
@@ -169,95 +76,253 @@ window.SPACE_MODE = document.body.getAttribute("data-space") || "journey";
try { try {
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: "high-performance" }); renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: "high-performance" });
} catch (e) { fallback(); return; } } catch (e) { fallback(); return; }
renderer.setPixelRatio(Math.min(window.devicePixelRatio, isMobile ? 1.2 : 1.6)); renderer.setPixelRatio(Math.min(devicePixelRatio, isMobile ? 1.2 : 1.6));
renderer.setSize(window.innerWidth, window.innerHeight); renderer.setSize(innerWidth, innerHeight);
renderer.toneMapping = THREE.ACESFilmicToneMapping; // 电影级色调映射
renderer.toneMappingExposure = 1.15;
renderer.outputColorSpace = THREE.SRGBColorSpace;
canvasHost.appendChild(renderer.domElement); canvasHost.appendChild(renderer.domElement);
/* ---------- 星空(两层) ---------- */ /* ---------- 泛光(只有高亮物体发光: 太阳/裂缝/代码粒子) ---------- */
function makeStars(count, spread, size, opacity) { var composer = null, bloomPass = null;
if (!isMobile) {
composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
bloomPass = new UnrealBloomPass(new THREE.Vector2(innerWidth, innerHeight), 0.85, 0.55, 0.78);
composer.addPass(bloomPass);
}
/* ============================================================
资产加载(真实纹理)
============================================================ */
var texLoader = new THREE.TextureLoader();
var loadState = { moon: false, sky: false };
/* ---------- 真实星野天球(银河横贯) ---------- */
var skyMesh = null;
texLoader.load("images/starfield.jpg", function (tex) {
tex.colorSpace = THREE.SRGBColorSpace;
tex.mapping = THREE.EquirectangularReflectionMapping;
var mat = new THREE.MeshBasicMaterial({ map: tex, side: THREE.BackSide, depthWrite: false });
mat.fog = false;
skyMesh = new THREE.Mesh(new THREE.SphereGeometry(1200, 48, 32), mat);
skyMesh.rotation.y = 2.2; // 让银河带处于好看的方位
skyMesh.rotation.z = 0.12;
scene.add(skyMesh);
loadState.sky = true;
}, undefined, function () { loadState.sky = true; });
/* ---------- 近景尘埃(保留视差最快层) ---------- */
function makeDustLayer(count, spread) {
var geo = new THREE.BufferGeometry(); var geo = new THREE.BufferGeometry();
var pos = new Float32Array(count * 3); var pos = new Float32Array(count * 3);
var col = new Float32Array(count * 3); var col = new Float32Array(count * 3);
for (var i = 0; i < count; i++) { for (var i = 0; i < count; i++) {
pos[i * 3] = (Math.random() - 0.5) * spread; pos[i*3] = (Math.random()-0.5) * spread;
pos[i * 3 + 1] = (Math.random() - 0.5) * spread; pos[i*3+1] = (Math.random()-0.5) * spread;
pos[i * 3 + 2] = (Math.random() - 0.5) * spread; pos[i*3+2] = (Math.random()-0.5) * spread;
var r = Math.random(); var b = 0.5 + Math.random() * 0.5;
if (r > 0.85) { col[i * 3] = 1.0; col[i * 3 + 1] = 0.9; col[i * 3 + 2] = 0.75; } // 暖星 col[i*3] = b * 0.85; col[i*3+1] = b * 0.95; col[i*3+2] = b;
else if (r > 0.7) { col[i * 3] = 0.75; col[i * 3 + 1] = 0.85; col[i * 3 + 2] = 1.0; } // 蓝星
else { col[i * 3] = 1; col[i * 3 + 1] = 1; col[i * 3 + 2] = 1; }
} }
geo.setAttribute("position", new THREE.BufferAttribute(pos, 3)); geo.setAttribute("position", new THREE.BufferAttribute(pos, 3));
geo.setAttribute("color", new THREE.BufferAttribute(col, 3)); geo.setAttribute("color", new THREE.BufferAttribute(col, 3));
var mat = new THREE.PointsMaterial({ var mat = new THREE.PointsMaterial({
size: size, vertexColors: true, transparent: true, opacity: opacity, size: 1.1, vertexColors: true, transparent: true, opacity: 0.55,
sizeAttenuation: true, depthWrite: false sizeAttenuation: true, depthWrite: false
}); });
return new THREE.Points(geo, mat); return new THREE.Points(geo, mat);
} }
var starsFar = makeStars(isMobile ? 1500 : 3200, 900, 1.6, 0.85); var nearDust = makeDustLayer(isMobile ? 100 : 220, 200);
var starsNear = makeStars(isMobile ? 120 : 260, 220, 2.4, 0.6); // 近处星尘(景深) rig.add(nearDust);
scene.add(starsFar);
rig.add(starsNear);
/* ---------- 太阳(远景) ---------- */ /* ---------- 太阳: 小而烈 + 泛光 + 横向光纹 ---------- */
var sunGroup = new THREE.Group(); var sunGroup = new THREE.Group();
// 亮度>1的核心,推过泛光阈值
var sunCore = new THREE.Mesh( var sunCore = new THREE.Mesh(
new THREE.SphereGeometry(14, 24, 24), new THREE.SphereGeometry(5, 24, 24),
new THREE.MeshBasicMaterial({ color: 0xfff8e7 }) new THREE.MeshBasicMaterial({ color: new THREE.Color(2.6, 2.3, 1.9) })
); );
sunGroup.add(sunCore); sunGroup.add(sunCore);
var glowTex = makeGlowTexture("rgba(255,244,214,1)", "rgba(255,244,214,0)", 256); // 柔和光晕(克制)
var sunGlow1 = new THREE.Sprite(new THREE.SpriteMaterial({ map: glowTex, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false })); var sunHalo = new THREE.Sprite(new THREE.SpriteMaterial({
sunGlow1.scale.set(110, 110, 1); map: glowTexture("rgba(255,232,190,0.85)", "rgba(255,170,90,0.6)"),
sunGroup.add(sunGlow1); transparent: true, opacity: 0.4, blending: THREE.AdditiveBlending, depthWrite: false
var sunGlow2 = new THREE.Sprite(new THREE.SpriteMaterial({ map: glowTex, transparent: true, opacity: 0.5, blending: THREE.AdditiveBlending, depthWrite: false })); }));
sunGlow2.scale.set(220, 220, 1); sunHalo.scale.set(46, 46, 1);
sunGroup.add(sunGlow2); sunGroup.add(sunHalo);
// 镜头光斑链 // 变形宽银幕横向光纹
var flareTex = makeGlowTexture("rgba(255,236,190,0.8)", "rgba(255,236,190,0)", 128); var streakC = document.createElement("canvas");
for (var f = 0; f < 3; f++) { streakC.width = 256; streakC.height = 8;
var flare = new THREE.Sprite(new THREE.SpriteMaterial({ map: flareTex, transparent: true, opacity: 0.25 - f * 0.06, blending: THREE.AdditiveBlending, depthWrite: false })); var sx = streakC.getContext("2d");
var fs2 = [22, 10, 6][f]; var sg = sx.createLinearGradient(0, 0, 256, 0);
flare.scale.set(fs2, fs2, 1); sg.addColorStop(0, "rgba(255,215,160,0)");
flare.position.set(18 + f * 14, -6 - f * 4, 0); sg.addColorStop(0.5, "rgba(255,235,200,0.85)");
sunGroup.add(flare); sg.addColorStop(1, "rgba(255,215,160,0)");
} sx.fillStyle = sg; sx.fillRect(0, 0, 256, 8);
var streakTex = new THREE.CanvasTexture(streakC);
var sunStreak = new THREE.Sprite(new THREE.SpriteMaterial({
map: streakTex, transparent: true, opacity: 0.55,
blending: THREE.AdditiveBlending, depthWrite: false
}));
sunStreak.scale.set(90, 1.6, 1);
sunGroup.add(sunStreak);
sunGroup.position.set(-150, 70, -260); sunGroup.position.set(-150, 70, -260);
scene.add(sunGroup); scene.add(sunGroup);
/* ---------- 月亮(破碎) ---------- */ /* ============================================================
月亮: 真实纹理 + 撕裂伤口 + 发光裂缝
============================================================ */
var moonGroup = new THREE.Group(); var moonGroup = new THREE.Group();
var MOON_R = 10; var MOON_R = 10;
var moonTex = makeMoonTexture();
var moonMat = new THREE.MeshStandardMaterial({ var moonMat = new THREE.MeshStandardMaterial({
map: moonTex, bumpMap: moonTex, bumpScale: 0.35, color: 0xbfc3c9, // 冷调青灰
roughness: 0.95, metalness: 0.02 roughness: 0.98, metalness: 0.0,
bumpScale: 0.6
}); });
var moon = new THREE.Mesh(new THREE.SphereGeometry(MOON_R, 64, 64), moonMat); var moon = null;
moonGroup.add(moon);
// 月亮轮廓微光(大气感) // 发光裂缝纹理(emissiveMap): 冲击点+放射裂纹+断面代码
function makeCrackEmissive() {
var W = 1024, H = 512;
var c = document.createElement("canvas");
c.width = W; c.height = H;
var x = c.getContext("2d");
x.fillStyle = "#000"; x.fillRect(0, 0, W, H);
var ix = IMPACT.u * W, iy = IMPACT.v * H;
// 断面发光代码结构
x.save();
x.beginPath(); x.arc(ix, iy, 58, 0, 7); x.clip();
x.strokeStyle = "rgba(90,255,195,0.5)"; x.lineWidth = 1;
for (var g = -58; g <= 58; g += 9) {
x.beginPath(); x.moveTo(ix + g, iy - 58); x.lineTo(ix + g, iy + 58); x.stroke();
x.beginPath(); x.moveTo(ix - 58, iy + g); x.lineTo(ix + 58, iy + g); x.stroke();
}
var chars = "01{}<>/=;#*";
x.font = "9px monospace"; x.textBaseline = "top";
for (var i = 0; i < 55; i++) {
x.fillStyle = "rgba(160,255,215," + (0.4 + Math.random() * 0.6) + ")";
x.fillText(chars[Math.floor(Math.random() * chars.length)], ix - 52 + Math.random() * 104, iy - 52 + Math.random() * 104);
}
x.restore();
// 冲击核心辉光
var core = x.createRadialGradient(ix, iy, 0, ix, iy, 80);
core.addColorStop(0, "rgba(200,255,230,1)");
core.addColorStop(0.3, "rgba(120,255,205,0.5)");
core.addColorStop(1, "rgba(80,230,175,0)");
x.fillStyle = core; x.beginPath(); x.arc(ix, iy, 80, 0, 7); x.fill();
// 放射裂纹(宽淡→窄亮)
x.lineCap = "round"; x.lineJoin = "round";
for (var c2 = 0; c2 < 6; c2++) {
var ang = (c2 / 6) * Math.PI * 2 + (Math.random() - 0.5) * 0.8;
var px = ix, py = iy;
var pts = [[px, py]];
var len = 0.16 + Math.random() * 0.2;
var steps = 6 + Math.floor(Math.random() * 4);
for (var s = 0; s < steps; s++) {
ang += (Math.random() - 0.5) * 0.8;
px += Math.cos(ang) * (len * W / steps);
py += Math.sin(ang) * (len * W / steps) * 0.55;
pts.push([px, py]);
}
[[7, "rgba(70,220,170,0.1)"], [3.5, "rgba(100,240,190,0.22)"], [1.4, "rgba(200,255,235,0.9)"]].forEach(function (ps) {
x.strokeStyle = ps[1]; x.lineWidth = ps[0];
x.beginPath(); x.moveTo(pts[0][0], pts[0][1]);
for (var k = 1; k < pts.length; k++) x.lineTo(pts[k][0], pts[k][1]);
x.stroke();
});
}
return new THREE.CanvasTexture(c);
}
// 顶点撕裂: 把伤口区域向内撕成参差轮廓
function tearGeometry(geo, woundDir) {
var pos = geo.attributes.position;
var v = new THREE.Vector3();
for (var i = 0; i < pos.count; i++) {
v.fromBufferAttribute(pos, i).normalize();
var ang = v.angleTo(woundDir);
if (ang < 0.45) {
var fall = 1 - ang / 0.45;
// 高频噪声 → 锯齿而非圆坑
var n = 0.5 + 0.5 * Math.sin(v.x * 13.1) * Math.sin(v.y * 9.7) * Math.sin(v.z * 11.3)
+ 0.25 * Math.sin(v.x * 31.7 + v.y * 23.9);
n = Math.max(0, Math.min(1, n));
var depth = fall * (0.4 + 2.8 * n * (0.35 + 0.65 * fall));
if (ang > 0.3) depth += (Math.random() - 0.5) * 0.4; // 撕裂缘参差
var newR = MOON_R - Math.max(0, depth);
pos.setXYZ(i, v.x * newR, v.y * newR, v.z * newR);
}
}
geo.computeVertexNormals();
}
var woundDirLocal = uvToSphere(IMPACT.u, IMPACT.v, 1).normalize();
var woundPoint = uvToSphere(IMPACT.u, IMPACT.v, MOON_R * 0.98);
texLoader.load("images/moon_color.jpg", function (colorTex) {
colorTex.colorSpace = THREE.SRGBColorSpace;
moonMat.map = colorTex;
moonMat.emissiveMap = makeCrackEmissive();
moonMat.emissive = new THREE.Color(0x9fffd8);
moonMat.emissiveIntensity = 2.1;
// 亮度转凹凸(NASA位移图下载失败时的运行时方案,效果扎实)
var img = colorTex.image;
var bc = document.createElement("canvas");
bc.width = 1024; bc.height = 512;
var bx = bc.getContext("2d");
bx.drawImage(img, 0, 0, 1024, 512);
var id = bx.getImageData(0, 0, 1024, 512);
var d = id.data;
for (var i = 0; i < d.length; i += 4) {
var lum = d[i] * 0.299 + d[i+1] * 0.587 + d[i+2] * 0.114;
lum = Math.max(0, Math.min(255, (lum - 110) * 1.7 + 100));
d[i] = d[i+1] = d[i+2] = lum;
}
bx.putImageData(id, 0, 0);
moonMat.bumpMap = new THREE.CanvasTexture(bc);
var geo = new THREE.SphereGeometry(MOON_R, isMobile ? 64 : 96, isMobile ? 48 : 72);
tearGeometry(geo, woundDirLocal);
moon = new THREE.Mesh(geo, moonMat);
moonGroup.add(moon);
loadState.moon = true;
}, undefined, function () { loadState.moon = true; });
// 月缘柔光(青灰)
var moonHalo = new THREE.Sprite(new THREE.SpriteMaterial({ var moonHalo = new THREE.Sprite(new THREE.SpriteMaterial({
map: makeGlowTexture("rgba(190,200,220,0.5)", "rgba(190,200,220,0)", 256), map: glowTexture("rgba(175,200,195,0.5)", "rgba(160,190,185,0.4)"),
transparent: true, opacity: 0.5, blending: THREE.AdditiveBlending, depthWrite: false transparent: true, opacity: 0.35, blending: THREE.AdditiveBlending, depthWrite: false
})); }));
moonHalo.scale.set(34, 34, 1); moonHalo.scale.set(30, 30, 1);
moonGroup.add(moonHalo); moonGroup.add(moonHalo);
// 碎片(3块, 不规则形状) // 伤口溢出辉光(活着的事件)
var woundGlow = new THREE.Sprite(new THREE.SpriteMaterial({
map: glowTexture("rgba(160,255,220,0.9)", "rgba(90,255,195,0.55)"),
transparent: true, opacity: 0.8, blending: THREE.AdditiveBlending, depthWrite: false
}));
woundGlow.scale.set(11, 11, 1);
woundGlow.position.copy(woundPoint).multiplyScalar(1.04);
moonGroup.add(woundGlow);
/* ---------- 大碎片(2块,顶点扰动,真月面) ---------- */
var fragments = []; var fragments = [];
var fragSpecs = [ var fragSpecs = [
{ size: 2.6, scatter: new THREE.Vector3(9, 4.5, 3), home: new THREE.Vector3(7.6, 3.4, 2.2), spin: 0.16 }, { size: 2.4, scatter: new THREE.Vector3(8.5, 4.5, 4), home: new THREE.Vector3(6.8, 3.6, 2.8), spin: 0.16, period: 14 },
{ size: 1.8, scatter: new THREE.Vector3(-7, -5, 4.5), home: new THREE.Vector3(-5.4, -3.9, 3.4), spin: -0.22 }, { size: 1.6, scatter: new THREE.Vector3(-6.5, -5, 5), home: new THREE.Vector3(-5, -3.8, 3.8), spin: -0.24, period: 9 }
{ size: 1.3, scatter: new THREE.Vector3(4, -8, -4), home: new THREE.Vector3(3.2, -6.2, -3.1), spin: 0.3 }
]; ];
fragSpecs.forEach(function (spec) { fragSpecs.forEach(function (spec) {
var geo = new THREE.IcosahedronGeometry(spec.size, 1); var geo = new THREE.IcosahedronGeometry(spec.size, 2);
var posAttr = geo.getAttribute("position"); var pa = geo.getAttribute("position");
for (var i = 0; i < posAttr.count; i++) { var vv = new THREE.Vector3();
var jitter = 0.75 + Math.random() * 0.45; for (var i = 0; i < pa.count; i++) {
posAttr.setXYZ(i, posAttr.getX(i) * jitter, posAttr.getY(i) * jitter, posAttr.getZ(i) * jitter); vv.fromBufferAttribute(pa, i).normalize();
var j = 0.7 + Math.random() * 0.55;
pa.setXYZ(i, vv.x * spec.size * j, vv.y * spec.size * j, vv.z * spec.size * j);
} }
geo.computeVertexNormals(); geo.computeVertexNormals();
var mesh = new THREE.Mesh(geo, moonMat); var mesh = new THREE.Mesh(geo, moonMat);
@@ -267,16 +332,76 @@ window.SPACE_MODE = document.body.getAttribute("data-space") || "journey";
fragments.push({ mesh: mesh, spec: spec, phase: Math.random() * Math.PI * 2 }); fragments.push({ mesh: mesh, spec: spec, phase: Math.random() * Math.PI * 2 });
}); });
// 修复光缝(绿色能量缝,随修复进度亮起) /* ---------- 碎石量级谱(InstancedMesh, 从伤口拖出) ---------- */
var DEBRIS_N = isMobile ? 90 : 220;
var debrisData = [];
var debrisMesh = null;
(function buildDebris() {
var rockGeo = new THREE.IcosahedronGeometry(1, 0);
var rp = rockGeo.getAttribute("position");
for (var i = 0; i < rp.count; i++) {
var j = 0.65 + Math.random() * 0.7;
rp.setXYZ(i, rp.getX(i) * j, rp.getY(i) * j, rp.getZ(i) * j);
}
rockGeo.computeVertexNormals();
debrisMesh = new THREE.InstancedMesh(rockGeo, moonMat, DEBRIS_N);
var tangent = new THREE.Vector3().crossVectors(woundDirLocal, new THREE.Vector3(0, 1, 0)).normalize();
var bitan = new THREE.Vector3().crossVectors(woundDirLocal, tangent).normalize();
var dummy = new THREE.Object3D();
for (var d = 0; d < DEBRIS_N; d++) {
var along = (Math.random() - 0.3) * 30; // 沿切线拖带
var out = 1.5 + Math.random() * 9; // 向外飞散距离
var pos = woundPoint.clone()
.add(tangent.clone().multiplyScalar(along))
.add(woundDirLocal.clone().multiplyScalar(out))
.add(bitan.clone().multiplyScalar((Math.random() - 0.5) * 5));
var scale = 0.06 + Math.pow(Math.random(), 2.2) * 0.55; // 大量小颗,少数大颗
var vel = woundDirLocal.clone().multiplyScalar(0.1 + Math.random() * 0.25)
.add(tangent.clone().multiplyScalar((Math.random() - 0.5) * 0.1));
var axis = new THREE.Vector3(Math.random()-0.5, Math.random()-0.5, Math.random()-0.5).normalize();
debrisData.push({
pos: pos, scale: scale, vel: vel, axis: axis,
rotSpeed: (Math.random() - 0.5) * 0.6, phase: Math.random() * Math.PI * 2,
wobble: 0.3 + Math.random() * 0.7, period: 8 + Math.random() * 12
});
dummy.position.copy(pos);
dummy.scale.setScalar(scale);
dummy.updateMatrix();
debrisMesh.setMatrixAt(d, dummy.matrix);
}
debrisMesh.instanceMatrix.needsUpdate = true;
moonGroup.add(debrisMesh);
})();
/* ---------- 尘埃云(烟尘,参考图灵魂) ---------- */
var dustClouds = [];
var tangent2 = new THREE.Vector3().crossVectors(woundDirLocal, new THREE.Vector3(0, 1, 0)).normalize();
for (var dc = 0; dc < (isMobile ? 3 : 5); dc++) {
var dustTex = glowTexture("rgba(150,175,165,0.55)", "rgba(140,170,160,0.35)", 256);
var dust = new THREE.Sprite(new THREE.SpriteMaterial({
map: dustTex, transparent: true, opacity: 0.05 + Math.random() * 0.06,
blending: THREE.NormalBlending, depthWrite: false
}));
var sc2 = 22 + Math.random() * 26;
dust.scale.set(sc2 * 1.6, sc2, 1);
var along2 = 4 + dc * 7 + Math.random() * 4;
dust.position.copy(woundPoint)
.add(tangent2.clone().multiplyScalar(along2))
.add(woundDirLocal.clone().multiplyScalar(2 + Math.random() * 4));
moonGroup.add(dust);
dustClouds.push({ sp: dust, base: dust.position.clone(), phase: Math.random() * Math.PI * 2, along: along2 });
}
// 修复光缝
var seams = []; var seams = [];
var seamTex = makeGlowTexture("rgba(77,255,166,1)", "rgba(77,255,166,0)", 64); var seamTex = glowTexture("rgba(160,255,220,1)", "rgba(90,255,195,0.6)", 64);
fragSpecs.forEach(function (spec) { fragSpecs.forEach(function (spec) {
var seam = new THREE.Sprite(new THREE.SpriteMaterial({ var seam = new THREE.Sprite(new THREE.SpriteMaterial({
map: seamTex, color: 0x4dffa6, transparent: true, opacity: 0, map: seamTex, color: 0x9fffd8, transparent: true, opacity: 0,
blending: THREE.AdditiveBlending, depthWrite: false blending: THREE.AdditiveBlending, depthWrite: false
})); }));
seam.scale.set(spec.size * 3.2, spec.size * 1.2, 1); seam.scale.set(spec.size * 3.2, spec.size * 1.2, 1);
seam.position.copy(spec.home).multiplyScalar(0.92); seam.position.copy(spec.home).multiplyScalar(0.94);
moonGroup.add(seam); moonGroup.add(seam);
seams.push(seam); seams.push(seam);
}); });
@@ -286,102 +411,99 @@ window.SPACE_MODE = document.body.getAttribute("data-space") || "journey";
/* ---------- 代码泄漏粒子 ---------- */ /* ---------- 代码泄漏粒子 ---------- */
var codeChars = "01{}<>/=;#*$fx"; var codeChars = "01{}<>/=;#*$fx";
var leakSprites = []; var charCache = {};
var LEAK_COUNT = isMobile ? 22 : 46;
var charTexCache = {};
function charTex(ch) { function charTex(ch) {
if (!charTexCache[ch]) charTexCache[ch] = makeCharSprite(ch, "#4dffa6"); if (!charCache[ch]) {
return charTexCache[ch]; var s = 48, c = document.createElement("canvas");
c.width = c.height = s;
var x = c.getContext("2d");
x.font = "bold 30px monospace"; x.textAlign = "center"; x.textBaseline = "middle";
x.shadowColor = "#9fffd8"; x.shadowBlur = 9; x.fillStyle = "#d6ffee";
x.fillText(ch, s / 2, s / 2);
charCache[ch] = new THREE.CanvasTexture(c);
}
return charCache[ch];
} }
for (var li = 0; li < LEAK_COUNT; li++) { var LEAK_N = isMobile ? 20 : 40;
var leaks = [];
var leakBase = woundPoint.clone().add(moonGroup.position);
for (var li = 0; li < LEAK_N; li++) {
var sp = new THREE.Sprite(new THREE.SpriteMaterial({ var sp = new THREE.Sprite(new THREE.SpriteMaterial({
map: charTex(codeChars[Math.floor(Math.random() * codeChars.length)]), map: charTex(codeChars[Math.floor(Math.random() * codeChars.length)]),
transparent: true, opacity: 0, depthWrite: false, blending: THREE.AdditiveBlending transparent: true, opacity: 0, depthWrite: false, blending: THREE.AdditiveBlending
})); }));
var sc = 0.5 + Math.random() * 0.7; var sc3 = 0.4 + Math.random() * 0.55;
sp.scale.set(sc, sc, 1); sp.scale.set(sc3, sc3, 1);
// 从裂缝附近发射 var emit = leakBase.clone().add(new THREE.Vector3((Math.random()-0.5)*3, (Math.random()-0.5)*3, (Math.random()-0.5)*3));
var emit = new THREE.Vector3(
moonGroup.position.x + 3 + Math.random() * 6,
moonGroup.position.y - 4 + Math.random() * 8,
moonGroup.position.z + 2 + Math.random() * 4
);
sp.position.copy(emit); sp.position.copy(emit);
scene.add(sp); scene.add(sp);
leakSprites.push({ leaks.push({
sp: sp, life: Math.random(), speed: 0.06 + Math.random() * 0.1, sp: sp, life: Math.random(), speed: 0.08 + Math.random() * 0.14,
drift: new THREE.Vector3((Math.random() - 0.5) * 0.5, (Math.random() - 0.5) * 0.5, (Math.random() - 0.5) * 0.3), drift: woundDirLocal.clone().multiplyScalar(0.4 + Math.random() * 0.5)
.add(new THREE.Vector3((Math.random()-0.5)*0.4, (Math.random()-0.5)*0.4, (Math.random()-0.5)*0.3)),
origin: emit origin: emit
}); });
} }
/* ---------- 光 ---------- */ /* ---------- 光照(太阳为唯一主光源) ---------- */
var sunLight = new THREE.DirectionalLight(0xfff3dd, 2.2); var sunLight = new THREE.DirectionalLight(0xffdcae, 2.6);
sunLight.position.set(-150, 70, -200); sunLight.position.set(-150, 70, -260);
scene.add(sunLight); scene.add(sunLight);
scene.add(new THREE.AmbientLight(0x334455, 0.5)); var fillCold = new THREE.DirectionalLight(0x5a6fc8, 0.4); // 背光面冷蓝紫
var rimLight = new THREE.DirectionalLight(0x8899ff, 0.4); // 背面冷光勾勒 fillCold.position.set(120, -40, 120);
scene.add(fillCold);
scene.add(new THREE.AmbientLight(0x2a3452, 0.55));
var rimLight = new THREE.DirectionalLight(0x8899ff, 0.35);
rimLight.position.set(80, -40, 60); rimLight.position.set(80, -40, 60);
scene.add(rimLight); scene.add(rimLight);
/* ============================================================ /* ============================================================
相机编排: 呼吸漂移 + 鼠标惯性 + 滚动驱动 相机编排(V6行为: 呼吸漂移 + 鼠标惯性 + 滚动驱动)
============================================================ */ ============================================================ */
var mouseX = 0, mouseY = 0; var mouseX = 0, mouseY = 0;
if (!isMobile) { if (!isMobile) {
document.addEventListener("mousemove", function (e) { document.addEventListener("mousemove", function (e) {
mouseX = (e.clientX / window.innerWidth - 0.5) * 2; mouseX = (e.clientX / innerWidth - 0.5) * 2;
mouseY = (e.clientY / window.innerHeight - 0.5) * 2; mouseY = (e.clientY / innerHeight - 0.5) * 2;
}); });
} }
function scrollProgress() { function scrollProgress() {
var h = document.documentElement.scrollHeight - window.innerHeight; var h = document.documentElement.scrollHeight - innerHeight;
return h > 0 ? Math.min(Math.max(window.scrollY / h, 0), 1) : 0; return h > 0 ? Math.min(Math.max(scrollY / h, 0), 1) : 0;
} }
function easeInOut(t) { return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; } function easeInOut(t) { return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; }
function lerp(a, b, t) { return a + (b - a) * t; } function lerp(a, b, t) { return a + (b - a) * t; }
var isJourney = window.SPACE_MODE === "journey"; var warpT = reducedMotion ? 1 : 0;
var warpT = reducedMotion ? 1 : 0; // 0→1 warp入场
var clock = new THREE.Clock(); var clock = new THREE.Clock();
var bootDone = false; var bootDone = false;
/* ============================================================ /* ---------- BOOT ---------- */
BOOT 加载序列 → warp 穿越入场
============================================================ */
var bootLines = [ var bootLines = [
{ t: "> 接入深空观测网络 ………… ", ok: "[OK]" }, { t: "> 接入深空观测网络 ………… ", ok: "[OK]" },
{ t: "> 望远镜阵列对焦 …………… ", ok: "[OK]" }, { t: "> 望远镜阵列对焦 …………… ", ok: "[OK]" },
{ t: "> 扫描目标: MOON.SYS ……… ", ok: "[FATAL]" }, { t: "> 扫描目标: MOON.SYS ……… ", ok: "[FATAL]" },
{ t: "> 检测到结构性碎裂 ………… ", ok: "[3 FRAGS]" }, { t: "> 检测到结构性碎裂 ………… ", ok: "[FRAGS]" },
{ t: "> 启动修复协议 ……………… ", ok: "[GO]" } { t: "> 启动修复协议 ……………… ", ok: "[GO]" }
]; ];
var bootBody = document.getElementById("spaceBootBody"); var bootBody = document.getElementById("spaceBootBody");
var bootBar = document.getElementById("spaceBootBar"); var bootBar = document.getElementById("spaceBootBar");
function runBoot() { function runBoot() {
if (!bootEl || reducedMotion) { finishBoot(); return; } if (!bootEl || reducedMotion) { finishBoot(); return; }
var i = 0; var i = 0;
var total = bootLines.length; (function next() {
function next() { if (i < bootLines.length) {
if (i < total) {
var line = document.createElement("div"); var line = document.createElement("div");
line.className = "terminal-line"; line.className = "terminal-line";
var isFatal = bootLines[i].ok.indexOf("FATAL") >= 0 || bootLines[i].ok.indexOf("FRAG") >= 0; var isFatal = bootLines[i].ok.indexOf("FATAL") >= 0 || bootLines[i].ok.indexOf("FRAG") >= 0;
line.innerHTML = '<span class="terminal-prompt">' + bootLines[i].t + '</span><span class="terminal-text ' + (isFatal ? "log-error" : "ok") + '">' + bootLines[i].ok + "</span>"; line.innerHTML = '<span class="terminal-prompt">' + bootLines[i].t + '</span><span class="terminal-text ' + (isFatal ? "log-error" : "ok") + '">' + bootLines[i].ok + "</span>";
if (bootBody) bootBody.appendChild(line); if (bootBody) bootBody.appendChild(line);
if (bootBar) bootBar.style.width = ((i + 1) / total * 100) + "%"; if (bootBar) bootBar.style.width = ((i + 1) / bootLines.length * 100) + "%";
i++; i++;
setTimeout(next, 340); setTimeout(next, 340);
} else { } else setTimeout(finishBoot, 420);
setTimeout(finishBoot, 420); })();
}
}
next();
} }
function finishBoot() { function finishBoot() {
if (bootEl) { if (bootEl) {
bootEl.classList.add("hide"); bootEl.classList.add("hide");
@@ -391,26 +513,27 @@ window.SPACE_MODE = document.body.getAttribute("data-space") || "journey";
document.body.classList.add("space-ready"); document.body.classList.add("space-ready");
} }
runBoot(); runBoot();
// 保险: 4.5s内必须入场
setTimeout(finishBoot, 4500); setTimeout(finishBoot, 4500);
/* ============================================================ /* ============================================================
主循环 主循环
============================================================ */ ============================================================ */
var dummy = new THREE.Object3D();
var tmpQ = new THREE.Quaternion();
function animate() { function animate() {
requestAnimationFrame(animate); requestAnimationFrame(animate);
var t = clock.getElapsedTime(); var t = clock.getElapsedTime();
var p = isJourney ? scrollProgress() : 0; var p = isJourney ? scrollProgress() : 0;
var ep = easeInOut(p); var ep = easeInOut(p);
// warp入场: 相机从远处冲刺到工位 // warp入场
if (warpT < 1) { if (warpT < 1) {
warpT = Math.min(1, warpT + 0.008); warpT = Math.min(1, warpT + 0.008);
var w = 1 - Math.pow(1 - warpT, 3); var w = 1 - Math.pow(1 - warpT, 3);
camera.position.z = lerp(220, isJourney ? 46 : 42, w); camera.position.z = lerp(220, isJourney ? 46 : 42, w);
starsFar.rotation.z += 0.002 * (1 - warpT); if (skyMesh) skyMesh.rotation.y += 0.004 * (1 - warpT);
} else if (isJourney) { } else if (isJourney) {
// 滚动驱动: 接近月亮
camera.position.z = lerp(46, 24, ep); camera.position.z = lerp(46, 24, ep);
camera.position.y = lerp(0, 3, ep); camera.position.y = lerp(0, 3, ep);
} else { } else {
@@ -422,66 +545,98 @@ window.SPACE_MODE = document.body.getAttribute("data-space") || "journey";
rig.position.y = Math.cos(t * 0.19) * 0.45; rig.position.y = Math.cos(t * 0.19) * 0.45;
rig.rotation.z = Math.sin(t * 0.1) * 0.008; rig.rotation.z = Math.sin(t * 0.1) * 0.008;
// 鼠标惯性跟随(像在太空里转头) // 鼠标惯性跟随
camera.rotation.y += ((-mouseX * 0.09) - camera.rotation.y) * 0.045; camera.rotation.y += ((-mouseX * 0.09) - camera.rotation.y) * 0.045;
camera.rotation.x += ((-mouseY * 0.06) - camera.rotation.x) * 0.045; camera.rotation.x += ((-mouseY * 0.06) - camera.rotation.x) * 0.045;
// 月亮自转 + 月组轻微摆动 // 月亮缓慢自转 + 呼吸
moon.rotation.y += 0.0009; if (moon) moon.rotation.y += 0.0006;
moonGroup.position.y = 1 + Math.sin(t * 0.35) * 0.35; moonGroup.position.y = 1 + Math.sin(t * 0.33) * 0.3;
moonGroup.rotation.z = Math.sin(t * 0.12) * 0.02; moonGroup.rotation.z = Math.sin(t * 0.11) * 0.015;
// 修复进度: journey模式随滚动到85%停住 / orbit模式固定0.3 // 伤口辉光脉动
woundGlow.material.opacity = 0.65 + Math.sin(t * 1.6) * 0.2;
// 修复进度(V6时序)
var repair = isJourney ? Math.min(Math.max((p - 0.42) / 0.46, 0), 1) * 0.85 : 0.3; var repair = isJourney ? Math.min(Math.max((p - 0.42) / 0.46, 0), 1) * 0.85 : 0.3;
var rt = repair / 0.85; // 0→1 var rt = repair / 0.85;
// 大碎片: 无衰减漂移,各自周期相位
fragments.forEach(function (f) { fragments.forEach(function (f) {
var target = f.spec.scatter.clone().lerp(f.spec.home, rt * rt); var target = f.spec.scatter.clone().lerp(f.spec.home, rt * rt);
// 悬浮: 在目标点附近小幅浮动 var amp = 0.5 * (1 - rt);
var w1 = (t / f.spec.period) * Math.PI * 2 + f.phase;
f.mesh.position.set( f.mesh.position.set(
target.x + Math.sin(t * 0.5 + f.phase) * 0.35 * (1 - rt), target.x + Math.sin(w1) * amp,
target.y + Math.cos(t * 0.42 + f.phase) * 0.3 * (1 - rt), target.y + Math.sin(w1 * 0.83 + 1.2) * amp * 0.8,
target.z + Math.sin(t * 0.31 + f.phase * 2) * 0.3 * (1 - rt) target.z + Math.cos(w1 * 0.71) * amp * 0.7
); );
f.mesh.rotation.x += 0.0016 * f.spec.spin * (1 - rt * 0.8); f.mesh.rotation.x += 0.0016 * f.spec.spin * (1 - rt * 0.8);
f.mesh.rotation.y += 0.0022 * f.spec.spin * (1 - rt * 0.8); f.mesh.rotation.y += 0.0022 * f.spec.spin * (1 - rt * 0.8);
}); });
// 光缝亮起 // 碎石场: 各自向外漂移+翻滚+不同步摆动(持续离散中)
seams.forEach(function (s, i) { if (debrisMesh) {
s.material.opacity = rt * 0.85 + Math.sin(t * 2 + i) * 0.06 * rt; for (var d = 0; d < DEBRIS_N; d++) {
var dd = debrisData[d];
dd.pos.add(dd.vel.clone().multiplyScalar(0.016 * (1 - rt * 0.6)));
var wob = (t / dd.period) * Math.PI * 2 + dd.phase;
dummy.position.set(
dd.pos.x + Math.sin(wob) * dd.wobble * 0.3,
dd.pos.y + Math.sin(wob * 0.83) * dd.wobble * 0.25,
dd.pos.z + Math.cos(wob * 0.71) * dd.wobble * 0.25
);
tmpQ.setFromAxisAngle(dd.axis, t * dd.rotSpeed + dd.phase);
dummy.quaternion.copy(tmpQ);
dummy.scale.setScalar(dd.scale);
dummy.updateMatrix();
debrisMesh.setMatrixAt(d, dummy.matrix);
}
debrisMesh.instanceMatrix.needsUpdate = true;
}
// 尘埃云缓慢流动
dustClouds.forEach(function (dc2, i) {
dc2.sp.position.x = dc2.base.x + Math.sin(t * 0.1 + dc2.phase) * 1.2 + t * 0.05 * (i % 2 ? 1 : -0.7);
dc2.sp.position.y = dc2.base.y + Math.cos(t * 0.08 + dc2.phase) * 0.9;
dc2.sp.material.opacity = 0.05 + 0.03 * Math.sin(t * 0.23 + dc2.phase) + 0.04;
}); });
// 代码泄漏(修复越少漏得越多) // 光缝
var leakRate = 1 - rt * 0.7; seams.forEach(function (s, i) {
leakSprites.forEach(function (L) { s.material.opacity = rt * 0.9 + Math.sin(t * 2.2 + i * 1.7) * 0.06 * rt;
L.life += L.speed * 0.016 * 60 * 0.016; });
// 代码泄漏
var leakRate = 1 - rt * 0.55;
leaks.forEach(function (L) {
L.life += L.speed * 0.016;
if (L.life > 1) { if (L.life > 1) {
L.life = 0; L.life = 0;
L.sp.position.copy(L.origin); L.sp.position.copy(L.origin);
L.sp.material.map = charTex(codeChars[Math.floor(Math.random() * codeChars.length)]); L.sp.material.map = charTex(codeChars[Math.floor(Math.random() * codeChars.length)]);
} }
L.sp.position.x += L.drift.x * 0.016; L.sp.position.add(L.drift.clone().multiplyScalar(0.016));
L.sp.position.y += L.drift.y * 0.016; L.sp.material.opacity = Math.sin(L.life * Math.PI) * 0.8 * leakRate;
L.sp.position.z += L.drift.z * 0.016;
L.sp.material.opacity = Math.sin(L.life * Math.PI) * 0.75 * leakRate;
}); });
// 太阳呼吸 // 太阳呼吸 + 光纹微闪
sunGlow1.material.opacity = 0.85 + Math.sin(t * 0.8) * 0.1; sunHalo.material.opacity = 0.36 + Math.sin(t * 0.7) * 0.06;
sunStreak.material.opacity = 0.45 + Math.sin(t * 1.1) * 0.12;
// 星尘缓流 // 星野极缓旋转 + 近尘埃视差
starsNear.rotation.y += 0.0002; if (skyMesh) skyMesh.rotation.y += 0.00008;
starsFar.rotation.y += 0.00004; nearDust.rotation.y += 0.0003;
renderer.render(scene, camera); if (composer) composer.render();
else renderer.render(scene, camera);
} }
animate(); animate();
/* ---------- 自适应 ---------- */ addEventListener("resize", function () {
window.addEventListener("resize", function () { camera.aspect = innerWidth / innerHeight;
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix(); camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight); renderer.setSize(innerWidth, innerHeight);
if (composer) composer.setSize(innerWidth, innerHeight);
}); });
})(); })();
+6 -2
View File
@@ -271,13 +271,17 @@
</div> </div>
<div class="footer-bottom"> <div class="footer-bottom">
<span>&copy; 2026 郑州晟算科技有限公司</span> <span>&copy; 2026 郑州晟算科技有限公司</span>
<span style="opacity:0.45;font-size:11px">Moon &amp; Stars Imagery: NASA · Solar System Scope (CC BY 4.0)</span>
<span>ground_control@shengsuan:~$ repairing_moon --progress 85%</span> <span>ground_control@shengsuan:~$ repairing_moon --progress 85%</span>
</div> </div>
</div> </div>
</footer> </footer>
<script src="js/lib/three.min.js"></script> <script type="importmap">
<script src="js/space.js"></script> {"imports":{"three":"./js/lib/three.module.js","three/addons/":"./js/lib/addons/"}}
</script>
<script>window.SPACE_MODE=document.body.getAttribute("data-space")||"orbit";</script>
<script type="module" src="js/space.js"></script>
<script src="js/main.js"></script> <script src="js/main.js"></script>
</body> </body>
</html> </html>