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:
+6
-2
@@ -247,13 +247,17 @@
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<span>© 2026 郑州晟算科技有限公司</span>
|
||||
<span style="opacity:0.45;font-size:11px">Moon & Stars Imagery: NASA · Solar System Scope (CC BY 4.0)</span>
|
||||
<span>ground_control@shengsuan:~$ repairing_moon --progress 85%</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="js/lib/three.min.js"></script>
|
||||
<script src="js/space.js"></script>
|
||||
<script type="importmap">
|
||||
{"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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+6
-2
@@ -237,6 +237,7 @@
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<span>© 2026 郑州晟算科技有限公司</span>
|
||||
<span style="opacity:0.45;font-size:11px">Moon & Stars Imagery: NASA · Solar System Scope (CC BY 4.0)</span>
|
||||
<span>ground_control@shengsuan:~$ repairing_moon --progress 85%</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -245,8 +246,11 @@
|
||||
<style>
|
||||
@keyframes radarSweep { to { transform: rotate(360deg); } }
|
||||
</style>
|
||||
<script src="js/lib/three.min.js"></script>
|
||||
<script src="js/space.js"></script>
|
||||
<script type="importmap">
|
||||
{"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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 246 KiB |
+6
-2
@@ -411,13 +411,17 @@
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<span>© 2026 郑州晟算科技有限公司</span>
|
||||
<span style="opacity:0.45;font-size:11px">Moon & Stars Imagery: NASA · Solar System Scope (CC BY 4.0)</span>
|
||||
<span>ground_control@shengsuan:~$ repairing_moon --progress 85%</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="js/lib/three.min.js"></script>
|
||||
<script src="js/space.js"></script>
|
||||
<script type="importmap">
|
||||
{"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>
|
||||
</body>
|
||||
</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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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
File diff suppressed because one or more lines are too long
+431
-276
@@ -1,10 +1,18 @@
|
||||
/* ============================================================
|
||||
晟算科技 · 太空场景引擎
|
||||
破碎的月亮 = 失控的软件世界 | 修复进度 85% 停住
|
||||
模式: journey(首页滚动驱动) / orbit(子页面轨道展示)
|
||||
晟算科技 · 太空场景引擎 v3 —— 电影级真实感
|
||||
真实月面(NASA/SSS) + 真实星野(银河) + ACES调色 + 泛光
|
||||
撕裂伤口 + 碎石量级谱 + 尘埃云
|
||||
模块加载(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 () {
|
||||
"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 bootEl = document.getElementById("spaceBoot");
|
||||
var canvasHost = document.getElementById("spaceCanvas");
|
||||
var isJourney = window.SPACE_MODE === "journey";
|
||||
|
||||
/* ---------- WebGL 不可用时的兜底 ---------- */
|
||||
function fallback() {
|
||||
if (bootEl) bootEl.remove();
|
||||
document.body.classList.add("space-fallback");
|
||||
// 退化为2D星空(CSS实现,见space.css)
|
||||
document.body.classList.add("space-fallback", "space-ready");
|
||||
}
|
||||
|
||||
if (!window.THREE) { fallback(); return; }
|
||||
var testCanvas = document.createElement("canvas");
|
||||
var gl = testCanvas.getContext("webgl") || testCanvas.getContext("experimental-webgl");
|
||||
if (!gl) { fallback(); return; }
|
||||
var testC = document.createElement("canvas");
|
||||
if (!(testC.getContext("webgl") || testC.getContext("experimental-webgl"))) { fallback(); return; }
|
||||
|
||||
/* ============================================================
|
||||
程序化纹理生成
|
||||
工具
|
||||
============================================================ */
|
||||
|
||||
// 径向光晕纹理(太阳辉光/镜头光斑)
|
||||
function makeGlowTexture(inner, outer, size) {
|
||||
function glowTexture(inner, mid, size) {
|
||||
var c = document.createElement("canvas");
|
||||
c.width = c.height = size;
|
||||
var ctx = c.getContext("2d");
|
||||
var g = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
|
||||
var x = c.getContext("2d");
|
||||
var g = x.createRadialGradient(size/2, size/2, 0, size/2, size/2, size/2);
|
||||
g.addColorStop(0, inner);
|
||||
g.addColorStop(0.35, inner.replace(/[\d.]+\)$/, "0.35)"));
|
||||
g.addColorStop(1, outer);
|
||||
ctx.fillStyle = g;
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
var t = new THREE.CanvasTexture(c);
|
||||
return t;
|
||||
}
|
||||
|
||||
// 月球纹理: 写实环形山 + 代码字符混合
|
||||
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);
|
||||
g.addColorStop(0.25, inner.replace(/[\d.]+\)$/, "0.5)"));
|
||||
g.addColorStop(0.55, mid.replace(/[\d.]+\)$/, "0.18)"));
|
||||
g.addColorStop(0.8, mid.replace(/[\d.]+\)$/, "0.05)"));
|
||||
g.addColorStop(1, "rgba(0,0,0,0)");
|
||||
x.fillStyle = g;
|
||||
x.fillRect(0, 0, size, size);
|
||||
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 camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 2000);
|
||||
var rig = new THREE.Group(); // 相机吊舱(负责呼吸式漂移)
|
||||
var camera = new THREE.PerspectiveCamera(55, innerWidth / innerHeight, 0.1, 3000);
|
||||
var rig = new THREE.Group();
|
||||
rig.add(camera);
|
||||
scene.add(rig);
|
||||
|
||||
@@ -169,95 +76,253 @@ window.SPACE_MODE = document.body.getAttribute("data-space") || "journey";
|
||||
try {
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: "high-performance" });
|
||||
} catch (e) { fallback(); return; }
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, isMobile ? 1.2 : 1.6));
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
renderer.setPixelRatio(Math.min(devicePixelRatio, isMobile ? 1.2 : 1.6));
|
||||
renderer.setSize(innerWidth, innerHeight);
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping; // 电影级色调映射
|
||||
renderer.toneMappingExposure = 1.15;
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
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 pos = new Float32Array(count * 3);
|
||||
var col = new Float32Array(count * 3);
|
||||
for (var i = 0; i < count; i++) {
|
||||
pos[i * 3] = (Math.random() - 0.5) * spread;
|
||||
pos[i * 3 + 1] = (Math.random() - 0.5) * spread;
|
||||
pos[i * 3 + 2] = (Math.random() - 0.5) * spread;
|
||||
var r = Math.random();
|
||||
if (r > 0.85) { col[i * 3] = 1.0; col[i * 3 + 1] = 0.9; col[i * 3 + 2] = 0.75; } // 暖星
|
||||
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; }
|
||||
pos[i*3] = (Math.random()-0.5) * spread;
|
||||
pos[i*3+1] = (Math.random()-0.5) * spread;
|
||||
pos[i*3+2] = (Math.random()-0.5) * spread;
|
||||
var b = 0.5 + Math.random() * 0.5;
|
||||
col[i*3] = b * 0.85; col[i*3+1] = b * 0.95; col[i*3+2] = b;
|
||||
}
|
||||
geo.setAttribute("position", new THREE.BufferAttribute(pos, 3));
|
||||
geo.setAttribute("color", new THREE.BufferAttribute(col, 3));
|
||||
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
|
||||
});
|
||||
return new THREE.Points(geo, mat);
|
||||
}
|
||||
var starsFar = makeStars(isMobile ? 1500 : 3200, 900, 1.6, 0.85);
|
||||
var starsNear = makeStars(isMobile ? 120 : 260, 220, 2.4, 0.6); // 近处星尘(景深)
|
||||
scene.add(starsFar);
|
||||
rig.add(starsNear);
|
||||
var nearDust = makeDustLayer(isMobile ? 100 : 220, 200);
|
||||
rig.add(nearDust);
|
||||
|
||||
/* ---------- 太阳(远景) ---------- */
|
||||
/* ---------- 太阳: 小而烈 + 泛光 + 横向光纹 ---------- */
|
||||
var sunGroup = new THREE.Group();
|
||||
// 亮度>1的核心,推过泛光阈值
|
||||
var sunCore = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(14, 24, 24),
|
||||
new THREE.MeshBasicMaterial({ color: 0xfff8e7 })
|
||||
new THREE.SphereGeometry(5, 24, 24),
|
||||
new THREE.MeshBasicMaterial({ color: new THREE.Color(2.6, 2.3, 1.9) })
|
||||
);
|
||||
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 }));
|
||||
sunGlow1.scale.set(110, 110, 1);
|
||||
sunGroup.add(sunGlow1);
|
||||
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);
|
||||
sunGroup.add(sunGlow2);
|
||||
// 镜头光斑链
|
||||
var flareTex = makeGlowTexture("rgba(255,236,190,0.8)", "rgba(255,236,190,0)", 128);
|
||||
for (var f = 0; f < 3; f++) {
|
||||
var flare = new THREE.Sprite(new THREE.SpriteMaterial({ map: flareTex, transparent: true, opacity: 0.25 - f * 0.06, blending: THREE.AdditiveBlending, depthWrite: false }));
|
||||
var fs2 = [22, 10, 6][f];
|
||||
flare.scale.set(fs2, fs2, 1);
|
||||
flare.position.set(18 + f * 14, -6 - f * 4, 0);
|
||||
sunGroup.add(flare);
|
||||
}
|
||||
// 柔和光晕(克制)
|
||||
var sunHalo = new THREE.Sprite(new THREE.SpriteMaterial({
|
||||
map: glowTexture("rgba(255,232,190,0.85)", "rgba(255,170,90,0.6)"),
|
||||
transparent: true, opacity: 0.4, blending: THREE.AdditiveBlending, depthWrite: false
|
||||
}));
|
||||
sunHalo.scale.set(46, 46, 1);
|
||||
sunGroup.add(sunHalo);
|
||||
// 变形宽银幕横向光纹
|
||||
var streakC = document.createElement("canvas");
|
||||
streakC.width = 256; streakC.height = 8;
|
||||
var sx = streakC.getContext("2d");
|
||||
var sg = sx.createLinearGradient(0, 0, 256, 0);
|
||||
sg.addColorStop(0, "rgba(255,215,160,0)");
|
||||
sg.addColorStop(0.5, "rgba(255,235,200,0.85)");
|
||||
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);
|
||||
scene.add(sunGroup);
|
||||
|
||||
/* ---------- 月亮(破碎) ---------- */
|
||||
/* ============================================================
|
||||
月亮: 真实纹理 + 撕裂伤口 + 发光裂缝
|
||||
============================================================ */
|
||||
var moonGroup = new THREE.Group();
|
||||
var MOON_R = 10;
|
||||
var moonTex = makeMoonTexture();
|
||||
var moonMat = new THREE.MeshStandardMaterial({
|
||||
map: moonTex, bumpMap: moonTex, bumpScale: 0.35,
|
||||
roughness: 0.95, metalness: 0.02
|
||||
color: 0xbfc3c9, // 冷调青灰
|
||||
roughness: 0.98, metalness: 0.0,
|
||||
bumpScale: 0.6
|
||||
});
|
||||
var moon = new THREE.Mesh(new THREE.SphereGeometry(MOON_R, 64, 64), moonMat);
|
||||
moonGroup.add(moon);
|
||||
var moon = null;
|
||||
|
||||
// 月亮轮廓微光(大气感)
|
||||
// 发光裂缝纹理(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({
|
||||
map: makeGlowTexture("rgba(190,200,220,0.5)", "rgba(190,200,220,0)", 256),
|
||||
transparent: true, opacity: 0.5, blending: THREE.AdditiveBlending, depthWrite: false
|
||||
map: glowTexture("rgba(175,200,195,0.5)", "rgba(160,190,185,0.4)"),
|
||||
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);
|
||||
|
||||
// 碎片(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 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: 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.3, scatter: new THREE.Vector3(4, -8, -4), home: new THREE.Vector3(3.2, -6.2, -3.1), spin: 0.3 }
|
||||
{ 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.6, scatter: new THREE.Vector3(-6.5, -5, 5), home: new THREE.Vector3(-5, -3.8, 3.8), spin: -0.24, period: 9 }
|
||||
];
|
||||
fragSpecs.forEach(function (spec) {
|
||||
var geo = new THREE.IcosahedronGeometry(spec.size, 1);
|
||||
var posAttr = geo.getAttribute("position");
|
||||
for (var i = 0; i < posAttr.count; i++) {
|
||||
var jitter = 0.75 + Math.random() * 0.45;
|
||||
posAttr.setXYZ(i, posAttr.getX(i) * jitter, posAttr.getY(i) * jitter, posAttr.getZ(i) * jitter);
|
||||
var geo = new THREE.IcosahedronGeometry(spec.size, 2);
|
||||
var pa = geo.getAttribute("position");
|
||||
var vv = new THREE.Vector3();
|
||||
for (var i = 0; i < pa.count; i++) {
|
||||
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();
|
||||
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 });
|
||||
});
|
||||
|
||||
// 修复光缝(绿色能量缝,随修复进度亮起)
|
||||
/* ---------- 碎石量级谱(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 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) {
|
||||
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
|
||||
}));
|
||||
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);
|
||||
seams.push(seam);
|
||||
});
|
||||
@@ -286,102 +411,99 @@ window.SPACE_MODE = document.body.getAttribute("data-space") || "journey";
|
||||
|
||||
/* ---------- 代码泄漏粒子 ---------- */
|
||||
var codeChars = "01{}<>/=;#*$fx";
|
||||
var leakSprites = [];
|
||||
var LEAK_COUNT = isMobile ? 22 : 46;
|
||||
var charTexCache = {};
|
||||
var charCache = {};
|
||||
function charTex(ch) {
|
||||
if (!charTexCache[ch]) charTexCache[ch] = makeCharSprite(ch, "#4dffa6");
|
||||
return charTexCache[ch];
|
||||
if (!charCache[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({
|
||||
map: charTex(codeChars[Math.floor(Math.random() * codeChars.length)]),
|
||||
transparent: true, opacity: 0, depthWrite: false, blending: THREE.AdditiveBlending
|
||||
}));
|
||||
var sc = 0.5 + Math.random() * 0.7;
|
||||
sp.scale.set(sc, sc, 1);
|
||||
// 从裂缝附近发射
|
||||
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
|
||||
);
|
||||
var sc3 = 0.4 + Math.random() * 0.55;
|
||||
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));
|
||||
sp.position.copy(emit);
|
||||
scene.add(sp);
|
||||
leakSprites.push({
|
||||
sp: sp, life: Math.random(), speed: 0.06 + Math.random() * 0.1,
|
||||
drift: new THREE.Vector3((Math.random() - 0.5) * 0.5, (Math.random() - 0.5) * 0.5, (Math.random() - 0.5) * 0.3),
|
||||
leaks.push({
|
||||
sp: sp, life: Math.random(), speed: 0.08 + Math.random() * 0.14,
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- 灯光 ---------- */
|
||||
var sunLight = new THREE.DirectionalLight(0xfff3dd, 2.2);
|
||||
sunLight.position.set(-150, 70, -200);
|
||||
/* ---------- 光照(太阳为唯一主光源) ---------- */
|
||||
var sunLight = new THREE.DirectionalLight(0xffdcae, 2.6);
|
||||
sunLight.position.set(-150, 70, -260);
|
||||
scene.add(sunLight);
|
||||
scene.add(new THREE.AmbientLight(0x334455, 0.5));
|
||||
var rimLight = new THREE.DirectionalLight(0x8899ff, 0.4); // 背面冷光勾勒
|
||||
var fillCold = new THREE.DirectionalLight(0x5a6fc8, 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);
|
||||
scene.add(rimLight);
|
||||
|
||||
/* ============================================================
|
||||
相机编排: 呼吸漂移 + 鼠标惯性 + 滚动驱动
|
||||
相机编排(V6行为: 呼吸漂移 + 鼠标惯性 + 滚动驱动)
|
||||
============================================================ */
|
||||
var mouseX = 0, mouseY = 0;
|
||||
if (!isMobile) {
|
||||
document.addEventListener("mousemove", function (e) {
|
||||
mouseX = (e.clientX / window.innerWidth - 0.5) * 2;
|
||||
mouseY = (e.clientY / window.innerHeight - 0.5) * 2;
|
||||
mouseX = (e.clientX / innerWidth - 0.5) * 2;
|
||||
mouseY = (e.clientY / innerHeight - 0.5) * 2;
|
||||
});
|
||||
}
|
||||
|
||||
function scrollProgress() {
|
||||
var h = document.documentElement.scrollHeight - window.innerHeight;
|
||||
return h > 0 ? Math.min(Math.max(window.scrollY / h, 0), 1) : 0;
|
||||
var h = document.documentElement.scrollHeight - innerHeight;
|
||||
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 lerp(a, b, t) { return a + (b - a) * t; }
|
||||
|
||||
var isJourney = window.SPACE_MODE === "journey";
|
||||
var warpT = reducedMotion ? 1 : 0; // 0→1 warp入场
|
||||
var warpT = reducedMotion ? 1 : 0;
|
||||
var clock = new THREE.Clock();
|
||||
var bootDone = false;
|
||||
|
||||
/* ============================================================
|
||||
BOOT 加载序列 → warp 穿越入场
|
||||
============================================================ */
|
||||
/* ---------- BOOT ---------- */
|
||||
var bootLines = [
|
||||
{ t: "> 接入深空观测网络 ………… ", ok: "[OK]" },
|
||||
{ t: "> 望远镜阵列对焦 …………… ", ok: "[OK]" },
|
||||
{ t: "> 扫描目标: MOON.SYS ……… ", ok: "[FATAL]" },
|
||||
{ t: "> 检测到结构性碎裂 ………… ", ok: "[3 FRAGS]" },
|
||||
{ t: "> 检测到结构性碎裂 ………… ", ok: "[FRAGS]" },
|
||||
{ t: "> 启动修复协议 ……………… ", ok: "[GO]" }
|
||||
];
|
||||
var bootBody = document.getElementById("spaceBootBody");
|
||||
var bootBar = document.getElementById("spaceBootBar");
|
||||
|
||||
function runBoot() {
|
||||
if (!bootEl || reducedMotion) { finishBoot(); return; }
|
||||
var i = 0;
|
||||
var total = bootLines.length;
|
||||
function next() {
|
||||
if (i < total) {
|
||||
(function next() {
|
||||
if (i < bootLines.length) {
|
||||
var line = document.createElement("div");
|
||||
line.className = "terminal-line";
|
||||
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>";
|
||||
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++;
|
||||
setTimeout(next, 340);
|
||||
} else {
|
||||
setTimeout(finishBoot, 420);
|
||||
}
|
||||
}
|
||||
next();
|
||||
} else setTimeout(finishBoot, 420);
|
||||
})();
|
||||
}
|
||||
|
||||
function finishBoot() {
|
||||
if (bootEl) {
|
||||
bootEl.classList.add("hide");
|
||||
@@ -391,26 +513,27 @@ window.SPACE_MODE = document.body.getAttribute("data-space") || "journey";
|
||||
document.body.classList.add("space-ready");
|
||||
}
|
||||
runBoot();
|
||||
// 保险: 4.5s内必须入场
|
||||
setTimeout(finishBoot, 4500);
|
||||
|
||||
/* ============================================================
|
||||
主循环
|
||||
============================================================ */
|
||||
var dummy = new THREE.Object3D();
|
||||
var tmpQ = new THREE.Quaternion();
|
||||
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
var t = clock.getElapsedTime();
|
||||
var p = isJourney ? scrollProgress() : 0;
|
||||
var ep = easeInOut(p);
|
||||
|
||||
// warp入场: 相机从远处冲刺到工位
|
||||
// warp入场
|
||||
if (warpT < 1) {
|
||||
warpT = Math.min(1, warpT + 0.008);
|
||||
var w = 1 - Math.pow(1 - warpT, 3);
|
||||
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) {
|
||||
// 滚动驱动: 接近月亮
|
||||
camera.position.z = lerp(46, 24, ep);
|
||||
camera.position.y = lerp(0, 3, ep);
|
||||
} 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.rotation.z = Math.sin(t * 0.1) * 0.008;
|
||||
|
||||
// 鼠标惯性跟随(像在太空里转头)
|
||||
// 鼠标惯性跟随
|
||||
camera.rotation.y += ((-mouseX * 0.09) - camera.rotation.y) * 0.045;
|
||||
camera.rotation.x += ((-mouseY * 0.06) - camera.rotation.x) * 0.045;
|
||||
|
||||
// 月亮自转 + 月组轻微摆动
|
||||
moon.rotation.y += 0.0009;
|
||||
moonGroup.position.y = 1 + Math.sin(t * 0.35) * 0.35;
|
||||
moonGroup.rotation.z = Math.sin(t * 0.12) * 0.02;
|
||||
// 月亮缓慢自转 + 呼吸
|
||||
if (moon) moon.rotation.y += 0.0006;
|
||||
moonGroup.position.y = 1 + Math.sin(t * 0.33) * 0.3;
|
||||
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 rt = repair / 0.85; // 0→1
|
||||
var rt = repair / 0.85;
|
||||
|
||||
// 大碎片: 无衰减漂移,各自周期相位
|
||||
fragments.forEach(function (f) {
|
||||
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(
|
||||
target.x + Math.sin(t * 0.5 + f.phase) * 0.35 * (1 - rt),
|
||||
target.y + Math.cos(t * 0.42 + f.phase) * 0.3 * (1 - rt),
|
||||
target.z + Math.sin(t * 0.31 + f.phase * 2) * 0.3 * (1 - rt)
|
||||
target.x + Math.sin(w1) * amp,
|
||||
target.y + Math.sin(w1 * 0.83 + 1.2) * amp * 0.8,
|
||||
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.y += 0.0022 * f.spec.spin * (1 - rt * 0.8);
|
||||
});
|
||||
|
||||
// 光缝亮起
|
||||
seams.forEach(function (s, i) {
|
||||
s.material.opacity = rt * 0.85 + Math.sin(t * 2 + i) * 0.06 * rt;
|
||||
// 碎石场: 各自向外漂移+翻滚+不同步摆动(持续离散中)
|
||||
if (debrisMesh) {
|
||||
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;
|
||||
leakSprites.forEach(function (L) {
|
||||
L.life += L.speed * 0.016 * 60 * 0.016;
|
||||
// 光缝
|
||||
seams.forEach(function (s, i) {
|
||||
s.material.opacity = rt * 0.9 + Math.sin(t * 2.2 + i * 1.7) * 0.06 * rt;
|
||||
});
|
||||
|
||||
// 代码泄漏
|
||||
var leakRate = 1 - rt * 0.55;
|
||||
leaks.forEach(function (L) {
|
||||
L.life += L.speed * 0.016;
|
||||
if (L.life > 1) {
|
||||
L.life = 0;
|
||||
L.sp.position.copy(L.origin);
|
||||
L.sp.material.map = charTex(codeChars[Math.floor(Math.random() * codeChars.length)]);
|
||||
}
|
||||
L.sp.position.x += L.drift.x * 0.016;
|
||||
L.sp.position.y += L.drift.y * 0.016;
|
||||
L.sp.position.z += L.drift.z * 0.016;
|
||||
L.sp.material.opacity = Math.sin(L.life * Math.PI) * 0.75 * leakRate;
|
||||
L.sp.position.add(L.drift.clone().multiplyScalar(0.016));
|
||||
L.sp.material.opacity = Math.sin(L.life * Math.PI) * 0.8 * 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;
|
||||
starsFar.rotation.y += 0.00004;
|
||||
// 星野极缓旋转 + 近尘埃视差
|
||||
if (skyMesh) skyMesh.rotation.y += 0.00008;
|
||||
nearDust.rotation.y += 0.0003;
|
||||
|
||||
renderer.render(scene, camera);
|
||||
if (composer) composer.render();
|
||||
else renderer.render(scene, camera);
|
||||
}
|
||||
animate();
|
||||
|
||||
/* ---------- 自适应 ---------- */
|
||||
window.addEventListener("resize", function () {
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
addEventListener("resize", function () {
|
||||
camera.aspect = innerWidth / innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
renderer.setSize(innerWidth, innerHeight);
|
||||
if (composer) composer.setSize(innerWidth, innerHeight);
|
||||
});
|
||||
})();
|
||||
|
||||
+6
-2
@@ -271,13 +271,17 @@
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<span>© 2026 郑州晟算科技有限公司</span>
|
||||
<span style="opacity:0.45;font-size:11px">Moon & Stars Imagery: NASA · Solar System Scope (CC BY 4.0)</span>
|
||||
<span>ground_control@shengsuan:~$ repairing_moon --progress 85%</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="js/lib/three.min.js"></script>
|
||||
<script src="js/space.js"></script>
|
||||
<script type="importmap">
|
||||
{"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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user