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 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 ## 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 ================================================ 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 < 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.u0 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=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 < 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 .