Ray Tracing Shader

Here’s a fairly pointless curiosity. I can’t claim anything in it as mine, it’s a webgl one I found somewhere and ported into Codea.

-- RayTracer 3Balls

-- Use this function to perform your initial setup
function setup()
    displayMode(FULLSCREEN)
    myMesh = mesh()
    --myMesh.shader = shader("Documents:ray")
    myMesh.shader = shader(rayShader.vertexShader, rayShader.fragmentShader)
    aVPosBuffer = myMesh:buffer("aVertexPosition")
    aPPosBuffer = myMesh:buffer("aPlotPosition")
    
    aVPosBuffer:resize(6)
    aPPosBuffer:resize(6)
    size = 0.8 -- 1.0 is fullscreen
    aVPosBuffer[1] = vec2(size,size)
    aVPosBuffer[2] = vec2(-size,size)
    aVPosBuffer[3] = vec2(size,-size)
    aVPosBuffer[4] = vec2(size,-size)
    aVPosBuffer[5] = vec2(-size,size)
    aVPosBuffer[6] = vec2(-size,-size)
    
    t=0
    s1 = vec3(0,0,0)
    s2 = vec3(0,0,0)
    s3 = vec3(0,0,0)
end

function calcCamera()
    cameraFrom = vec3(math.sin(t*0.4)*28, math.sin(t*0.13)*5+5, math.cos(t*0.4)*28)
    cameraTo = vec3(0,0,0)
    cameraPersp = 6
    up = vec3(0,1,0)
    cameraDir = (cameraTo - cameraFrom):normalize()

    cameraLeft = cameraDir:cross(up):normalize()
    cameraUp = cameraLeft:cross(cameraDir):normalize()
    cameraCenter = cameraFrom + cameraDir * cameraPersp

    ratio = WIDTH/HEIGHT
    cameraTopLeft = cameraCenter + cameraUp + cameraLeft * ratio
    cameraBotLeft = cameraCenter - cameraUp + cameraLeft * ratio
    cameraTopRight = cameraCenter + cameraUp - cameraLeft * ratio
    cameraBotRight = cameraCenter - cameraUp - cameraLeft * ratio
    aPPosBuffer[1] = cameraTopRight
    aPPosBuffer[2] = cameraTopLeft
    aPPosBuffer[3] = cameraBotRight
    aPPosBuffer[4] = cameraBotRight
    aPPosBuffer[5] = cameraTopLeft
    aPPosBuffer[6] = cameraBotLeft    
end

function calcBalls()
    s1.x = math.sin(t * 1.1) * 1.5
    s1.y = math.cos(t * 1.3) * 1.5
    s1.z = math.sin(t + math.pi/3) * 1.5
    s2.x = math.cos(t * 1.2) * 1.5
    s2.y = math.sin(t * 1.4) * 1.5
    s2.z = math.sin(t*1.25 - math.pi/3) * 1.5
    s3.x = math.cos(t * 1.15) * 1.5
    s3.y = math.sin(t * 1.37) * 1.5
    s3.z = math.sin(t*1.27) * 1.5
end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    background(0, 0, 0)
    
    calcBalls()

    calcCamera()
            
    myMesh.shader.cameraPos = cameraFrom
    myMesh.shader.sphere1Center = s1
    myMesh.shader.sphere2Center = s2
    myMesh.shader.sphere3Center = s3
    
    myMesh:draw()
    
    t = t + 0.03
    if t> math.pi * 200 then
        t = t - math.pi * 200
    end
end

rayShader = {
vertexShader = [[
attribute vec2 aVertexPosition;
attribute vec3 aPlotPosition;

varying vec3 vPosition;

void main(void)
{
gl_Position = vec4(aVertexPosition, 1.0, 1.0);
vPosition = aPlotPosition;
}
]],
fragmentShader = [[
     precision highp float;

const vec3 lightDir = vec3(0.577350269, 0.577350269, -0.577350269);
varying vec3 vPosition;
uniform vec3 cameraPos;
uniform vec3 sphere1Center;
uniform vec3 sphere2Center;
uniform vec3 sphere3Center;

bool intersectSphere(vec3 center, vec3 lStart, vec3 lDir,
out float dist) {
vec3 c = center - lStart;
float b = dot(lDir, c);
float d = b*b - dot(c, c) + 1.0;
if (d < 0.0) {
dist = 10000.0;
return false;
}

dist = b - sqrt(d);
if (dist < 0.0) {
dist = 10000.0;
return false;
}

return true;
}

vec3 lightAt(vec3 N, vec3 V, vec3 color) {
vec3 L = lightDir;
vec3 R = reflect(-L, N);

float c = 0.3 + 0.4 * pow(max(dot(R, V), 0.0), 30.0) + 0.7 * dot(L, N);

if (c > 1.0) {
return mix(color, vec3(1.6, 1.6, 1.6), c - 1.0);
}

return c * color;
}

bool intersectWorld(vec3 lStart, vec3 lDir, out vec3 pos,
out vec3 normal, out vec3 color) {
float d1, d2, d3;
bool h1, h2, h3;

h1 = intersectSphere(sphere1Center, lStart, lDir, d1);
h2 = intersectSphere(sphere2Center, lStart, lDir, d2);
h3 = intersectSphere(sphere3Center, lStart, lDir, d3);

if (h1 && d1 < d2 && d1 < d3) {
pos = lStart + d1 * lDir;
normal = pos - sphere1Center;
color = vec3(0.0, 0.0, 0.9);
}
else if (h2 && d2 < d3) {
pos = lStart + d2 * lDir;
normal = pos - sphere2Center;
color = vec3(0.9, 0.0, 0.0);
}
else if (h3) {
pos = lStart + d3 * lDir;
normal = pos - sphere3Center;
color = vec3(0.0, 0.9, 0.0);
}
else if (lDir.y < -0.01) {
pos = lStart + ((lStart.y + 2.7) / -lDir.y) * lDir;
if (pos.x*pos.x + pos.z*pos.z > 30.0) {
return false;
}
normal = vec3(0.0, 1.0, 0.0);
if (fract(pos.x / 5.0) > 0.5 == fract(pos.z / 5.0) > 0.5) {
color = vec3(1.0);
}
else {
color = vec3(0.0);
}
}
else {
return false;
}

return true;
}

void main(void)
{
vec3 cameraDir = normalize(vPosition - cameraPos);

vec3 p1, norm, p2;
vec3 col, colT, colM, col3;
if (intersectWorld(cameraPos, cameraDir, p1,
norm, colT)) {
col = lightAt(norm, -cameraDir, colT);
colM = (colT + vec3(0.7)) / 1.7;
cameraDir = reflect(cameraDir, norm);
if (intersectWorld(p1, cameraDir, p2, norm, colT)) {
col += lightAt(norm, -cameraDir, colT) * colM;
colM *= (colT + vec3(0.7)) / 1.7;
cameraDir = reflect(cameraDir, norm);
if (intersectWorld(p2, cameraDir, p1, norm, colT)) {
col += lightAt(norm, -cameraDir, colT) * colM;
}
}

gl_FragColor = vec4(col, 1.0);
}
else {
discard;
//gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
}
}]]
}

That’s awesome, but way out of my league. Thanks for posting it!

Got my head around how this works a little more, the version in the below gist now runs pretty well full screen.

https://gist.github.com/sp4cemonkey/5605858

Gist and video in previous post updated. Added shadows to the render, which slows it again, but still runs fairly smoothly on iPad2

Wow!

@spacemonkey it looks great but can you change the balls they some times merge together:-)
Can you tell me where i can find how this render work. I like to know how i can use this effects in a project of my own.

@Dreamdancer the balls merge because they move through each other. I’m trying to hook it up for touch, but about to give up for the night, my first approach killed performance and also look super ugly :wink:

As to how it works and other projects, I am happy to talk about my understanding of how this stuff works, although I’m far from an expert, however, don’t think about it for projects outside of doing it because it’s there. This is a very inflexible technique and a bad fit for being part of anything bigger.

@spacemonkey i like to know how reflection works, that mirror effect ,what are the steps you have to follow to create it.

@Dreamdancer ray tracing is a good way of getting very nice realistic lighting, however it’s down sides are, 1) it’s performance tends to be very poor, in this example that is helped by moving it to the GPU, but that makes it very inflexible, 2) ray tracing while looking really nice, is difficult to make look realistic because it’s all based on fairly raw maths so modelling highly real objects can be quite a lot of work.

To do reflections in a more real world game oriented fashion what people tend to do is go for environment mapping http://en.wikipedia.org/wiki/Reflection_mapping as this gives a pretty good look while being significantly simpler to compute.

OpenGL ES supports Cube Map textures for this, however I’m not sure Codea does… I’ll have another look at this some time this week and see if I can come up with an example of an environment mapping style example.

@spacemonkey that will be great, thanks