1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
|
# Ray Tracing in Curved Spacetime: A Detailed Overview
This ray tracer simulates how light behaves near a black hole, including the effects of curved spacetime and interactions with objects like stars and an accretion disk. The implementation follows a step-by-step approach, which includes initializing rays, tracing their paths through spacetime, detecting intersections with objects, and finally rendering the scene.
## 1. Ray Initialization
The first step in ray tracing is to create rays originating from the camera that will eventually traverse through spacetime. Each ray represents a possible path of light.
### Camera Setup
The camera is positioned in three-dimensional space and is defined by its orientation and field of view. It has the following parameters:
```glsl
layout(std140, binding = 1) uniform Camera
{
vec3 camPos; float _pad0;
vec3 camRight; float _pad1;
vec3 camUp; float _pad2;
vec3 camForward; float _pad3;
float tanHalfFov; // Field of view
float aspect; // Aspect ratio
bool moving; // Whether the camera is moving
int _pad4;
} cam;
```
- `camPos`: The 3D position of the camera.
- `camRight`, `camUp`, `camForward`: Orthonormal vectors defining the camera's orientation.
- `tanHalfFov` and `aspect`: Determine the camera’s field of view and the shape of the image plane.
### Ray Generation per Pixel
For each pixel on the image, we calculate the corresponding direction in world space and generate a ray pointing in that direction:
```glsl
float u = (2.0 * (pix.x + 0.5) / WIDTH - 1.0) * cam.aspect * cam.tanHalfFov;
float v = (1.0 - 2.0 * (pix.y + 0.5) / HEIGHT) * cam.tanHalfFov;
vec3 dir = normalize(u * cam.camRight - v * cam.camUp + cam.camForward);
Ray ray = InitRay(cam.camPos, dir);
```
Here, `u` and `v` are normalized coordinates on the image plane. The `InitRay` function takes the camera position and the computed direction to create a ray in both Cartesian and spherical coordinates.
### Ray Structure
Each ray contains not only the standard 3D Cartesian position but also spherical coordinates and velocity components, which are necessary for simulating curved spacetime:
```glsl
struct Ray
{
float x, y, z; // Cartesian coordinates
float r, theta, phi; // Spherical coordinates
float dr, dtheta, dphi; // Radial and angular velocities
float E, L; // Conserved quantities in Schwarzschild geometry
};
```
### Converting Cartesian to Spherical Coordinates
The `InitRay` function converts the position and direction of the ray into spherical coordinates and computes initial velocities:
```glsl
Ray InitRay(vec3 pos, vec3 dir)
{
Ray ray;
ray.x = pos.x;
ray.y = pos.y;
ray.z = pos.z;
// Spherical coordinates
ray.r = length(pos);
ray.theta = acos(pos.z / ray.r);
ray.phi = atan(pos.y, pos.x);
// Convert direction vector to spherical velocities
float dx = dir.x;
float dy = dir.y;
float dz = dir.z;
ray.dr = sin(ray.theta)*cos(ray.phi)*dx + sin(ray.theta)*sin(ray.phi)*dy + cos(ray.theta)*dz;
ray.dtheta = (cos(ray.theta)*cos(ray.phi)*dx + cos(ray.theta)*sin(ray.phi)*dy - sin(ray.theta)*dz) / ray.r;
ray.dphi = (-sin(ray.phi)*dx + cos(ray.phi)*dy) / (ray.r * sin(ray.theta));
// Calculate conserved quantities (energy E and angular momentum L)
ray.L = ray.r * ray.r * sin(ray.theta) * ray.dphi;
float f = 1.0 - SagA_rs / ray.r;
float dt_dL = sqrt((ray.dr*ray.dr)/f + ray.r*ray.r*(ray.dtheta*ray.dtheta + sin(ray.theta)*sin(ray.theta)*ray.dphi*ray.dphi));
ray.E = f * dt_dL;
return ray;
}
```
## 2. Geodesic Integration
Once the ray is initialized, we trace its path through curved spacetime using the Schwarzschild metric. This requires solving the geodesic equations for the ray.
### Integration Loop
The ray is advanced step by step using a numerical integrator (Runge-Kutta 4th order). The loop continues until the ray either escapes the scene, falls into the black hole, or intersects an object.
```glsl
for (int i = 0; i < maxSteps; ++i)
{
if (ray.r > exitDistance) break;
if (ray.r > ESCAPE_R) break;
if (Intercept(ray, SagA_rs)) { hitBlackHole = true; break; }
currentStepSize = CalculateAdaptiveStepSize(ray, D_LAMBDA);
RK4Step(ray, currentStepSize);
lambda += currentStepSize;
vec3 newPos = vec3(ray.x, ray.y, ray.z);
if (CrossesEquatorialPlane(prevPos, newPos)) { hitDisk = true; break; }
if (i % objectCheckInterval == 0 && InterceptObject(ray)) { hitObject = true; break; }
prevPos = newPos;
}
```
### Runge-Kutta 4 (RK4)
The RK4 integrator provides high accuracy by computing intermediate slopes and averaging them to advance the ray:
```glsl
void RK4Step(inout Ray ray, float dL)
{
vec3 k1a, k1b;
GeodesicRHS(ray, k1a, k1b);
ray.r += dL * k1a.x;
ray.theta += dL * k1a.y;
ray.phi += dL * k1a.z;
ray.dr += dL * k1b.x;
ray.dtheta += dL * k1b.y;
ray.dphi += dL * k1b.z;
// Update Cartesian coordinates
ray.x = ray.r * sin(ray.theta) * cos(ray.phi);
ray.y = ray.r * sin(ray.theta) * sin(ray.phi);
ray.z = ray.r * cos(ray.theta);
}
```
### Geodesic Derivatives
The function `GeodesicRHS` computes the derivatives needed for integration:
```glsl
void GeodesicRHS(Ray ray, out vec3 d1, out vec3 d2)
{
float r = ray.r;
float theta = ray.theta;
float dr = ray.dr;
float dtheta = ray.dtheta;
float dphi = ray.dphi;
float f = 1.0 - SagA_rs / r;
float dt_dL = ray.E / f;
d1 = vec3(dr, dtheta, dphi);
d2.x = - (SagA_rs / (2.0 * r*r)) * f * dt_dL * dt_dL
+ (SagA_rs / (2.0 * r*r * f)) * dr * dr
+ r * (dtheta*dtheta + sin(theta)*sin(theta)*dphi*dphi);
d2.y = -2.0*dr*dtheta/r + sin(theta)*cos(theta)*dphi*dphi;
d2.z = -2.0*dr*dphi/r - 2.0*cos(theta)/(sin(theta)) * dtheta * dphi;
}
```
## 3. Intersection Testing
Rays can intersect three types of entities: the black hole, spherical objects, and the accretion disk.
### Black Hole Intersection
A ray hitting the event horizon is considered absorbed:
```glsl
bool Intercept(Ray ray, float rs)
{
return ray.r <= rs;
}
```
### Object Intersection
The scene can contain multiple spherical objects such as stars or planets:
```glsl
bool InterceptObject(Ray ray)
{
vec3 P = vec3(ray.x, ray.y, ray.z);
for (int i = 0; i < numObjects; ++i)
{
vec3 center = objPosRadius[i].xyz;
float radius = objPosRadius[i].w;
float distSq = dot(P - center, P - center);
if (distSq > radius * radius * 4.0) continue;
if (distSq <= radius * radius)
{
objectColor = objColor[i];
hitCenter = center;
hitRadius = radius;
return true;
}
}
return false;
}
```
### Accretion Disk Intersection
The accretion disk lies in the equatorial plane and is checked by detecting if the ray crosses this plane:
```glsl
bool CrossesEquatorialPlane(vec3 oldPos, vec3 newPos)
{
bool crossed = (oldPos.y * newPos.y <0.0);
if (crossed) { diskIntersection = newPos; }
return crossed;
}
```
## 4. Shading and Color Computation
Once an intersection is found, we compute the color of the pixel based on the object hit and relativistic effects such as gravitational redshift and Doppler shift.
```glsl
vec3 ComputeColor(Ray ray)
{
if (hitBlackHole) return vec3(0.0); // Black hole is black
if (hitDisk) return SampleDiskTexture(diskIntersection);
if (hitObject) return ApplyLighting(ray, objectColor, hitCenter, hitRadius);
return SampleBackground(ray); // Background stars, etc.
}
````
This ensures that each pixel reflects both the geometrical position and relativistic effects along the ray.
## 5. Rendering Loop
Finally, the main rendering loop iterates over every pixel on the screen, traces a ray, and stores the computed color:
```glsl
for (int y = 0; y < HEIGHT; ++y)
{
for (int x = 0; x < WIDTH; ++x)
{
Ray ray = InitRayForPixel(x, y);
TraceRay(ray);
vec3 color = ComputeColor(ray);
framebuffer[y*WIDTH + x] = vec4(color, 1.0);
}
}
```
|