WebGL黑洞着色器:广义相对论真实吸积盘效果

基于WebGL(Three.js)技术实现的广义相对论着色器引擎。

Github开源
该引擎采用了一种新型的合成方式来实现真正近似黑洞吸积盘的效果。该引擎能够很好的体现在吸积盘高速旋转时产生的红移和蓝移效应。
这是一种极低成本得到最近似真实黑洞影响的一种实现方式。代码的实现和改进一部分采用了Gimini3pro进行优化。

图1是红移着色器开到最高的效果图 图二是没有开红移着色器的效果。
现在的黑洞吸积盘是顺时针旋转的,能很明显的对比出红移的效果。另外还有一个较为极端的红移展示(这样子的吸积盘表现是因为设定时是一个漏斗形状的初始状态)

效果展示的差不多了,我们来聊聊具体实现。

架构概览:CPU-GPU 混合计算

为了在保持物理交互性的同时实现数十万粒子的相对论视觉特效,我们采用了一种分层架构:

层级负责内容数据类型
CPU(JavaScript)N-body轨道积分、碰撞检测、吸积逻辑、粒子生命周期Float32Array
GPU(Vertex Shader)引力红移、多普勒效应、相对论集束、引力透镜、颜色混合attribute vec3
GPU(Fragment Shader)纹理、光晕、像素合成gl_FragColor

物理引擎实现(粒子系统)

这里实现了基础的多体运动N-body洛希极限解体(这里为了简便直接简化为继承原有运动状态的粒子团了)。这里采用了半隐式欧拉积分。 他算的足够快,对于复杂的实时模拟上来看,它在稳定性和性能之间取得了平衡。为了实现红移,我把粒子当成光子,计算他相对于摄像机的速度,远离就是更“红”更暗,靠近就更“蓝”更亮(也实现了多普勒效应)

// 伪代码:核心物理循环for(let i =0; i < particleCount; i++){// 1. 计算合力 (牛顿引力) vec3 acc =calculateGravity(position[i], attractors);// 2. 更新速度 velocity[i]+= acc * dt;// 3. 更新位置 position[i]+= velocity[i]* dt;// 4. 史瓦西半径检查 (视界吸收)if(distance(position[i], blackhole)< SchwarzschildRadius *1.05){respawnParticle(i);// 粒子落入黑洞,重置}}

虚实粒子渲染:

这里涉及到一种独特的渲染方法。为了模拟吸积盘等的绚丽场景,我们总不能真的渲染几百万个粒子吧,那样我们的电脑可能就会爆炸了。
我们可以通过一个真实的粒子运动来推断他四周的粒子云的运动方式应该是类似的。这样就可以大大减少计算的数量达到更多的真实的粒子效果。

系统架构:
classParticleManager{// 三个核心缓冲区 positions =newFloat32Array(maxCount *3);// 位置 (虚拟) velocities =newFloat32Array(maxCount *3);// 速度 (虚拟) colors =newFloat32Array(maxCount *3);// 颜色 (虚拟)// 虚实连接点:从实体位置初始化虚拟粒子emit(pos, vel, count, radius, spread, colorHex){// 基于实体位置生成虚拟粒子云this.positions[idx]= pos.x + r * Math.sin(phi)* Math.cos(theta);// ... 速度继承 + 随机扩散}}
着色器中的虚实融合
// 顶点着色器关键逻辑voidmain(){ vec3 physPos = position;// 虚拟粒子位置 vec3 renderPos = physPos;// 渲染位置(可能被透镜扭曲)// 但计算红移时,考虑实体黑洞的位置for(int i =0; i < uBHCount; i++){ vec3 bhPos = uBHPositions[i];// 实体黑洞位置 float r =distance(physPos, bhPos);// 虚粒子到实体的距离// 实体影响虚粒子的渲染效果 totalGravPotential += uRs[i]/max(r, uRs[i]*1.01);}// 虚粒子颜色受实体引力影响而改变if(totalShift >1.1){ finalColor =mix(baseColor,vec3(0.4,0.6,1.0), t);}}
特殊效果:虚粒子的"实体化"模拟
//吸积盘的内落效果// 在粒子更新循环中if(CONFIG.enableAccretion && dist < b.radius *1.2){ b.absorbMass(CONFIG.PARTICLE_MASS);// 虚拟粒子被实体吸收this.lifetimes[i]=0;// 虚拟粒子消失this.positions[idx]=999999;// 移出视野//虽然这样会堆积一堆发光的垃圾但是我懒得改了。还能产生一些星云效果嘻嘻。}
//粒子坠入黑洞的视觉效果// 在着色器中视界消隐for(int i =0; i <2; i++){ float r =distance(physPos, uBHPositions[i]); float rs = uRs[i];// 虚拟粒子在视界内透明度为0(模拟被吞噬) vAlpha *=smoothstep(rs *0.95, rs *1.1, r);}

广义相对论渲染

我采用了一种类似于星际穿越剧组渲染黑洞的方法。通过逆向解析光线弯曲来实现引力透镜和爱因斯坦环的近似效果。
我简化了一些物理原理。

简化假设:
平面近似:假设所有光线在同一平面内传播
弱场近似:使用线性叠加而非精确场方程
静态场:忽略黑洞运动带来的效应
单次散射:忽略光线绕行多圈的情况

首先是收集黑洞的信息:

// 从所有天体中找到黑洞const bhs = bodies.filter(b=> b.isBlackhole);// 按质量排序(最重要的先处理) bhs.sort((a,b)=> b.mass - a.mass);// 限制数量(性能优化)const maxLensCount =4;// 最多支持4个黑洞同时透镜 lensPass.uniforms.uCount.value = Math.min(bhs.length, maxLensCount);

坐标变换(世界坐标到屏幕坐标)因为计算效果是通过摄像机的视角进行计算的。

for(let i =0; i < maxLensCount; i++){if(i < bhs.length){const bh = bhs[i];// 关键步骤:3D世界坐标 → 2D屏幕坐标// 1. 使用Three.js的project()方法const p = bh.position.clone().project(camera);// 此时p.x, p.y范围是[-1, 1],z是深度// 2. 归一化到[0,1]范围 lensPass.uniforms.uPositions.value[i].set( p.x *0.5+0.5,// 映射到[0,1] p.y *0.5+0.5, p.z );// 3. 计算屏幕空间史瓦西半径// 物理半径 → 视角 → 屏幕像素const dist = camera.position.distanceTo(bh.position);const physicalRs = bh.calcSchwarzschildRadius();// 2GM/c²const angularRs = physicalRs / dist;// 弧度const screenRs = angularRs *2.0;// 经验缩放因子 lensPass.uniforms.uRadii.value[i]= screenRs;}else{// 填充无效数据 lensPass.uniforms.uRadii.value[i]=0;}}

着色器:

// lensing-fs uniform sampler2D tDiffuse;// 输入纹理(已渲染的场景) uniform vec2 uResolution;// 屏幕分辨率 uniform int uCount;// 黑洞数量(0-4) uniform vec3 uPositions[4];// 黑洞屏幕位置(x,y,z) uniform float uRadii[4];// 屏幕空间史瓦西半径 uniform float uStrength;// 透镜强度参数 varying vec2 vUv;voidmain(){// 初始化为原始UV坐标 vec2 uv = vUv; vec2 finalUv = uv; float totalMask =0.0;// 视界遮罩 float photonRing =0.0;// 光子环累积// 考虑屏幕宽高比(保证圆形扭曲) vec2 aspect =vec2(uResolution.x / uResolution.y,1.0);// 遍历所有黑洞for(int i =0; i <4; i++){if(i >= uCount)break;// 跳过无效黑洞// 获取当前黑洞参数 vec2 center = uPositions[i].xy;// 屏幕位置(x,y) float rs = uRadii[i];// 屏幕空间史瓦西半径// 计算当前像素到黑洞的距离(考虑宽高比) vec2 dir =(uv - center)* aspect; float dist =length(dir);// 视界处理if(dist < rs){// 像素在视界内,标记为完全屏蔽 totalMask =1.0;}// 扭曲计算if(dist > rs){// 关键公式:偏转角度 ∝ 1/(距离 - rs) float deflection =(rs * uStrength *0.12)/max(dist - rs,0.001);// 限制最大偏转(防止过度扭曲) deflection =min(deflection,0.5);// 计算扭曲方向:从黑洞指向像素 vec2 offset =normalize(uv - center)* deflection;// 逆向采样:应用扭曲 finalUv -= offset;// 光子环效果// 在1.1倍史瓦西半径附近添加发光效果 float glow =exp(-(dist - rs *1.1)*30.0/(rs +0.001)); photonRing += glow *0.8;}}// 最终颜色合成// 1. 采样扭曲后的纹理 vec4 color =texture2D(tDiffuse, finalUv);// 2. 添加光子环颜色(橙黄色) vec4 ringColor =vec4(1.0,0.7,0.3,1.0)* photonRing;// 3. 应用视界遮罩:视界内为纯黑色 gl_FragColor =mix(color + ringColor,vec4(0.0,0.0,0.0,1.0), totalMask);}

最关键的就是逆向扭曲算法:
Q = P − ∑ i δ i ( P ) \mathbf{Q} = \mathbf{P} - \sum_i \mathbf{\delta}_i(\mathbf{P}) Q=P−i∑​δi​(P)
其中 δ i ( P ) \delta_i(\mathbf{P}) δi​(P) 是黑洞 i i i 在像素 P \mathbf{P} P处产生的偏转。
偏转函数:
δ ( r ) = k ⋅ r s r − r s , r > r s \delta(r) = k \cdot \frac{r_s}{r - r_s}, \quad r > r_s δ(r)=k⋅r−rs​rs​​,r>rs​
其中:
r r r:像素到黑洞中心距离
r s r_s rs​:屏幕空间的施瓦西半径
k k k:可调节系数

多重黑洞处理

δ total ( P ) = ∑ i = 1 N δ i ( P ) \mathbf{\delta}_{\text{total}}(\mathbf{P}) = \sum_{i=1}^{N} \mathbf{\delta}_i(\mathbf{P}) δtotal​(P)=i=1∑N​δi​(P)
采用了线性叠加的方案,这里是和真实的大相径庭的,当黑洞相互靠近时,非线性效应显著,会产生复杂的焦散线和多重镜像。但是暂时没办法只能妥协。

最后较为复杂的实现点就这些,也是一些必要的简化部分,ui界面和一些额外的功能采用了Gimini3pro和GLM4.7模型,帮我节省了很多的开发时间,系统还有一些小Bug和缺失的功能,但是无伤大雅后面会慢慢补上。(有点事先写到这,更多的技术细节马上补充)

Read more

5个最火AI写作镜像推荐:0配置开箱即用,10块钱全试遍

5个最火AI写作镜像推荐:0配置开箱即用,10块钱全试遍 你是不是也遇到过这种情况?老师布置了一篇分析报告,要求用AI工具辅助完成。你兴致勃勃打开电脑,搜索“AI写作工具”,结果跳出来一堆GitHub项目、命令行指令、Python依赖库……看着满屏的英文和代码,瞬间头大如斗。作为文科生,你只想写点文字,哪懂什么环境配置、CUDA驱动、PyTorch安装? 别慌,你不是一个人。我当年也是从“打开终端就手抖”的小白走过来的。今天这篇文章,就是专门为不想折腾技术、只想马上写出好内容的你准备的。 我们不讲代码原理,不搞复杂部署,只聚焦一件事:5个真正“0配置、开箱即用”的AI写作镜像,每一个都能在几分钟内启动,直接通过网页输入提示词,生成高质量文章、报告、文案甚至小红书爆款内容。最关键的是——平均每个镜像体验成本不到2块钱,10块钱就能把5个全试一遍! 这些镜像都来自ZEEKLOG星图平台的预置资源,内置了完整的运行环境(包括CUDA、PyTorch、vLLM等),你只需要点击“一键部署”,等待几分钟,

Copilot实战:如何用AI助手高效完成1.5万行Python项目(附完整提示词模板)

Copilot实战:如何用AI助手高效完成1.5万行Python项目(附完整提示词模板) 最近在折腾一个不算太小的Python项目,代码量最终堆到了1.5万行左右。整个过程里,我几乎把Copilot当成了我的“第二大脑”。说实话,它确实没法独立完成一个项目,但如果你知道怎么跟它“对话”,怎么给它“喂”对的信息,它带来的效率提升是惊人的。这篇文章,我就想抛开那些泛泛而谈的“AI编程革命”,从一个真实项目参与者的角度,聊聊怎么让Copilot真正成为你手边最趁手的工具,而不是一个时灵时不灵的玩具。我会分享我踩过的坑、总结出的具体提示词模板,以及如何管理项目文件来最大化AI助手的效用。如果你也厌倦了在简单重复的代码上浪费时间,希望把精力集中在真正的架构和逻辑设计上,那么接下来的内容,或许能给你一些实在的启发。 1. 从“玩具”到“工具”:重新定位你的AI编程伙伴 很多开发者初次接触Copilot时,都抱着一种“让它写代码给我看”的心态。这往往导致最初的兴奋迅速被挫败感取代——生成的代码牛头不对马嘴,或者稍微复杂一点的需求就卡壳。问题的核心在于,我们错误地将其定位为一个“全自动代码生成

从Alpaca到ShareGPT:Llama Factory数据格式全解析

从Alpaca到ShareGPT:Llama Factory数据格式全解析 作为一名数据工程师,在准备大模型微调数据时,你是否经常纠结于选择哪种数据格式?Alpaca、ShareGPT、Vicuna...各种格式的文档分散在不同地方,手动转换又容易出错。本文将带你全面解析Llama Factory支持的数据格式,帮助你快速测试不同格式的效果。 这类任务通常需要GPU环境支持,目前ZEEKLOG算力平台提供了包含Llama Factory镜像的预置环境,可快速部署验证。下面我们就从实际应用场景出发,详细介绍如何高效使用这些数据格式。 Llama Factory数据格式概述 Llama Factory作为大模型微调的热门框架,支持多种主流数据格式,主要分为两大类: * 指令监督微调格式:以Alpaca为代表,适合单轮问答任务 * 多轮对话格式:以ShareGPT为代表,适合聊天场景 每种格式都有特定的字段要求,理解这些差异是成功微调的第一步。 Alpaca格式详解 Alpaca格式是单轮指令微调的标准格式,包含三个核心字段: { "instruction": "解释

AI绘画建筑设计提示词:从基础到高级的完整创作指南

AI绘画建筑设计提示词:从基础到高级的完整创作指南

一、核心逻辑:高质量建筑提示词的 7 大组成部分 AI 对建筑的理解需要 “分层引导”,一个完整的提示词通常包含 7 个关键模块,你可根据需求灵活组合或删减,基础逻辑为:先明确 “画什么”,再定义 “怎么画”,最后优化 “画得好”。具体结构如下: [主体/建筑类型] + [风格/建筑师参考] + [环境/场景设定] + [细节与材质] + [构图与视角] + [灯光与氛围] + [画质/技术参数] 这一结构能让 AI 清晰捕捉设计核心,避免因信息模糊导致的 “偏离预期”,是高效创作的基础框架。 二、分模块详解:建筑提示词词汇库与应用技巧 1. 主体 / 建筑类型:明确 “画什么” 的核心 这是提示词的 “根基”,需精准定义建筑的功能与形态,避免笼统表述。