Full Code of chunky/sqlraytracer for AI

master 9956cebb6d1c cached
10 files
40.1 KB
12.3k tokens
12 symbols
1 requests
Download .txt
Repository: chunky/sqlraytracer
Branch: master
Commit: 9956cebb6d1c
Files: 10
Total size: 40.1 KB

Directory structure:
gitextract_4npa0ned/

├── .gitignore
├── LICENSE
├── README.md
├── anim.sh
├── create.sh
├── debug_rays.sh
├── postgres_connection.sh
├── raytracer.sql
├── setup.sql
└── show_scene.sh

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
debug_rays
show_scene
scenelist_override.txt
scenelist.txt
*.ppm
anim


================================================
FILE: LICENSE
================================================
Copyright (c) 2021 Gary Briggs <chunky@icculus.org>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# A Pure SQL Raytracer

Everyone writes a raytracer sooner or later. This is mine.

## Example Outputs

<img src="example_outputs/dielectricparty.png" width=200> <img src="example_outputs/oneglassball.png" width=200> <img src="example_outputs/onemirrorball.png" width=200> <img src="example_outputs/glassmatrix.png" width=200> <img src="example_outputs/airy.png" width=200> <img src="example_outputs/onediamondball.png" width=200> <img src="example_outputs/oneantiglassball.png" width=200> <img src="example_outputs/adjacentballs.png" width=200> <img src="example_outputs/onegreyball.png" width=200> <img src="example_outputs/reflectiontest.png" width=200> <img src="example_outputs/twodiffuseballs.png" width=200> <img src="example_outputs/onegreenball.png" width=200> <img src="example_outputs/threemirrors.png" width=200> <img src="example_outputs/twomirrorballs.png" width=200> <img src="example_outputs/busyday.png" width=200>

## Usage

```shell
sh create.sh
```

```postgres_connection.sh``` contains host/database/user/pass/etc.
There are no exotic needs other than "postgres, like version 10 and up
or something"

For what it's worth, I created mine thus on my ubuntu desktop:
```shell
sudo su - postgres
createuser --pwprompt raytracer
createdb -O raytracer raytracer
```

### Levers for development and rendering

While doing development, obviously a few-minute render time is a pretty
poor cycle time. There are a few levers you can pull to speed things up
and reduce quality. They're on "camera" and "img" in setup.sql:

* *samples\_per\_px* - This is the number of rays/sub-samples per pixel.
  - 1 or 2 is fine during debugging
  - 15-20 gives "workable" pictures
  - Going above 50 doesn't generate much visible improvement
* *max\_ray\_depth* - The maximum number of ray bounces
  - For simple scenes, it usually makes no more than 5 or so bounces
* *res\_x* and *res\_y* - Final image resolution
  - Smaller is faster

The main CTE carries a lot of stuff that's unnecessary to final output.
This is so I can examine rays bouncing through the scene with:
```sql
SELECT * FROM rays WHERE img_x=100 AND img_y=250
```

There's a script to get a quick-and-dirty view of a scene using gnuplot;
the script ```show_scene.sh``` should generate a folder of outputs.

## Database

This is implemented in pure SQL. It doesn't do anything like CREATE
FUNCTION or other nonportables, except for the trigger to do animation,
which obviously doesn't count.

At the same time, there are some not-entirely-common features of SQL
that it needs:

* JOIN LATERAL
* PARTITION BY inside of a RECURSIVE CTE
* Math functions like SIN()

So although I started developing this in SQLite, I ended up leaning
on PostgreSQL. As I write this, it works in postgres and hasn't been
tested in anything else.

## Interesting Implementation Pieces

Such as it is, I did find myself solving some problems in interesting
ways.

### JOIN LATERAL

JOIN LATERAL is a way to do a correlated subquery in a JOIN, instead of
just in a WHERE clause. I use this as a way to hoist calculations and
do many of them only once and, in some cases, avoid excessive duplication.

### Diffuse Scattering

This requires sampling a uniform sphere. I generate a lot of random
samples ahead of time [sample with rejection -> scale points to sphere
surface], and number them.

Figuring out a way to join each ray to a single random row from these
precalculated scatters was weird; can't just join to RANDOM() because
every ray got joined to the same, random, scatter. Can't just select
with a typical calculation on a normal because that leads to stripes
in the picture.  So, instead, I schlep out a later few decimals of one
dimension of a normal, then join to that. It's "random" but also
unique-enough-per-ray.

### Recursive CTEs

Raytracing very naturally tracks how recursive CTEs work. One of the
things I ran into was a clean way to identify which ray is the one to
account for. Using a window function ordering by intercept (t) worked
well. Every iteration, this query intersects a ray with *everything*
in front of it and does all of the associated calculations, but then in
the WHERE clause will reject everything except the thing the ray
actually hit.

Also, there's something really beautiful about the simplicity of the
core of the final rollup [edited for clarity]:
```sql
 SELECT img_x, img_y,
         SUM(POW(color_mult * ray_col_r/samples_per_px, gamma)) col_r,
         SUM(POW(color_mult * ray_col_g/samples_per_px, gamma)) col_g,
         SUM(POW(color_mult * ray_col_b/samples_per_px, gamma)) col_b
    FROM rays
    GROUP BY img_y, img_x
```

### Scenes, materials, etc

Because this is in SQL, I can store multiple scenes in the database. Which
one is actually rendered is selected in the "camera" table.

## Standing on the Necks of Giants

Two years before I wrote this "The most advanced MySQL raytracer on the
market right now" did the rounds on social media:
https://www.pouet.net/prod.php?which=83222

I had a few things in mind that I wanted to do differently [worse?]:

* Demoscene is an artform. I'm not golfing, this isn't minified
* Not a single query; that can be done with CTEs, but ehhhhhhhh
* Animation as an endgame [see ```anim.sh```]
* Mainly, I'm just buggering around with the wrong tool for the job

### I asked Claude for an opinion

*"Why would someone write something like this in the first place?"*

> This is a fascinating example of SQL as a general-purpose programming language taken to an extreme. Someone wrote a complete ray tracer in PostgreSQL SQL, which is both impressive and... questionable.
> 
> [...]
> 
> This feels like either a brilliant computer science student's thesis project, a senior engineer's weekend experiment, or someone trapped in a very restrictive database-only environment who refused to give up on their ray tracing dreams.
> 
> It's the programming equivalent of building a race car out of Lego - technically impressive, but you probably shouldn't actually drive it.

I can't tell if I'm flattered or offended.

## References

Most of this is built following the "Ray Tracing in One Weekend"
series: https://raytracing.github.io/ , then making allowances for
the deliberately obtuse way I'm coding it.


Gary <chunky@icculus.org>



================================================
FILE: anim.sh
================================================
#!/bin/sh

. ./postgres_connection.sh

scenename="busyday"
outfolder=anim
dt=0.05
gravity=-9.8

psql \
	--host=${PGHOST} \
	--port=${PGPORT} \
	--username=${PGUSER} \
	--dbname=${PGDB} \
	--file=setup.sql \
	--file=raytracer.sql \
	--command="UPDATE camera SET sceneid=(SELECT sceneid FROM scene WHERE scenename='${scenename}')"

mkdir -p ${outfolder}

for frame in `seq 0 1000`
do
  echo "Frame ${frame}"
  psql \
  	--host=${PGHOST} \
  	--port=${PGPORT} \
  	--username=${PGUSER} \
  	--dbname=${PGDB} \
	--command="INSERT INTO updateworld(dt, grav_x, grav_y, grav_z) VALUES (${dt}, 0.0, ${gravity}, 0.0)" \
  	--command="\\timing" \
	--command="\\copy (select * from ppm) to './${outfolder}/${scenename}_${frame}.ppm' csv"

done

ffmpeg \
	-r 25 \
	-i ./${outfolder}/${scenename}_%d.ppm \
	-crf 25 \
       	-pix_fmt yuv420p \
	./${outfolder}/${scenename}.mp4



================================================
FILE: create.sh
================================================
#!/bin/sh

. ./postgres_connection.sh

# Creating this file overrides which scenes get rendered
scenelist_override=scenelist_override.txt

scenelist=scenelist.txt

outputdir=example_outputs

mkdir -p ${outputdir}

psql \
	--host=${PGHOST} \
	--port=${PGPORT} \
	--username=${PGUSER} \
	--dbname=${PGDB} \
	--file=setup.sql \
	--file=raytracer.sql \
	--command="\\timing" \
	--command="\\copy (select scenename from scene) to './${outputdir}/${scenelist}' csv"

test -e ${scenelist_override} && cp ${scenelist_override} ${outputdir}/${scenelist}

while read scenename
do
  echo ""
  echo "Rendering scene ${scenename}"
  psql \
  	--host=${PGHOST} \
  	--port=${PGPORT} \
  	--username=${PGUSER} \
  	--dbname=${PGDB} \
  	--command="UPDATE camera SET sceneid=(SELECT sceneid FROM scene WHERE scenename='${scenename}')" \
  	--command="\\timing" \
	--command="\\copy (select * from ppm) to './${outputdir}/${scenename}.ppm' csv"

  if [ "$(uname)" == "Darwin" ]; then
    open ./${outputdir}/${scenename}.ppm
  else
    xdg-open ./${outputdir}/${scenename}.ppm
  fi
  
  convert ./${outputdir}/${scenename}.ppm \
	  -gravity SouthEast -pointsize 30 -fill black -annotate +10+10 "${scenename}" \
	  ./${outputdir}/${scenename}.png

done < ./${outputdir}/${scenelist}



================================================
FILE: debug_rays.sh
================================================
#!/bin/sh

. ./postgres_connection.sh

debug_dir=debug_rays
mkdir -p ${debug_dir}
cd ${debug_dir}

psql \
	--host=${PGHOST} \
        --port=${PGPORT} \
        --username=${PGUSER} \
        --dbname=${PGDB} \
        --command="\\copy (select sphereid, cx, cy, cz, radius from sphere inner join camera c ON c.sceneid=sphere.sceneid) to './spheres.csv' with csv header" \
	--command="\\copy (select * from rays WHERE img_x=125 AND img_y between 120 and 195 and 0=img_y%5) to './rays.csv' with csv header"

img_size=2048

scale_vector=5

cat <<EOH > gnuplot.gp
set terminal png size ${img_size},${img_size}
set xrange [-60:60]
set yrange [-60:60]
set datafile separator ','
set key autotitle columnhead

set output 'debug_rays_xz.png'
set xlabel "X"
set ylabel "Z"
plot \\
        "spheres.csv" u 2:4:5 w circles t 'spheres', \\
	"rays.csv" u 12:14:(${scale_vector}*\$19/\$22):(${scale_vector}*\$21/\$22) w vectors t 'normals', \\
	"rays.csv" u 12:14:(${scale_vector}*\$15):(${scale_vector}*\$17) w vectors filled head t 'rays'

set output 'debug_rays_zx.png'
set xlabel "Z"
set ylabel "X"
plot \\
        "spheres.csv" u 4:2:5 w circles t 'spheres', \\
	"rays.csv" u 14:12:(${scale_vector}*\$21/\$22):(${scale_vector}*\$19/\$22) w vectors t 'normals', \\
	"rays.csv" u 14:12:(${scale_vector}*\$17):(${scale_vector}*\$15) w vectors filled head t 'rays'

set output 'debug_rays_xy.png'
set xlabel "X"
set ylabel "Y"
plot \\
        "spheres.csv" u 2:3:5 w circles t 'spheres', \\
	"rays.csv" u 12:13:(${scale_vector}*\$19/\$22):(${scale_vector}*\$20/\$22) w vectors t 'normals', \\
	"rays.csv" u 12:13:(${scale_vector}*\$15):(${scale_vector}*\$16) w vectors t 'rays'

set output 'debug_rays_yx.png'
set xlabel "Y"
set ylabel "X"
plot \\
        "spheres.csv" u 3:2:5 w circles t 'spheres', \\
	"rays.csv" u 13:12:(${scale_vector}*\$20/\$22):(${scale_vector}*\$19/\$22) w vectors t 'normals', \\
	"rays.csv" u 13:12:(${scale_vector}*\$16):(${scale_vector}*\$15) w vectors t 'rays'

set output 'debug_rays_yz.png'
set xlabel "Y"
set ylabel "Z"
plot \\
        "spheres.csv" u 3:4:5 w circles t 'spheres', \
	"rays.csv" u 13:14:(${scale_vector}*\$20/\$22):(${scale_vector}*\$21/\$22) w vectors t 'normals', \\
	"rays.csv" u 13:14:(${scale_vector}*\$16):(${scale_vector}*\$17) w vectors t 'rays'

set output 'debug_rays_zy.png'
set xlabel "Z"
set ylabel "Y"
plot \\
        "spheres.csv" u 4:3:5 w circles t 'spheres', \\
	"rays.csv" u 14:13:(${scale_vector}*\$21/\$22):(${scale_vector}*\$20/\$22) w vectors t 'normals', \\
	"rays.csv" u 14:13:(${scale_vector}*\$17):(${scale_vector}*\$16) w vectors t 'rays'

set output '3view.png'
set xrange [-120:120]
set yrange [-120:120]
set zrange [-120:120]
set xlabel "X"
set ylabel "Y"
set zlabel "Z"
splot \\
	"spheres.csv" u 2:3:4:5 w circles t 'spheres', \\
	"rays.csv" u 12:13:14:(${scale_vector}*\$19/\$22):(${scale_vector}*\$20/\$22):(${scale_vector}*\$21/\$22) w vectors filled head t 'normals', \\
	"rays.csv" u 12:13:14:(${scale_vector}*\$15):(${scale_vector}*\$16):(${scale_vector}*\$17) w vectors filled head t 'rays'

set terminal qt
replot

EOH

gnuplot gnuplot.gp

montage \
	-tile 2x2 \
	-geometry ${img_size}x${img_size} \
	debug_rays_xy.png \
       	debug_rays_zy.png \
       	debug_rays_xz.png \
       	3view.png \
	debug_rays.png

xdg-open debug_rays.png



================================================
FILE: postgres_connection.sh
================================================
PGHOST=localhost
PGPORT=5432
PGUSER=raytracer
PGDB=raytracer

PGPASSWORD=raytracer
export PGPASSWORD



================================================
FILE: raytracer.sql
================================================
DROP TABLE IF EXISTS sphere_sample CASCADE;
CREATE TABLE IF NOT EXISTS sphere_sample (x DOUBLE PRECISION NOT NULL, y DOUBLE PRECISION NOT NULL, z DOUBLE PRECISION NOT NULL,
	a DOUBLE PRECISION NOT NULL, b DOUBLE PRECISION NOT NULL, c DOUBLE PRECISION NOT NULL, sampleno INTEGER NOT NULL, n_samples INTEGER NOT NULL);
INSERT INTO sphere_sample
     WITH square_sample AS (SELECT 2.0*(RANDOM() - 0.5) AS a1, 2.0*(RANDOM() - 0.5) AS b1, 2.0*(RANDOM() - 0.5) AS c1
          FROM generate_series(1, 5000)),
     ball_sample AS (SELECT a1 AS a, b1 AS b, c1 AS c, SQRT(a1*a1+b1*b1+c1*c1) AS radius FROM square_sample WHERE 1>=(a1*a1+b1*b1+c1*c1)),
     sphere_sample AS (SELECT a/radius AS x, b/radius AS y, c/radius AS z, a, b, c, ROW_NUMBER() OVER () AS sampleno, COUNT(*) OVER () AS n_samples FROM ball_sample)
     SELECT x,y,z,a,b,c,sampleno,n_samples FROM sphere_sample;
DROP INDEX IF EXISTS idx_ss;
CREATE INDEX IF NOT EXISTS idx_ss ON sphere_sample(sampleno);

DROP VIEW IF EXISTS rays CASCADE;
CREATE VIEW rays AS
    WITH RECURSIVE
     xs AS (SELECT 0 AS u, 0.0 AS img_frac_x UNION ALL SELECT u+1, (u+1.0)/img.res_x FROM xs, img WHERE xs.u<img.res_x-1),
     ys AS (SELECT 0 AS v, 0.0 AS img_frac_y UNION ALL SELECT v+1, (v+1.0)/img.res_y FROM ys, img WHERE ys.v<img.res_y-1),
     px_sample_n(px_sample_n, px_jitter_u, px_jitter_v) AS (SELECT 1, (RANDOM()-0.5) * (fov_rad_x/res_x), (RANDOM()-0.5) * (fov_rad_y/res_y)
                FROM camera, img
        UNION ALL SELECT px_sample_n+1, (RANDOM()-0.5) * (fov_rad_x/res_x), (RANDOM()-0.5) * (fov_rad_y/res_y)
                FROM px_sample_n, camera, img WHERE px_sample_n<camera.samples_per_px),
     rs(img_x, img_y, sceneid, depth, max_ray_depth, samples_per_px, px_sample_n, color_mult,
          ray_col_r, ray_col_g, ray_col_b,
          x1, y1, z1,
          dir_x, dir_y, dir_z,
          dir_lensquared,
          n_x, n_y, n_z, n_len,
          stop_tracing, ray_len_idx, hit_sphereid, n_sphere_samples, inside_dielectric) AS
        -- Send out initial set of rays from camera
         (SELECT xs.u, ys.v, c.sceneid, -1, max_ray_depth, samples_per_px, px_sample_n, CAST(2.0 AS DOUBLE PRECISION),
                CAST(NULL AS DOUBLE PRECISION), CAST(NULL AS DOUBLE PRECISION), CAST(NULL AS DOUBLE PRECISION),
                 c.x, c.y, c.z,
                 (SIN(c.rot_y-(fov_rad_x/2.0)+img_frac_x*fov_rad_x) + px_jitter_u) /
                    SQRT(((SIN(c.rot_y-(fov_rad_x/2.0)+img_frac_x*fov_rad_x) + px_jitter_u)*(SIN(c.rot_y-(fov_rad_x/2.0)+img_frac_x*fov_rad_x) + px_jitter_u) +
                      (SIN(c.rot_x-(fov_rad_y/2.0)+img_frac_y*fov_rad_y) + px_jitter_v)*(SIN(c.rot_x-(fov_rad_y/2.0)+img_frac_y*fov_rad_y) + px_jitter_v) + 1.0)),
                 (SIN(c.rot_x-(fov_rad_y/2.0)+img_frac_y*fov_rad_y) + px_jitter_v) /
                    SQRT(((SIN(c.rot_y-(fov_rad_x/2.0)+img_frac_x*fov_rad_x) + px_jitter_u)*(SIN(c.rot_y-(fov_rad_x/2.0)+img_frac_x*fov_rad_x) + px_jitter_u) +
                      (SIN(c.rot_x-(fov_rad_y/2.0)+img_frac_y*fov_rad_y) + px_jitter_v)*(SIN(c.rot_x-(fov_rad_y/2.0)+img_frac_y*fov_rad_y) + px_jitter_v) + 1.0)),
                 CAST(1.0 AS DOUBLE PRECISION) /
                    SQRT(((SIN(c.rot_y-(fov_rad_x/2.0)+img_frac_x*fov_rad_x) + px_jitter_u)*(SIN(c.rot_y-(fov_rad_x/2.0)+img_frac_x*fov_rad_x) + px_jitter_u) +
                      (SIN(c.rot_x-(fov_rad_y/2.0)+img_frac_y*fov_rad_y) + px_jitter_v)*(SIN(c.rot_x-(fov_rad_y/2.0)+img_frac_y*fov_rad_y) + px_jitter_v) + 1.0)),
                 CAST(1.0 AS DOUBLE PRECISION),
                 CAST(NULL AS DOUBLE PRECISION), CAST(NULL AS DOUBLE PRECISION), CAST(NULL AS DOUBLE PRECISION), CAST(NULL AS DOUBLE PRECISION),
                 CAST(0 AS BOOLEAN), CAST(1 AS BIGINT), CAST(NULL AS INTEGER),
                 (SELECT COUNT(*) FROM sphere_sample), FALSE
              FROM camera c, img, xs, ys, px_sample_n
        UNION ALL
         -- Collide all rays with spheres
          SELECT img_x, img_y, rs.sceneid, depth+1, max_ray_depth, samples_per_px, px_sample_n,
                 (CASE WHEN norm_x IS NULL THEN 0.5 ELSE mirror_frac END)*color_mult,
                 CASE WHEN discrim>0 THEN (CASE
                                                WHEN shade_normal THEN mat_col_r*(1+norm_x)/2
                                                ELSE mat_col_r
                                           END)
                     ELSE 1.0-(0.5*((dir_y/SQRT(dir_lensquared)+1.0)))+0.2*(0.5*((dir_y/SQRT(dir_lensquared)+1.0))) END,
                 CASE WHEN discrim>0 THEN (CASE
                                                WHEN shade_normal THEN mat_col_g*(1+norm_y)/2
                                                ELSE mat_col_g
                                           END)
                     ELSE 1.0-(0.5*((dir_y/SQRT(dir_lensquared)+1.0)))+0.3*(0.5*((dir_y/SQRT(dir_lensquared)+1.0))) END,
                 CASE WHEN discrim>0 THEN (CASE
                                                WHEN shade_normal THEN mat_col_b*(1+norm_z)/2
                                                ELSE mat_col_b
                                           END)
                     ELSE 1.0-(0.5*((dir_y/SQRT(dir_lensquared)+1.0)))+1.0*(0.5*((dir_y/SQRT(dir_lensquared)+1.0))) END,
                 -- x1, y1, z1
                 hit_x, hit_y, hit_z,
                 -- dir_x, dir_y, dir_z
CASE WHEN is_metal 
     THEN (dir_x - 2 * norm_x * dot_ray_norm) / reflection_len
     WHEN is_dielectric AND must_reflect
     THEN reflec_dir_x / final_dir_len
     WHEN is_dielectric 
     THEN refrac_dir_x / final_dir_len
     ELSE diffuse_dir_x/diffuse_dir_len
END,
CASE WHEN is_metal 
     THEN (dir_y - 2 * norm_y * dot_ray_norm) / reflection_len
     WHEN is_dielectric AND must_reflect
     THEN reflec_dir_y / final_dir_len
     WHEN is_dielectric 
     THEN refrac_dir_y / final_dir_len
     ELSE diffuse_dir_y/diffuse_dir_len
END,
CASE WHEN is_metal 
     THEN (dir_z - 2 * norm_z * dot_ray_norm) / reflection_len
     WHEN is_dielectric AND must_reflect
     THEN reflec_dir_z / final_dir_len
     WHEN is_dielectric 
     THEN refrac_dir_z / final_dir_len
     ELSE diffuse_dir_z/diffuse_dir_len
END,
                 1.0,
                 norm_x, norm_y, norm_z, 1.0,
                 discrim IS NULL, ROW_NUMBER() OVER (PARTITION BY img_x, img_y, depth+1, px_sample_n
                                                          ORDER BY t),
                 sphereid, n_sphere_samples, (inside_dielectric AND NOT must_reflect) OR (NOT inside_dielectric AND NOT is_dielectric)
           FROM rs
           LEFT JOIN LATERAL
               (SELECT s.*, discrim,
                       CASE WHEN t_near > 0.001 THEN t_near ELSE t_far END AS t
                         FROM sphere s,
                              LATERAL (SELECT (x1-s.cx)*dir_x+(y1-s.cy)*dir_y+(z1-s.cz)*dir_z AS hb) hbv,
                              LATERAL (SELECT hb*hb - ((x1-s.cx)*(x1-s.cx)+(y1-s.cy)*(y1-s.cy)+(z1-s.cz)*(z1-s.cz)-s.radius2)*dir_lensquared AS discrim) dv,
                              LATERAL (SELECT (-hb - SQRT(discrim))/dir_lensquared AS t_near,
                                              (-hb + SQRT(discrim))/dir_lensquared AS t_far) tv
                         WHERE s.sceneid=rs.sceneid AND discrim > 0
                       ) hit_sphere ON t>0.001
           LEFT JOIN LATERAL
               (SELECT x1+dir_x*t AS hit_x, y1+dir_y*t AS hit_y, z1+dir_z*t AS hit_z,
                       x1+dir_x*t-cx AS norm_x_nonunit, y1+dir_y*t-cy AS norm_y_nonunit, z1+dir_z*t-cz AS norm_z_nonunit,
                       SQRT((x1+dir_x*t-cx)*(x1+dir_x*t-cx)+(y1+dir_y*t-cy)*(y1+dir_y*t-cy)+(z1+dir_z*t-cz)*(z1+dir_z*t-cz)) AS norm_len,
                       ROW_NUMBER() OVER (PARTITION BY img_x, img_y, depth, px_sample_n ORDER BY t ASC) AS t_idx
                       WHERE t>0
               ) sphere_normal ON t_idx=1
           LEFT JOIN LATERAL
               (SELECT CASE WHEN (norm_x_nonunit * dir_x + norm_y_nonunit * dir_y + norm_z_nonunit * dir_z) > 0
                         THEN -norm_x_nonunit/norm_len 
                         ELSE norm_x_nonunit/norm_len END AS norm_x,
                    CASE WHEN (norm_x_nonunit * dir_x + norm_y_nonunit * dir_y + norm_z_nonunit * dir_z) > 0
                         THEN -norm_y_nonunit/norm_len 
                         ELSE norm_y_nonunit/norm_len END AS norm_y,
                    CASE WHEN (norm_x_nonunit * dir_x + norm_y_nonunit * dir_y + norm_z_nonunit * dir_z) > 0
                         THEN -norm_z_nonunit/norm_len 
                         ELSE norm_z_nonunit/norm_len END AS norm_z
               ) sphere_unit_normal ON norm_x IS NOT NULL
           LEFT JOIN LATERAL
               (SELECT dir_x*norm_x + dir_y*norm_y + dir_z*norm_z AS dot_ray_norm,
                       SQRT((dir_x - 2 * norm_x * (dir_x*norm_x + dir_y*norm_y + dir_z*norm_z)) * (dir_x - 2 * norm_x * (dir_x*norm_x + dir_y*norm_y + dir_z*norm_z)) +
                       (dir_y - 2 * norm_y * (dir_x*norm_x + dir_y*norm_y + dir_z*norm_z)) * (dir_y - 2 * norm_y * (dir_x*norm_x + dir_y*norm_y + dir_z*norm_z)) +
                       (dir_z - 2 * norm_z * (dir_x*norm_x + dir_y*norm_y + dir_z*norm_z)) * (dir_z - 2 * norm_z * (dir_x*norm_x + dir_y*norm_y + dir_z*norm_z))) AS reflection_len
               ) dot_ray_norm ON norm_x IS NOT NULL
           LEFT JOIN material ON material.materialid=hit_sphere.materialid
           LEFT JOIN LATERAL
               (SELECT x, y, z,
                       x+norm_x AS diffuse_dir_x, y+norm_y AS diffuse_dir_y, z+norm_z AS diffuse_dir_z,
                       SQRT((x+norm_x)*(x+norm_x)+(y+norm_y)*(y+norm_y)+(z+norm_z)*(z+norm_z)) AS diffuse_dir_len
                FROM sphere_sample ss WHERE ss.sampleno=1+CAST(FLOOR(ABS((100000*dir_x)-FLOOR(100000*dir_x))*n_sphere_samples) AS INTEGER)
               ) diffuse_scatter ON norm_x IS NOT NULL
           LEFT JOIN LATERAL
               (SELECT (CASE WHEN is_dielectric AND (norm_x_nonunit * dir_x + norm_y_nonunit * dir_y + norm_z_nonunit * dir_z) > 0
                             THEN eta
                             WHEN is_dielectric
                             THEN 1.0/eta
                             ELSE eta END) AS ir) index_of_refraction ON norm_x IS NOT NULL
LEFT JOIN LATERAL
    (SELECT ABS(dir_x*norm_x + dir_y*norm_y + dir_z*norm_z) AS cos_theta,
            ((1.0-ir)/(1.0+ir))*((1.0-ir)/(1.0+ir)) AS r0
    ) refract_cos_theta ON norm_x IS NOT NULL
LEFT JOIN LATERAL
    (SELECT 
        -- Discriminant for total internal reflection check
        1.0 - ir*ir*(1.0-cos_theta*cos_theta) AS refrac_discriminant,
        -- Correct refraction direction
        ir * dir_x + (ir * cos_theta - SQRT(GREATEST(0.0, 1.0 - ir*ir*(1.0-cos_theta*cos_theta)))) * norm_x AS refrac_dir_x,
        ir * dir_y + (ir * cos_theta - SQRT(GREATEST(0.0, 1.0 - ir*ir*(1.0-cos_theta*cos_theta)))) * norm_y AS refrac_dir_y,
        ir * dir_z + (ir * cos_theta - SQRT(GREATEST(0.0, 1.0 - ir*ir*(1.0-cos_theta*cos_theta)))) * norm_z AS refrac_dir_z,
        -- Fresnel reflectance
        r0 + (1.0 - r0)*pow(1.0-cos_theta, 5) AS reflectance
    ) refrac_vec ON norm_x IS NOT NULL
LEFT JOIN LATERAL
    (SELECT 
        dir_x - 2.0 * (dir_x*norm_x + dir_y*norm_y + dir_z*norm_z) * norm_x AS reflec_dir_x,
        dir_y - 2.0 * (dir_x*norm_x + dir_y*norm_y + dir_z*norm_z) * norm_y AS reflec_dir_y,
        dir_z - 2.0 * (dir_x*norm_x + dir_y*norm_y + dir_z*norm_z) * norm_z AS reflec_dir_z,
        -- Total internal reflection check
        (refrac_discriminant < 0) OR (reflectance > RANDOM()) AS must_reflect
    ) reflec_vec ON norm_x IS NOT NULL
    LEFT JOIN LATERAL
    (SELECT 
        CASE WHEN must_reflect 
             THEN SQRT(reflec_dir_x*reflec_dir_x + reflec_dir_y*reflec_dir_y + reflec_dir_z*reflec_dir_z)
             ELSE SQRT(refrac_dir_x*refrac_dir_x + refrac_dir_y*refrac_dir_y + refrac_dir_z*refrac_dir_z)
        END AS final_dir_len
    ) final_dir_len ON norm_x IS NOT NULL

          LEFT JOIN LATERAL
               (SELECT SQRT((reflec_dir_x+refrac_dir_x)*(reflec_dir_x+refrac_dir_x)+
                       (reflec_dir_y+refrac_dir_y)*(reflec_dir_y+refrac_dir_y)+
                       (reflec_dir_z+refrac_dir_z)*(reflec_dir_z+refrac_dir_z)) refrac_len
               ) refrac_len ON norm_x IS NOT NULL
              WHERE depth<max_ray_depth AND NOT stop_tracing AND ray_len_idx=1
             )
   SELECT * FROM rs WHERE ray_len_idx=1;
-- select * from rays;

DROP VIEW IF EXISTS do_render;
CREATE VIEW do_render AS
 SELECT A.img_x, -A.img_y,
         GREATEST(0.0, LEAST(1.0, SUM(POW(A.color_mult * COALESCE(A.ray_col_r, 0.0)/A.samples_per_px, gamma)))) col_r,
         GREATEST(0.0, LEAST(1.0, SUM(POW(A.color_mult * COALESCE(A.ray_col_g, 0.0)/A.samples_per_px, gamma)))) col_g,
         GREATEST(0.0, LEAST(1.0, SUM(POW(A.color_mult * COALESCE(A.ray_col_b, 0.0)/A.samples_per_px, gamma)))) col_b
    FROM rays A, img
     WHERE A.depth>=0
    GROUP BY -A.img_y, A.img_x
    ORDER BY -A.img_y, A.img_x;

DROP VIEW IF EXISTS ppm;
CREATE VIEW ppm AS
 WITH maxcol(mc) AS (SELECT 255)
    SELECT 'P3'
  UNION ALL
    SELECT res_x || ' ' || res_y || ' ' || mc FROM img, maxcol
  UNION ALL
    SELECT CAST(col_r*mc AS INTEGER) || ' ' || CAST(col_g*mc AS INTEGER) || ' ' || CAST(col_b*mc AS INTEGER)
      FROM do_render, maxcol;
  ;

-- SELECT * FROM rays WHERE img_x=2 AND img_y=2;


================================================
FILE: setup.sql
================================================
DROP TABLE IF EXISTS material CASCADE;
CREATE TABLE material (materialid SERIAL PRIMARY KEY, name TEXT,
  mat_col_r DOUBLE PRECISION, mat_col_g DOUBLE PRECISION, mat_col_b DOUBLE PRECISION,
  is_metal BOOLEAN NOT NULL, shade_normal BOOLEAN NOT NULL, mirror_frac DOUBLE PRECISION NOT NULL,
  is_dielectric BOOLEAN NOT NULL, eta DOUBLE PRECISION NOT NULL DEFAULT 1.0);
INSERT INTO material (name, mat_col_r, mat_col_g, mat_col_b, is_metal, shade_normal, mirror_frac, is_dielectric, eta) VALUES
    ('dark', 0.1, 0.1, 0.1, FALSE, FALSE, 0.1, FALSE, 1.0),
    ('red', 0.95, 0.0, 0.0, FALSE, TRUE, 0.5, FALSE, 1.0),
    ('green', 0.0, 0.95, 0.0, FALSE, TRUE, 0.5, FALSE, 1.0),
    ('blue', 0.0, 0.0, 0.95, TRUE, TRUE, 0.5, FALSE, 1.0),
    ('grey', 0.1, 0.1, 0.1, FALSE, FALSE, 0.5, FALSE, 1.0),
    ('bright', 1.0, 1.0, 1.0, TRUE, TRUE, 0.5, FALSE, 1.0),
    ('mirror', NULL, NULL, NULL, TRUE, FALSE, 0.99, FALSE, 1.0),
    ('bluemirror', 0.0, 0.0, 0.3, TRUE, FALSE, 0.9, FALSE, 1.0),
    ('greenmirror', 0.0, 0.2, 0.0, TRUE, FALSE, 0.9, FALSE, 1.0),
    ('notquiteair', NULL, NULL, NULL, FALSE, FALSE, 1.0, TRUE, 1.00000001),
    ('glass', NULL, NULL, NULL, FALSE, FALSE, 0.95, TRUE, 1.5),
    ('greenglass', 0.0, 0.2, 0.0, FALSE, FALSE, 0.8, TRUE, 1.5),
    ('diamond', NULL, NULL, NULL, FALSE, FALSE, 0.99, TRUE, 2.4),
    ('antiglass', NULL, NULL, NULL, FALSE, FALSE, 0.99, TRUE, 0.2)
;

DROP TABLE IF EXISTS scene CASCADE;
CREATE TABLE IF NOT EXISTS scene (sceneid SERIAL PRIMARY KEY,
   scenename TEXT UNIQUE NOT NULL);
INSERT INTO scene (scenename) VALUES ('dielectricparty'),
                                     ('oneglassball'),
                                     ('onediamondball'),
                                     ('oneantiglassball'),
                                     ('onegreyball'),
                                     ('onegreenball'),
                                     ('twomirrorballs'),
                                     ('twodiffuseballs'),
                                     ('onemirrorball'),
                                     ('reflectiontest'),
                                     ('threemirrors'),
                                     ('adjacentballs'),
                                     ('glassmatrix'),
                                     ('airy'),
                                     ('busyday');

DROP TABLE IF EXISTS sphere CASCADE;
CREATE TABLE sphere (sphereid SERIAL, sceneid INTEGER NOT NULL REFERENCES scene(sceneid),
  cx DOUBLE PRECISION NOT NULL, cy DOUBLE PRECISION NOT NULL, cz DOUBLE PRECISION NOT NULL,
  radius DOUBLE PRECISION, radius2 DOUBLE PRECISION, materialid INTEGER NOT NULL REFERENCES material(materialid) DEFERRABLE,
  vel_x DOUBLE PRECISION NOT NULL DEFAULT 0.0, vel_y DOUBLE PRECISION NOT NULL DEFAULT 0.0, vel_z DOUBLE PRECISION NOT NULL DEFAULT 0.0,
  coefficient_of_restitution DOUBLE PRECISION NOT NULL DEFAULT 1.0);

INSERT INTO sphere (cx, cy, cz, radius, materialid, sceneid) VALUES
(0, 24, -10, 5,
   (SELECT materialid FROM material WHERE name='bright'), (SELECT sceneid FROM scene WHERE scenename='reflectiontest')),
(0, 5, 0, 5,
   (SELECT materialid FROM material WHERE name='red'), (SELECT sceneid FROM scene WHERE scenename='reflectiontest')),
(-17, 15, -30, 15,
   (SELECT materialid FROM material WHERE name='bluemirror'), (SELECT sceneid FROM scene WHERE scenename='reflectiontest')),
(24, 23, 10, 23,
   (SELECT materialid FROM material WHERE name='greenmirror'), (SELECT sceneid FROM scene WHERE scenename='reflectiontest')),

(0, -1250, 0, 1250,
   (SELECT materialid FROM material WHERE name='grey'), (SELECT sceneid FROM scene WHERE scenename='twomirrorballs')),
(20, 25, -30, 25,
   (SELECT materialid FROM material WHERE name='greenmirror'), (SELECT sceneid FROM scene WHERE scenename='twomirrorballs')),
(-20, 25, 0, 25,
   (SELECT materialid FROM material WHERE name='mirror'), (SELECT sceneid FROM scene WHERE scenename='twomirrorballs')),

(0, -1250, 0, 1250,
   (SELECT materialid FROM material WHERE name='grey'), (SELECT sceneid FROM scene WHERE scenename='twodiffuseballs')),
(20, 25, -30, 25,
   (SELECT materialid FROM material WHERE name='dark'), (SELECT sceneid FROM scene WHERE scenename='twodiffuseballs')),
(-20, 25, 0, 25,
   (SELECT materialid FROM material WHERE name='green'), (SELECT sceneid FROM scene WHERE scenename='twodiffuseballs')),

(0, -1250, 0, 1250,
   (SELECT materialid FROM material WHERE name='grey'), (SELECT sceneid FROM scene WHERE scenename='onemirrorball')),
(-20, 25, 0, 25,
   (SELECT materialid FROM material WHERE name='mirror'), (SELECT sceneid FROM scene WHERE scenename='onemirrorball')),

(0, -1250, 0, 1250,
   (SELECT materialid FROM material WHERE name='grey'), (SELECT sceneid FROM scene WHERE scenename='oneglassball')),
(0, 25, -10, 25,
   (SELECT materialid FROM material WHERE name='glass'), (SELECT sceneid FROM scene WHERE scenename='oneglassball')),
(20, 25, 80, 25,
   (SELECT materialid FROM material WHERE name='red'), (SELECT sceneid FROM scene WHERE scenename='oneglassball')),

(0, -1250, 0, 1250,
   (SELECT materialid FROM material WHERE name='grey'), (SELECT sceneid FROM scene WHERE scenename='onediamondball')),
(0, 25, -10, 25,
   (SELECT materialid FROM material WHERE name='diamond'), (SELECT sceneid FROM scene WHERE scenename='onediamondball')),
(20, 25, 80, 25,
   (SELECT materialid FROM material WHERE name='red'), (SELECT sceneid FROM scene WHERE scenename='onediamondball')),

(0, -1250, 0, 1250,
   (SELECT materialid FROM material WHERE name='grey'), (SELECT sceneid FROM scene WHERE scenename='oneantiglassball')),
(0, 25, -10, 25,
   (SELECT materialid FROM material WHERE name='antiglass'), (SELECT sceneid FROM scene WHERE scenename='oneantiglassball')),
(20, 25, 80, 25,
   (SELECT materialid FROM material WHERE name='red'), (SELECT sceneid FROM scene WHERE scenename='oneantiglassball')),

(0, -1250, 0, 1250,
   (SELECT materialid FROM material WHERE name='grey'), (SELECT sceneid FROM scene WHERE scenename='dielectricparty')),
(0, 12, 0, NULL,
   (SELECT materialid FROM material WHERE name='glass'), (SELECT sceneid FROM scene WHERE scenename='dielectricparty')),
(25, 12, 0, NULL,
   (SELECT materialid FROM material WHERE name='antiglass'), (SELECT sceneid FROM scene WHERE scenename='dielectricparty')),
(-25, 12, 0, NULL,
   (SELECT materialid FROM material WHERE name='diamond'), (SELECT sceneid FROM scene WHERE scenename='dielectricparty')),
(15, 10, 20, NULL,
   (SELECT materialid FROM material WHERE name='red'), (SELECT sceneid FROM scene WHERE scenename='dielectricparty')),
(-5, 10, 30, NULL,
   (SELECT materialid FROM material WHERE name='green'), (SELECT sceneid FROM scene WHERE scenename='dielectricparty')),

(0, -1250, 0, 1250,
   (SELECT materialid FROM material WHERE name='grey'), (SELECT sceneid FROM scene WHERE scenename='airy')),
(0, 12, 0, NULL,
   (SELECT materialid FROM material WHERE name='notquiteair'), (SELECT sceneid FROM scene WHERE scenename='airy')),
(25, 12, 0, NULL,
   (SELECT materialid FROM material WHERE name='notquiteair'), (SELECT sceneid FROM scene WHERE scenename='airy')),
(-25, 12, 0, NULL,
   (SELECT materialid FROM material WHERE name='notquiteair'), (SELECT sceneid FROM scene WHERE scenename='airy')),
(15, 10, 20, NULL,
   (SELECT materialid FROM material WHERE name='red'), (SELECT sceneid FROM scene WHERE scenename='airy')),
(-5, 10, 30, NULL,
   (SELECT materialid FROM material WHERE name='green'), (SELECT sceneid FROM scene WHERE scenename='airy')),

(0, -1250, 0, 1250,
   (SELECT materialid FROM material WHERE name='green'), (SELECT sceneid FROM scene WHERE scenename='adjacentballs')),
(-24, 12, 0, 12,
   (SELECT materialid FROM material WHERE name='red'), (SELECT sceneid FROM scene WHERE scenename='adjacentballs')),
(0, 12, 0, 12,
   (SELECT materialid FROM material WHERE name='mirror'), (SELECT sceneid FROM scene WHERE scenename='adjacentballs')),
(24, 12, 0, 12,
   (SELECT materialid FROM material WHERE name='bright'), (SELECT sceneid FROM scene WHERE scenename='adjacentballs')),

(0, -1250, 0, 1250,
   (SELECT materialid FROM material WHERE name='grey'), (SELECT sceneid FROM scene WHERE scenename='onegreyball')),
(20, 25, 0, 25,
   (SELECT materialid FROM material WHERE name='grey'), (SELECT sceneid FROM scene WHERE scenename='onegreyball')),

(0, -1250, 0, 1250,
   (SELECT materialid FROM material WHERE name='grey'), (SELECT sceneid FROM scene WHERE scenename='onegreenball')),
(20, 25, 0, 25,
   (SELECT materialid FROM material WHERE name='green'), (SELECT sceneid FROM scene WHERE scenename='onegreenball')),

(-20, 15, -15, 22,
   (SELECT materialid FROM material WHERE name='mirror'), (SELECT sceneid FROM scene WHERE scenename='threemirrors')),
(0, 0, 0, 5,
   (SELECT materialid FROM material WHERE name='mirror'), (SELECT sceneid FROM scene WHERE scenename='threemirrors')),
(30, -15, 0, 25,
   (SELECT materialid FROM material WHERE name='mirror'), (SELECT sceneid FROM scene WHERE scenename='threemirrors')),

(0, -1250, 0, 1250,
   (SELECT materialid FROM material WHERE name='grey'), (SELECT sceneid FROM scene WHERE scenename='busyday')),

(0, -1250, 0, 1250,
   (SELECT materialid FROM material WHERE name='grey'), (SELECT sceneid FROM scene WHERE scenename='glassmatrix')),
(20, 25, 80, 25,
   (SELECT materialid FROM material WHERE name='red'), (SELECT sceneid FROM scene WHERE scenename='glassmatrix'))
;

INSERT INTO sphere (cx, cy, cz, radius, materialid, sceneid, coefficient_of_restitution)
SELECT (RANDOM()-0.5) * 100, 50 + RANDOM() * 50, (RANDOM()-0.5) * 100, RANDOM() * 5.0,
       1+CAST((RANDOM()*(SELECT MAX(materialid)-1 FROM material)) AS INTEGER), (SELECT sceneid FROM scene WHERE scenename='busyday'),
       0.7*RANDOM()+0.3
    FROM generate_series(1, 20)
    GROUP BY generate_series;

INSERT INTO sphere (cx, cy, cz, radius, materialid, sceneid, coefficient_of_restitution)
WITH params(r, x1, y1) AS (SELECT 5, -60, -15)
SELECT x1 + 2 * r * x, y1 + 2 * r * y, -30, r,
      (SELECT materialid FROM material WHERE name='glass'), (SELECT sceneid FROM scene WHERE scenename='glassmatrix'),
      1.0
    FROM generate_series(1, 11) X, generate_series(1, 11) Y, params;

UPDATE sphere SET radius = cy WHERE radius IS NULL;
UPDATE sphere SET radius2 = radius*radius WHERE radius2 IS NULL;

DROP TABLE IF EXISTS camera CASCADE;
CREATE TABLE camera (cameraid INTEGER PRIMARY KEY, sceneid INTEGER NOT NULL REFERENCES scene(sceneid),
  x DOUBLE PRECISION NOT NULL, y DOUBLE PRECISION NOT NULL, z DOUBLE PRECISION NOT NULL,
  rot_x DOUBLE PRECISION NOT NULL, rot_y DOUBLE PRECISION NOT NULL, rot_z DOUBLE PRECISION NOT NULL,
  fov_rad_x DOUBLE PRECISION NOT NULL, fov_rad_y DOUBLE PRECISION NOT NULL,
  max_ray_depth INTEGER NOT NULL, samples_per_px INTEGER NOT NULL);
INSERT INTO camera (cameraid, x, y, z, rot_x, rot_y, rot_z, fov_rad_x, fov_rad_y, max_ray_depth, samples_per_px, sceneid)
  VALUES (1.0, 0.0, 65.0, -120.0, -0.34, 0.0, 0.0, PI()/3.0, PI()/3.0,
          30, 50, (SELECT sceneid FROM scene WHERE scenename='busyday'));

DROP TABLE IF EXISTS img CASCADE;
CREATE TABLE img (res_x INTEGER NOT NULL, res_y INTEGER NOT NULL, gamma DOUBLE PRECISION);
    INSERT INTO img (res_x, res_y, gamma)
        VALUES (650, 650, 1.0);

CREATE OR REPLACE FUNCTION animate_spheres()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
    BEGIN
        UPDATE sphere SET vel_x = vel_x + NEW.grav_x*NEW.dt,
                          vel_y = vel_y + NEW.grav_y*NEW.dt,
                          vel_z = vel_z + NEW.grav_z*NEW.dt
          WHERE sceneid=(SELECT sceneid FROM camera) AND cy>0;
        UPDATE sphere SET vel_y = -vel_y*coefficient_of_restitution WHERE radius>cy
          AND sceneid=(SELECT sceneid FROM camera);
        UPDATE sphere SET cx=cx+vel_x*NEW.dt,
                          cy=cy+vel_y*NEW.dt,
                          cz=cz+vel_z*NEW.dt
          WHERE sceneid=(SELECT sceneid FROM camera);
        RETURN NEW;
    END;
$$ ;

DROP VIEW IF EXISTS updateworld;
CREATE VIEW updateworld AS (SELECT 0.0 AS dt, 0.0 AS grav_x, 0.0 AS grav_y, 0.0 AS grav_z);
CREATE TRIGGER trig_update_world INSTEAD OF INSERT ON updateworld FOR EACH ROW
    EXECUTE PROCEDURE animate_spheres();

-- INSERT INTO updateworld (dt, grav_x, grav_y, grav_z) VALUES (0.1, 0.0, -9.8, 0.0);
-- select cx, cy, cz, vel_x, vel_y, vel_z from sphere
--   where sceneid=(SELECT sceneid FROM scene WHERE scenename='busyday');

================================================
FILE: show_scene.sh
================================================
#!/bin/sh

. ./postgres_connection.sh

show_scene_dir=show_scene

scenelist=scenelist.txt

psql \
        --host=${PGHOST} \
        --port=${PGPORT} \
        --username=${PGUSER} \
        --dbname=${PGDB} \
        --file=setup.sql \
        --file=raytracer.sql \
        --command="\\timing" \
        --command="\\copy (select scenename from scene) to './${show_scene_dir}/${scenelist}' csv"

mkdir -p ${show_scene_dir}
cd ${show_scene_dir}

while read scenename
do
  echo ""
  echo "Rendering scene ${scenename}"


psql \
	--host=${PGHOST} \
        --port=${PGPORT} \
        --username=${PGUSER} \
        --dbname=${PGDB} \
        --command="\\copy (select sphereid, cx, cy, cz, radius from sphere inner join scene s on s.sceneid=sphere.sceneid where s.scenename='${scenename}') to './spheres.csv' with csv header" \

img_size=2048

scale_vector=5

cat <<EOH > gnuplot.gp
set terminal png size ${img_size},${img_size}
set xrange [-60:60]
set yrange [-60:60]
set datafile separator ','
set key autotitle columnhead

set output '${scenename}_xz.png'
set xlabel "X"
set ylabel "Z"
plot \\
        "spheres.csv" u 2:4:5 w circles t 'spheres'

set output '${scenename}_zx.png'
set xlabel "Z"
set ylabel "X"
plot \\
        "spheres.csv" u 4:2:5 w circles t 'spheres'

set output '${scenename}_xy.png'
set xlabel "X"
set ylabel "Y"
plot \\
        "spheres.csv" u 2:3:5 w circles t 'spheres'

set output '${scenename}_yx.png'
set xlabel "Y"
set ylabel "X"
plot \\
        "spheres.csv" u 3:2:5 w circles t 'spheres'

set output '${scenename}_yz.png'
set xlabel "Y"
set ylabel "Z"
plot \\
        "spheres.csv" u 3:4:5 w circles t 'spheres'

set output '${scenename}_zy.png'
set xlabel "Z"
set ylabel "Y"
plot \\
        "spheres.csv" u 4:3:5 w circles t 'spheres'

set output '${scenename}_3view.png'
set xrange [-120:120]
set yrange [-120:120]
set zrange [-120:120]
set xlabel "X"
set ylabel "Y"
set zlabel "Z"
splot \\
	"spheres.csv" u 2:3:4:5 w circles t 'spheres'

set terminal qt
replot

EOH

gnuplot gnuplot.gp

montage \
	-tile 2x2 \
	-geometry ${img_size}x${img_size} \
	${scenename}_xy.png \
       	${scenename}_zy.png \
       	${scenename}_xz.png \
       	${scenename}_3view.png \
	fullscene_${scenename}.png


done < ./${scenelist}

xdg-open .

Download .txt
gitextract_4npa0ned/

├── .gitignore
├── LICENSE
├── README.md
├── anim.sh
├── create.sh
├── debug_rays.sh
├── postgres_connection.sh
├── raytracer.sql
├── setup.sql
└── show_scene.sh
Download .txt
SYMBOL INDEX (12 symbols across 2 files)

FILE: raytracer.sql
  type sphere_sample (line 2) | CREATE TABLE IF NOT EXISTS sphere_sample (x DOUBLE PRECISION NOT NULL, y...
  type idx_ss (line 11) | CREATE INDEX IF NOT EXISTS idx_ss ON sphere_sample(sampleno)
  type rays (line 14) | CREATE VIEW rays AS
  type do_render (line 188) | CREATE VIEW do_render AS
  type ppm (line 199) | CREATE VIEW ppm AS

FILE: setup.sql
  type material (line 2) | CREATE TABLE material (materialid SERIAL PRIMARY KEY, name TEXT,
  type scene (line 24) | CREATE TABLE IF NOT EXISTS scene (sceneid SERIAL PRIMARY KEY,
  type sphere (line 43) | CREATE TABLE sphere (sphereid SERIAL, sceneid INTEGER NOT NULL REFERENCE...
  type camera (line 178) | CREATE TABLE camera (cameraid INTEGER PRIMARY KEY, sceneid INTEGER NOT N...
  type img (line 188) | CREATE TABLE img (res_x INTEGER NOT NULL, res_y INTEGER NOT NULL, gamma ...
  function animate_spheres (line 192) | CREATE OR REPLACE FUNCTION animate_spheres()
  type updateworld (line 212) | CREATE VIEW updateworld AS (SELECT 0.0 AS dt, 0.0 AS grav_x, 0.0 AS grav...
Condensed preview — 10 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (43K chars).
[
  {
    "path": ".gitignore",
    "chars": 70,
    "preview": "debug_rays\nshow_scene\nscenelist_override.txt\nscenelist.txt\n*.ppm\nanim\n"
  },
  {
    "path": "LICENSE",
    "chars": 1076,
    "preview": "Copyright (c) 2021 Gary Briggs <chunky@icculus.org>\n\nPermission is hereby granted, free of charge, to any person obtaini"
  },
  {
    "path": "README.md",
    "chars": 6272,
    "preview": "# A Pure SQL Raytracer\n\nEveryone writes a raytracer sooner or later. This is mine.\n\n## Example Outputs\n\n<img src=\"exampl"
  },
  {
    "path": "anim.sh",
    "chars": 865,
    "preview": "#!/bin/sh\n\n. ./postgres_connection.sh\n\nscenename=\"busyday\"\noutfolder=anim\ndt=0.05\ngravity=-9.8\n\npsql \\\n\t--host=${PGHOST}"
  },
  {
    "path": "create.sh",
    "chars": 1266,
    "preview": "#!/bin/sh\n\n. ./postgres_connection.sh\n\n# Creating this file overrides which scenes get rendered\nscenelist_override=scene"
  },
  {
    "path": "debug_rays.sh",
    "chars": 3316,
    "preview": "#!/bin/sh\n\n. ./postgres_connection.sh\n\ndebug_dir=debug_rays\nmkdir -p ${debug_dir}\ncd ${debug_dir}\n\npsql \\\n\t--host=${PGHO"
  },
  {
    "path": "postgres_connection.sh",
    "chars": 102,
    "preview": "PGHOST=localhost\nPGPORT=5432\nPGUSER=raytracer\nPGDB=raytracer\n\nPGPASSWORD=raytracer\nexport PGPASSWORD\n\n"
  },
  {
    "path": "raytracer.sql",
    "chars": 13414,
    "preview": "DROP TABLE IF EXISTS sphere_sample CASCADE;\nCREATE TABLE IF NOT EXISTS sphere_sample (x DOUBLE PRECISION NOT NULL, y DOU"
  },
  {
    "path": "setup.sql",
    "chars": 12408,
    "preview": "DROP TABLE IF EXISTS material CASCADE;\nCREATE TABLE material (materialid SERIAL PRIMARY KEY, name TEXT,\n  mat_col_r DOUB"
  },
  {
    "path": "show_scene.sh",
    "chars": 2264,
    "preview": "#!/bin/sh\n\n. ./postgres_connection.sh\n\nshow_scene_dir=show_scene\n\nscenelist=scenelist.txt\n\npsql \\\n        --host=${PGHOS"
  }
]

About this extraction

This page contains the full source code of the chunky/sqlraytracer GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 10 files (40.1 KB), approximately 12.3k tokens, and a symbol index with 12 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!