Gargantua

Drag to orbit • Scroll to zoom

``` JS module: ``` import * as THREE from 'three'; const container = document.body; const scene = new THREE.Scene(); const camera = new THREE.OrthographicCamera(-1,1,1,-1,0,1); const renderer = new THREE.WebGLRenderer({ antialias:false, powerPreference:'high-performance' }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); container.appendChild(renderer.domElement); const geometry = new THREE.PlaneGeometry(2,2); const material = new THREE.ShaderMaterial({ vertexShader: `...`, fragmentShader: `...`, uniforms: { iResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, iTime: { value: 0 }, cameraPos: { value: new THREE.Vector3(0, 3, -10) }, diskInner: { value: 2.6 }, diskOuter: { value: 14.0 }, blackHoleRadius: { value: 1.0 } } }); scene.add(new THREE.Mesh(geometry, material)); const spherical = new THREE.Spherical(10.0, Math.PI/2 - 0.25, 0); let isDragging = false, lastX=0, lastY=0; ... function animate(time) { requestAnimationFrame(animate); if(!isDragging) spherical.theta += 0.0005; const pos = new THREE.Vector3().setFromSpherical(spherical); material.uniforms.cameraPos.value.copy(pos); material.uniforms.iTime.value = time * 0.001; renderer.render(scene, camera); } animate(0); ``` For the shader, ensure no syntax errors. Let's draft: Vertex shader: ``` varying vec2 vUv; void main() { vUv = uv; gl_Position = vec4(position, 1.0); } ``` Fragment shader: ``` precision highp float; uniform vec2 iResolution; uniform float iTime; uniform vec3 cameraPos; uniform float diskInner; uniform float diskOuter; uniform float blackHoleRadius; varying vec2 vUv; #define R_S blackHoleRadius #define MAX_STEPS 140 float hash(vec3 p) { ... } float noise(vec3 p) { ... } vec3 blackbody(float t) { ... } vec3 starfield(vec3 d) { ... } vec3 sampleDisk(vec2 p, vec3 rayDir) { ... } void main() { vec2 uv = (vUv - 0.5) * 2.0; uv.x *= iResolution.x / iResolution.y; vec3 ro = cameraPos; vec3 ta = vec3(0.0); vec3 ww = normalize(ta - ro); vec3 uu = normalize(cross(ww, vec3(0.0, 1.0, 0.0))); vec3 vv = normalize(cross(uu, ww)); vec3 rd = normalize(uv.x * uu + uv.y * vv + 1.8 * ww); vec3 p = ro; vec3 v = rd; vec3 col = vec3(0.0); float diskHit = 0.0; for(int i=0; i diskInner && hr < diskOuter) { vec3 c = sampleDisk(hp.xz, v); float alpha = 0.85; col = mix(col, c, alpha); diskHit = 1.0; } } p = pNext; if(r > 50.0) break; } if(diskHit < 0.5) { col = starfield(v); } // tone mapping / gamma col = col / (1.0 + col); col = pow(col, vec3(0.4545)); gl_FragColor = vec4(col, 1.0); } ``` But if the ray is captured, we set col black and break. If it hits disk, we set diskHit. At the end, if diskHit < 0.5, sample starfield. However, if ray is captured, diskHit may still be 0, and we'd sample starfield incorrectly. Need a separate captured flag. Use `bool captured = false; bool hitDisk = false;`. In GLSL, bool is fine. Also, for disk hits, after capture, we break with black. At the end: ``` if(captured) col = vec3(0.0); else if(!hitDisk) col = starfield(v); ``` But if we hit the disk and then continue and get captured, we should still show black? Actually, if captured after disk hit, the disk image would be inside the shadow, which is physically not visible. But in our integration, once captured, we break and set black, overwriting disk color. Better: if captured, ignore disk and output black. So set captured flag and break. But if we hit the disk before capture, that means the disk is inside the black hole shadow? Actually, the disk is outside the event horizon, so if the ray hits the disk, it shouldn't be captured afterward unless it passes through the disk and then into the black hole. That could happen for the back side of the disk. In that case, the disk point is visible, and then the ray continues to the black hole? No, if the disk is opaque, the ray is absorbed/scattered, so we should stop at the disk. But for visualization, we can still show the disk color and not continue. So if we hit disk, we can break? But there may be multiple lensed images along the ray. Hmm. Simplify: don't break on disk hit; continue integration and blend subsequent crossings. But if captured after, output black. This may cause some disk images to be erased. To avoid, only set captured if r < R_s and we haven't hit the disk in a way that matters. But it's complex. Alternative: use alpha blending and don't break. At the end, if captured, set col black. This means any disk crossings before capture are lost. But rays that hit the disk are usually not captured because the disk is at r > inner radius, and after passing through the disk, the ray continues. If it then goes into the black hole, that disk image would be behind the shadow. So it's okay to black it out. Actually, the disk is in the equatorial plane, and rays hitting the disk before reaching the black hole are absorbed. So we should stop at the first disk hit. But gravitational lensing can make a single ray intersect the disk multiple times. For realism, the first intersection should occlude subsequent ones. But for the iconic "lensed disk above and below" effect, we often want to see both front and back sides. Given the complexity, let's do this: accumulate all disk crossings with alpha blending, and if captured, final color is black. This is