In
document.body.style.minHeight = '200px';
let status = document.createElement('div');
status.innerHTML = 'Lines:';
document.body.appendChild( status );
//let ww = window.innerWidth;
//let wh = window.innerHeight;
let ww = 600;
let wh = 400;
let container = document.createElement('div');
document.body.appendChild( container );
container.id = 'container';
let opts = document.createElement('div');
document.body.appendChild(opts);
opts.style.position = 'fixed';
opts.style.right = '10px';
opts.style.top = '10px';
function checkbox( iargs = {id:undefined, checked:undefined, text:undefined} )
{
let el = document.getElementById(iargs.id);
if ( !el ) {
el = document.createElement('div');
el.id = iargs.id;
//el.style.border = '1px solid gray';
opts.appendChild( el );
el.appendChild( document.createElement('label') );
el.appendChild( document.createElement('input') );
el.children[0].style['vertical-align'] = 'top';
el.children[1].setAttribute("type", "checkbox");
}
if ( iargs.text ) el.children[0].innerHTML = iargs.text;
if ( iargs.checked ) el.children[1].checked = iargs.checked;
el.children[1].onchange = ()=>{ setup(); };
return el.children[1].checked;
}
checkbox( { id:'refract', checked:false, text:'show refract rays' } );
checkbox( { id:'normals', checked:true, text:'show normals' } );
checkbox( { id:'reflect', checked:true, text:'show reflect rays' } );
function inputnumber( iargs = {id:undefined, value:undefined, range:undefined, text:undefined ,step:undefined } )
{
let el = document.getElementById(iargs.id);
if ( !el ) {
el = document.createElement('div');
el.id = iargs.id;
//el.style.border = '1px solid gray';
opts.appendChild( el );
el.appendChild( document.createElement('label') );
el.appendChild( document.createElement('input') );
el.children[0].style['vertical-align'] = 'top';
el.children[1].setAttribute("type", "number");
el.children[1].setAttribute("min", 1);
el.children[1].setAttribute("type", "number");
}
if ( iargs.text ) el.children[0].innerHTML = iargs.text;
if ( iargs.range ) el.children[1].min = iargs.range[0];
if ( iargs.range ) el.children[1].max = iargs.range[1];
if ( iargs.value ) el.children[1].value = iargs.value;
if ( iargs.step ) el.children[1].step = iargs.step;
el.children[1].onchange = ()=>{ setup(); };
el.children[1].onblur = ()=>{ setup(); };
return el.children[1].value;
}
inputnumber( { id:'numrays', value:3, range:[1,6], step:1, text:'number rays' } );
inputnumber( { id:'eta', value:1.3, range:[0, 3], step:0.1, text:'eta' } );
inputnumber( { id:'maxlines', value:40, range:[1,100], step:1, text:'max lines' } );
inputnumber( { id:'maxbounces',value:10, range:[0,100], step:1, text:'max ray bounces' } );
//--------------------------------------------------
//--------------------------------------------------
function setup()
{
container.innerHTML = '';
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttributeNS(null, "width", ww);
svg.setAttributeNS(null, "height", wh);
svg.setAttributeNS(null, "fill", 'currentColor');
svg.style.border = '1px solid blue';
container.appendChild( svg );
function addCircle( c, r, id, col='green' )
{
var newCircle = document.createElementNS('http://www.w3.org/2000/svg','circle');
newCircle.id = id;
newCircle.setAttribute('cx', c.x );
newCircle.setAttribute('cy', c.y );
newCircle.setAttribute('r', r );
newCircle.setAttribute("fill", 'transparent');
newCircle.setAttribute("stroke-width", 2);
newCircle.setAttribute("stroke", col);
svg.appendChild( newCircle );
}
// addCircle( {'x':200, 'y':100}, 50, 'c0' );
let shapes = [];
shapes.push( { c:{x:160, y:110}, r:50, id:'c0', type:'circle' } );
shapes.push( { c:{x:100, y:330}, r:40, id:'c1', type:'circle' } );
shapes.push( { c:{x:250, y:230}, r:35, id:'c2', type:'circle' } );
shapes.forEach( (s)=>{ addCircle( s.c, s.r, s.id ); });
function showNormal(afs = { p:undefined, n:undefined, col:'black' } )
{
if ( !afs.col ) afs.col = 'black';
let n0 = document.createElementNS('http://www.w3.org/2000/svg','line');
n0.style.color = afs.col;
n0.setAttribute("stroke", afs.col);
let e = add(afs.p, scale(afs.n,15.0));
svg.appendChild( n0 );
n0.setAttribute('x1', ''+afs.p.x);
n0.setAttribute('y1', ''+afs.p.y);
n0.setAttribute('x2', ''+e.x);
n0.setAttribute('y2', ''+e.y);
// n0.setAttribute('marker-end', 'url(#arrowhead)');
// all this code just for an arrow head!
{
let arrowID = 'arrow' + Math.random();
let defs = document.createElementNS('http://www.w3.org/2000/svg', "defs");
let marker = document.createElementNS('http://www.w3.org/2000/svg', "marker");
marker.setAttribute("id", arrowID);
marker.setAttribute("markerWidth", 10);
marker.setAttribute("markerHeight", 7);
marker.setAttribute("markerUnits", "strokeWidth");
marker.setAttribute("refX", 0); // warning 'case sensitive' refx != refX
marker.setAttribute("refY", 3.5);
marker.setAttribute("orient", "auto");
let path = document.createElementNS('http://www.w3.org/2000/svg', "path");
path.setAttribute("d","M 0 0 L 10 3.5 L 0 7 z");
path.setAttribute("fill", afs.col); // arrow head
marker.appendChild(path);
defs.appendChild(marker);
svg.appendChild(defs);
n0.setAttribute('marker-end', `url(#${arrowID})`);
}
}
function setLine(id, s, e, opt)
{
let i0 = document.getElementById( id );
if ( i0 == null )
{
i0 = document.createElementNS('http://www.w3.org/2000/svg','line');
i0.id = id;
//var randomColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16);
let rbyte = ()=>{ return Math.floor((Math.random()*256))%256; };
let randomColor = `rgb(${rbyte()}, ${rbyte()}, ${rbyte()})`;
i0.setAttribute("stroke", randomColor );
svg.appendChild( i0 );
}
i0.setAttribute('x1', ''+s.x);
i0.setAttribute('y1', ''+s.y);
i0.setAttribute('x2', ''+e.x);
i0.setAttribute('y2', ''+e.y);
if ( typeof(opt) != 'undefined' )
Object.entries(opt).forEach( (o)=>{ const [k,v]=o; i0.setAttribute( k, v ); } );
}
function sub (a, b) { return {x:(a.x-b.x), y:(a.y-b.y)}; }
function add (a, b) { return {x:(a.x+b.x), y:(a.y+b.y)}; }
function dist (a, b) { return Math.sqrt((a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y)); };
function norm (a ) { let d = Math.sqrt(a.x*a.x + a.y*a.y); return {x:a.x/d, y:a.y/d}; }
function len (a ) { return Math.sqrt(a.x*a.x + a.y*a.y); };
function scale (a, s) { return { x:a.x*s, y:a.y*s }; }
function dot (a, b) { return ( a.x*b.x + a.y*b.y ); }
function lineIntersectCircle( line, circle )
{
let d0 = dist( line.s, circle.c );
let d1 = dist( line.e, circle.c );
let d0inside = d0 < circle.r;
let d1inside = d1 < circle.r;
if ( d0inside != d1inside )
{
let pen = circle.r - ( d1<d0 ? d1 : d0 );
let n = norm( sub( line.e, circle.c ) );
let hitpoint = add( line.e, scale(norm(sub(line.e,line.s)), -pen*1.5 ) );
return { hit:true, normal:n, hitpoint:hitpoint, inside:d1inside, pen:pen };
}
return { hit:false, normal:{x:0,y:0}, hitpoint:{x:0,y:0} };
}
function reflect( i, n )
{
// I - 2.0 * dot(N, I) * N.
return norm( sub( scale(i, 1), scale( n, 2 * dot(n, i) ) ) );
}
// source material index of refraction (IOR)' / 'destination material IOR
function refract( i, n, eta )
{
// k = 1.0 - eta * eta * (1.0 - dot(N, I) * dot(N, I));
// if (k < 0.0)
// R = vec(0.0);
// else
// R = eta * I - (eta * dot(N, I) + sqrt(k)) * N;
let k = 1.0 - eta * eta * ( 1.0 - dot(n,i) * dot(n,i) );
if ( k < 0.0 ) // normal and the incident are tangental
{
k = 0.01;
}
let s = eta * dot(n,i) + Math.sqrt(k);
return norm( sub( scale(i, eta), scale(n, s ) ) );
}
let lines = [ ];
/*
// add point light
let p0 = { x:50, y:50 };
for (let k=0; k<5; k++)
{
let sin = Math.sin;
let cos = Math.cos;
let t = Math.PI * 2 * (k/5) + 0.1;
let n = {x: sin(t) , y: cos(t) };
let e = add( p0, n );
let ii = { id:'iv'+k, s:p0, e:e, speed:3, 'alive':true };
lines.push( ii );
setLine( ii.id, ii.s, ii.e );
}
*/
let origin = {x:20, y:200};
let target = {x:50, y:200};
let forward = norm( sub( target, origin ) );
let up = {x:forward.y, y:forward.x };
let numrays = inputnumber( {id:'numrays'} );
let efov = 3;
lines.push( { id:'ic0', s:origin, e:target, speed:5, 'alive':true, depth:0 } );
addCircle( origin, 2, 'cc0', col='black' );
for (let n=1; n<numrays; n++)
{
let eu = add( target, scale(up, (n)*efov) );
let ed = add( target, scale(up, -(n)*efov) );
lines.push( { id:'icu'+n, s:origin, e:eu, speed:5, 'alive':true, depth:0} );
lines.push( { id:'icd'+n, s:origin, e:ed, speed:5, 'alive':true, depth:0} );
addCircle( eu, 2, 'cc0', col='black' );
addCircle( ed, 2, 'cc0', col='black' );
}
function updaterays()
{
if ( lines.length >= inputnumber( {id:'maxlines'}) ) throw('max lines reached:' + lines.length);
status.innerHTML = ('Lines:' + lines.length );
lines.forEach( (line)=>{
let hit = false;
if ( line.alive == false ) return;
if ( typeof(line.depth) == 'undefined' ) throw('line without depth data');
if ( line.depth > inputnumber( {id:'maxbounces'} ) ) return;
shapes.forEach( (s)=>{
let res = lineIntersectCircle( line, s );
if ( res.hit )
{
if ( checkbox( { id:'normals' } ) )
showNormal( {p:res.hitpoint, n:res.normal} );
line.e = res.hitpoint;
setLine( line.id, line.s, line.e );
let svgCircle = document.getElementById( 'c0' );
svgCircle.setAttribute("stroke", "red");
let i = norm( sub( line.e, line.s ) );
// refraction/transparent (through the object)
if ( checkbox( { id:'refract' } ) )
{
let normal = res.normal;
if ( !res.inside ) // inside to outside
normal = scale(normal, -1);
// another way of checking - 'inverting' the normal you could also use
// dot(i,n) > 0 - means leaving the sphere
// if ( dot(i,n) > 0 ) == '!res.inside'
//showNormal( {p:line.e, n:normal, col:'blue'} );
//showNormal( {p:line.e, n:i, col:'red' } );
// eta is the ratio between the refractive index of the medium being left (air is 1) and the medium being entered. Once you enter an object and hit the back side, you need to refract by 1/eta basically.
let eta = inputnumber( {id:'eta'} );
if ( !res.inside ) eta = 1/eta;
let rf = refract( i, normal, eta );
//showNormal( {p:line.e, n:rf} );
//{
let hp = add( res.hitpoint, scale(norm(sub(line.e, line.s)), 3.0) );
//showNormal( {p:hp, n:rf} );
let s0 = hp;
let e0 = add( hp, scale(rf, 2.0) );
let newline = { id:'refa'+lines.length, s:s0, e:e0, speed:5, 'alive':true, depth:line.depth+1 };
lines.push( newline );
if ( res.inside )
setLine( newline.id, newline.s, newline.e, opt={'stroke-dasharray':'4'} );
//}
}
// reflected/bounce off the object
if ( checkbox( { id:'reflect' } ) )
if ( res.inside ) // outside - inside
{
let rl = reflect( i, res.normal );
let s0 = add( line.e, scale(norm(sub(line.e,line.s)), -3.0) );
let e0 = add( s0, scale(rl, 1.0) );
let newline = { id:'refb'+lines.length, s:s0, e:e0, speed:5, 'alive':true, depth:line.depth+1 };
//showNormal( {p:s0, n:scale(rl,-1)} );
lines.push( newline );
}
line.alive = false;
hit |= true;
return;
}
});// end foreach shapes
if ( hit ) return;
let d = dist(line.s, line.e);
line.e = add( line.s, scale(norm( sub(line.e, line.s) ), d+line.speed) );
setLine( line.id, line.s, line.e );
if ( line.e.x > ww || line.e.y > wh ||
line.e.x < 0 || line.e.y < 0 )
{
//console.log('new line');
line.alive = false;
let n = scale(norm( sub(line.e, line.s) ), line.speed+0.5 );
if ( line.e.y < 0 || line.e.y > wh ) n.y *= -1;
if ( line.e.x < 0 || line.e.x > ww ) n.x *= -1;
let ee = add( line.e, scale(n, 1.0) );
let newline = { id:'id'+lines.length, s:line.e, e:ee, speed:5, 'alive':true, depth:line.depth+1 };
lines.push( newline );
}
});// end forEach(..)
}
for (let g=0; g<1000; g++){ clearInterval(g); }
let count = 0;
let iid = setInterval( ()=>{
try {
count++;
if ( count > 15000 ) clearInterval( iid );
updaterays();
}
catch( e )
{
console.log('error:', e );
clearInterval( iid );
}
}, 50);
}// end setup
window.onload = ()=>{ setup(); };
console.log('ready..');
JS
Scn
Out
In
document.body.style.minHeight = '200px';
let status = document.createElement('div');
status.innerHTML = 'Lines:';
document.body.appendChild( status );
//let ww = window.innerWidth;
//let wh = window.innerHeight;
let ww = 600;
let wh = 400;
let container = document.createElement('div');
document.body.appendChild( container );
container.id = 'container';
let opts = document.createElement('div');
document.body.appendChild(opts);
opts.style.position = 'fixed';
opts.style.right = '10px';
opts.style.top = '10px';
function checkbox( iargs = {id:undefined, checked:undefined, text:undefined} )
{
let el = document.getElementById(iargs.id);
if ( !el ) {
el = document.createElement('div');
el.id = iargs.id;
//el.style.border = '1px solid gray';
opts.appendChild( el );
el.appendChild( document.createElement('label') );
el.appendChild( document.createElement('input') );
el.children[0].style['vertical-align'] = 'top';
el.children[1].setAttribute("type", "checkbox");
}
if ( iargs.text ) el.children[0].innerHTML = iargs.text;
if ( iargs.checked ) el.children[1].checked = iargs.checked;
el.children[1].onchange = ()=>{ setup(); };
return el.children[1].checked;
}
checkbox( { id:'refract', checked:false, text:'show refract rays' } );
checkbox( { id:'normals', checked:false, text:'show normals' } );
checkbox( { id:'reflect', checked:false, text:'show reflect rays' } );
checkbox( { id:'fresnel', checked:true, text:'show fresnel magnitude' } );
function inputnumber( iargs = {id:undefined, value:undefined, range:undefined, text:undefined ,step:undefined } )
{
let el = document.getElementById(iargs.id);
if ( !el ) {
el = document.createElement('div');
el.id = iargs.id;
//el.style.border = '1px solid gray';
opts.appendChild( el );
el.appendChild( document.createElement('label') );
el.appendChild( document.createElement('input') );
el.children[0].style['vertical-align'] = 'top';
el.children[1].setAttribute("type", "number");
el.children[1].setAttribute("min", 1);
el.children[1].setAttribute("type", "number");
}
if ( iargs.text ) el.children[0].innerHTML = iargs.text;
if ( iargs.range ) el.children[1].min = iargs.range[0];
if ( iargs.range ) el.children[1].max = iargs.range[1];
if ( iargs.value ) el.children[1].value = iargs.value;
if ( iargs.step ) el.children[1].step = iargs.step;
el.children[1].onchange = ()=>{ setup(); };
el.children[1].onblur = ()=>{ setup(); };
return el.children[1].value;
}
inputnumber( { id:'numrays', value:20, range:[1,10], step:1, text:'number rays' } );
inputnumber( { id:'eta', value:1.3, range:[0, 3], step:0.1, text:'eta' } );
inputnumber( { id:'maxlines', value:40, range:[1,100], step:1, text:'max lines' } );
inputnumber( { id:'maxbounces',value:'0', range:[0,100], step:1, text:'max ray bounces' } );
inputnumber( { id:'numspheres',value:1, range:[1,5], step:1, text:'number spheres' } );
inputnumber( { id:'yfov', value:0.5, range:[1,5], step:1, text:'y fov spacing' } );
//--------------------------------------------------
//--------------------------------------------------
function setup()
{
container.innerHTML = '';
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttributeNS(null, "width", ww);
svg.setAttributeNS(null, "height", wh);
svg.setAttributeNS(null, "fill", 'currentColor');
svg.style.border = '1px solid blue';
container.appendChild( svg );
function setCircle( defs = { c:undefined, r:undefined, id:undefined, col:'green', enabled:true} )
{
let newCircle = document.getElementById( defs.id );
if ( !newCircle )
{
newCircle = document.createElementNS('http://www.w3.org/2000/svg','circle');
newCircle.id = defs.id;
svg.appendChild( newCircle );
}
newCircle.setAttribute('cx', defs.c.x );
newCircle.setAttribute('cy', defs.c.y );
newCircle.setAttribute('r', defs.r );
newCircle.setAttribute("fill", 'transparent');
newCircle.setAttribute("stroke-width", 2);
newCircle.setAttribute("stroke", defs.col);
newCircle.style.display = defs.enabled ? 'block' : 'none';
}
// setCircle( { {'x':200, 'y':100}, 50, 'c0' } );
let shapes = [];
shapes.push( { c:{x:250, y:205}, r:55, id:'c0', type:'circle', col:'green', enabled:true } );
shapes.push( { c:{x:200, y:90}, r:50, id:'c1', type:'circle', col:'orange', enabled:true } );
shapes.push( { c:{x:100, y:330}, r:40, id:'c2', type:'circle', col:'purple', enabled:true } );
shapes.push( { c:{x:350, y:300}, r:15, id:'c3', type:'circle', col:'magenta', enabled:true } );
shapes.push( { c:{x:390, y:60 }, r:25, id:'c4', type:'circle', col:'yellow', enabled:true } );
shapes.forEach( (s)=>{ setCircle( { c:s.c, r:s.r, id:s.id, col:s.col } ); });
function showNormal(afs = { p:undefined, n:undefined, col:'black', len:15 } )
{
if ( !afs.col ) afs.col = 'black';
if ( !afs.len ) afs.len = 15;
let n0 = document.createElementNS('http://www.w3.org/2000/svg','line');
n0.style.color = afs.col;
n0.setAttribute("stroke", afs.col);
let e = add(afs.p, scale(afs.n,afs.len));
svg.appendChild( n0 );
n0.setAttribute('x1', ''+afs.p.x);
n0.setAttribute('y1', ''+afs.p.y);
n0.setAttribute('x2', ''+e.x);
n0.setAttribute('y2', ''+e.y);
// n0.setAttribute('marker-end', 'url(#arrowhead)');
// all this code just for an arrow head!
{
let arrowID = 'arrow' + Math.random();
let defs = document.createElementNS('http://www.w3.org/2000/svg', "defs");
let marker = document.createElementNS('http://www.w3.org/2000/svg', "marker");
marker.setAttribute("id", arrowID);
marker.setAttribute("markerWidth", 10);
marker.setAttribute("markerHeight", 7);
marker.setAttribute("markerUnits", "strokeWidth");
marker.setAttribute("refX", 0); // warning 'case sensitive' refx != refX
marker.setAttribute("refY", 3.5);
marker.setAttribute("orient", "auto");
let path = document.createElementNS('http://www.w3.org/2000/svg', "path");
path.setAttribute("d","M 0 0 L 10 3.5 L 0 7 z");
path.setAttribute("fill", afs.col); // arrow head
marker.appendChild(path);
defs.appendChild(marker);
svg.appendChild(defs);
n0.setAttribute('marker-end', `url(#${arrowID})`);
}
}
function setLine(id, s, e, opt)
{
let i0 = document.getElementById( id );
if ( i0 == null )
{
i0 = document.createElementNS('http://www.w3.org/2000/svg','line');
i0.id = id;
//var randomColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16);
let rbyte = ()=>{ return Math.floor((Math.random()*256))%256; };
let randomColor = `rgb(${rbyte()}, ${rbyte()}, ${rbyte()})`;
i0.setAttribute("stroke", randomColor );
svg.appendChild( i0 );
}
i0.setAttribute('x1', ''+s.x);
i0.setAttribute('y1', ''+s.y);
i0.setAttribute('x2', ''+e.x);
i0.setAttribute('y2', ''+e.y);
if ( typeof(opt) != 'undefined' )
Object.entries(opt).forEach( (o)=>{ const [k,v]=o; i0.setAttribute( k, v ); } );
}
function sub (a, b) { return {x:(a.x-b.x), y:(a.y-b.y)}; }
function add (a, b) { return {x:(a.x+b.x), y:(a.y+b.y)}; }
function dist (a, b) { return Math.sqrt((a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y)); };
function norm (a ) { let d = Math.sqrt(a.x*a.x + a.y*a.y); return {x:a.x/d, y:a.y/d}; }
function len (a ) { return Math.sqrt(a.x*a.x + a.y*a.y); };
function scale (a, s) { return { x:a.x*s, y:a.y*s }; }
function dot (a, b) { return ( a.x*b.x + a.y*b.y ); }
function lineIntersectCircle( line, circle )
{
let d0 = dist( line.s, circle.c );
let d1 = dist( line.e, circle.c );
let d0inside = d0 < circle.r;
let d1inside = d1 < circle.r;
if ( d0inside != d1inside )
{
let pen = circle.r - ( d1<d0 ? d1 : d0 );
let n = norm( sub( line.e, circle.c ) );
let hitpoint = add( line.e, scale(norm(sub(line.e,line.s)), -pen*1.5 ) );
return { hit:true, normal:n, hitpoint:hitpoint, inside:d1inside, pen:pen };
}
return { hit:false, normal:{x:0,y:0}, hitpoint:{x:0,y:0} };
}
function reflect( i, n )
{
// I - 2.0 * dot(N, I) * N.
return norm( sub( scale(i, 1), scale( n, 2 * dot(n, i) ) ) );
}
// source material index of refraction (IOR)' / 'destination material IOR
function refract( i, n, eta )
{
// k = 1.0 - eta * eta * (1.0 - dot(N, I) * dot(N, I));
// if (k < 0.0)
// R = vec(0.0);
// else
// R = eta * I - (eta * dot(N, I) + sqrt(k)) * N;
let k = 1.0 - eta * eta * ( 1.0 - dot(n,i) * dot(n,i) );
if ( k < 0.0 ) // normal and the incident are tangental
{
k = 0.01;
}
let s = eta * dot(n,i) + Math.sqrt(k);
return norm( sub( scale(i, eta), scale(n, s ) ) );
}
let lines = [ ];
/*
// add point light
let p0 = { x:50, y:50 };
for (let k=0; k<5; k++)
{
let sin = Math.sin;
let cos = Math.cos;
let t = Math.PI * 2 * (k/5) + 0.1;
let n = {x: sin(t) , y: cos(t) };
let e = add( p0, n );
let ii = { id:'iv'+k, s:p0, e:e, speed:3, 'alive':true };
lines.push( ii );
setLine( ii.id, ii.s, ii.e );
}
*/
let origin = {x:20, y:200};
let target = {x:50, y:200};
let forward = norm( sub( target, origin ) );
let up = {x:forward.y, y:forward.x };
let numrays = inputnumber( {id:'numrays'} );
let yfov = inputnumber( {id:'yfov'} );
lines.push( { id:'ic0', s:origin, e:target, speed:5, 'alive':true, depth:0 } );
setCircle( { c:origin, r:2, id:'cc0', col:'black' } );
for (let n=1; n<numrays; n++)
{
let eu = add( target, scale(up, (n)*yfov) );
let ed = add( target, scale(up, -(n)*yfov) );
lines.push( { id:'icu'+n, s:origin, e:eu, speed:5, 'alive':true, depth:0} );
lines.push( { id:'icd'+n, s:origin, e:ed, speed:5, 'alive':true, depth:0} );
setCircle( { c:eu, r:2, id:'cc0', col:'black' } );
setCircle( { c:ed, r:2, id:'cc0', col:'black' } );
}
function fresnel( c1, c2, i, n )
{
/*
e.g., color = reflectionColor * f + refractionColor * ( 1 - f )
Schlicks approximation
R(theta) = R0 + (1-R0)(1-cos(theta))^5 (0<theta<90)
R0 = ( (n1-n2)/(n1+n2) )^2
*/
let r0 = Math.pow( (c1-c2)/(c1+c2), 2 );
let cosX = dot( n, i );
if ( c1 > c2 )
{
let s = c1/c2;
let sinT2 = s*s*(1-cosX*cosX);
if ( sinT2 > 1.0 ) return 1.0;
cosX = Math.sqrt( 1.0 - sinT2 );
}
return r0+(1-r0) * Math.pow( (1 - cosX), 1 ); // 1- 5 (5 defaut) amplify the visual
// setting the pow to 1 makes the curve into an inverse of the dot
}
function updaterays()
{
if ( lines.length >= inputnumber( {id:'maxlines'}) ) throw('max lines reached:' + lines.length);
status.innerHTML = ('Lines:' + lines.length );
shapes.forEach( (s,i)=>{
s.enabled = true;
if ( i >= inputnumber( {id:'numspheres'} ) ) s.enabled = false;
setCircle( s );
});
lines.forEach( (line)=>{
let hit = false;
if ( line.alive == false ) return;
if ( typeof(line.depth) == 'undefined' ) throw('line without depth data');
if ( line.depth > inputnumber( {id:'maxbounces'} ) ) return;
shapes.forEach( (s)=>{
if ( !s.enabled ) return;
let res = lineIntersectCircle( line, s );
if ( res.hit )
{
if ( checkbox( { id:'normals' } ) )
showNormal( {p:res.hitpoint, n:res.normal} );
line.e = res.hitpoint;
setLine( line.id, line.s, line.e );
let svgCircle = document.getElementById( 'c0' );
svgCircle.setAttribute("stroke", "red");
let i = norm( sub( line.e, line.s ) );
//showNormal( {p:line.e, n:scale(i,-1), c:'green' } );
if ( checkbox( { id:'fresnel' } ) )
{
let eta = inputnumber( {id:'eta'} );
let f = fresnel( 1.0, eta, scale(i, -1), res.normal );
//f = dot( i, res.normal );
showNormal( {p:res.hitpoint, n:scale( res.normal, f ), col:'red', len:f*50} );
}
// refraction/transparent (through the object)
if ( checkbox( { id:'refract' } ) )
{
let normal = res.normal;
if ( !res.inside ) // inside to outside
normal = scale(normal, -1);
// another way of checking - 'inverting' the normal you could also use
// dot(i,n) > 0 - means leaving the sphere
// if ( dot(i,n) > 0 ) == '!res.inside'
//showNormal( {p:line.e, n:normal, c:'blue'} );
// eta is the ratio between the refractive index of the medium being left (air is 1) and the medium being entered. Once you enter an object and hit the back side, you need to refract by 1/eta basically.
let eta = inputnumber( {id:'eta'} );
if ( !res.inside ) eta = 1/eta;
let rf = refract( i, normal, eta );
//showNormal( {p:line.e, n:rf} );
//{
let hp = add( res.hitpoint, scale(norm(sub(line.e, line.s)), 3.0) );
//showNormal( {p:hp, n:rf} );
let s0 = hp;
let e0 = add( hp, scale(rf, 2.0) );
let newline = { id:'refa'+lines.length, s:s0, e:e0, speed:5, 'alive':true, depth:line.depth+1 };
lines.push( newline );
if ( res.inside )
setLine( newline.id, newline.s, newline.e, opt={'stroke-dasharray':'4'} );
//}
}
// reflected/bounce off the object
if ( checkbox( { id:'reflect' } ) )
if ( res.inside ) // outside - inside
{
let rl = reflect( i, res.normal );
let s0 = add( line.e, scale(norm(sub(line.e,line.s)), -3.0) );
let e0 = add( s0, scale(rl, 1.0) );
let newline = { id:'refb'+lines.length, s:s0, e:e0, speed:5, 'alive':true, depth:line.depth+1 };
//showNormal( {p:s0, n:scale(rl,-1)} );
lines.push( newline );
}
line.alive = false;
hit |= true;
return;
}
});// end foreach shapes
if ( hit ) return;
let d = dist(line.s, line.e);
line.e = add( line.s, scale(norm( sub(line.e, line.s) ), d+line.speed) );
setLine( line.id, line.s, line.e );
if ( line.e.x > ww || line.e.y > wh ||
line.e.x < 0 || line.e.y < 0 )
{
//console.log('new line');
line.alive = false;
let n = scale(norm( sub(line.e, line.s) ), line.speed+0.5 );
if ( line.e.y < 0 || line.e.y > wh ) n.y *= -1;
if ( line.e.x < 0 || line.e.x > ww ) n.x *= -1;
let ee = add( line.e, scale(n, 1.0) );
let newline = { id:'id'+lines.length, s:line.e, e:ee, speed:5, 'alive':true, depth:line.depth+1 };
lines.push( newline );
}
});// end forEach(..)
}
for (let g=0; g<1000; g++){ clearInterval(g); }
let count = 0;
let iid = setInterval( ()=>{
try {
count++;
if ( count > 15000 ) clearInterval( iid );
updaterays();
}
catch( e )
{
console.log('error:', e );
clearInterval( iid );
}
}, 50);
}// end setup
window.onload = ()=>{ setup(); };
console.log('ready..');
JS
Scn
Out
Contact/Report Bugs
You can contact me at: bkenwright@xbdev.net