├── playground ├── github.png ├── fullscreen.css ├── README.md ├── demos │ ├── julia.js │ ├── mandelbulb.js │ ├── kifs.js │ ├── twist.js │ ├── let_it_shine.js │ ├── pipe.js │ ├── alien_portal.js │ ├── union.js │ ├── texture_transforms.js │ ├── voxels_and_edges.js │ ├── superformula.js │ ├── noise.js │ ├── intro.js │ ├── snare.js │ ├── textures.js │ ├── geometries.js │ ├── postprocessing.js │ ├── constructors.js │ ├── portal2.js │ ├── csg.js │ ├── procedural_textures.js │ ├── hydra.js │ ├── gifs_and_links.js │ ├── p5.js │ ├── textures_tutorial.js │ ├── audio.js │ ├── livecoding.js │ ├── tutorial_1.js │ └── lighting.js ├── index.htm ├── main.css └── codemirror.css ├── js ├── index.js ├── int.js ├── float.js ├── objects.js ├── utils.js ├── background.js ├── osc.js ├── fog.js ├── vignette.js ├── textureWrap.js ├── noise.js ├── audio.js ├── alterations.js ├── sceneNode.js ├── var.js ├── transform.js ├── mergepass.js ├── vec.js ├── camera.js ├── renderFragmentShader.js ├── scene.js ├── material.js ├── distanceDeformations.js └── distanceOperations.js ├── .gitignore ├── demo.html ├── gulpfile.js ├── README.markdown ├── LICENSE ├── package.json ├── CREDITS.markdown └── docs └── index.html /playground/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charlieroberts/marching/HEAD/playground/github.png -------------------------------------------------------------------------------- /playground/fullscreen.css: -------------------------------------------------------------------------------- 1 | .CodeMirror-fullscreen { 2 | position: fixed; 3 | top: 0; left: 0; right: 0; bottom: 0; 4 | height: auto; 5 | z-index: 9; 6 | } 7 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Marching = require( './main.js' ) 4 | 5 | Marching.__export = Marching.export 6 | Marching.export = obj => { 7 | obj.march = Marching.createScene.bind( Marching ) 8 | Marching.__export( obj ) 9 | } 10 | 11 | window.Marching = Marching 12 | 13 | module.exports = Marching 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Numerous always-ignore extensions 2 | *.diff 3 | *.err 4 | *.orig 5 | *.log 6 | *.rej 7 | *.swo 8 | *.swp 9 | *.vi 10 | *~ 11 | *.sass-cache 12 | 13 | # OS or Editor folders 14 | .DS_Store 15 | Thumbs.db 16 | .cache 17 | .project 18 | .settings 19 | .tmproj 20 | *.esproj 21 | 22 | node_modules 23 | 24 | nbproject 25 | *.sublime-project 26 | *.sublime-workspace 27 | -------------------------------------------------------------------------------- /js/int.js: -------------------------------------------------------------------------------- 1 | const emit_int = function( a ) { 2 | if( a % 1 !== 0 ) 3 | return Math.round( a ) 4 | else 5 | return a 6 | } 7 | 8 | const IntPrototype = { 9 | type: 'int', 10 | emit() { return emit_int( this.x ) }, 11 | emit_decl() { return "" } 12 | } 13 | 14 | 15 | const Int = function( x=0 ) { 16 | const f = Object.create( IntPrototype ) 17 | f.x = x 18 | return f 19 | } 20 | 21 | module.exports = Int 22 | -------------------------------------------------------------------------------- /playground/README.md: -------------------------------------------------------------------------------- 1 | To build the marching.js playground, run the following command from the top level of this repo (*not* from within the playground directory): 2 | 3 | `npx browserify playground/environment.js -o playground/playground.bundle.js` 4 | 5 | You can then start a server in the top level: 6 | 7 | `npx http-server . -p 10000` 8 | 9 | ... and access the page at `http://localhost:10000/playground/index.htm` 10 | -------------------------------------------------------------------------------- /playground/demos/julia.js: -------------------------------------------------------------------------------- 1 | module.exports=`mat1 = Material( 'phong', Vec3(.0),Vec3(.5),Vec3(1), 32, Vec3(0,.25,1) ) 2 | tex = Texture( 'cellular', { strength:.15, scale:20 }) 3 | 4 | march( 5 | Julia(1.5) 6 | .material( mat1 ) 7 | .texture( tex ) 8 | .bump( tex, .05 ) 9 | ) 10 | .light( 11 | Light( Vec3(5,5,8), Vec3(1), .025 ) 12 | ) 13 | .fog( 1, Vec3(0) ) 14 | .render() 15 | .camera(0,0,1.75) 16 | ` 17 | -------------------------------------------------------------------------------- /js/float.js: -------------------------------------------------------------------------------- 1 | const emit_float = function( a ) { 2 | if (a % 1 === 0) 3 | return a.toFixed( 1 ) 4 | else 5 | return a 6 | } 7 | 8 | const FloatPrototype = { 9 | type: 'float', 10 | emit() { return emit_float( this.x ) }, 11 | emit_decl() { return "" } 12 | } 13 | 14 | 15 | const Float = function( x=0 ) { 16 | const f = Object.create( FloatPrototype ) 17 | f.x = x 18 | return f 19 | } 20 | 21 | module.exports = Float 22 | -------------------------------------------------------------------------------- /playground/demos/mandelbulb.js: -------------------------------------------------------------------------------- 1 | module.exports = `mat1 = Material( 'phong', Vec3(.0),Vec3(.5),Vec3(1), 32, Vec3(0) ) 2 | 3 | march( 4 | Mandelbulb( 6.5 ) 5 | .rotate( 270, 1,0,0 ) 6 | .translate( 0,0,.5 ) 7 | .material( mat1 ), 8 | 9 | Plane( Vec3(0,0,1), .5 ).material( 'white glow') 10 | ) 11 | .light( 12 | Light( Vec3(-3,2,4), Vec3(1), .25 ), 13 | Light( Vec3(0,0,4), Vec3(1), .95 ) 14 | ) 15 | .render() 16 | .camera( 0,0,3 )` 17 | -------------------------------------------------------------------------------- /playground/demos/kifs.js: -------------------------------------------------------------------------------- 1 | module.exports =`// inspired by http://roy.red/folding-the-koch-snowflake-.html 2 | // this is still under development 3 | march( 4 | r = Rotation( 5 | // number of iterations, fold amount, radius, threshold, scale, center, material 6 | k = KIFS( 6,.25,.01,.01, 2, null, Material.white ), 7 | Vec3( 1 ) 8 | ) 9 | ) 10 | .render( 3,true ) 11 | .camera( 0,0,3 ) 12 | 13 | onframe = t=> { 14 | k.fold = -.1 + sin( t ) * .5 15 | r.angle = t/4 16 | }` 17 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 24 | 25 | -------------------------------------------------------------------------------- /playground/demos/twist.js: -------------------------------------------------------------------------------- 1 | module.exports =`Material.default = Material.grey 2 | 3 | m = march( 4 | StairsUnion( 5 | Repeat( 6 | t = Twist( 7 | PolarRepeat( 8 | Cylinder( Vec2(.025,2.75) ), 9 | 10, 10 | .25 11 | ).rotate( 270, 1,0,0 ), 12 | Vec3(0) 13 | ), 14 | Vec3(2.5,0,0) 15 | ), 16 | Plane(), 17 | .35 18 | ) 19 | ) 20 | .light( Light( Vec3(.4), Vec3(.5) ) ) 21 | .background( Vec3(.5,.6,.7) ) 22 | .render(3, true ) 23 | .camera( 2, 0, 6 ) 24 | 25 | onframe = time => { 26 | t.amount = Math.sin(time/4)*5 27 | }` 28 | -------------------------------------------------------------------------------- /playground/demos/let_it_shine.js: -------------------------------------------------------------------------------- 1 | module.exports = `march( 2 | int = Intersection( 3 | Sphere( 2 ).material(), 4 | box = Mandelbox( .3,2.5,7 ).material( 'red' ) 5 | ) 6 | ) 7 | .background( 8 | Vec3(.125,0,.125) 9 | ) 10 | .light( 11 | Light( Vec3(5), Vec3(1,0,1), .1) 12 | ) 13 | .post( 14 | Focus( .575 ), 15 | rays = Godrays( 1.01,.01,1,.65 ) 16 | ) 17 | .fog( 18 | .2, Vec3(.125,0,.125) 19 | ) 20 | .render( 'fractal.low' ) 21 | .camera( 0,0,3.5 ) 22 | 23 | onframe = t => { 24 | box.size = 2.4 + sin(t) * .15 25 | box.fold = .3 + cos(t/2) * .1 26 | int.rotate(t*10,.5,.5,sin(t/5)) 27 | } 28 | 29 | rays.color = [1,.5,1]` 30 | -------------------------------------------------------------------------------- /playground/demos/pipe.js: -------------------------------------------------------------------------------- 1 | module.exports = `Marching.lighting.mode = 'orenn' 2 | 3 | march( 4 | SmoothUnion( 5 | rot = Rotation( 6 | pipe = Pipe( 7 | Box(), 8 | sphere = Sphere(2.5, null, Material.blue), 9 | .5 10 | ), 11 | Vec3(1,.5,.5) 12 | ), 13 | Plane( Vec3(0,1,.75), 2.25 ), 14 | .125 15 | ) 16 | ) 17 | .light( 18 | Light( Vec3(0,5,0), Vec3(0,.25,1), .05 ) 19 | ) 20 | .background( Vec3(.1) ) 21 | .shadow( 8 ) 22 | .render(3, true) 23 | .camera( 0,0,7 ) 24 | 25 | callbacks.push( time => { 26 | pipe.c = .8 + Math.abs(Math.sin(time/2)) / 4 27 | rot.angle = time 28 | sphere.radius = 2 + Math.abs( Math.sin(time/4)) 29 | })` 30 | -------------------------------------------------------------------------------- /playground/demos/alien_portal.js: -------------------------------------------------------------------------------- 1 | module.exports = `mat = Material( 'phong', Vec3(.05), Vec3(1), Vec3(3), 64, Vec3( 0,6,4) ) 2 | 3 | m = march( 4 | StairsUnion( 5 | Plane( Vec3(0,.5,0) ).material( mat ), 6 | PolarRepeat( 7 | PolarRepeat( 8 | Torus82().material( mat ), 9 | 20, 10 | 2.75 11 | ).rotate(90, 1,0,0 ), 12 | 25, 13 | 2 14 | ), 15 | .25 16 | ), 17 | Plane( Vec3(0,.5,0) ) 18 | .bump( Texture('noise', { strength:1.5, scale:13 }) ) 19 | ) 20 | .fog( .15, Vec3(0) ) 21 | .light( Light( Vec3(0,.25,0), Vec3(1,.5,.25), .125 ) ) 22 | .post( Antialias(2), Bloom(.35,1.25,4,4), Focus(.05, .005 )) 23 | .resolution(1) 24 | .render() 25 | .camera( 0, 0, 10 )` 26 | -------------------------------------------------------------------------------- /js/objects.js: -------------------------------------------------------------------------------- 1 | const { Var, float_var_gen, vec2_var_gen, vec3_var_gen, vec4_var_gen } = require( './var.js' ) 2 | const { Vec2, Vec3, Vec4 } = require( './vec.js' ) 3 | const SceneNode = require( './sceneNode.js' ) 4 | const { param_wrap, MaterialIDAlloc } = require( './utils.js' ) 5 | 6 | 7 | const { Union, SmoothUnion, Intersection, Substraction } = require( './distanceOperations.js' ) 8 | const Subtraction = Substraction 9 | const { Scale, Repeat } = require( './domainOperations' ) 10 | 11 | 12 | const __out = { 13 | Vec2, 14 | Vec3, 15 | Vec4, 16 | Var, 17 | SceneNode, 18 | Union, 19 | Intersection, 20 | Substraction, 21 | Subtraction, 22 | Scale, 23 | Repeat, 24 | SmoothUnion 25 | } 26 | 27 | Object.assign( __out, require( './primitives.js' ) ) 28 | 29 | module.exports = __out 30 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp') 2 | var browserify = require('browserify') 3 | var source = require('vinyl-source-stream') 4 | var watchify = require( 'watchify' ) 5 | var gutil = require( 'gulp-util' ) 6 | const tsify = require('tsify') 7 | 8 | gulp.task('build', function () { 9 | 10 | return browserify({ 11 | debug:true, 12 | entries: './js/index.js' 13 | }) 14 | .plugin( tsify ) 15 | .bundle() 16 | .pipe( source('index.js') ) 17 | .pipe( gulp.dest( './dist/' ) ) 18 | 19 | }) 20 | 21 | watchify.args.entries = './js/index.js' 22 | watchify.args.debug = true 23 | 24 | var b = watchify( browserify( watchify.args ) ) 25 | b.on( 'update', bundle ) 26 | b.on( 'log', gutil.log ) 27 | 28 | gulp.task('default', bundle) 29 | 30 | function bundle() { 31 | return b.bundle().pipe( source('index.js') ).pipe( gulp.dest( './dist/' ) ) 32 | } 33 | -------------------------------------------------------------------------------- /playground/demos/union.js: -------------------------------------------------------------------------------- 1 | module.exports = `// unions and smooth unions can be used to 2 | // combine two geometries together. a Union 3 | // creates boundaries with reasonably well-defined 4 | // edges. 5 | 6 | scene([ 7 | Union( 8 | Sphere(), 9 | TriPrism( null, Vec2(2) ) 10 | ) 11 | ], 90 ) 12 | 13 | 14 | // smooth union blends these edges together, 15 | // at an adjustable amount 16 | scene([ 17 | s = SmoothUnion( 18 | Sphere(), 19 | TriPrism( null, Vec2(2) ), 20 | .5 21 | ) 22 | ], 90 ) 23 | 24 | callbacks.push( time => s.blend = (time/4) % 1 ) 25 | 26 | 27 | // add some noise for fun... 28 | scene([ 29 | r = Rotation( 30 | SmoothUnion( 31 | Sphere( Noise(.25, 1.5 ) ), 32 | TriPrism( null, Vec2(3) ) 33 | ), 34 | Vec3(1,0,0), 35 | 1.5 36 | ) 37 | ]) 38 | 39 | callbacks.push( time => r.angle = time )` 40 | -------------------------------------------------------------------------------- /playground/demos/texture_transforms.js: -------------------------------------------------------------------------------- 1 | module.exports =`// In this demo the important point 2 | // to note is that transformations can be applied to 3 | // most functions, not just geometries. Here, a 4 | // rotation is applied to a Union while translation 5 | // is applied to the domain created by Repeat(). 6 | 7 | march( 8 | rpt = Repeat( 9 | union = Union2( 10 | cyl = Cylinder(Vec2(1,1.5)) 11 | .texture('dots', {scale:2}), 12 | cyl2 = Cylinder(Vec2(.95,1.5)) 13 | .rotate(90,0,0,1) 14 | .texture('stripes', {scale:1}), 15 | cyl3 = Cylinder(Vec2(.95,1.5)) 16 | .rotate(90,1,0,0) 17 | .texture('checkers', {scale:5}) 18 | ) 19 | .scale(.15), 20 | .75 21 | ) 22 | ) 23 | .fog( .5, Vec3( 0 ) ) 24 | .render( 'repeat.low' ) 25 | 26 | onframe = time => { 27 | union.rotate( time*65,1,.5,.5 ) 28 | rpt.translate( 0,0,time/3 ) 29 | }` 30 | -------------------------------------------------------------------------------- /playground/demos/voxels_and_edges.js: -------------------------------------------------------------------------------- 1 | module.exports = `// this uses a voxelized scene with 2 | // a procedural texture generated by hydra 3 | // alongside of various post-processing effects, 4 | // inclulding edge detection and depth-of-field. 5 | 6 | // hit alt+c to hide the code editor 7 | // and take control of the camera using the WASD 8 | // and arrow keys. if your graphics card can handle 9 | // it, try changing the quality variable to 'med' or 10 | // 'high' and then re-executing all the code below. 11 | 12 | quality = 'low' 13 | 14 | use('hydra').then( ()=> { 15 | 16 | hydra = Hydra() 17 | hydra.osc(10,.1,10).out() 18 | 19 | march( 20 | Repeat( 21 | sphere = Sphere(.75).texture( hydra.texture() ), 22 | 2 23 | ) 24 | ) 25 | .voxel(.05) 26 | .fog( .5, Vec3(0) ) 27 | .post( Edge(), Invert(1), f = Focus(.15) ) 28 | .render( 'voxel.'+quality ) 29 | 30 | sphere.tex.scale = 8 31 | 32 | onframe = time => sphere.radius = .65 + sin( time/5 ) * .15 33 | 34 | })` 35 | -------------------------------------------------------------------------------- /js/utils.js: -------------------------------------------------------------------------------- 1 | const Var = require('./var.js').Var 2 | const { Vec2, Vec3, Vec4 } = require( './vec.js' ) 3 | 4 | // Wrapper 5 | function param_wrap( v, __default, name=null ) { 6 | if( v === undefined || v === null ) return __default() 7 | if( v.__isVar === true ) return v 8 | 9 | return Var( v, name ) 10 | } 11 | 12 | const MaterialID = { 13 | current: 0, 14 | alloc() { 15 | return MaterialID.current++ 16 | }, 17 | clear() { 18 | MaterialID.current = 0 19 | } 20 | } 21 | 22 | const processVec2 = function( val ) { 23 | if( typeof val === 'number' ) 24 | val = Vec2( color ) 25 | else if( Array.isArray( val ) ) 26 | val = Vec2( val[0], val[1] ) 27 | 28 | return val 29 | } 30 | 31 | const processVec3 = function( val ) { 32 | if( typeof val === 'number' ) 33 | val = Vec3( val ) 34 | else if( Array.isArray( val ) ) 35 | val = Vec3( val[0], val[1] || val[0], val[2] || val[0] ) 36 | 37 | return val 38 | } 39 | 40 | module.exports = { param_wrap, MaterialID, processVec2, processVec3 } 41 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # marching.js 2 | 3 | [Online Playground (Chrome/Firefox)](https://charlieroberts.github.io/marching/playground/) 4 | [Atom plugin](https://atom.io/packages/atom-marching) 5 | [Reference](https://charlieroberts.github.io/marching/docs/index.html) 6 | 7 | [![Screenshot](https://i.postimg.cc/P5HWp4bm/Screen-Shot-2022-02-08-at-8-55-03-PM.png)](https://postimg.cc/vcPgC5NB) 8 | Marching.js is a JavaScript shader compiler specifically focused on ray marching via signed distance functions. The goals of this project are: 9 | 10 | - Expose beginning programmers to constructive solid geometry (CSG) concepts 11 | - Enable JS programmers to explore CSG without having to learn GLSL 12 | - Provide a terse API suitable for live coding performance 13 | 14 | Marching.js builds on the [work of many other people](https://github.com/charlieroberts/marching/blob/master/CREDITS.markdown). 15 | 16 | ## Development 17 | The library is compiled using gulp. Run `npm install` to install all necessary dependencies, and then `npm run build` to build the library. 18 | -------------------------------------------------------------------------------- /playground/demos/superformula.js: -------------------------------------------------------------------------------- 1 | module.exports = `// a 3D superformula essentially two 2D supershapes, 2 | // first six coefficients govern one, second 3 | // six coefficients govern the other. 4 | 5 | mat1 = Material( 'phong', Vec3(.0),Vec3(0),Vec3(1), 16, Vec3(0,.25,4) ) 6 | 7 | m = march( 8 | s = SuperFormula( 9 | 1, 1, 16, 1, 1, 1, 10 | 1, 1, 16, 1, 1, 1 11 | ) 12 | .translate( 0, .5, .85 ) 13 | .material( mat1 ) 14 | .texture( 'truchet', { color:.125, scale:30 } ), 15 | 16 | Plane( Vec3(0,1,0), 1 ).material( Material('phong', Vec3(.15), Vec3(1) ) ) 17 | ) 18 | .light( 19 | Light( Vec3(0,5,0), Vec3(.25,.25,.5), .5 ), 20 | Light( Vec3(3,3,0), Vec3(.5), .5 ) 21 | ) 22 | .fog( .25, Vec3(0) ) 23 | .render( 2, true ) 24 | .camera( 0,0,5 ) 25 | 26 | onframe = time => { 27 | t = 12 28 | s.n1_1 = Math.PI + Math.sin( time ) 29 | s.n1_2 = Math.PI + Math.cos( time ) 30 | s.m_1 = Math.sin( time / 2 ) * t 31 | s.m_2 = Math.cos( time / 2 ) * t 32 | s.rotate( time * 10, 0,1,0 ) 33 | } 34 | 35 | // thanks to https://github.com/Softwave/glsl-superformula` 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Charlie Roberts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /playground/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | marching.js playground 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 23 | 24 | 25 | 26 | 27 |
28 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /playground/demos/noise.js: -------------------------------------------------------------------------------- 1 | module.exports = `// Noise() can be used in place of any float or Vector member. 2 | 3 | march( 4 | // noisy radius 5 | Sphere( Noise(), Vec3(-2,0,0) ), 6 | 7 | // noisy y-size 8 | Box( Vec3(2,0,0), Vec3( 1, Noise(.25,1), 0 ) ) 9 | ).render() 10 | 11 | // Noise( (float)strength=.25, (float)bias=1, (float)timeMod=1 ) 12 | 13 | march( Sphere( Noise() ) ).render() 14 | march( Sphere( Noise(.5) ) ).render() 15 | march( Sphere( Noise(.25,2,.5) ) ).render() 16 | 17 | // Displace() works a bit differently, in that it 18 | // takes the output of a distance function and alters 19 | // it, rather than providing an input to a distance 20 | // function (like Noise()). 21 | 22 | // displace the rays colliding with our box 23 | d = Displace( Box( Vec3(0), Vec3(2,4,2)), Vec3(1) ) 24 | 25 | // rotate the displacement on the x and z axes 26 | r = Rotation( d, Vec3(1,0,1), 0 ) 27 | 28 | // compile the shader 29 | march( r ).render( ) 3, true ) 30 | 31 | callbacks.push( 32 | // change rotattion 33 | time => r.angle = time, 34 | // change displacement 35 | time => d.displacement.x = 1 + Math.sin(time/4) % 1 36 | ) 37 | 38 | // move the camera back to see the whole scene 39 | camera.pos.z = 10` 40 | -------------------------------------------------------------------------------- /playground/demos/intro.js: -------------------------------------------------------------------------------- 1 | module.exports = `march( 2 | Repeat( 3 | StairsDifference( 4 | Sphere(2), 5 | Repeat( Sphere( .1 ), .25 ), 6 | .25, 7 | 10 8 | ), 9 | 6 10 | ).translate( 0,-1.5 ), 11 | Plane().texture( 'cellular', { strength:-.5, scale:10 }) 12 | ) 13 | .fog( .1, Vec3(0,0,.25) ) 14 | .background( Vec3(0,0,.25) ) 15 | .render() 16 | 17 | /* __--__--__--__--__--__--__--____ 18 | 19 | select code and hit ctrl+enter to 20 | execute. alt+enter (option+enter on 21 | a mac) executes a block of code. 22 | ctrl+shift+g toggles hiding 23 | the gui/code. try the other demos 24 | using the menu in the upper right 25 | corner. when you're ready to start 26 | coding go through the tutorials 27 | found in the same menu. Click on 28 | the help link for a reference. 29 | 30 | For a nice intro on ray marching and 31 | signed distance functions,which are 32 | the techniques used by marching.js, 33 | see: 34 | 35 | https://bit.ly/2qRMrpe 36 | 37 | Finally, if you'd prefer to work 38 | outside of the browser, try the 39 | Atom plugin for marching.js: 40 | 41 | https://atom.io/packages/atom-marching 42 | 43 | ** __--__--__--__--__--__--__--__*/` 44 | -------------------------------------------------------------------------------- /playground/demos/snare.js: -------------------------------------------------------------------------------- 1 | module.exports = `// because, like, marching.js, snare drums, marching... 2 | 3 | const grey = Material( 'phong', Vec3(0), Vec3(3), Vec3(2), 32, Vec3(0,0,2) ), 4 | white = Material( 'phong', Vec3(0), Vec3(50), Vec3(1), 8, Vec3(0,50,2) ), 5 | a = .45, b = .25 // for side bands on drum 6 | 7 | stick = ()=> { 8 | return Union( 9 | Groove( 10 | Cylinder( Vec2(.2,2.5), Vec3(.5,4.5,-.5) ) 11 | .move( .5,4.5,-.5 ) 12 | .material( grey ), 13 | Capsule( Vec3(.5, 2,-.5), Vec3(.5,2.25,-.5), 1 ).material( grey ), 14 | .08 15 | ), 16 | Capsule( Vec3(.5, 2,-.5), Vec3(.5,2.25,-.5), .175 ).material( grey ) 17 | ) 18 | } 19 | 20 | stickL = stick() 21 | .rotate( 300, 0,0,1 ) 22 | .move( -1.5,.25, 0 ) 23 | 24 | stickR = stick() 25 | .rotate( 300, 0,.75,.5 ) 26 | .move( -2.05,.0, -.5 ) 27 | 28 | drum = RoundUnion( 29 | Union2( 30 | ChamferDifference( 31 | Cylinder( Vec2(2.25, .45) ).material( grey ), 32 | Cylinder( Vec2(2,.4) ).material( grey ), 33 | .1 34 | ), 35 | Cylinder( Vec2(2,.3) ).material( white ) 36 | ), 37 | PolarRepeat( 38 | Quad( Vec3(0,-a,-b), Vec3(0,-a,0), Vec3(0,a,b), Vec3(0,a,0) ).material( grey ), 39 | 15, 40 | 2.25 41 | ), 42 | .05 43 | ) 44 | 45 | march( 46 | drum.rotate( 60, 1,1,0 ), 47 | stickL, 48 | stickR 49 | ) 50 | .background(Vec3(0)) 51 | .shadow(.75) 52 | .light( Light( Vec3(2,5,8), Vec3(1), .125 ) ) 53 | .render() 54 | .camera( 0,0,7 )` 55 | -------------------------------------------------------------------------------- /playground/demos/textures.js: -------------------------------------------------------------------------------- 1 | module.exports =`zigzag = Box(.5) 2 | .move( -2,1.25 ) 3 | .texture( 4 | 'zigzag', 5 | { scale:5 } 6 | ) 7 | 8 | dots = Box(.5) 9 | .move( -0,1.25 ) 10 | .texture( 11 | 'dots', 12 | { 13 | scale:10, 14 | color:[1,0,0] 15 | } 16 | ) 17 | 18 | noise = Box(.5) 19 | .move( 2,1.25 ) 20 | .texture( 21 | 'noise', 22 | { 23 | wrap:true, 24 | scale:20, 25 | color:[1,0,0] 26 | } 27 | ) 28 | 29 | truchet = Box(.5) 30 | .move( -2,-.15 ) 31 | .texture( 32 | 'truchet', 33 | { 34 | scale:20, 35 | color:[0,1,1] 36 | } 37 | ) 38 | 39 | stripes = Box(.5) 40 | .move( -0,-.15) 41 | .texture( 42 | 'stripes', 43 | { 44 | scale:10, 45 | color:[1,0,0] 46 | } 47 | ) 48 | 49 | checkers = Box(.5) 50 | .move( 2, -.15 ) 51 | .texture( 52 | 'checkers', 53 | { 54 | scale:20, 55 | color1:[0,1,1], 56 | color2:[1,0,0] 57 | } 58 | ) 59 | 60 | cellular = Box(.5) 61 | .move( -2, -1.55 ) 62 | .texture( 63 | 'cellular', 64 | { 65 | scale:10, 66 | strength:1 67 | } 68 | ) 69 | 70 | rainbows = Box(.5) 71 | .move( -0,-1.55) 72 | .texture( 73 | 'rainbow', 74 | { 75 | scale:4 76 | } 77 | ) 78 | 79 | voronoi = Box(.5) 80 | .move( 2,-1.55 ) 81 | .texture( 82 | 'voronoi', 83 | { 84 | wrap:true, 85 | scale:10, 86 | mode:2 87 | } 88 | ) 89 | 90 | bg = Plane( Vec3(0,0,1), .5 ).material('white glow') 91 | 92 | march( 93 | zigzag, dots, noise, 94 | truchet, stripes, cellular, 95 | checkers, rainbows, voronoi, bg 96 | ) 97 | .render()` 98 | -------------------------------------------------------------------------------- /js/background.js: -------------------------------------------------------------------------------- 1 | const SceneNode = require( './sceneNode.js' ), 2 | { param_wrap, MaterialID } = require( './utils.js' ), 3 | { Var, float_var_gen, vec2_var_gen, vec3_var_gen, vec4_var_gen } = require( './var.js' ) 4 | 5 | const { Vec2, Vec3, Vec4 } = require( './vec.js' ) 6 | 7 | const BG = function( Scene, SDF ) { 8 | 9 | const Background = function( color ) { 10 | if( SDF.memo.background === undefined ) { 11 | const bg = Object.create( Background.prototype ) 12 | 13 | if( color !== undefined && color.type === 'vec3' ) color = Vec4( color.x, color.y, color.z, 1 ) 14 | const __color = param_wrap( Vec4( color ), vec4_var_gen( 0,0,0,1, 'bg' ), 'bg' ) 15 | 16 | Object.defineProperty( bg, 'color', { 17 | get() { return __color }, 18 | set( v ) { 19 | __color.var.set( v ) 20 | } 21 | }) 22 | 23 | // this refers to the current scene via implicit binding in scene.js 24 | //this.postprocessing.push( bg ) 25 | bg.__backgroundColor = color 26 | this.__background = bg 27 | 28 | SDF.memo.background = true 29 | } 30 | return this 31 | } 32 | 33 | Background.prototype = SceneNode() 34 | 35 | Object.assign( Background.prototype, { 36 | emit() { 37 | return ''// this.color.emit() 38 | }, 39 | 40 | emit_decl() { 41 | //let str = this.color.emit_decl() 42 | //SDF.memo.background = true 43 | 44 | const out = this.__backgroundColor === undefined 45 | ? 'vec4 bg = vec4(0.,0.,0.,1.);' 46 | : `vec4 bg = vec4(${ this.__backgroundColor.x }, ${this.__backgroundColor.y}, ${this.__backgroundColor.z}, 1.);` 47 | 48 | return out 49 | }, 50 | 51 | update_location( gl, program ) { 52 | this.color.update_location( gl, program ) 53 | }, 54 | 55 | upload_data( gl ) { 56 | this.color.upload_data( gl ) 57 | } 58 | }) 59 | 60 | return Background 61 | } 62 | 63 | module.exports = BG 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "marching", 3 | "version": "1.2.0", 4 | "description": "Marching.js is a JavaScript library that compiles GLSL ray marchers.", 5 | "main": "js/index.js", 6 | "devDependencies": { 7 | "@bandaloo/merge-pass": "^0.6.4", 8 | "browserify": "^14.5.0", 9 | "codemirror": "^5.5.0", 10 | "get-pixels": "^3.3.2", 11 | "gl-mat4": "^1.2.0", 12 | "gl-texture2d": "^2.1.0", 13 | "gl-vec3": "^1.1.3", 14 | "glsl-diffuse-oren-nayar": "^1.0.2", 15 | "glsl-noise": "0.0.0", 16 | "glsl-sdf-primitives": "0.0.1", 17 | "glsl-specular-gaussian": "^1.0.0", 18 | "glsl-superformula": "^1.0.3", 19 | "glsl-voronoi-noise": "^1.2.2", 20 | "glsl-worley": "^1.0.2", 21 | "glslify": "^6.4.1", 22 | "gulp": "^4.0.2", 23 | "gulp-util": "^3.0.8", 24 | "mousetrap": "^1.6.1", 25 | "osc": "^2.4.4", 26 | "toastr": "^2.1.4", 27 | "tsify": "^4.0.2", 28 | "tweakpane": "^1.5.3", 29 | "typescript": "^3.9.6", 30 | "vinyl-source-stream": "^2.0.0", 31 | "vinyl-transform": "^1.0.0", 32 | "watchify": "^3.10.0" 33 | }, 34 | "scripts": { 35 | "test": "echo \"Error: no test specified\" && exit 1", 36 | "build": "npx gulp build", 37 | "watch": "npx gulp", 38 | "playground": "npx browserify playground/environment.js -o playground/playground.bundle.js" 39 | }, 40 | "author": "Charlie Roberts", 41 | "license": "MIT", 42 | "repository": { 43 | "type": "git", 44 | "url": "git+https://github.com/charlieroberts/marching.git" 45 | }, 46 | "keywords": [ 47 | "glsl", 48 | "ray", 49 | "marching", 50 | "graphics", 51 | "csg", 52 | "constructive", 53 | "solid", 54 | "geometry", 55 | "javascript", 56 | "webgl" 57 | ], 58 | "bugs": { 59 | "url": "https://github.com/charlieroberts/marching/issues" 60 | }, 61 | "browserify": { 62 | "transform": [ 63 | "glslify" 64 | ] 65 | }, 66 | "homepage": "https://github.com/charlieroberts/marching#readme" 67 | } 68 | -------------------------------------------------------------------------------- /js/osc.js: -------------------------------------------------------------------------------- 1 | const osc = require( 'osc/dist/osc-browser' ), 2 | { Vec2, Vec3, Vec4 } = require( './vec.js' ) 3 | 4 | function unpackArg( arg ) { 5 | if( Array.isArray( arg ) ) 6 | return Vec4( arg.map( a => a.value ) ) 7 | 8 | return arg.value 9 | } 10 | 11 | const OSC = { 12 | client: null, 13 | 14 | start( url ) { 15 | console.log("STARTING", url) 16 | OSC.client = new osc.WebSocketPort({ url, metadata: true, unpackSingleArgs: true }) 17 | OSC.client.on( 'message', OSC.messageCallback ) 18 | OSC.client.open() 19 | console.log("STARTING", OSC.client) 20 | }, 21 | 22 | stop() { 23 | OSC.client.close() 24 | }, 25 | 26 | messageCallback(message) { 27 | const parts = message.address.split('/') 28 | 29 | let object = window 30 | for( const part of parts.slice( 1, -1 ) ) { 31 | object = object[part] 32 | 33 | if( !object ) { 34 | throw new Error( `can't handle OSC message ${message.address}: no such key '${part}'` ) 35 | } 36 | } 37 | 38 | const key = parts[parts.length - 1] 39 | switch( key ) { 40 | /* methods */ 41 | case 'translate': 42 | case 'rotate': 43 | case 'scale': 44 | case 'fog': 45 | case 'material': 46 | object[key].apply( object, message.args.map( unpackArg ) ) 47 | break 48 | 49 | /* variables */ 50 | default: { 51 | const old = object[key] 52 | 53 | if( !old ) { 54 | throw new Error( `can't handle OSC message ${message.address}: no such key ${key}` ) 55 | } 56 | 57 | switch( old.type ) { 58 | case 'int': 59 | case 'float': 60 | object[key] = message.args.value 61 | break 62 | 63 | case 'vec2': 64 | case 'vec3': 65 | case 'vec4': 66 | object[key] = Vec4( message.args.map( a => a.value ) ) 67 | break 68 | 69 | default: 70 | throw new Error( `can't handle OSC message ${message.address}: type ${old.type}` ) 71 | } 72 | } 73 | } 74 | } 75 | } 76 | 77 | module.exports = OSC 78 | -------------------------------------------------------------------------------- /playground/demos/geometries.js: -------------------------------------------------------------------------------- 1 | module.exports = `v3 = Vec3, v2 = Vec2 2 | 3 | mat1 = Material( 'phong', .05, 1, .5 ) 4 | 5 | // Torus: Vec2 radius(outer,inner) 6 | torus = Torus( v2(.5,.1) ) 7 | .move( -2.25,1.5 ) 8 | .rotate( 90, 1,0,0 ) 9 | .material( mat1 ) 10 | 11 | // Torus82: Vec2 radius 12 | torus82 = Torus82() 13 | .move( -.75,1.5,0 ) 14 | .rotate( 90, 1,0,0 ) 15 | .material( mat1 ) 16 | 17 | // Torus88: Vec2 radius 18 | torus88 = Torus88() 19 | .move( .6,1.5,0 ) 20 | .rotate( 90, 1,0,0 ) 21 | .material( mat1 ) 22 | 23 | // Sphere: float radius 24 | sphere = Sphere(.65) 25 | .move( 2.25,1.5,0 ) 26 | .material( mat1 ) 27 | 28 | // Box: Vec3 size 29 | box = Box( .5 ) 30 | .move( -2,0,0 ) 31 | .material( mat1 ) 32 | 33 | // Cylinder: Vec2( radius, height ) 34 | cylinder = Cylinder( v2(.35,.5) ) 35 | .move( -.75,0,0 ) 36 | .material( mat1 ) 37 | 38 | // Cone: Vec3 dimensions 39 | cone = Cone( v3(.1, .075, .925) ) 40 | .move( .6,.45,0 ) 41 | .material( mat1 ) 42 | 43 | // Octahedron: float size 44 | octahedron = Octahedron( .65 ) 45 | .move(2.25,0,0) 46 | .material( mat1 ) 47 | 48 | // HexPrism: Vec2 size(radius, depth) 49 | hexPrism = HexPrism( v2(.6,.45) ) 50 | .move( -2,-1.5,0 ) 51 | .material( mat1 ) 52 | 53 | // TriPrism: Vec2 size(radius, depth) 54 | triPrism = TriPrism( v2(.85,.3) ) 55 | .move( -.5,-1.75,0 ) 56 | .material( mat1 ) 57 | 58 | // RoundBox: Vec3 size, roundness 59 | roundBox = RoundBox( v3(.45), .15 ) 60 | .move( 1.15,-1.5,0 ) 61 | .material( mat1 ) 62 | 63 | // Capsule: Vec3 start, Vec3 end, float radius 64 | capsule = Capsule( v3( 0, -.55, 0), v3(0,.4,0), .25 ) 65 | .move( 2.5,-1.5, 0 ) 66 | .material( mat1 ) 67 | 68 | mat = Material( 'phong', v3(0), v3(.1), v3(.25) ) 69 | // Plane: Vec3 normal, float distance 70 | plane = Plane( v3(0,0,1), 1).material( mat ) 71 | 72 | march( 73 | torus, torus82, torus88, sphere, 74 | box, cylinder, cone, capsule, 75 | octahedron, hexPrism, triPrism, roundBox, 76 | plane 77 | ) 78 | .light( 79 | Light( Vec3(2,0,5), Vec3(1), .2 ) 80 | ) 81 | .render() 82 | .camera( 0,0, 6 )` 83 | -------------------------------------------------------------------------------- /CREDITS.markdown: -------------------------------------------------------------------------------- 1 | # Credits 2 | 3 | [Chi Shen](http://shenchi.github.io) performed [initial development work on this project](https://github.com/shenchi/glsllab). 4 | 5 | The project uses code from [Stack.gl ecosystem](http://stack.gl/), specifically: 6 | - [glsl-raytrace](https://github.com/glslify/glsl-raytrace) by [Hugh Kennedy](https://github.com/hughsk). 7 | - [glsl-sdf-normal](https://github.com/glslify/glsl-sdf-normal) by [Hugh Kennedy](https://github.com/hughsk). 8 | - [glsl-camera-ray](https://github.com/glslify/glsl-camera-ray) by [Hugh Kennedy](https://github.com/hughsk). 9 | - many geometric primitives from [glsl-sdf-primitives](https://github.com/marklundin/glsl-sdf-primitives) by [Mark Lundin](https://github.com/marklundin). 10 | - Ambient occlusion and soft shadows are taken from the [glsl-sdf-ops package](https://github.com/marklundin/glsl-sdf-ops) by [Mark Lundin](https://github.com/marklundin). 11 | 12 | The project takes a variety of operators from the exceptional [hg_sdf library](http://mercury.sexy/hg_sdf/) by the [Mercury demogroup](http://mercury.sexy/). 13 | 14 | The defaut lighting scheme is adapted from [this demo](https://www.shadertoy.com/view/Xds3zN) by [Inigo Quilez](http://www.iquilezles.org). Many operators are also taken from his [excellent tutorial on SDFs](http://www.iquilezles.org/www/articles/distfunctions/distfunctions.htm). 15 | 16 | The Mandelbulb primitive is taken from [this demo](https://www.shadertoy.com/view/ltfSWn) by [Inigo Quilez](http://www.iquilezles.org). 17 | 18 | The Mandalay fractal is taken from [this demo](https://www.shadertoy.com/view/MdV3Wz) by Dave Hoskins. 19 | 20 | The Julia fractal primitive is taken from [this demo](https://www.shadertoy.com/view/MsfGRr) by [Inigo Quilez](http://www.iquilezles.org). 21 | 22 | The SuperFormula primitive is taken from [a repo](https://github.com/Softwave/glsl-superformula) by [JC Leyba](http://s0ftwave.com). 23 | 24 | A [matrix data structure by Evan Wallace](https://github.com/evanw/lightgl.js/). 25 | 26 | The Worley noise texture is taken from [a repo](https://github.com/Erkaman/glsl-worley) by [Eric Arnebäck](https://erkaman.github.io/). 27 | -------------------------------------------------------------------------------- /js/fog.js: -------------------------------------------------------------------------------- 1 | const SceneNode = require( './sceneNode.js' ), 2 | { param_wrap, MaterialID } = require( './utils.js' ), 3 | { Var, float_var_gen, vec2_var_gen, vec3_var_gen, vec4_var_gen, int_var_gen, VarAlloc } = require( './var.js' ) 4 | 5 | const { Vec2, Vec3, Vec4 } = require( './vec.js' ) 6 | 7 | const Fogger = function( Scene, SDF ) { 8 | 9 | const Fog = function( amount=0.055, color ) { 10 | const fog = Object.create( Fog.prototype ) 11 | const __amount = param_wrap( amount, float_var_gen( amount ) ) 12 | 13 | Object.defineProperty( fog, 'amount', { 14 | get() { return __amount }, 15 | set( v ) { 16 | __amount.set( v ) 17 | } 18 | }) 19 | 20 | const __color = param_wrap( Vec3(color), vec3_var_gen( 0,0,0 ) ) 21 | 22 | Object.defineProperty( fog, 'color', { 23 | get() { return __color }, 24 | set( v ) { 25 | __color.var.set( v ) 26 | } 27 | }) 28 | 29 | // this refers to the current scene via implicit binding in scene.js 30 | this.postprocessing.push( fog ) 31 | 32 | return this 33 | } 34 | 35 | Fog.prototype = SceneNode() 36 | 37 | Object.assign( Fog.prototype, { 38 | emit() { 39 | return ` color.rgb = applyFog( color.rgb, t.x, ${this.amount.emit()} );` 40 | }, 41 | 42 | emit_decl() { 43 | let str = this.amount.emit_decl() + this.color.emit_decl() 44 | const preface = ` vec3 applyFog( in vec3 rgb, in float distance, in float amount ) { 45 | float fogAmount = 1. - exp( -distance * amount ); 46 | vec3 fogColor = ${this.color.emit()}; 47 | return mix( rgb, fogColor, fogAmount ); 48 | } 49 | ` 50 | if( SDF.memo.fog === undefined ) { 51 | str = str + preface 52 | SDF.memo.fog = true 53 | }else{ 54 | str = '' 55 | } 56 | 57 | return str 58 | }, 59 | 60 | update_location( gl, program ) { 61 | this.amount.update_location( gl, program ) 62 | this.color.update_location( gl, program ) 63 | }, 64 | 65 | upload_data( gl ) { 66 | this.amount.upload_data( gl ) 67 | this.color.upload_data( gl ) 68 | } 69 | }) 70 | 71 | return Fog 72 | } 73 | 74 | module.exports = Fogger 75 | -------------------------------------------------------------------------------- /js/vignette.js: -------------------------------------------------------------------------------- 1 | const SceneNode = require( './sceneNode.js' ), 2 | { param_wrap, MaterialID } = require( './utils.js' ), 3 | { Var, float_var_gen, vec2_var_gen, vec3_var_gen, vec4_var_gen, int_var_gen, VarAlloc } = require( './var.js' ) 4 | 5 | const Vignette = function( Scene, SDF ) { 6 | 7 | const Vgn = function( radius=0.1, smoothness=16 ) { 8 | const vgn = Object.create( Vgn.prototype ) 9 | const __radius = param_wrap( radius, float_var_gen( radius ) ) 10 | 11 | Object.defineProperty( vgn, 'radius', { 12 | get() { return __radius }, 13 | set( v ) { 14 | __radius.set( v ) 15 | } 16 | }) 17 | 18 | const __smoothness = param_wrap( smoothness, float_var_gen( smoothness ) ) 19 | 20 | Object.defineProperty( vgn, 'smoothness', { 21 | get() { return __smoothness }, 22 | set( v ) { 23 | __smoothness.set( v ) 24 | } 25 | }) 26 | 27 | // this refers to the current scene via implicit binding in scene.js 28 | this.postprocessing.push( vgn ) 29 | 30 | return this 31 | } 32 | 33 | Vgn.prototype = SceneNode() 34 | 35 | Object.assign( Vgn.prototype, { 36 | emit() { 37 | return ` color *= vignette( uv, ${this.radius.emit()}, ${this.smoothness.emit()} );` 38 | }, 39 | 40 | emit_decl() { 41 | let str = this.radius.emit_decl() + this.smoothness.emit_decl() 42 | // taken from https://gist.github.com/r-lyeh-archived/170b53fcdc0e17afcf15 43 | // originally iq 44 | const preface = ` float vignette(vec2 uv, float radius, float smoothness) { 45 | return radius + 0.5*smoothness*uv.x*uv.y*(1.0-uv.x)*(1.0-uv.y); 46 | } 47 | ` 48 | if( SDF.memo.vgn === undefined ) { 49 | str = str + preface 50 | SDF.memo.vgn = true 51 | }else{ 52 | str = '' 53 | } 54 | 55 | return str 56 | }, 57 | 58 | update_location( gl, program ) { 59 | this.radius.update_location( gl, program ) 60 | this.smoothness.update_location( gl, program ) 61 | }, 62 | 63 | upload_data( gl ) { 64 | this.radius.upload_data( gl ) 65 | this.smoothness.upload_data( gl ) 66 | } 67 | }) 68 | 69 | return Vgn 70 | } 71 | 72 | module.exports = Vignette 73 | -------------------------------------------------------------------------------- /playground/demos/postprocessing.js: -------------------------------------------------------------------------------- 1 | module.exports = `/* __--__--__--__--__--__--__--____ 2 | 3 | Marching.js wraps the mergepass lib 4 | for performing post-processing: 5 | 6 | https://tinyurl.com/y4zxayey 7 | 8 | There's a bunch of presets that make 9 | fun effects. These effects are called 10 | "post" processing because they are 11 | applied to the marching.js scene after 12 | it has been rendered... think of them 13 | as filters in Photoshop. 14 | 15 | All post-proccessing effects are 16 | applied through the .post() method. 17 | 18 | ** __--__--__--__--__--__--__--__*/ 19 | 20 | // simple but fun---godrays 21 | 22 | march( 23 | i = Intersection( 24 | Box(1.5), 25 | Repeat( Sphere(.125), .5 ) 26 | ) 27 | ) 28 | .post( rays = Godrays() ) 29 | .render('med') 30 | 31 | onframe = t => i.rotate( t*10, 1,.5,.25 ) 32 | 33 | // try below one line at a time 34 | rays.color = [1,0,1] 35 | rays.decay = 1.01 36 | // change z position of rays to put 37 | // them in the box 38 | rays.threshold = .25 39 | 40 | 41 | // __--__--__--__--__--__--__--____ 42 | // ok, let's try and example based 43 | // on simulating a depth-of-field effect. 44 | // this will blur areas out of focus. 45 | 46 | march( 47 | Repeat( 48 | Box().texture('dots').scale(.25), 49 | 1.5 50 | ) 51 | ) 52 | .post( f = Focus() ) 53 | .fog( .15, Vec3(0) ) 54 | .render('med') 55 | 56 | onframe = t => camera.pos.z = t/3 57 | 58 | // change depth target 59 | f.depth = .15 60 | // change width of focus 61 | f.radius = .05 62 | 63 | 64 | // __--__--__--__--__--__--__--____ 65 | // last but not least, let's combine 66 | // a bunch of options. look in the reference 67 | // for more post-processing effects to layer! 68 | 69 | march( 70 | ri = RoundIntersection( 71 | s = Sphere(2).material('green').texture('dots', { scale:75 }), 72 | r = Repeat( 73 | Box().scale(.1).material('green').texture('dots', { scale:10 }), 74 | .5 75 | ) 76 | ) 77 | ) 78 | .post( 79 | bloom = Bloom(.25,8), 80 | g = Godrays(), 81 | Edge() 82 | ) 83 | .render('med') 84 | 85 | z = 0 86 | onframe = t => { 87 | z += abs( sin(t/2) * .02 ) 88 | r.translate( 0,0,z ) 89 | ri.rotate( t*15,1,.5,.25 ) 90 | } 91 | 92 | g.decay = 1.01` 93 | -------------------------------------------------------------------------------- /js/textureWrap.js: -------------------------------------------------------------------------------- 1 | module.exports = ` 2 | // p = point on surface, p0 = object center 3 | vec2 getUVCubic(vec3 p ){ 4 | vec3 absp = abs(p); 5 | 6 | // First conditional: If the point is in one of the sextants to the 7 | // left or right of the x-axis, the uv cordinate will be (0.5*p.zy)/(p.x). 8 | // If you trace a line out to a zy plane that is 0.5 units from the zero origin, 9 | // (0.5*p.xyz)/(p.x) will be the result, and 10 | // the yz components will be our uv coordinates, hence (0.5*p.zy)/(p.x). 11 | 12 | vec2 uv = ((absp.x>=absp.y)&&(absp.x>=absp.z)) 13 | ? (0.5*p.zy)/(p.x) 14 | : ((absp.y>=absp.z)&&(absp.y>=absp.x)) ? (0.5*p.xz)/(p.y) : (-0.5*p.xy)/(p.z); 15 | 16 | //We still need to determine which side our uv cordinates are on so 17 | //that the texture orients the right way. Note that there's some 18 | // redundancy there, which I'll fix at some stage. For now, it works, so I'm not touching it. :) 19 | if( ((p.x<0.)&&(absp.x>=absp.y)&&(absp.x>=absp.z)) 20 | || ((p.y<0.)&&(absp.y>=absp.z)&&(absp.y>=absp.x)) 21 | || ((p.z>0.)&&(absp.z>=absp.x)&&(absp.z>=absp.y)) ) uv.y*=-1.; 22 | 23 | // Mapping the uv range from [-0.5, 0.5] to [0.0, 1.0]. 24 | return (uv+0.5); 25 | } 26 | vec4 triplanar(vec3 n, vec4 texx, vec4 texy, vec4 texz, bool adjust3d, bool rescale) { 27 | //if (doflipz) n.z = -n.z; 28 | if (rescale) { 29 | texx = 2.0*texx - 1.0; 30 | texy = 2.0*texy - 1.0; 31 | texz = 2.0*texz - 1.0; 32 | } 33 | if (adjust3d) { 34 | texx.x *= sign(n.x); 35 | texy.y *= sign(n.y); 36 | texz.z *= sign(n.z); 37 | } 38 | //if (justtexy) return texy; 39 | vec3 weights = abs(n); 40 | //if (doweightcorrection) weights /= dot(weights,vec3(1)); // Keep spherical! 41 | return mat4(texx,texy,texz,vec4(0))*vec4(weights,0); 42 | } 43 | ` 44 | /* 45 | module.exports = `vec3 t3(sampler2D tex, vec3 p, vec3 n) 46 | { 47 | mat3 R = mat3(vec3(cos(T),sin(T),0),vec3(-sin(T),cos(T),0),vec3(0,0,-1)); 48 | p *= R/8.0; 49 | n *= R; 50 | #ifdef Smooth 51 | return (texture(tex,p.xy).rgb*n.z*n.z 52 | +texture(tex,p.zy).rgb*n.x*n.x 53 | +texture(tex,p.xz).rgb*n.y*n.y); 54 | #else 55 | return (texture(tex,p.xy).rgb 56 | +texture(tex,p.zy).rgb 57 | +texture(tex,p.xz).rgb)/3.0; 58 | #endif 59 | }` 60 | */ 61 | -------------------------------------------------------------------------------- /js/noise.js: -------------------------------------------------------------------------------- 1 | const glsl = require( 'glslify' ) 2 | const SceneNode = require( './sceneNode.js' ) 3 | const { param_wrap, MaterialID } = require( './utils.js' ) 4 | const { Var, float_var_gen, vec2_var_gen, vec3_var_gen, vec4_var_gen } = require( './var.js' ) 5 | 6 | const getNoise = function( SDF ) { 7 | Noise = function( strength=.25, bias=1, timeMod=1 ) { 8 | const op = Object.create( Noise.prototype ) 9 | op.type = 'string' 10 | op.isGen = true 11 | 12 | const defaultValues = [.5,.5,.5] 13 | 14 | op.matId = MaterialID.alloc() 15 | 16 | const __strength = param_wrap( strength, float_var_gen( strength ) ) 17 | const __timeMod = param_wrap( timeMod, float_var_gen( timeMod ) ) 18 | 19 | Object.defineProperty( op, 'strength', { 20 | get() { return __strength }, 21 | set(v) { 22 | __strength.var.set( v ) 23 | } 24 | }) 25 | Object.defineProperty( op, 'timeMod', { 26 | get() { return __timeMod }, 27 | set(v) { 28 | __timeMod.var.set( v ) 29 | } 30 | }) 31 | const __bias = param_wrap( bias, float_var_gen( bias ) ) 32 | 33 | Object.defineProperty( op, 'bias', { 34 | get() { return __bias}, 35 | set(v) { 36 | __bias.var.set( v ) 37 | } 38 | }) 39 | return op 40 | } 41 | 42 | Noise.prototype = SceneNode() 43 | 44 | Noise.prototype.emit = function ( __name ) { 45 | let name = __name === undefined ? 'p' : __name 46 | 47 | const out = `(${this.bias.emit()} + snoise( vec4( p.xyz, time * ${this.timeMod.emit()} )) * ${this.strength.emit()})` 48 | 49 | const output = { 50 | out, 51 | preface:'' 52 | } 53 | 54 | return output 55 | } 56 | Noise.prototype.glsl = glsl` #pragma glslify: noise = require('glsl-noise/simplex/4d')` 57 | 58 | Noise.prototype.emit_decl = function () { 59 | let str = this.strength.emit_decl() + this.timeMod.emit_decl() + this.bias.emit_decl() 60 | 61 | if( SDF.memo.noise === undefined ) { 62 | str = Noise.prototype.glsl + str 63 | SDF.memo.noise = true 64 | } 65 | 66 | return str 67 | }; 68 | 69 | Noise.prototype.update_location = function(gl, program) { 70 | this.strength.update_location( gl, program ) 71 | this.timeMod.update_location( gl, program ) 72 | this.bias.update_location( gl, program ) 73 | } 74 | 75 | Noise.prototype.upload_data = function(gl) { 76 | this.strength.upload_data( gl ) 77 | this.timeMod.upload_data( gl ) 78 | this.bias.upload_data( gl ) 79 | } 80 | 81 | return Noise 82 | 83 | } 84 | 85 | module.exports = getNoise 86 | -------------------------------------------------------------------------------- /playground/demos/constructors.js: -------------------------------------------------------------------------------- 1 | module.exports=`/* __--__--__--__--__--__--__--____ 2 | 3 | Note: this tutorial is only for people 4 | who know GLSL and want to incorporate 5 | their own shader code into marching.js 6 | 7 | If you know some GLSL, it's not too 8 | hard to add your own forms to 9 | marching.js. The process consists of: 10 | 11 | 1. Defining all the interactive 12 | properties of your form. 13 | 14 | 2. Defining a shader function that 15 | will create your form when the proper 16 | arguments are passed. This function 17 | will only be added to the shader once. 18 | 19 | 3. Defining a call to your shader 20 | function that passes the correct 21 | arguments; this function call will 22 | be inserted into the shader wherever 23 | an instance of your form is placed 24 | in the scene graph. 25 | 26 | ** __--__--__--__--__--__--__--__*/ 27 | 28 | spongeDesc = { 29 | // 'parameters' define our points of interaction. 30 | // types are float, int, vec2, vec3, and vec4 31 | parameters:[ 32 | { name:'frequency', type:'float', default:5 } 33 | ], 34 | 35 | // this is the primary signed distance function 36 | // used by your form. The first argument should 37 | // always be a point you're testing, subsequent 38 | // arguments are your parameters. 39 | glslify: 40 | \`float sineSponge( vec3 p, float frequency ) { 41 | p *= frequency; 42 | return (sin(p.x) + sin(p.y) + sin( p.z )) / 3. / frequency; 43 | }\`, 44 | 45 | // this is a function that will insert code calling 46 | // to your distance function wherever your form is 47 | // placed in a graph. It is passed the name of 48 | // the point (of type vec3) that is being sampled 49 | // by the ray marcher. Following passing 50 | // the point, you will pass each of the input parameters 51 | // to your signed distance function (in this case, only frequency) 52 | primitiveString( pName ) { 53 | return \`sineSponge( \${pName}, \${this.frequency.emit()} )\` 54 | } 55 | } 56 | 57 | // create and store resulting constructor in a global variable 58 | Sponge = Marching.primitives.create( 'Sponge', spongeDesc ) 59 | 60 | march( 61 | obj = RoundDifference( 62 | Sphere(2).material('glue'), 63 | s = Sponge( 5 ).material('glue'), 64 | .125 65 | ) 66 | ) 67 | .light( 68 | Light( Vec3(0,5,5), Vec3(1), .5 ) 69 | ) 70 | .background( Vec3(.125) ) 71 | .render(3,true).camera(0,0,5) 72 | 73 | onframe = t => { 74 | obj.rotate( t*10 ) 75 | s.frequency = 10 + sin(t/2) * 5 76 | }` 77 | -------------------------------------------------------------------------------- /playground/demos/portal2.js: -------------------------------------------------------------------------------- 1 | module.exports = `mat1 = Material( 'phong', Vec3(.5), Vec3(1), Vec3(3), 64, Vec3( 0,1,4 ) ) 2 | mat2 = Material( 'phong', Vec3(.05), Vec3(1), Vec3(3), 64, Vec3( 2,4,8 ) ) 3 | mat3 = Material( 'phong', Vec3(.5), Vec3(1), Vec3(0), 16, Vec3( 0,1,1 ) ) 4 | mat4 = Material( 'phong', Vec3(.05), Vec3(1,0,0), Vec3(1), 16, Vec3( 4,2,5 ) ) 5 | mat5 = Material( 'phong', Vec3(.05), Vec3(.5,.5,.5), Vec3(.5,.5,.5), 32, Vec3( 0,2,.25 ) ) 6 | mat6 = Material( 'phong', Vec3(.05), Vec3(.5), Vec3(1), 64, Vec3( 0,4,8 ) ) 7 | mat7 = Material( 'orenn', Vec3(.05), Vec3(1), Vec3(1), 12, Vec3( 0,0,1 ) ) 8 | 9 | lights = [ 10 | Light( Vec3(0,2,0), Vec3(1,.35,.35), 1 ), 11 | Light( Vec3(4,0,-12), Vec3(1), .25 ), 12 | Light( Vec3(-4,0,-12), Vec3(1), .25 ), 13 | Light( Vec3(0,-2,0), Vec3(1,.35,.35), 2 ) 14 | ] 15 | 16 | twopi = Math.PI * 2 17 | count = 25 18 | radius = 6 19 | for( let i = 0; i < count; i++ ) { 20 | const percent = i / count 21 | lights.push( Light( 22 | Vec3( Math.sin( percent * twopi ) * radius, 2, Math.cos( percent * twopi ) * radius ), 23 | Vec3(1,1,.75), 24 | 2. 25 | )) 26 | } 27 | 28 | lightSpheres = PolarRepeat( Sphere(.2 ).material( mat5 ), 25, radius ).translate( 4,0,0 ) 29 | 30 | column = Difference( 31 | Cylinder( Vec2(.1,2.5) ).material( mat1 ), 32 | Repeat( 33 | Box( Vec3(.1) ).material( mat6 ), 34 | Vec3(0,1,0) 35 | ), 36 | .1 37 | ) 38 | columns = PolarRepeat( column, 12, .85 ) 39 | 40 | candelabra = Union2( 41 | StairsIntersection( 42 | PolarRepeat( 43 | PolarRepeat( 44 | t = Torus88( Vec2(.75,.1) ).material( mat1 ), 45 | 15, 46 | 3.75 47 | ).rotate( 180,1,0,0 ), 48 | count, 49 | 2 50 | ), 51 | Plane( Vec3(0,-.25,0) ).material( mat1 ), 52 | .1 53 | ), 54 | lightSpheres, 55 | Sphere( .25, Vec3(0,3.5,0) ).material( mat5 ), 56 | Sphere( .25, Vec3(0,-1,0) ).material( mat5 ) 57 | ).translate( 0,-1,0) 58 | 59 | roomRadius = 8.5 60 | m = march( 61 | Union2( 62 | RoundUnion( 63 | candelabra, 64 | Plane( Vec3( 0,-1,0 ), 2.5 ).material( mat3 ), 65 | .5 66 | ), 67 | columns, 68 | 69 | Difference( 70 | Onion( 71 | Cylinder( Vec2( roomRadius, 2.5 ), Vec3(0,.45,0) ).material( mat7 ), 72 | .175 73 | ), 74 | PolarRepeat( Box( Vec3(.7,1.5,.125) ).material( mat5 ), 40, roomRadius+.5 ) 75 | ) 76 | ) 77 | ) 78 | .fog( .125, Vec3(0) ) 79 | .light( ...lights ) 80 | .render() 81 | .camera( 0, 0, roomRadius-.5 )` 82 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Marching.js 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 43 | 44 | 45 | 46 | 47 |
48 |
49 |

Marching.js

50 | 55 |
56 |
57 | 58 | 59 |
60 |
61 | 62 |
63 | 66 |
67 |
68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /playground/demos/csg.js: -------------------------------------------------------------------------------- 1 | module.exports = `/* __--__--__--__--__--__--__--__-- 2 | 3 | "constructive solid geometry (CSG)" 4 | is the name given to techniques for 5 | combining various geometries in 6 | different ways. in this tutorial, 7 | we'll re-create the example shown 8 | on the wikipedia page for CSG: 9 | 10 | https://bit.ly/2Fs2GV6 11 | 12 | our first step will be to create 13 | a rounded box, by taking the 14 | intersection of a box and a sphere. 15 | we'll go ahead and render it to see 16 | what it looks like. 17 | 18 | ** __--__--__--__--__--__--__--__*/ 19 | 20 | roundedSphere = Intersection( 21 | Box( .775 ).material( 'red' ), 22 | Sphere( 1 ).material( 'blue' ) 23 | ) 24 | 25 | march( roundedSphere ).render() 26 | 27 | /* __--__--__--__--__--__--__--__-- 28 | 29 | it's a little tricky to get a feel 30 | for it viewing it straight on, so 31 | let's rotate it along two axes. 32 | 33 | ** __--__--__--__--__--__--__--__*/ 34 | 35 | roundedSphere = Intersection( 36 | Box( .775 ).material( 'red' ), 37 | Sphere( 1 ).material( 'blue' ) 38 | ) 39 | 40 | // rotate() takes an angle followed by 41 | // x,y, and z axis for rotation 42 | roundedSphere.rotate( 45, 1,1,0 ) 43 | 44 | march( roundedSphere ).render() 45 | 46 | /* __--__--__--__--__--__--__--__-- 47 | 48 | great. next we want to make a cross 49 | that we'll subtract from our rounded 50 | sphere. We can do this by combining 51 | three cylinders. We'll rotate one 52 | on the z-axis and one on the x-axis. 53 | The Union2 operator is a shortcut 54 | to combine as many objects as we 55 | want (regular Union only lets us 56 | combine two). 57 | 58 | ** __--__--__--__--__--__--__--__*/ 59 | 60 | crossRadius = .5 61 | crossHeight = 1 62 | dimensions = Vec2(crossRadius, crossHeight ) 63 | 64 | cross = Union2( 65 | Cylinder( dimensions ).material( 'green' ), 66 | Cylinder( dimensions ) 67 | .material( 'green' ) 68 | .rotate( 270, 0,0,1 ), 69 | Cylinder( dimensions ) 70 | .material( 'green' ) 71 | .rotate( 270, 1,0,0 ) 72 | ) 73 | 74 | march( cross ).render() 75 | 76 | /* __--__--__--__--__--__--__--__-- 77 | 78 | OK, now we put it all together by 79 | subtracting the cross from our 80 | rounded sphere. we will animate the 81 | rotation of ourfinal geometry to 82 | get a good view from a bunch of 83 | angles. 84 | 85 | ** __--__--__--__--__--__--__--__*/ 86 | 87 | roundedSphere = Intersection( 88 | Box( .775 ).material( 'red' ), 89 | Sphere( 1 ).material( 'blue' ) 90 | ) 91 | 92 | crossRadius = .5 93 | crossHeight = 1 94 | dimensions = Vec2(crossRadius, crossHeight ) 95 | 96 | cross = Union2( 97 | Cylinder( dimensions ).material( 'green' ), 98 | Cylinder( dimensions ) 99 | .material( 'green' ) 100 | .rotate( 270, 0,0,1 ), 101 | Cylinder( dimensions ) 102 | .material( 'green' ) 103 | .rotate( 270, 1,0,0 ) 104 | ) 105 | 106 | march( 107 | obj = Difference( 108 | roundedSphere, 109 | cross 110 | ).rotate( 45, 1,.5,0 ) 111 | ) 112 | .render( 3, true ) 113 | 114 | callbacks.push( t => obj.rotate( t * 25 ) )` 115 | -------------------------------------------------------------------------------- /playground/demos/procedural_textures.js: -------------------------------------------------------------------------------- 1 | module.exports = `/* Marching.js lets you define your own 2 | procedural textures; this is a very 3 | similar process to defining your own 4 | GLSL geometries, covered in another 5 | tutorial. You give your texture a name, 6 | define a set of parameters you would 7 | like to expose for control, and then 8 | write a snippet of GLSL that generates 9 | a color based on the current pixel position 10 | and the values of the various parameters 11 | you defined. 12 | 13 | Below is the 'dots' texture included 14 | in Marching.js in use; we'll be 15 | extending it to enable some different 16 | control parameters to get a feel for the 17 | process of defining a texture.*/ 18 | 19 | march( s = Sphere(1.5).texture('dots') ) 20 | .fog( .15, Vec3(0)) 21 | .render( 3, true ) 22 | 23 | onframe = t => s.rotate(t*10, 1,0,0 ) 24 | 25 | /* Below is the code for 'dots', renamed 26 | to 'dots2', and put into action. One 27 | important aspect to notice is that the 28 | 'name' property of the definition must 29 | match the name of the GLSL function that 30 | you define.*/ 31 | 32 | def = { 33 | // define the name for our texture 34 | name:'dots2', 35 | // define parameters for interaction 36 | parameters: [ 37 | { name:'scale', type:'float', default:5 }, 38 | { name:'color', type:'vec3', default:[1,1,1] } 39 | ], 40 | // define our GLSL function, which must output a 41 | // RGB color in a vec3, and must be named the same 42 | // as our definition's 'name' property. 43 | glsl:\` 44 | vec3 dots2( vec3 pos, float count, vec3 color ) { 45 | vec3 tex; 46 | tex = vec3( color - smoothstep(0.3, 0.32, length(fract(pos*(round(count/2.)+.5)) -.5 )) ); 47 | return tex; 48 | }\` 49 | } 50 | 51 | // To create a function, call Texture.create 52 | // and pass a defintion. 53 | 54 | Texture.create( def ) 55 | 56 | // use it 57 | march( s = Sphere(1.5).texture('dots2') ) 58 | .fog( .15, Vec3(0)) 59 | .render( 3, true ) 60 | 61 | // That should look the same as the original 62 | // texture in this tutorial. Let's add a couple 63 | // new parameters: the first will control the 64 | // base radius of our circles, while the second 65 | // will control the softness of the circle edges. 66 | // Larger values for softness will generate 67 | // larger circles with soft edges. Both these 68 | // parameters will be used in the call to 69 | // smoothstep in our GLSL code. 70 | 71 | def = { 72 | name:'dots2', 73 | parameters: [ 74 | { name:'scale', type:'float', default:10 }, 75 | { name:'radius', type:'float', default:.35 }, 76 | { name:'spread', type:'float', default:.02 }, 77 | { name:'color', type:'vec3', default:[1,1,1] } 78 | ], 79 | glsl:\`vec3 dots2( vec3 pos, float scale, float radius, float spread, vec3 color ) { 80 | vec3 tex; 81 | tex = vec3( color - smoothstep(radius, radius+spread, length(fract(pos*(round(scale/2.)+.5)) -.5 )) ); 82 | return tex; 83 | }\` , 84 | } 85 | 86 | Texture.create( def ) 87 | 88 | march( s = Sphere(1.5).texture('dots2', { radius:.05 }) ).render(5, true) 89 | 90 | // animate our parameters 91 | onframe = t => { 92 | s.rotate(t*10,1,0,0) 93 | s.texture.color.x = sin(t) 94 | s.texture.color.y = cos(t/3) 95 | s.texture.spread = t % .5 96 | s.texture.scale = t % 10 97 | }` 98 | -------------------------------------------------------------------------------- /playground/demos/hydra.js: -------------------------------------------------------------------------------- 1 | module.exports = `/* __--__--__--__--__--__--__--__-- 2 | 3 | It's **highly** recommended that you 4 | complete the "texturing" tutorial 5 | before beginning this one. 6 | 7 | Marching.js can use Hydra as a 8 | texture generator for its 3D forms. 9 | Once you've created a texture using 10 | Hydra, you can live code with it 11 | in a similar fashion to any other 12 | Hydra sketch. 13 | 14 | If you've never tried Hydra before, 15 | go here for more info + tons of fun: 16 | http://hydra.ojack.xyz 17 | 18 | ** __--__--__--__--__--__--__--__*/ 19 | 20 | // the first step is to load Hydra, 21 | // which isn't included in marching.js 22 | // by default. The use() function lets 23 | // us do this. Run the code below by 24 | // hitting ctrl+enter 25 | use('hydra') 26 | 27 | // you should see a notification appear 28 | // at the bottom of your screen when 29 | // hydra is ready to go. Use the Hydra() 30 | // constructor to make a new hydra object. 31 | // When you make calls to hydra functions 32 | // they will draw to a canvas stored in 33 | // this hydra object. 34 | hydra = Hydra() 35 | osc(50, .05, 10).modulate( noise(15) ).out() 36 | 37 | // ok, next we need to texture a marching.js 38 | // object with the hydra object we created. 39 | 40 | march( 41 | box = Box(1.5).texture( hydra.texture() ) 42 | ).render('med') 43 | 44 | onframe = t => box.rotate(t*5,1,1,1) 45 | 46 | // note that you can easily live code 47 | // the texture by simply re-running the 48 | // osc().modulate().out() line after 49 | // making changes. or here's a different 50 | // idea from Hydra creator @_ojack_ (slightly modded). 51 | // you can run the whole block by placing 52 | // your cursor in it and hitting alt+enter 53 | 54 | pattern = () => osc(150, 0).kaleid(200).scale(1, 0.4) 55 | pattern() 56 | .scrollX(0.5, 0.05) 57 | .mult(pattern()) 58 | .out() 59 | 60 | // ok, let's use this in a more complex scene. Again, 61 | // you can execute the 62 | hydra = Hydra() 63 | pattern = () => osc(50, 0).kaleid(200).scale(1, 0.4) 64 | pattern() 65 | .scrollX(0.5, 0.05) 66 | .mult(pattern()) 67 | .out() 68 | 69 | march( 70 | rpt = Repeat( 71 | box = Box(.35).texture( hydra.texture() ), 72 | 2 73 | ) 74 | ) 75 | .fog(.1,Vec3(0)) 76 | .render('med') 77 | 78 | onframe = t => { 79 | box.rotate(t*5,1,1,1) 80 | rpt.distance = 1.5+ sin(t)*.35 81 | } 82 | 83 | // from the texture tutorial, we know 84 | // we can scale the texture 85 | 86 | box.tex.scale = 3 87 | 88 | // or... 89 | 90 | box.tex.scale = .5 91 | 92 | // and we can also change the strength 93 | // of the texture, which determines 94 | // how much it overrides the default 95 | // lighting of the material 96 | box.tex.strength = .25 97 | 98 | /* finally, fractals + hydra === _ _ 99 | ( v ) 100 | \\ / 101 | v 102 | */ 103 | 104 | hydra = Hydra() 105 | osc(50, .05, .5).modulate( noise(15) ).out() 106 | 107 | march( 108 | m = Mandelbox(1.25,1.1,3).texture( hydra.texture() ).scale(.5) 109 | ).render('fractal.med') 110 | 111 | m.tex.strength = .5 112 | m.tex.wrap = Texture.mirror 113 | m.tex.scale = 4 114 | 115 | onframe = t => { 116 | m.rotate(t*15,1,1,1) 117 | m.size = 1 + sin(t/2) * .125 118 | }` -------------------------------------------------------------------------------- /playground/demos/gifs_and_links.js: -------------------------------------------------------------------------------- 1 | module.exports=`/* __--__--__--__--__--__--__--____ 2 | 3 | Marching.js lets you both download 4 | animated gifs and generate links. 5 | Together, you can post a short clip 6 | on social media alongside a link 7 | that enables people to easily 8 | experiment with your code. 9 | 10 | Links can be generated at any time 11 | but creating gifs requires a tiny 12 | bit of extra wok. Both are described 13 | below. 14 | 15 | If you remember, please use 16 | #marchingjs when you post online! 17 | 18 | ** __--__--__--__--__--__--__--__*/ 19 | 20 | // First let's generate a link to this code. 21 | // delete the long block comment above, then 22 | // run the line below by highlighting it and 23 | // hitting control+enter 24 | getlink('text for link blah') 25 | 26 | // You'll notice there's a short link, for 27 | // copying and pasting into an email, pdf 28 | // or other document that suppots rich text. 29 | // Most browsers will also let you copy the 30 | // generated URL if you control-click on the 31 | // link. As shown above, you can specify the 32 | // text you'd like to use for your link. Also, 33 | // if your call to getlink() is on the last 34 | // line of your sketch, it won't appear in the 35 | // geneated link. So, you should probably always 36 | // run it at the bottom of your sketch. Finally, 37 | // the code in the link will automatically run 38 | // in the playground. 39 | 40 | // IMPORTANT: There's a maximum length in most 41 | // browsers for URLs. Make sure your link works 42 | // the way you expect it to before posting it! 43 | 44 | // OK, now let's make an animated gif. First, 45 | // we have to load an additional library. Run 46 | // the line of code below with control+enter: 47 | 48 | use('gif') 49 | 50 | // You'll see a notification when the library 51 | // has loaded. Now we can add a call to .gif, 52 | // and pass in: 53 | // width - default 600 54 | // height - default 335 55 | // length (in frames) - default 60 56 | // quality (1-10) - default 5 57 | // interframe delay (in ms) - default 17 (for 60 fps) 58 | // filename - default 'marching.gif' 59 | 60 | // Here's an example of what should be a "perfect" 61 | // 400x400 loop for one second (make sure you 62 | // ran use('gif') before trying this): 63 | 64 | march( 65 | j = Julia().scale(1.5) 66 | ) 67 | .gif( 400,400,59 ) 68 | .render( 'fractal.high' ) 69 | 70 | let fc = 0 71 | onframe = time => { 72 | j.fold = 3.5 + sin(fc) 73 | fc += (Math.PI*2)/60 74 | } 75 | 76 | // last but not least, when you make a call 77 | // to .use(), it will take a bit to download the 78 | // requested libraries. You can just wait for 79 | // a notification to appear that the library has 80 | // been loaded before running additional code, 81 | // but if you'd like to publish a link that runs 82 | // everyhing automatically this becomes problematic. 83 | // use() returns a promise that will resolve when 84 | // the library is loaded, so you can delay execution 85 | // as follows: 86 | 87 | use('hydra').then( ()=> { 88 | h = Hydra() 89 | osc( 50,.15,.25 ).modulate( noise(15) ).out() 90 | 91 | march( 92 | m = Box().texture( h.texture() ) 93 | ) 94 | .render('med') 95 | .camera(0,0,4) 96 | 97 | onframe = t => m.rotate(t*5, .5,1,.5 ) 98 | 99 | h.texture.strength = .5 100 | })` -------------------------------------------------------------------------------- /playground/demos/p5.js: -------------------------------------------------------------------------------- 1 | module.exports=`/* __--__--__--__--__--__--__--__-- 2 | 3 | It's **highly** recommended that you 4 | complete the "texturing" tutorial 5 | before beginning this one. 6 | 7 | Marching.js can use p5.js as a 8 | texture generator for its 3D forms. 9 | Once you've created a texture using 10 | p5, you can use live code what is 11 | drawn to it by changing the onframe() 12 | method, which is equivalent to draw() 13 | in p5.js. 14 | 15 | Never played with p5.js before? 16 | https://p5js.org/ 17 | 18 | ** __--__--__--__--__--__--__--__*/ 19 | 20 | // the first step is to load p5, 21 | // which isn't included in marching.js 22 | // by default. The use() function lets 23 | // us do this. Run the code below by 24 | // hitting ctrl+enter 25 | use('p5') 26 | 27 | // you should see a notification appear 28 | // at the bottom of your screen when 29 | // p5 is ready to go. Use the P5() 30 | // constructor to make a new p5 object. 31 | // All the p5 drawing functions will then 32 | // be found in ths object, we'll use them 33 | // to create a red/white texture; 34 | pfive = P5() 35 | pfive.background(255,255,255) 36 | pfive.fill(255,0,0) 37 | pfive.rect(0,0,pfive.width/2,pfive.height) 38 | 39 | // ok, next we need to texture a marching.js 40 | // object with the p5 object we created. 41 | 42 | march( 43 | box = Box(1.5).texture( pfive.texture() ) 44 | ).render('med') 45 | 46 | onframe = t => box.rotate(t*5,1,1,1) 47 | 48 | // we can make this more fun by animating 49 | // inside of our onframe() method: 50 | 51 | onframe = t => { 52 | pfive.background(255,255,255) 53 | pfive.rect(0,0,(.5+sin(t)*.5)*pfive.width,pfive.height) 54 | box.rotate(t*5,1,1,1) 55 | } 56 | 57 | // scale the texture 58 | box.tex.scale = 4 59 | 60 | // we can load external p5 libraries from 61 | // content delivery networks like jsdelivr. Let's 62 | // load p5.Polar by Liz Peng (https://github.com/liz-peng/p5.Polar) 63 | // which is a fun library for creating circular patterns 64 | // using polar coordinates. 65 | 66 | use( 'https://cdn.jsdelivr.net/gh/liz-peng/p5.Polar/p5.Polar.js' ) 67 | 68 | // you should see a notification appear at the bottom 69 | // once the library has loaded. We can now run some drawing 70 | // code... doing it outside of onframe() would be roughly 71 | // the same as doing it in setup() in a normal p5 sketch: 72 | pfive = P5(600,600) 73 | pfive.setCenter( pfive.width/2, pfive.height/2) 74 | pfive.stroke( 255,255,255,64 ) 75 | pfive.colorMode( pfive.HSB, 255 ) 76 | 77 | // we'll create a more complex scene below. The use of 78 | // the 'blackhole' material causes the spheres to ignore 79 | // much of the lighting information in the scene in favor 80 | // of the p5 texture. 81 | march( 82 | rpt = Repeat( 83 | box = Sphere(1).material('blackhole').texture( pfive.texture() ), 84 | 3 85 | ) 86 | ) 87 | .fog(.1,Vec3(0)) 88 | .render('repeat.med') 89 | 90 | onframe = t => { 91 | box.rotate(t*5,1,1,1) 92 | pfive.background(0) 93 | for( let i = 0; i < 10; i++ ) { 94 | pfive.fill( pfive.color( i * 25, i * 25, 255, 32 ) ) 95 | pfive.polarEllipses( 96 | i*6, 80 - i*6.5, 80-i*6.5, 30 * i * (.85 +(sin(t) * .15)), 97 | (...args) => { 98 | args[1] = (t*2)/(i+1) 99 | return args 100 | } 101 | ) 102 | } 103 | } 104 | 105 | // we can also change the strength 106 | // of the texture, which determines 107 | // how much it overrides the default 108 | // lighting of the material 109 | box.tex.strength = .5` -------------------------------------------------------------------------------- /js/audio.js: -------------------------------------------------------------------------------- 1 | const Audio = { 2 | __hasInput: false, 3 | ctx: null, 4 | bins:null, 5 | 6 | start( bins=null ) { 7 | if( Audio.__hasInput === false ) { 8 | Audio.ctx = new AudioContext() 9 | Audio.createInput().then( input => { 10 | if( bins !== null ) Audio.bins = bins 11 | Audio.createFFT() 12 | input.connect( Audio.FFT ) 13 | 14 | Audio.interval = setInterval( Audio.fftCallback, 1000/60 ) 15 | //window.FFT = Audio.FFT 16 | }) 17 | Audio.__hasInput = true 18 | }else{ 19 | if( bins !== null ) Audio.bins = bins 20 | } 21 | }, 22 | 23 | createInput() { 24 | console.log( 'connecting audio input...' ) 25 | 26 | const p = new Promise( resolve => { 27 | navigator.mediaDevices.getUserMedia({ audio:true, video:false }) 28 | .then( stream => { 29 | console.log( 'audio input connected' ) 30 | Audio.input = Audio.ctx.createMediaStreamSource( stream ) 31 | //Audio.mediaStreamSource.connect( Gibberish.node ) 32 | Audio.__hasInput = true 33 | resolve( Audio.input ) 34 | }) 35 | .catch( err => { 36 | console.log( 'error opening audio input:', err ) 37 | }) 38 | }) 39 | return p 40 | }, 41 | 42 | createFFT() { 43 | Audio.FFT = Audio.ctx.createAnalyser() 44 | 45 | let __windowSize = 512 46 | Object.defineProperty( Audio, 'windowSize', { 47 | configurable:true, 48 | get() { return __windowSize }, 49 | set(v){ 50 | __windowSize = v 51 | Audio.FFT.fftSize = v 52 | Audio.FFT.values = new Uint8Array( Audio.FFT.frequencyBinCount ) 53 | } 54 | }) 55 | 56 | Audio.windowSize = 512 57 | }, 58 | 59 | fftCallback() { 60 | Audio.FFT.getByteFrequencyData( Audio.FFT.values ) 61 | 62 | let lowSum, midSum, highSum, lowCount, midCount, highCount 63 | lowSum = midSum = highSum = lowCount = midCount = highCount = 0 64 | 65 | let frequencyCounter = 0 66 | 67 | // does this start at 0Hz? ack... can't remember... does it include DC offset? 68 | const hzPerBin = (Audio.ctx.sampleRate / 2) / Audio.FFT.frequencyBinCount 69 | const lowRange = 180, midRange = 1400, highRange = Audio.ctx.sampleRate / 2 70 | 71 | if( Audio.bins === null ) { 72 | for( let i = 1; i < Audio.FFT.frequencyBinCount; i++ ) { 73 | if( frequencyCounter < lowRange ) { 74 | lowSum += Audio.FFT.values[ i ] 75 | lowCount++ 76 | }else if( frequencyCounter < midRange ) { 77 | midSum += Audio.FFT.values[ i ] 78 | midCount++ 79 | }else{ 80 | highSum += Audio.FFT.values[ i ] 81 | highCount++ 82 | } 83 | 84 | frequencyCounter += hzPerBin 85 | } 86 | 87 | Audio.low = (lowSum / lowCount) / 255 88 | Audio.mid = (midSum / midCount) / 255 || 0 89 | Audio.high = (highSum / highCount) / 255 90 | }else{ 91 | const sums = {} 92 | for( let bin = 0; bin < Audio.bins.length; bin++ ) { 93 | const frequency = Audio.bins[ bin ] 94 | 95 | sums[ bin ] = { count: 0, value: 0 } 96 | 97 | for( let i = 1; i < Audio.FFT.frequencyBinCount; i++ ) { 98 | if( frequencyCounter < frequency ) { 99 | sums[ bin ].value += Audio.FFT.values[i] 100 | sums[ bin ].count++ 101 | frequencyCounter += hzPerBin 102 | }else{ 103 | break 104 | } 105 | } 106 | Audio[ bin ] = (sums[ bin ].value / sums[ bin ].count) / 255 107 | } 108 | } 109 | } 110 | } 111 | 112 | module.exports = Audio 113 | -------------------------------------------------------------------------------- /playground/demos/textures_tutorial.js: -------------------------------------------------------------------------------- 1 | module.exports=`/* __--__--__--__--__--__--__--__-- 2 | 3 | Marching.js provides a variety of 4 | ways to create procedural textures. 5 | In order of increasing complexity, 6 | they are: 7 | 8 | 1. Using texture presets 9 | 2. Using HTML objects 10 | 3. Using Hydra, another live coding 11 | system for 2D visual chaos 12 | 4. Writing your own shaders 13 | 14 | #4 is covered in the "defining 15 | procedural textures" tutorial, we'll 16 | cover thhe rest here. 17 | 18 | ** __--__--__--__--__--__--__--__*/ 19 | 20 | // marching.js comes with a number 21 | // of texture presets that share 22 | // some properties; you can see most 23 | // of them in theh "textures catalog" 24 | // demo. Let's look at "truchet" to get 25 | // a sense for how these presets work. 26 | 27 | tex = Texture( 'truchet' ) 28 | march( Box().texture( tex ) ).render() 29 | 30 | // calling Texture() makes a new texture 31 | // object that can be passed to the 32 | // .texture() method of any geometry or 33 | // combinator. If you call .texture() 34 | // on a combinator, the texture will be 35 | // applied to all it's members. 36 | 37 | tex = Texture( 'checkers' ) 38 | march( 39 | StairsUnion( 40 | Box(), 41 | Box( Vec3(.5,2,.5) ) 42 | ) 43 | .texture( tex ) 44 | .rotate(45,1,1,1) 45 | ).render() 46 | 47 | // while some properties are unique 48 | // to specific textures, others can 49 | // be found on all of them: scale, 50 | // strength, and uv. Examples of each 51 | // are shown below. Here we'll call 52 | // the .texture() method and pass in 53 | // names of presets instead of passing 54 | // in a texture object. When you do this, 55 | // you can also pass a dictionary of property 56 | // setters. 57 | 58 | // the scale property scales texture coordinates 59 | march( 60 | Sphere().translate(1,0,0).texture( 'checkers', { scale:5 }), 61 | Sphere().translate(-1,0,0).texture( 'checkers', { scale:15 }) 62 | ).render() 63 | 64 | // you can also provide coordinate offset using 65 | // uv property. When you want to refer to the texture 66 | // of an object, you can do so with the following form: 67 | // 68 | // objvariable.texture.propertyname = value 69 | 70 | march( 71 | s1 = Sphere().translate(1,0,0).texture( 'checkers', { scale:5 }), 72 | s2= Sphere().translate(-1,0,0).texture( 'checkers', { scale:15 }) 73 | ).render( 'med' ) 74 | 75 | onframe = t => { 76 | s1.texture.uv.x = t/5 77 | s2.texture.uv.y = t/5 78 | } 79 | 80 | // last but not least, many textures (but possiibly not all) 81 | // have a "strength" property that blends the texture with 82 | // the non-textured lighting. 83 | 84 | march( 85 | Box(.5).translate(-1.1,0,0).texture( 'rainbow', { strength:0, scale:10 }), 86 | Box(.5).translate(0,0,0).texture( 'rainbow', { strength:.25, scale:10 }), 87 | Box(.5).translate(1.1,0,0).texture( 'rainbow', { strength:1, scale:10 }) 88 | ).render() 89 | 90 | // there's one additional preset, 'feedback', that is a special case. 91 | // it creates a texture of the entire scene, that you can then 92 | // use to texture individual objects. Try running this one twice if you 93 | // don't get full feedback... there's a bug I need to fix in there. 94 | 95 | t = Texture( 'feedback' ) 96 | 97 | march( 98 | RoundUnion( 99 | b = Box(1).texture( t ), 100 | c = Sphere().texture('dots').translate(2,0,0) 101 | ), 102 | Plane( Vec3(0,0,1) ).texture('stripes') 103 | ) 104 | .render('low') 105 | .camera(0,0,3.5) 106 | 107 | onframe = time => { 108 | t.update() 109 | b.rotate(time*5, 1,.5,.25 ) 110 | c.rotate(time*4, .5,1,.25 ) 111 | }` 112 | -------------------------------------------------------------------------------- /playground/demos/audio.js: -------------------------------------------------------------------------------- 1 | module.exports = ` 2 | /* __--__--__--__--__--__--__--____ 3 | Audio-Reactive Visuals 4 | 5 | marching.js will perform an FFT analysis 6 | of any sound/music fed to the browser. When 7 | you first start the FFT, you'll be asked to 8 | choose an audio device to listen to. You can 9 | later change this in Chrome by clicking on 10 | the camera icon in the browser window's location 11 | bar. 12 | 13 | By using software like SoundFlower or JACK you 14 | can virtually route audio from your favorite music 15 | software into marching.js... or you can simply use a 16 | microphone / standard audio input. 17 | __--__--__--__--__--__--__--____ */ 18 | 19 | // create a scene to play with 20 | march( 21 | si = StairsIntersection( 22 | Sphere(2).material( 'white' ), 23 | repeat = Repeat( 24 | sphere = Sphere(.125), 25 | Vec3(.5) 26 | ), 27 | .125 28 | ) 29 | ).render( 4, true ) 30 | 31 | // start our FFT 32 | FFT.start() 33 | 34 | // animate 35 | onframe = time => { 36 | si.rotate( time * 15 ) 37 | 38 | // our FFT object has low,mid, and high 39 | // properties that we can assign to elements 40 | // of our ray marching scene 41 | repeat.distance.x = FFT.low 42 | repeat.distance.y = FFT.mid 43 | repeat.distance.z = FFT.high 44 | sphere.radius = FFT.mid * FFT.high 45 | } 46 | 47 | si.d = 4 48 | si.c = .5 49 | 50 | /* __--__--__--__--__--__--__--____ 51 | increasing the window size (how many samples 52 | of audio the FFT looks at) will result in 53 | less hectic animations. The window size must 54 | be a power of 2; doubling and halving it is 55 | an easy way to experiment with different sizes. 56 | __--__--__--__--__--__--__--____ */ 57 | 58 | // run multiple times for greater effect 59 | FFT.windowSize *= 2 60 | 61 | /* __--__--__--__--__--__--__--____ 62 | One fun combinator use with the FFT is 63 | Switch, which enables you to alternate 64 | between two geometries depending on whether 65 | or not an input exceeds a certain threshold. 66 | __--__--__--__--__--__--__--____ */ 67 | 68 | // super-simple Switch example 69 | march( 70 | s = Switch( 71 | Sphere(), 72 | Box() 73 | ) 74 | ).render( 3, true ) 75 | 76 | // the threshold property is unhelpfully 77 | // named 'c' for now... 78 | onframe = t => s.c = t/2 % 1 79 | 80 | 81 | // extending our first example with Switch... 82 | march( 83 | si = StairsIntersection( 84 | swt = Switch( 85 | s = Sphere(2).material('red'), 86 | b = Box(1.75).material('white') 87 | ), 88 | repeat = Repeat( 89 | sphere = Sphere(.125), 90 | Vec3(.25) 91 | ), 92 | .125/2 93 | ), 94 | Plane( Vec3(0,1,0), 1.35 ) 95 | ) 96 | .fog(.25, Vec3(0) ) 97 | .render( 4, true ) 98 | 99 | onframe = t => { 100 | si.rotate( t * 15 ) 101 | // try scaling the FFT results 102 | // by different values to control 103 | // the switch effect 104 | swt.c = FFT.low * 1 105 | 106 | repeat.distance.x = FFT.mid * FFT.low 107 | fft = (FFT.low + FFT.mid + FFT.high) 108 | 109 | // scale both our sphere and our box on every 110 | // frame, since we don't know which will be active 111 | s.radius = fft 112 | b.size = fft * .75 113 | 114 | sphere.radius = FFT.high / 2 115 | } 116 | 117 | /* __--__--__--__--__--__--__--____ 118 | By default, the FFT assigns frequences 119 | < 150 Hz o FFT.low, 150-1400Hz to 120 | FFT.mid, and all remaining frequencies 121 | to FFT.high. However, you can also 122 | define your own definitions by passing 123 | an array to FFT.start(); if you do this 124 | the averages will then be in FFT[0], 125 | FFT[1], FFT[2] etc. Below is the first 126 | example from this tutorial, modified 127 | to use more bins. 128 | __--__--__--__--__--__--__--____ */ 129 | 130 | march( 131 | si = StairsIntersection( 132 | Sphere(2).material( 'white' ), 133 | repeat = Repeat( 134 | sphere = Sphere(.125), 135 | Vec3(.5) 136 | ), 137 | .125 138 | ) 139 | ).render( 4, true ) 140 | 141 | // start our FFT 142 | FFT.start([100,200,400,800,1600,3200,6400,12800,22050]) 143 | 144 | onframe = time => { 145 | si.rotate( time * 15 ) 146 | 147 | repeat.distance.x = FFT[0] 148 | repeat.distance.y = FFT[1] + FFT[2] 149 | repeat.distance.z = FFT[3] 150 | sphere.radius = FFT[4] * FFT[5] 151 | } 152 | ` 153 | -------------------------------------------------------------------------------- /playground/demos/livecoding.js: -------------------------------------------------------------------------------- 1 | module.exports = `/* __--__--__--__--__--__--__--____ 2 | Live Coding 3 | 4 | There are a couple of techniques 5 | that can make live coding a bit 6 | easier in marching.js. First, if 7 | you're doing any audio-reactive 8 | visuals, be sure to check out the 9 | audio / FFT tutorial. 10 | 11 | You'll notice that many demos 12 | and tutorials use a pattern where 13 | we name objects in our graph for 14 | subsequent manipulations. This is 15 | an important step to get used to 16 | when you're setting up your graph. 17 | __--__--__--__--__--__--__--____ */ 18 | 19 | march( 20 | rpt = Repeat( 21 | Sphere(.125), 22 | Vec3(.5) 23 | ) 24 | ).render( 3, true ) 25 | 26 | // now we can change the distance later... 27 | rpt.distance.x = 1 28 | 29 | /* __--__--__--__--__--__--__--____ 30 | Easy enough. However, a problem occurs 31 | if you then re-execute your calls 32 | to march / render... the repeat distance 33 | is reset to the the value in its 34 | constructor. During live coding 35 | performances you often want to 36 | maintain state (as much as possible) 37 | while making changes to the graph, so 38 | that abrupt changes don't occur. 39 | 40 | If you run the line: 41 | 42 | Marching.useProxies = true 43 | 44 | ...a new behavior is enabled. When 45 | you create an object in 46 | the global namespace, a 'proxy' is 47 | created. IF you reassign a new 48 | object to this proxy, the proxy 49 | will copy all the properties of 50 | the previous object to the new one. 51 | This enables you to re-execute 52 | the graph while maintaining state, 53 | and makes it much easier to acheive 54 | continuity when live coding. Try running 55 | the code below: 56 | __--__--__--__--__--__--__--____ */ 57 | 58 | // run this line by itself before 59 | // executing other code 60 | Marching.useProxies = true 61 | 62 | march( 63 | rpt2 = Repeat( 64 | Sphere(.125), 65 | Vec3(.5) 66 | ) 67 | ).render( 3, true ) 68 | 69 | rpt2.distance.x = 1 70 | 71 | // if you re-execute the graph 72 | // now, you'll notice the distance.x 73 | // value is maintained. This only 74 | // works because we enabled proxies 75 | // and assigned our Repeat object 76 | // to the same global variable. 77 | 78 | /* __--__--__--__--__--__--__--____ 79 | Another useful technique, that relies 80 | on assigning names to objects, is 81 | to "fade", or transition gradually, 82 | to a new state in objects. 83 | __--__--__--__--__--__--__--____ */ 84 | 85 | march( s = Sphere(0) ).render( 3, true ) 86 | fade( 's','radius', 2, 10 ) 87 | 88 | /* __--__--__--__--__--__--__--____ 89 | In the example above, we give the 90 | name of the object we want to manipulate, 91 | the name of the property, the value 92 | we want to transition to, and the 93 | length in seconds of the transition. 94 | There is quadratic easing on the fade. 95 | 96 | By using dot notation in the property 97 | string, we can fade individual vector 98 | members. 99 | __--__--__--__--__--__--__--____ */ 100 | 101 | 102 | march( b = Box() ).render( 3, true ) 103 | fade( 'b','size.x', 2, 10 ) 104 | 105 | /* __--__--__--__--__--__--__--____ 106 | it's also worth noting we can 107 | fade a whole vector by simply 108 | leaving out the dot notation. The 109 | example below fades the size on 110 | the x,y,and z axis in one line of 111 | code. Note we have to use a new name 112 | to avoid the proxy effect! 113 | __--__--__--__--__--__--__--____ */ 114 | 115 | march( b2 = Box() ).render( 3, true ) 116 | fade( 'b2','size', 1.5, 10 ) 117 | 118 | /* __--__--__--__--__--__--__--____ 119 | We can use the same vector shortcut when 120 | manipulate animation at the frame level. 121 | __--__--__--__--__--__--__--____ */ 122 | 123 | march( 124 | rpt2 = Repeat( 125 | b3 = Box(), 126 | Vec3(1) 127 | ) 128 | ) 129 | .fog( .25, Vec3(0) ) 130 | .render( 3, true ) 131 | 132 | onframe = t => { 133 | // manipulate one vector member 134 | rpt2.distance.x = .5 + Math.sin( t/3 ) * .125 135 | 136 | // manipulate entire vector at once 137 | b3.size = .1 + Math.cos( t/2 ) * .075 138 | } 139 | 140 | /* __--__--__--__--__--__--__--____ 141 | Hopefully this is enough to get you 142 | started live coding. Between the use of 143 | onframe, fade, the fft, and proxies, there's 144 | a number of tools to get started. 145 | __--__--__--__--__--__--__--____ */` 146 | -------------------------------------------------------------------------------- /js/alterations.js: -------------------------------------------------------------------------------- 1 | const SceneNode = require( './sceneNode.js' ) 2 | const { param_wrap, MaterialID } = require( './utils.js' ) 3 | const { Var, float_var_gen, vec2_var_gen, vec3_var_gen, vec4_var_gen, int_var_gen } = require( './var.js' ) 4 | 5 | const ops = { 6 | Onion: { 7 | func( sdf,thickness ) { return `vec2( opOnion( ${sdf}.x, ${thickness} ), ${sdf}.y )` }, 8 | variables:[['thickness', 'float', .03]], 9 | def:` 10 | float opOnion( in float sdf, in float thickness ){ 11 | return abs(sdf)-thickness; 12 | } 13 | vec2 opOnion( vec2 sdf, float thickness ) { 14 | float x = 0.; 15 | 16 | sdf.x = opOnion( sdf.x, thickness ); 17 | 18 | return sdf; 19 | } 20 | ` 21 | }, 22 | Halve: { 23 | func( sdf, direction ) { return `vec2( opHalve( ${sdf}.x, p, ${direction} ), ${sdf}.y )` }, 24 | variables:[['direction','int',0]], 25 | def:` 26 | float opHalve( in float sdf, vec4 p, in int dir ){ 27 | float _out = 0.; 28 | switch( dir ) { 29 | case 0: 30 | _out = max( sdf, p.y ); 31 | break; 32 | case 1: 33 | _out = max( sdf, -p.y ); 34 | break; 35 | case 2: 36 | _out = max( sdf, p.x ); 37 | break; 38 | case 3: 39 | _out = max( sdf, -p.x ); 40 | break; 41 | } 42 | 43 | return _out; 44 | } 45 | 46 | vec2 opHalve( vec2 sdf, vec4 p, int dir ) { 47 | float x = 0.; 48 | 49 | x = opHalve( sdf.x, p, dir ); 50 | 51 | sdf.x = x; 52 | 53 | return sdf; 54 | } 55 | ` 56 | }, 57 | Round: { 58 | func( sdf, amount ) { return `vec2( ${sdf}.x - ${amount}, ${sdf}.y )` }, 59 | variables:[['amount','float',.1]], 60 | def:` 61 | 62 | ` 63 | } 64 | } 65 | 66 | 67 | 68 | //vec4 opElongate( in vec3 p, in vec3 h ) { 69 | // //return vec4( p-clamp(p,-h,h), 0.0 ); // faster, but produces zero in the interior elongated box 70 | 71 | // vec3 q = abs(p)-h; 72 | // return vec4( max(q,0.0), min(max(q.x,max(q.y,q.z)),0.0) ); 73 | //} 74 | const pushString = function( name ) { 75 | const glslobj = ops[ name ].def 76 | 77 | // some definitions are a single string, and not split into 78 | // separate float and opOut functions 79 | if( typeof glslobj === 'string' ) { 80 | if( Alterations.__glsl.indexOf( glslobj ) === -1 ) { 81 | Alterations.__glsl.push( glslobj ) 82 | } 83 | } 84 | } 85 | 86 | const Alterations = { 87 | __glsl:[], 88 | __getGLSL() { 89 | return this.__glsl.join('\n') 90 | }, 91 | __clear() { this.__glsl.length = 0 } 92 | } 93 | 94 | for( let name in ops ) { 95 | 96 | // get codegen function 97 | let op = ops[ name ] 98 | 99 | // create constructor 100 | Alterations[ name ] = function( sdf, ...args ) { 101 | const __op = Object.create( Alterations[ name ].prototype ) 102 | __op.sdf = sdf 103 | __op.variables = [] 104 | __op.__desc = { parameters:[] } 105 | 106 | for( let i = 0; i < op.variables.length; i++ ) { 107 | const propArray = op.variables[ i ] 108 | const propName = propArray[ 0 ] 109 | const propType = propArray[ 1 ] 110 | const propValue = args[ i ] === undefined ? propArray[ 2 ] : args[ i ] 111 | 112 | __op.__desc.parameters.push({ name:propName, value:propValue }) 113 | let param 114 | 115 | switch( propType ) { 116 | case 'int': 117 | param = int_var_gen( propValue )() 118 | break; 119 | default: 120 | param = float_var_gen( propValue )() 121 | break; 122 | } 123 | 124 | Object.defineProperty( __op, propName, { 125 | get() { return param }, 126 | set(v) { param.set( v ) } 127 | }) 128 | 129 | __op.variables.push( param ) 130 | } 131 | 132 | __op.matId = MaterialID.alloc() 133 | 134 | return __op 135 | } 136 | 137 | Alterations[ name ].prototype = SceneNode() 138 | 139 | Alterations[ name ].prototype.emit = function ( __name ) { 140 | const emitterA = this.sdf.emit( __name ) 141 | pushString( name ) 142 | //const emitterB = this.b.emit() 143 | 144 | const output = { 145 | out: op.func( emitterA.out, ...this.variables.map( v => v.emit() ) ), 146 | preface: (emitterA.preface || '') 147 | } 148 | 149 | return output 150 | } 151 | 152 | Alterations[name].prototype.emit_decl = function () { 153 | let str = this.sdf.emit_decl() 154 | for( let v of this.variables ) { 155 | str += v.emit_decl() 156 | } 157 | 158 | return str 159 | }; 160 | 161 | Alterations[name].prototype.update_location = function(gl, program) { 162 | this.sdf.update_location( gl, program ) 163 | for( let v of this.variables ) v.update_location( gl, program ) 164 | } 165 | 166 | Alterations[name].prototype.upload_data = function(gl) { 167 | this.sdf.upload_data( gl ) 168 | for( let v of this.variables ) v.upload_data( gl ) 169 | 170 | } 171 | } 172 | 173 | Alterations.Halve.UP = 0 174 | Alterations.Halve.DOWN = 1 175 | Alterations.Halve.LEFT = 3 176 | Alterations.Halve.RIGHT = 2 177 | 178 | module.exports = Alterations 179 | -------------------------------------------------------------------------------- /js/sceneNode.js: -------------------------------------------------------------------------------- 1 | const SceneNode = ()=> Object.create( SceneNode.prototype ) 2 | const Matrix = require( './external/matrix.js' ) 3 | 4 | SceneNode.prototype = { 5 | active: 1, 6 | 7 | // register functions passed as property values 8 | // to callbacks, and assign initial value by 9 | // running the function 10 | __processFunction( obj, value, name, shouldAdd=false ) { 11 | if( typeof value === 'function' ) { 12 | const __value = value 13 | let fnc = null 14 | 15 | if( typeof obj[ name ] === 'function' ) { 16 | fnc = t => obj[ name ]( __value( t ) ) 17 | }else{ 18 | fnc = shouldAdd 19 | ? t => obj[ name ] += __value( t ) 20 | : t => obj[ name ] =__value( t ) 21 | } 22 | 23 | Marching.postrendercallbacks.push( fnc ) 24 | 25 | value = value( 0 ) 26 | } 27 | 28 | return value 29 | }, 30 | 31 | emit() { return "#NotImplemented#"; }, 32 | 33 | emit_decl() { return ""; }, 34 | 35 | update_location(gl, program) {}, 36 | 37 | upload_data(gl) {}, 38 | 39 | __dirty() { 40 | const params = this.params || this.parameters 41 | params.forEach( v => { 42 | if( this[ v.name ] !== undefined ) { 43 | this[ v.name ].dirty = true 44 | } 45 | }) 46 | 47 | if( this.transform !== undefined ) { 48 | this.transform.dirty = true 49 | } 50 | }, 51 | 52 | getID() { 53 | let id = this.id 54 | 55 | if( id === undefined && this.sdf !== undefined ) { 56 | id = this.sdf.getID() 57 | } 58 | 59 | return id 60 | }, 61 | 62 | getCenter() { 63 | let center = this.center 64 | 65 | if( center === undefined && this.sdf !== undefined ) { 66 | if( this.sdf.getCenter === undefined ) { 67 | center = this.sdf.__wrapped.getCenter() 68 | }else{ 69 | center = this.sdf.getCenter() 70 | } 71 | } 72 | 73 | return center 74 | }, 75 | 76 | move( ...args ) { 77 | return this.translate( ...args ) 78 | }, 79 | 80 | moveBy( x,y,z ) { 81 | x = this.__processFunction( this.transform.translation, x, 'x', true ) 82 | y = this.__processFunction( this.transform.translation, y, 'y', true ) 83 | z = this.__processFunction( this.transform.translation, z, 'z', true ) 84 | 85 | if( x !== undefined && x !== null ) this.transform.translation.x += x 86 | if( y !== undefined && y !== null ) this.transform.translation.y += y 87 | if( z !== undefined && z !== null ) this.transform.translation.z += z 88 | 89 | return this 90 | }, 91 | 92 | rotate( angle, x,y,z ) { 93 | angle = this.__processFunction( this.transform.rotation, angle, 'angle' ) 94 | this.transform.rotation.angle = angle 95 | 96 | x = this.__processFunction( this.transform.rotation.axis, x, 'x' ) 97 | y = this.__processFunction( this.transform.rotation.axis, y, 'y' ) 98 | z = this.__processFunction( this.transform.rotation.axis, z, 'z' ) 99 | if( x !== undefined ) this.transform.rotation.axis.x = x 100 | if( y !== undefined ) this.transform.rotation.axis.y = y 101 | if( z !== undefined ) this.transform.rotation.axis.z = z 102 | 103 | return this 104 | }, 105 | 106 | rotateX( angle ) { 107 | angle = this.__processFunction( this.transform.rotation, angle, 'angle' ) 108 | this.transform.rotation.angle = angle 109 | 110 | 111 | }, 112 | 113 | rotateBy( angle,x,y,z ) { 114 | this.transform.__rotations.push( Matrix.rotate( angle,x,y,z ) ) 115 | return this 116 | }, 117 | 118 | translate( x,y,z ) { 119 | x = this.__processFunction( this.transform.translation, x, 'x' ) 120 | y = this.__processFunction( this.transform.translation, y, 'y' ) 121 | z = this.__processFunction( this.transform.translation, z, 'z' ) 122 | if( x !== undefined && x !== null ) this.transform.translation.x = x 123 | if( y !== undefined && y !== null ) this.transform.translation.y = y 124 | if( z !== undefined && z !== null ) this.transform.translation.z = z 125 | 126 | return this 127 | }, 128 | 129 | scale( amount ) { 130 | amount = this.__processFunction( this, amount, 'scale' ) 131 | if( amount !== undefined ) this.transform.scale = amount 132 | return this 133 | }, 134 | 135 | scaleBy( amount ) { 136 | amount = this.__processFunction( this, amount, 'scaleBy' ) 137 | if( amount !== undefined ) this.transform.scale += amount 138 | return this 139 | }, 140 | 141 | material( mat ) { 142 | this.__setMaterial( mat ) 143 | return this 144 | }, 145 | 146 | texture( tex,props ) { 147 | this.__setTexture( tex,props ) 148 | return this 149 | }, 150 | 151 | bump( tex,strength ) { 152 | this.__setBump( tex,strength ) 153 | return this 154 | } 155 | } 156 | 157 | const ops = [ 'repeat', 'polarRepeat', 'elongation' ] 158 | 159 | ops.forEach( op => { 160 | const constructorName = op[0].toUpperCase() + op.slice(1) 161 | SceneNode.prototype[ op ] = function( ...args ) { 162 | this[ op ] = this[ op ].bind( this ) 163 | Object.assign( this[ op ], SceneNode.prototype ) 164 | this.__target = this[ op ] 165 | this[ '__'+op ] = Marching[ constructorName ]( this, ...args, this[ op ] ) 166 | this[ op ].transform = this[ '__'+op ].transform 167 | return this 168 | } 169 | }) 170 | 171 | module.exports = SceneNode 172 | -------------------------------------------------------------------------------- /playground/demos/tutorial_1.js: -------------------------------------------------------------------------------- 1 | module.exports = `/* __--__--__--__--__--__--__--__-- 2 | 3 | let's start by making a simple 4 | scene with one sphere. highlight 5 | the lines below and hit ctrl+enter 6 | to run them. make sure not to high- 7 | light these instructions. you can 8 | also hit alt+enter (option in macOS) 9 | to run an entire block at once. use 10 | ctrl+. (period) to clear graphics. 11 | 12 | ** __--__--__--__--__--__--__--__*/ 13 | 14 | sphere1 = Sphere() 15 | 16 | march( sphere1 ) 17 | .render( 2, true ) 18 | 19 | /* __--__--__--__--__--__--__--__-- 20 | 21 | the march() method accepts an array 22 | of objects that can be geometric 23 | primitives or transformations. we 24 | can now change the radius of our 25 | sphere: 26 | 27 | ** __--__--__--__--__--__--__--__*/ 28 | 29 | sphere1.radius = 1.25 30 | 31 | /* __--__--__--__--__--__--__--__-- 32 | 33 | or change its position... 34 | 35 | ** __--__--__--__--__--__--__--__*/ 36 | 37 | sphere1.move( .5, -.5 ) 38 | 39 | /* __--__--__--__--__--__--__--__-- 40 | 41 | note that we can only make these 42 | changes after the initial render if 43 | a value of true is passed to our 44 | render function as its second 45 | parameter. Depending on the 46 | computer you use, you probably will 47 | want to turn the resolution down 48 | (the first arg) when animating the 49 | scene. If no value or a value of 50 | false is passed as the second 51 | argument, marching.js will create a 52 | static image at maximum quality. 53 | 54 | we can also register a onframe function 55 | to change properties over time. this 56 | runs once per video frame, and is 57 | passed the current time. here we'll 58 | use the time to change the sphere's 59 | position. 60 | 61 | ** __--__--__--__--__--__--__--__*/ 62 | 63 | onframe = time => { 64 | sphere1.move( 65 | Math.sin( time/2 ) * 2, 66 | null, 67 | Math.sin( time ) * 4 68 | ) 69 | } 70 | 71 | /* __--__--__--__--__--__--__--__-- 72 | 73 | this library uses signed distance 74 | functions (SDFs) to render geometry 75 | and perform transformations. You 76 | can do fun stuff with SDFs. Below 77 | we'll render a box, but subtract a 78 | sphere from its center. 79 | 80 | ** __--__--__--__--__--__--__--__*/ 81 | 82 | march( 83 | Difference( 84 | Box(), Sphere( 1.35 ) 85 | ) 86 | ) 87 | .render() 88 | 89 | /* __--__--__--__--__--__--__--__-- 90 | 91 | we can animate this as well. we'll 92 | turn down the quality first... 93 | try turning it back up and see if 94 | your computer can handle it. 95 | 96 | ** __--__--__--__--__--__--__--__*/ 97 | 98 | sphere2 = Sphere( 1.35 ) 99 | box1 = Box() 100 | 101 | march( 102 | Difference( box1, sphere2 ) 103 | ) 104 | .render( 3, true ) 105 | 106 | onframe = time => sphere2.radius = 1.25 + Math.sin( time ) * .1 107 | 108 | 109 | /* __--__--__--__--__--__--__--__-- 110 | 111 | One fun transform we can do is to 112 | repeat a shape throughout our scene, 113 | We can define how coarse/fine these 114 | repetitions are. 115 | 116 | ** __--__--__--__--__--__--__--__*/ 117 | 118 | march( 119 | Repeat( 120 | Sphere( .25 ), 121 | 1 122 | ) 123 | ) 124 | .render() 125 | 126 | /* __--__--__--__--__--__--__--__-- 127 | 128 | The vector we pass as the second 129 | argument to repeat determines the 130 | spacing; higher numbers yield fewer 131 | repeats. What if we want to take two 132 | shapes and repeat them? In order to 133 | do that we need to create a union. 134 | 135 | ** __--__--__--__--__--__--__--__*/ 136 | 137 | sphere3 = Sphere( .35 ) 138 | box3 = Box( Vec3( .35 ) ) 139 | sphereBox = RoundUnion( sphere3, box3, .9 ) 140 | 141 | march( 142 | Repeat( sphereBox, 2 ) 143 | ) 144 | .render( 3, true ) 145 | 146 | onframe = time => sphere3.radius = Math.sin( time ) * .75 147 | 148 | /* __--__--__--__--__--__--__--__-- 149 | 150 | Hopefully your computer can handle 151 | that, but if not, you can always 152 | lower the resolution further or 153 | shrink your browser window. Lowering 154 | your monitor resolution while doing 155 | realtime experiments or performances 156 | will also help (especially with 157 | hidpi or "retina" displays). in 158 | addition to improving efficiency, 159 | we can also change the performance 160 | of our raymarcher to get fun glitch 161 | effects. 162 | 163 | ** __--__--__--__--__--__--__--__*/` 164 | 165 | 166 | /* 167 | march( 168 | Sphere( Noise() ) 169 | ) 170 | // halve the resolution 171 | .resolution(.5) 172 | // only take one sample per ray 173 | .steps(1) 174 | // how far do our rays go? 175 | .farPlane(10) 176 | // ignore quality parameter in favor 177 | // of the other settings we've defined 178 | // and animate 179 | .render(null, true)*/ 180 | -------------------------------------------------------------------------------- /js/var.js: -------------------------------------------------------------------------------- 1 | const { Vec2, Vec3, Vec4 } = require( './vec.js' ) 2 | const float = require( './float.js' ) 3 | const int = require( './int.js' ) 4 | 5 | // Var 6 | const VarAlloc = { 7 | current: 0, 8 | clear() { 9 | VarAlloc.current = 0 10 | }, 11 | alloc() { 12 | return VarAlloc.current++ 13 | } 14 | } 15 | 16 | const Var = function( value, fixedName = null, __type ) { 17 | const v = Object.create( Var.prototype ) 18 | v.varName = fixedName !== null ? fixedName : 'var' + VarAlloc.alloc() 19 | v.value = value 20 | v.type = v.value.type 21 | if( v.type === undefined ) v.type = __type || 'float' 22 | 23 | value.var = v 24 | 25 | if( v.type !== 'float' && v.type !== 'int' ) { 26 | Object.defineProperties( v, { 27 | x: { 28 | get() { return this.value.x }, 29 | set(v){ this.value.x = v; this.dirty = true } 30 | }, 31 | y: { 32 | get() { return this.value.y }, 33 | set(v){ this.value.y = v; this.dirty = true } 34 | }, 35 | z: { 36 | get() { return this.value.z }, 37 | set(v){ this.value.z = v; this.dirty = true } 38 | }, 39 | w: { 40 | get() { return this.value.w }, 41 | set(v){ this.value.w = v; this.dirty = true } 42 | }, 43 | r: { 44 | get() { return this.value.x }, 45 | set(v){ this.value.x = v; this.dirty = true } 46 | }, 47 | g: { 48 | get() { return this.value.y }, 49 | set(v){ this.value.y = v; this.dirty = true } 50 | }, 51 | b: { 52 | get() { return this.value.z }, 53 | set(v){ this.value.z = v; this.dirty = true } 54 | }, 55 | }) 56 | } 57 | 58 | return v 59 | } 60 | 61 | Var.hardcode = false 62 | const emit_float = function( a ) { 63 | if (a % 1 === 0) 64 | return a.toFixed( 1 ) 65 | else 66 | return a 67 | } 68 | 69 | Var.prototype = { 70 | dirty: true, 71 | 72 | loc: -1, 73 | 74 | emit() { 75 | let out 76 | if( this.value.isGen ) { 77 | const vecOut = this.value.emit() 78 | out = vecOut.preface + vecOut.out 79 | 80 | }else{ 81 | out = this.varName 82 | } 83 | 84 | return out 85 | }, 86 | 87 | emit_decl() { 88 | let out = '' 89 | if( this.value.isGen ) { 90 | out = this.value.emit_decl() 91 | }else{ 92 | if( Var.hardcode === true ) { 93 | 94 | if( typeof this.value.emit !== 'function' ) { 95 | if( this.type === 'float' ) { 96 | out = `${this.type} ${this.varName} = ${emit_float(this.value)};\n` 97 | }else{ 98 | out = `${this.type} ${this.varName} = ${this.value};\n` 99 | } 100 | }else{ 101 | let val = this.value.emit() 102 | if( typeof val !== 'string' ) val = val.out 103 | out = val !== undefined ? `${this.type} ${this.varName} = ${val};\n` : '' 104 | } 105 | }else{ 106 | out = `uniform ${this.type} ${this.varName};\n` 107 | } 108 | } 109 | return out 110 | }, 111 | 112 | set(v) { this.value = v; this.dirty = true; }, 113 | 114 | update_location(gl, program) { 115 | if( this.value.isGen ) { 116 | this.value.update_location( gl, program ) 117 | return 118 | } 119 | 120 | //if( this.loc === -1 ) 121 | this.loc = gl.getUniformLocation(program, this.varName) 122 | }, 123 | 124 | upload_data(gl) { 125 | if( !this.dirty ) return 126 | 127 | if( this.value.isGen ) { 128 | this.value.upload_data( gl ) 129 | this.dirty = false 130 | return 131 | } 132 | let v = this.value 133 | if (typeof v === 'number' ) { 134 | gl.uniform1f( this.loc, v ) 135 | }else if ( v instanceof Vec2 ) { 136 | gl.uniform2f(this.loc, v.x, v.y ) 137 | } else if( v instanceof Vec3 ) { 138 | gl.uniform3f(this.loc, v.x, v.y, v.z ) 139 | } else if( v instanceof Vec4 ) { 140 | gl.uniform4f(this.loc, v.x, v.y, v.z, v.w ) 141 | } else { 142 | // for color variables 143 | if( this.type === 'float' ) { 144 | gl.uniform1f( this.loc, v.x ) 145 | }else{ 146 | gl.uniform1i( this.loc, v.x ) 147 | } 148 | } 149 | 150 | this.dirty = false 151 | } 152 | } 153 | 154 | 155 | function int_var_gen(x,name=null) { 156 | let output = ()=> { 157 | let out = Var( int(x), name, 'int' ) 158 | return out 159 | } 160 | 161 | return output 162 | } 163 | function float_var_gen(x,name=null) { return ()=> { return Var( float(x), name, 'float' ) } } 164 | 165 | function vec2_var_gen(x, y,name=null) { 166 | return ()=> Var( Vec2(x, y), name ) 167 | } 168 | 169 | function vec3_var_gen(x=0, y, z,name=null) { 170 | return ()=> Var( Vec3(x, y, z), name ) 171 | } 172 | 173 | function vec4_var_gen( x, y, z, w, name=null ) { 174 | return Var( Vec4( x, y, z, w ), name ) 175 | } 176 | //function float_var_gen(x,name=null) { return ()=> { return Var( float(x), name, 'float' ) } } 177 | 178 | //function vec2_var_gen(x, y,name=null) { return ()=> Var( Vec2(x, y), name ) } 179 | 180 | //function vec3_var_gen(x, y, z,name=null) { return ()=> Var( Vec3(x, y, z), name ) } 181 | 182 | //function vec4_var_gen( x, y, z, w, name=null ) { return Var( Vec4( x, y, z, w ), name ) } 183 | 184 | module.exports = { Var, float_var_gen, vec2_var_gen, vec3_var_gen, vec4_var_gen, int_var_gen, VarAlloc } 185 | 186 | /*function float_var_gen(x,name=null) { return ()=> { return Var( float(x), name, 'float' ) } } 187 | 188 | function vec2_var_gen(x, y,name=null) { 189 | if( y === undefined ) y = x 190 | return ()=> Var( Vec2(x, y), name ) 191 | } 192 | 193 | function vec3_var_gen(x, y, z,name=null) { 194 | if( y === undefined ) y = x 195 | if( z === undefined ) z = x 196 | return ()=> Var( Vec3(x, y, z), name ) 197 | } 198 | 199 | function vec4_var_gen( x, y, z, w, name=null ) { 200 | if( y === undefined ) y = x 201 | if( z === undefined ) z = x 202 | if( w === undefined ) w = x 203 | return Var( Vec4( x, y, z, w ), name ) 204 | } 205 | */ 206 | -------------------------------------------------------------------------------- /js/transform.js: -------------------------------------------------------------------------------- 1 | const { param_wrap, MaterialID } = require( './utils.js' ) 2 | const { Var, float_var_gen, vec2_var_gen, vec3_var_gen, vec4_var_gen, int_var_gen, VarAlloc } = require( './var.js' ) 3 | const Matrix = require( './external/matrix.js' ) 4 | window.Matrix = Matrix 5 | const MatrixWrap = function ( shouldInvert = false ) { 6 | const m = Object.create( MatrixWrap.prototype ) 7 | m.dirty = true 8 | m.translation = {} 9 | m.scale = {} 10 | m.shouldInvert = shouldInvert 11 | m.shouldRotate = true 12 | m.rotation = { 13 | axis: {} 14 | } 15 | m.parent = null 16 | 17 | let tx = 0, ty = 0, tz = 0 18 | Object.defineProperties( m.translation, { 19 | x: { 20 | get() { return tx }, 21 | set(v){ 22 | tx = v 23 | //m.__data = m.__data.multiply( Matrix.translate( tx, ty, tz ) ) 24 | m.dirty = true 25 | } 26 | }, 27 | y: { 28 | get() { return ty }, 29 | set(v){ 30 | ty = v 31 | //m.__data = m.__data.multiply( Matrix.translate( tx, ty, tz ) ) 32 | m.dirty = true 33 | } 34 | }, 35 | z: { 36 | get() { return tz }, 37 | set(v){ 38 | tz = v 39 | //m.__data = m.__data.multiply( Matrix.translate( tx, ty, tz ) ) 40 | m.dirty = true 41 | } 42 | }, 43 | }) 44 | 45 | // scaling must be sent as separate uniform to avoid sdf over estimation 46 | let scale = 1 47 | Object.defineProperty( m,'scale', { 48 | get() { return scale }, 49 | set(v){ 50 | scale = v 51 | //m.__data = m.__data.multiply( Matrix.rotate( angle, rx, ry, rz ) ) 52 | m.dirty = true 53 | } 54 | }) 55 | 56 | /* FOR NON-UNIFORM SCALING: 57 | * 58 | * 1. comment out scale property above 59 | * 2. uncomment scale property below 60 | * 3. change emit_decl to use a vec3 for scale 61 | * 4. change upload_data to upload a 3f 62 | * 5. In "primitives.js", replace line 155 (part of emit) to use compensated scaling 63 | */ 64 | 65 | //let sx = 1, sy = 1, sz = 1 66 | //Object.defineProperties( m.scale, { 67 | // x: { 68 | // get() { return sx }, 69 | // set(v){ 70 | // sx = v 71 | // //m.__data = m.__data.multiply( Matrix.scale( sx, sy, sz ) ) 72 | // m.dirty = true 73 | // } 74 | // }, 75 | // y: { 76 | // get() { return sy }, 77 | // set(v){ 78 | // sy = v 79 | // //m.__data = m.__data.multiply( Matrix.scale( sx, sy, sz ) ) 80 | // m.dirty = true 81 | // } 82 | // }, 83 | // z: { 84 | // get() { return sz }, 85 | // set(v){ 86 | // sz = v 87 | // //m.__data = m.__data.multiply( Matrix.scale( sx, sy, sz ) ) 88 | // m.dirty = true 89 | // } 90 | // }, 91 | //}) 92 | 93 | let angle = 0 94 | Object.defineProperty( m.rotation, 'angle', { 95 | get() { return angle }, 96 | set(v){ 97 | angle = v 98 | //m.__data = m.__data.multiply( Matrix.rotate( angle, rx, ry, rz ) ) 99 | m.dirty = true 100 | } 101 | }) 102 | 103 | let rx = 0, ry = 0, rz = 0 104 | Object.defineProperties( m.rotation.axis, { 105 | x: { 106 | get() { return rx }, 107 | set(v){ 108 | rx = v 109 | //m.__data = m.__data = Matrix.rotate( angle, rx, ry, rz, m.__data ) 110 | m.dirty = true 111 | } 112 | }, 113 | y: { 114 | get() { return ry }, 115 | set(v){ 116 | ry = v 117 | //m.__data = m.__data = Matrix.rotate( angle, rx, ry, rz, m.__data ) 118 | m.dirty = true 119 | } 120 | }, 121 | z: { 122 | get() { return rz }, 123 | set(v){ 124 | rz = v 125 | //m.__data = m.__data = Matrix.rotate( angle, rx, ry, rz, m.__data ) 126 | m.dirty = true 127 | } 128 | }, 129 | }) 130 | 131 | m.__rotations = [] 132 | m.__id = VarAlloc.alloc() 133 | m.__dirty = function() {} 134 | m.__data = Matrix.identity() 135 | m.__Matrix = Matrix 136 | m.varName = 'transform' + m.__id 137 | 138 | return m 139 | } 140 | 141 | MatrixWrap.prototype = { 142 | type: 'matrix', 143 | 144 | emit() { return this.varName }, 145 | 146 | emit_scale() { return this.varName + '_scale' }, 147 | 148 | emit_decl() { 149 | const decl = ` uniform mat4 ${this.varName}; 150 | uniform float ${this.varName}_scale; 151 | ` 152 | 153 | return decl 154 | }, 155 | 156 | update_location(gl, program) { 157 | this.loc = gl.getUniformLocation( program, this.varName ) 158 | this.loc_scale = gl.getUniformLocation( program, this.varName+'_scale' ) 159 | }, 160 | 161 | upload_data(gl) { 162 | if( !this.dirty ) return 163 | 164 | this.internal() 165 | 166 | if( this.shouldInvert === true ) { 167 | const inverse = Matrix.inverse( this.__data ) 168 | gl.uniformMatrix4fv( this.loc, false, inverse.m ) 169 | }else{ 170 | gl.uniformMatrix4fv( this.loc, false, this.__data.m ) 171 | } 172 | //gl.uniform3f(this.loc_scale, this.scale.x, this.scale.y, this.scale.z ) 173 | 174 | // scaling must be sent as separate uniform to avoid sdf over-estimation 175 | gl.uniform1f(this.loc_scale, this.scale ) 176 | 177 | this.dirty = false 178 | }, 179 | 180 | 181 | internal() { 182 | this.__data = Matrix.identity() 183 | if( this.parent != null ) this.__data = this.parent.__data 184 | 185 | this.__data = this.__data.multiply( Matrix.translate( this.translation.x, this.translation.y, this.translation.z ) ) 186 | 187 | // handle cumulative rotations via .rotateBy() method 188 | this.__rotations.forEach( r => { 189 | this.__data = this.__data.multiply( r ) 190 | }) 191 | 192 | // handle absolute rotations via .rotate() method... should this be aliased to rotateTo() ? 193 | if( this.shouldRotate ) { 194 | this.__data = this.__data.multiply( Matrix.rotate( this.rotation.angle, this.rotation.axis.x, this.rotation.axis.y, this.rotation.axis.z ) ) 195 | } 196 | 197 | this.__data = this.__data.multiply( Matrix.scale( this.scale, this.scale, this.scale ) ) 198 | }, 199 | 200 | invert( shouldInvert = true) { 201 | this.shouldInvert = shouldInvert 202 | this.dirty = true 203 | }, 204 | 205 | apply( transform = null, shouldInvert = false ) { 206 | this.parent = transform 207 | this.dirty = true 208 | } 209 | 210 | } 211 | 212 | module.exports = MatrixWrap 213 | -------------------------------------------------------------------------------- /playground/demos/lighting.js: -------------------------------------------------------------------------------- 1 | module.exports =`/* __--__--__--__--__--__--__--__-- 2 | 3 | By default, marching.js uses a 4 | lighting system consisting of a 5 | skydome(fill), a single front light, 6 | and a back light. This lighting 7 | system includes ambient occlusion 8 | and basic shadows, as taken from 9 | this demo by Inigo Quilez: 10 | 11 | https://www.shadertoy.com/view/Xds3zN 12 | 13 | However, each Material in marching.js 14 | can uses its own lighting algorithm. 15 | In the example below, the left sphere and 16 | the ground plane use the default lighting 17 | algorithm while the right sphere uses 18 | the normal for each pixel to determine 19 | its color. 20 | 21 | ** __--__--__--__--__--__--__--__*/ 22 | 23 | march( 24 | Sphere( 1 ).translate(-1.25), 25 | Sphere( 1 ) 26 | .translate(1,0,0) 27 | .material( 'normal' ), 28 | Plane() 29 | ) 30 | .render() 31 | 32 | /* __--__--__--__--__--__--__--__-- 33 | 34 | For most shapes, the third values 35 | passed to constructors will determine 36 | the material used by the shape. In 37 | the last example we saw that there is 38 | a "preset" to use normals for lighting; 39 | there are also presets for common 40 | colors. 41 | 42 | ** __--__--__--__--__--__--__--__*/ 43 | 44 | march( 45 | Sphere( 1 ) 46 | .translate( -1.25 ) 47 | .material( 'green' ), 48 | Sphere( 1 ) 49 | .translate(1) 50 | .material( 'red' ), 51 | Plane().material( 'yellow' ) 52 | ) 53 | .render() 54 | 55 | /* __--__--__--__--__--__--__--__-- 56 | 57 | Note that each geomety's color is 58 | determined by both properties of its 59 | material and the lights in the scene. 60 | For example, we could change the scene 61 | above to use magenta light, our green 62 | sphere will essentially show up as 63 | black (magenta light contains no green). 64 | 65 | ** __--__--__--__--__--__--__--__*/ 66 | 67 | march( 68 | Sphere( 1 ) 69 | .translate( -1.25 ) 70 | .material( 'green' ), 71 | Sphere( 1 ) 72 | .translate(1) 73 | .material( 'red' ), 74 | Plane().material( 'yellow' ) 75 | ) 76 | .light( Light( Vec3(2,2,3), Vec3(1,0,1) ) ) 77 | .render() 78 | 79 | /* __--__--__--__--__--__--__--__-- 80 | 81 | To really get full control over our 82 | lighting, we'll want to avoid the 83 | default lighting scheme, which has 84 | some properties baked in, and 85 | customize everything by hand. The 86 | example below uses the Phong 87 | lighting model, with an additioanl 88 | Fresnel effect added. We'll also 89 | use a simple white light positioned 90 | to the upper left of the scene's 91 | center. 92 | 93 | ** __--__--__--__--__--__--__--__*/ 94 | 95 | mat1 = Material( 'phong', Vec3(.05), Vec3(.5), Vec3(1), 8 ) 96 | march( 97 | Sphere( 1 ).material( mat1 ), 98 | Plane().material( mat1 ) 99 | ) 100 | .light( Light( Vec3(2,2,3), Vec3(1) ) ) 101 | .render() 102 | 103 | /* __--__--__--__--__--__--__--__-- 104 | 105 | In the example above, our material 106 | uses the 'phong' lighting model, has 107 | a ambient RGB coefficient of .05, 108 | diffuse RGB coefficient of .5, 109 | specular RGB coefficients of 1. The 110 | last number determines the diffuseness 111 | of the specular highlights, with lower 112 | numbers yielding more diffuse highlights. 113 | 114 | We can add a final Vec3 to control 115 | the Fresnel effect on the material, 116 | which can create a halo effect around 117 | a geometry. The three parameters for 118 | the Fresnel effect are bias, scale, 119 | and power. 120 | 121 | ** __--__--__--__--__--__--__--__*/ 122 | 123 | mat1 = Material( 'phong', Vec3(.05), Vec3(.5), Vec3(1), 8, Vec3(1,50,5) ) 124 | mat2 = Material( 'phong', Vec3(.05), Vec3(.5), Vec3(1), 8 ) 125 | march( 126 | Sphere( 1 ).material( mat1 ), 127 | Plane().material( mat2 ) 128 | ) 129 | .light( Light( Vec3(2,2,3), Vec3(1) ) ) 130 | .render() 131 | 132 | /* __--__--__--__--__--__--__--__-- 133 | 134 | We can have lots of lights! However, 135 | this will increase rendering time, 136 | so tread carefully if you're doing 137 | realtime work. 138 | 139 | ** __--__--__--__--__--__--__--__*/ 140 | 141 | mat1 = Material( 'phong', Vec3(.05), Vec3(.5), Vec3(1), 8, Vec3(1,50,5) ) 142 | mat2 = Material( 'phong', Vec3(.05), Vec3(.5), Vec3(1), 8 ) 143 | march( 144 | Sphere( 1 ).material( mat1 ), 145 | Plane().material( mat2 ) 146 | ) 147 | .light( 148 | Light( Vec3(2,2,3), Vec3(1) ), 149 | Light( Vec3(-2,2,3), Vec3(1,0,0) ), 150 | Light( Vec3(0,0,-3), Vec3(0,0,1) ), 151 | ) 152 | .render() 153 | 154 | /* __--__--__--__--__--__--__--__-- 155 | 156 | Two other elements that affect 157 | lighting in marching.js are fog and 158 | shadows. The fog() method accepts 159 | two arguments, a color for the fog 160 | and an intensity coefficient. For 161 | typical fog effects, set the fog 162 | color to be the same as the background 163 | color for the scene. 164 | 165 | ** __--__--__--__--__--__--__--__*/ 166 | 167 | march( 168 | Sphere() 169 | .translate(-1.25,0,0) 170 | .material( 'green' ), 171 | Sphere() 172 | .translate(1) 173 | .material( 'red' ), 174 | Plane().material( 'yellow' ) 175 | ) 176 | .background( Vec3(0,0,.5) ) 177 | .fog( .125, Vec3(0,0,.5) ) 178 | .render() 179 | 180 | /* __--__--__--__--__--__--__--__-- 181 | 182 | The effect is especially easy to see 183 | on fields of repeated geoemtries. Run 184 | the code below, and then uncomment 185 | the fog and run it again. 186 | 187 | ** __--__--__--__--__--__--__--__*/ 188 | 189 | mat1 = Material( 'phong', Vec3(.05), Vec3(.5), Vec3(1), 8, Vec3(1,4,1) ) 190 | march( 191 | Repeat( 192 | Sphere( .25 ).material( mat1 ), 193 | Vec3( .75) 194 | ) 195 | ) 196 | .light( 197 | Light( Vec3(2,2,3), Vec3(1) ), 198 | Light( Vec3(-2,2,3), Vec3(1,0,0) ), 199 | ) 200 | .background( Vec3(0) ) 201 | //.fog( .5, Vec3(0) ) 202 | .render() 203 | 204 | /* __--__--__--__--__--__--__--__-- 205 | 206 | Last but not least, we can change 207 | the softness of shadows in our scene 208 | by adjusting a shadow coefficient. 209 | Lower values (such as 2) yield soft, 210 | diffuse shadows while high values 211 | (like 16 or 32) yield shadows with 212 | hard edges. You can also pass a value 213 | of 0 to remove shadows from a scene. 214 | Experiment with passing different 215 | values to the shadow method below. 216 | 217 | ** __--__--__--__--__--__--__--__*/ 218 | 219 | mat1 = Material( 'phong', Vec3(.05), Vec3(.5), Vec3(1), 8, Vec3(0,1,2) ) 220 | march( 221 | Box( .75 ) 222 | .translate( 0,.25,1 ) 223 | .material( mat1 ), 224 | Plane().material( mat1 ) 225 | ) 226 | .light( Light( Vec3(-1,2,2), Vec3(1) ) ) 227 | .shadow(2) 228 | .render()` 229 | -------------------------------------------------------------------------------- /js/mergepass.js: -------------------------------------------------------------------------------- 1 | const MP = require( '@bandaloo/merge-pass' ) 2 | 3 | const FX = { 4 | merger:null, 5 | chain: [], 6 | MP, 7 | clear() { 8 | this.deleteMerger() 9 | this.chain.length = 0 10 | }, 11 | 12 | deleteMerger() { 13 | if( this.merger !== null ) { 14 | this.merger.delete() 15 | this.merger = null 16 | } 17 | }, 18 | 19 | init( colorTexture, depthTexture, gl ) { 20 | this.merger = new MP.Merger( 21 | this.chain, 22 | colorTexture, 23 | gl, 24 | // pass null to create second scratch channel 25 | // this is the samplerNum for arguments 26 | { channels: [ depthTexture, null, null ] } 27 | ) 28 | }, 29 | 30 | run( time ) { 31 | merger.draw( time ) 32 | }, 33 | 34 | post( ...fx ) { 35 | //FX.chain = fx.map( v => v.__wrapped__ ) 36 | FX.chain.length = 0 37 | 38 | fx.forEach( v => { 39 | if( Array.isArray( v.__wrapped__ ) ) { 40 | v.__wrapped__.forEach( w => FX.chain.push( w ) ) 41 | }else{ 42 | FX.chain.push( v.__wrapped__ ) 43 | } 44 | }) 45 | }, 46 | 47 | export( obj ) { 48 | obj.Antialias = FX.Antialias 49 | obj.Blur = FX.Blur 50 | obj.Bloom = FX.Bloom 51 | obj.BloomOld = FX.BloomOld 52 | obj.Brightness = FX.Brightness 53 | obj.Contrast = FX.Contrast 54 | obj.Edge = FX.Edge 55 | obj.Focus = FX.Focus 56 | obj.Glow = FX.Glow 57 | obj.Godrays = FX.Godrays 58 | obj.Hue = FX.Hue 59 | obj.Invert = FX.Invert 60 | obj.MotionBlur = FX.MotionBlur 61 | }, 62 | 63 | wrapProperty( obj, name, __value, transform=null ) { 64 | __value = transform === null ? __value : transform( __value ) 65 | const primitive = MP.float( MP.mut( __value ) ) 66 | 67 | let value = __value 68 | Object.defineProperty( obj, name, { 69 | get() { return value }, 70 | set(v) { 71 | value = transform === null ? v : transform( v ) 72 | primitive.setVal( value ) 73 | } 74 | }) 75 | 76 | return primitive 77 | }, 78 | 79 | Bloom( __threshold=0, __boost = .5, __horizontal=1, __vertical=1, taps = 9, reps = 3, num=1 ) { 80 | const fx = {}, 81 | threshold = FX.wrapProperty( fx, 'threshold', __threshold ), 82 | boost = FX.wrapProperty( fx, 'amount', __boost ), 83 | horizontal = FX.wrapProperty( fx, 'vertical', __vertical ), 84 | vertical = FX.wrapProperty( fx, 'horizontal', __horizontal ) 85 | 86 | fx.__wrapped__ = MP.bloom( threshold, horizontal, vertical, boost, num, taps, reps ) 87 | 88 | return fx 89 | }, 90 | 91 | BloomOld( __threshold=0, __boost=.5 ) { 92 | const fx = {}, 93 | threshold = FX.wrapProperty( fx, 'threshold', __threshold ), 94 | boost = FX.wrapProperty( fx, 'amount', __boost ) 95 | 96 | fx.__wrapped__ = MP.bloom( threshold, boost ) 97 | 98 | return fx 99 | }, 100 | 101 | Blur( amount=3, reps=2, taps=5 ) { 102 | const fx = {} 103 | 104 | const __amount = FX.wrapProperty( fx, 'amount', amount ) 105 | fx.__wrapped__ = MP.blur2d( __amount, __amount, reps, taps ) 106 | 107 | return fx 108 | }, 109 | 110 | Brightness( __amount=.25 ) { 111 | const fx = {}, 112 | amount = FX.wrapProperty( fx, 'amount', __amount ) 113 | 114 | fx.__wrapped__ = MP.brightness( amount ) 115 | 116 | return fx 117 | }, 118 | 119 | Contrast( __amount=.5 ) { 120 | const fx = {}, 121 | amount = FX.wrapProperty( fx, 'amount', __amount ) 122 | 123 | fx.__wrapped__ = MP.contrast( amount ) 124 | 125 | return fx 126 | }, 127 | 128 | Edge( mode=0, color=1 ) { 129 | const fx = {} 130 | 131 | switch( mode ) { 132 | case 0: fx.__wrapped__ = MP.sobel(); break; 133 | case 1: fx.__wrapped__ = MP.edge( color,0 ); break; 134 | case 2: fx.__wrapped__ = MP.edgecolor( MP.vec4(...color) ); break; 135 | } 136 | 137 | return fx 138 | }, 139 | 140 | Focus( __depth=0, __radius=.01 ) { 141 | const fx = {}, 142 | depth = FX.wrapProperty( fx, 'depth', __depth, v => 1 - v ), 143 | radius = FX.wrapProperty( fx, 'radius', __radius ) 144 | 145 | fx.__wrapped__ = MP.dof( depth, radius ) 146 | 147 | return fx 148 | }, 149 | 150 | Glow( __contrast=1.2, __brightness = .15, __blur=1, __adjust=-.5, loops=5 ) { 151 | const fx = {}, 152 | contrast = FX.wrapProperty( fx, 'contrast', __contrast ), 153 | brightness = FX.wrapProperty( fx, 'brightness', __brightness ), 154 | blur = FX.wrapProperty( fx, 'blur', __blur ), 155 | adjust = FX.wrapProperty( fx, 'adjust', __adjust ) 156 | 157 | fx.__wrapped__ = [ 158 | MP.loop([ 159 | MP.gauss(MP.vec2(blur, 0)), 160 | MP.gauss(MP.vec2(0, blur)), 161 | MP.brightness( brightness ), 162 | MP.contrast( contrast), 163 | ], loops ), 164 | MP.brightness( adjust ), 165 | MP.op( MP.fcolor(), "+", MP.input() ) 166 | ] 167 | 168 | return fx 169 | }, 170 | 171 | 172 | Godrays( __decay=1, __weight=.01, __density=1, __threshold=.9, __newColor=[.5,.15,0,1] ) { 173 | const fx = {}, 174 | decay = FX.wrapProperty( fx, 'decay', __decay ), 175 | weight = FX.wrapProperty( fx, 'weight', __weight ), 176 | density = FX.wrapProperty( fx, 'density', __density ), 177 | threshold = FX.wrapProperty( fx, 'threshold', __threshold, v => 1 - v ) 178 | 179 | const newColor = MP.mut( MP.pvec4( ...__newColor ) ) 180 | 181 | let value = __newColor 182 | Object.defineProperty( fx, 'color', { 183 | get() { return value }, 184 | set(v) { 185 | value = Array.isArray(v) ? v : [v,v,v,v] 186 | fx.__wrapped__.setNewColor( MP.pvec4( ...value ) ) 187 | } 188 | }) 189 | 190 | 191 | fx.__wrapped__ = MP.godrays({ 192 | decay, weight, density, 193 | 194 | convertDepth: { 195 | threshold, 196 | newColor 197 | } 198 | }) 199 | 200 | return fx 201 | }, 202 | 203 | Hue( __shift=.5, __threshold = .99 ) { 204 | const fx = {}, 205 | frag = MP.fcolor(), 206 | depth = MP.channel(0), 207 | shift = FX.wrapProperty( fx, 'shift', __shift ), 208 | threshold = FX.wrapProperty( fx, 'threshold', __threshold, v => 1 - v ) 209 | 210 | let control 211 | fx.__wrapped__ = MP.hsv2rgb( 212 | MP.changecomp( 213 | MP.rgb2hsv( MP.fcolor() ), 214 | MP.cfloat( MP.tag `length(${depth}.rgb) >= ${threshold} ? ${shift} : 0.` ), 215 | "r", 216 | "+" 217 | ) 218 | ) 219 | 220 | return fx 221 | }, 222 | 223 | Invert( __threshold = .99 ) { 224 | const fx = {}, 225 | frag = MP.fcolor(), 226 | depth = MP.channel(0), 227 | threshold = FX.wrapProperty( fx, 'threshold', __threshold, v => 1 - v ) 228 | 229 | fx.__wrapped__ = MP.cvec4( MP.tag `length(${depth}.rgb) >= ${threshold} ? (1. - vec4(${frag}.rgb, 0.)) : ${frag}` ) 230 | 231 | 232 | return fx 233 | }, 234 | 235 | MotionBlur( __amount = .7) { 236 | const fx = {}, 237 | amount = FX.wrapProperty( fx, 'amount', __amount, v => 1-v ) 238 | 239 | // 1 is the sampler for blur, and 2 is for motionbblur 240 | fx.__wrapped__ = MP.motionblur( 2, amount ) 241 | 242 | return fx 243 | }, 244 | 245 | Antialias( mult=1 ) { 246 | return { __wrapped__: MP.loop([ MP.fxaa() ], mult ) } 247 | }, 248 | 249 | } 250 | 251 | module.exports = FX 252 | -------------------------------------------------------------------------------- /js/vec.js: -------------------------------------------------------------------------------- 1 | // add functions to animation callback if needed 2 | const process = function( vec, dim, arg ) { 3 | 4 | if( typeof arg === 'function' ) { 5 | const fnc = arg 6 | Marching.postrendercallbacks.push( t => { 7 | vec[ dim ] = fnc( t ) 8 | vec.dirty = true 9 | vec.var.dirty = true 10 | }) 11 | 12 | // set initial value with t=0 13 | arg = fnc( 0 ) 14 | } 15 | 16 | return arg 17 | } 18 | 19 | const Vec2 = function (x=0, y=0) { 20 | if( x.type === 'vec2' ) return x 21 | const v = Object.create( Vec2.prototype ) 22 | if( Array.isArray( x ) ) { 23 | v.x = process(v, 'x', x[0]); v.y = process(v, 'y', x[1]); 24 | } else if( y === undefined ) { 25 | v.x = process(v, 'x', x) 26 | v.y = process(v, 'y', x) 27 | }else{ 28 | v.x = process(v,'x',x); v.y = process(v,'y',y); 29 | } 30 | 31 | return v 32 | } 33 | 34 | Vec2.prototype = { 35 | type: 'vec2', 36 | emit() { return "vec2(" + this.x + "," + this.y + ")" }, 37 | emit_decl() { return ""; }, 38 | copy() { 39 | return Vec2( this.x, this.y ) 40 | } 41 | } 42 | 43 | const Vec3 = function (x=0, y, z) { 44 | if( x.type === 'vec3' ) return x 45 | const v = Object.create( Vec3.prototype ) 46 | let vx =0,vy=0,vz=0 47 | Object.defineProperties( v, { 48 | x: { 49 | get() { return vx }, 50 | set(v) { vx = v; this.dirty = true; } 51 | }, 52 | 53 | y: { 54 | get() { return vy }, 55 | set(v) { vy = v; this.dirty = true; } 56 | }, 57 | 58 | z: { 59 | get() { return vz }, 60 | set(v) { vz = v; this.dirty = true; } 61 | }, 62 | r: { 63 | get() { return vx }, 64 | set(v) { vx = v; this.dirty = true; } 65 | }, 66 | 67 | g: { 68 | get() { return vy }, 69 | set(v) { vy = v; this.dirty = true; } 70 | }, 71 | 72 | b: { 73 | get() { return vz }, 74 | set(v) { vz = v; this.dirty = true; } 75 | }, 76 | 77 | }) 78 | 79 | if( Array.isArray( x ) ) { 80 | v.x = process(v,'x',x[0]); v.y = process(v,'y',x[1]); v.z = process(v,'z',x[2]); 81 | } else if( y === undefined ) { 82 | v.x = process(v, 'x', x) 83 | v.y = process(v, 'y', x) 84 | v.z = process(v, 'z', x) 85 | }else{ 86 | v.x = process(v,'x',x); v.y = process(v,'y',y); v.z=process(v,'z',z) 87 | } 88 | 89 | v.isGen = v.x.type === 'string' || v.y.type === 'string' || v.z.type === 'string' 90 | return v 91 | }; 92 | 93 | Vec3.prototype = { 94 | type: 'vec3', 95 | emit() { 96 | let out = `vec3(` 97 | let preface = '' 98 | 99 | if( this.x.type === 'string' ) { 100 | const xout = this.x.emit() 101 | out += xout.out + ',' 102 | }else{ 103 | out += this.x + ',' 104 | } 105 | 106 | if( this.y.type === 'string' ) { 107 | const yout = this.y.emit() 108 | out += yout.out + ',' 109 | }else{ 110 | out += this.y + ',' 111 | } 112 | if( this.z.type === 'string' ) { 113 | const zout = this.z.emit() 114 | out += zout.out 115 | }else{ 116 | out += this.z 117 | } 118 | 119 | out += ')' 120 | 121 | return { out, preface } 122 | }, 123 | emit_decl() { 124 | let out = '' 125 | if( this.x.type === 'string' ) { 126 | out += this.x.emit_decl() 127 | } 128 | if( this.y.type === 'string' && this.x !== this.y ) { 129 | out += this.y.emit_decl() 130 | } 131 | if( this.z.type === 'string' && this.z !== this.y && this.z !== this.x ) { 132 | out += this.z.emit_decl() 133 | } 134 | return out 135 | }, 136 | 137 | update_location(gl, program) { 138 | if( this.isGen ) { 139 | if( this.x.type === 'string' ) { 140 | this.x.update_location(gl,program) 141 | } 142 | if( this.y.type === 'string' && this.x !== this.y ) { 143 | this.y.update_location(gl,program) 144 | } 145 | if( this.z.type === 'string' && this.z !== this.y && this.z !== this.x ) { 146 | this.z.update_location(gl,program) 147 | } 148 | } 149 | }, 150 | 151 | upload_data(gl) { 152 | if( this.isGen ) { 153 | if( this.x.type === 'string' ) { 154 | this.x.upload_data(gl) 155 | } 156 | if( this.y.type === 'string' && this.x !== this.y ) { 157 | this.y.upload_data(gl) 158 | } 159 | if( this.z.type === 'string' && this.z !== this.y && this.z !== this.x ) { 160 | this.z.upload_data(gl) 161 | } 162 | } 163 | }, 164 | 165 | copy() { 166 | return Vec3( this.x, this.y, this.z ) 167 | } 168 | 169 | } 170 | 171 | const Vec4 = function (x=0, y, z, w) { 172 | if( x.type === 'vec4' ) return x 173 | const v = Object.create( Vec4.prototype ) 174 | 175 | if( Array.isArray( x ) ) { 176 | v.x = x[0]; v.y = x[1]; v.z = x[2]; v.w = x[3] 177 | } else if( y === undefined && z === undefined) { 178 | v.x = v.y = v.z = v.w = x 179 | }else{ 180 | v.x = x; v.y = y; v.z = z; v.w = w;; 181 | } 182 | 183 | v.isGen = v.x.type === 'string' || v.y.type === 'string' || v.z.type === 'string' 184 | 185 | return v 186 | }; 187 | 188 | Vec4.prototype = { 189 | type: 'vec4', 190 | emit() { 191 | let out = `vec4(` 192 | let preface = '' 193 | 194 | if( this.x.type === 'string' ) { 195 | const xout = this.x.emit() 196 | out += xout.out + ',' 197 | }else{ 198 | out += this.x + ',' 199 | } 200 | 201 | if( this.y.type === 'string' ) { 202 | const yout = this.y.emit() 203 | out += yout.out + ',' 204 | }else{ 205 | out += this.y + ',' 206 | } 207 | 208 | if( this.z.type === 'string' ) { 209 | const zout = this.z.emit() 210 | out += zout.out 211 | }else{ 212 | out += this.z 213 | } 214 | 215 | if( this.w.type === 'string' ) { 216 | const wout = this.w.emit() 217 | out += wout.out 218 | }else{ 219 | out += this.w 220 | } 221 | 222 | out += ')' 223 | 224 | return { out, preface } 225 | }, 226 | emit_decl() { 227 | let out = '' 228 | if( this.x.type === 'string' ) { 229 | out += this.x.emit_decl() 230 | } 231 | if( this.y.type === 'string' && this.x !== this.y ) { 232 | out += this.y.emit_decl() 233 | } 234 | if( this.z.type === 'string' && this.z !== this.y && this.z !== this.x ) { 235 | out += this.z.emit_decl() 236 | } 237 | if( this.w.type === 'string' && this.w !== this.y && this.w !== this.x && this.w !== this.z ) { 238 | out += this.w.emit_decl() 239 | } 240 | return out 241 | }, 242 | 243 | update_location(gl, program) { 244 | if( this.isGen ) { 245 | if( this.x.type === 'string' ) { 246 | this.x.update_location(gl,program) 247 | } 248 | if( this.y.type === 'string' && this.x !== this.y ) { 249 | this.y.update_location(gl,program) 250 | } 251 | if( this.z.type === 'string' && this.z !== this.y && this.z !== this.x ) { 252 | this.z.update_location(gl,program) 253 | } 254 | if( this.w.type === 'string' && this.w !== this.y && this.w !== this.x && this.w !== this.z ) { 255 | this.w.update_location(gl,program) 256 | } 257 | } 258 | }, 259 | 260 | upload_data(gl) { 261 | if( this.isGen ) { 262 | if( this.x.type === 'string' ) { 263 | this.x.upload_data(gl) 264 | } 265 | if( this.y.type === 'string' && this.x !== this.y ) { 266 | this.y.upload_data(gl) 267 | } 268 | if( this.z.type === 'string' && this.z !== this.y && this.z !== this.x ) { 269 | this.z.upload_data(gl) 270 | } 271 | if( this.w.type === 'string' && this.w !== this.y && this.w !== this.x && this.w !== this.z ) { 272 | this.w.upload_data(gl) 273 | } 274 | } 275 | }, 276 | 277 | copy() { 278 | return Vec4( this.x, this.y, this.z, this.w ) 279 | } 280 | } 281 | // Vec4 282 | 283 | //let Vec4 = function (x, y, z, w) { 284 | // const v = Object.create( Vec4.prototype ) 285 | // v.x = x; v.y = y; v.z = z; v.w = w 286 | 287 | // return v 288 | //}; 289 | 290 | //Vec4.prototype = { 291 | // type: 'vec4', 292 | // emit() { return "vec4(" + this.x + "," + this.y + "," + this.z + "," + this.w + ")"; }, 293 | // emit_decl() { return ""; } 294 | //} 295 | 296 | 297 | 298 | 299 | 300 | module.exports = { Vec2, Vec3, Vec4 } 301 | -------------------------------------------------------------------------------- /js/camera.js: -------------------------------------------------------------------------------- 1 | const vec3 = require('gl-vec3') 2 | const mat4 = require('gl-mat4') 3 | 4 | // camera adapted from https://github.com/shama/first-person-camera 5 | function FirstPersonCamera(opts) { 6 | if (!(this instanceof FirstPersonCamera)) return new FirstPersonCamera(opts) 7 | opts = opts || {} 8 | this.position = opts.position || vec3.create() 9 | this.rotation = opts.rotation || vec3.create() 10 | this.positionSpeed = opts.positionSpeed || -.5 11 | this.rotationSpeed = opts.rotationSpeed || .01 12 | } 13 | module.exports = FirstPersonCamera 14 | 15 | FirstPersonCamera.prototype.view = function(out) { 16 | if (!out) out = mat4.create() 17 | // altered x/y ordering from original 18 | mat4.rotateY(out, out, this.rotation[1]) 19 | mat4.rotateX(out, out, this.rotation[0]) 20 | mat4.rotateZ(out, out, this.rotation[2] - Math.PI) 21 | mat4.translate(out, out, [-this.position[0], -this.position[1], -this.position[2]]) 22 | 23 | return out 24 | } 25 | 26 | FirstPersonCamera.prototype.control = function(dt, move, mouse, prevMouse) { 27 | var speed = (this.positionSpeed / 1000) * dt 28 | var dir = [0,0,0] 29 | if (move[0]) dir[2] -= speed * (Marching.keys.Alt ? 4 : 1 ) 30 | else if (move[1]) dir[2] += speed * (Marching.keys.Alt ? 4 : 1 ) 31 | if (move[2]) dir[0] += speed * (Marching.keys.Alt ? 4 : 1 ) 32 | else if (move[3]) dir[0] -= speed * (Marching.keys.Alt ? 4 : 1 ) 33 | if (move[4]) dir[1] -= speed * (Marching.keys.Alt ? 4 : 1 ) 34 | else if (move[5]) dir[1] += speed * (Marching.keys.Alt ? 4 : 1 ) 35 | this.move(dir) 36 | // just use arrow keys instead of mouse 37 | // this.pointer(mouse, prevMouse) 38 | } 39 | 40 | FirstPersonCamera.prototype.move = function(dir) { 41 | if (dir[0] !== 0 || dir[1] !== 0 || dir[2] !== 0) { 42 | var cam = mat4.create() 43 | mat4.rotateY(cam, cam, this.rotation[1]) 44 | mat4.rotateX(cam, cam, this.rotation[0]) 45 | vec3.transformMat4(dir, dir, cam) 46 | vec3.add(this.position, this.position, dir) 47 | this.parent.pos.dirty = true 48 | 49 | } 50 | } 51 | 52 | //FirstPersonCamera.prototype.pointer = function(da, db) { 53 | // var dt = [da[0] - db[0], da[1]- db[1]] 54 | // var rot = this.rotation 55 | // rot[1] -= dt[0] * this.rotationSpeed 56 | // if (rot[1] < 0) rot[1] += Math.PI * 2 57 | // if (rot[1] >= Math.PI * 2) rot[1] -= Math.PI * 2 58 | // rot[0] -= dt[1] * this.rotationSpeed 59 | // if (rot[0] < -Math.PI * .5) rot[0] = -Math.PI*0.5 60 | // if (rot[0] > Math.PI * .5) rot[0] = Math.PI*0.5 61 | //} 62 | 63 | const Camera = { 64 | init( gl, program, handler ) { 65 | 66 | const camera = FirstPersonCamera({ 67 | fov: 190, 68 | near:.01, 69 | far:10, 70 | direction:[0,0,1], 71 | viewport:[1,1,1,-1] 72 | }) 73 | camera.rotation = [0,Math.PI,Math.PI] 74 | Camera.__camera = camera 75 | camera.parent = this 76 | 77 | const camera_pos = gl.getUniformLocation( program, 'camera_pos' ) 78 | const camera_normal = gl.getUniformLocation( program, 'camera_normal' ) 79 | const camera_rot = gl.getUniformLocation( program, 'camera_rot' ) 80 | const ucamera = gl.getUniformLocation( program, 'camera' ) 81 | 82 | this.pos = { dirty:false } 83 | this.dir = { dirty:true } 84 | this.__rot = { dirty:true, value:0 } 85 | 86 | Object.defineProperty( this, 'rotation', { 87 | configurable:true, 88 | get() { return this.__rot.value }, 89 | set(v) { 90 | this.__rot.value = v 91 | this.__rot.dirty = true 92 | } 93 | }) 94 | 95 | let px = 0, py =0, pz = 5, nx = 0, ny = 0, nz = 0 96 | Object.defineProperties( this.pos, { 97 | x: { 98 | get() { return px }, 99 | set(v) { px = camera.position[0] = v;this.dirty = true; } 100 | }, 101 | 102 | y: { 103 | get() { return py }, 104 | set(v) { py = camera.position[1] = v; this.dirty = true; } 105 | }, 106 | 107 | z: { 108 | get() { return pz }, 109 | set(v) { pz = camera.position[2] = v; this.dirty = true; } 110 | }, 111 | }) 112 | 113 | Object.defineProperties( this.dir, { 114 | x: { 115 | get() { return nx }, 116 | set(v) { nx = camera.rotation[0] = v; this.dirty = true; } 117 | }, 118 | 119 | y: { 120 | get() { return ny }, 121 | set(v) { ny = camera.rotation[1] = v; this.dirty = true; } 122 | }, 123 | 124 | z: { 125 | get() { return nz }, 126 | set(v) { nz = camera.rotation[2] = v; this.dirty = true; } 127 | }, 128 | }) 129 | 130 | let init = false 131 | gl.uniform3f( camera_normal, this.dir.x, this.dir.y, this.dir.z ) 132 | camera.position = [this.pos.x, this.pos.y, this.pos.z ] 133 | //camera.update() 134 | gl.uniform3f( camera_pos, this.pos.x, this.pos.y, this.pos.z ) 135 | gl.uniformMatrix4fv( ucamera, false, camera.view() ) 136 | gl.uniform1f( camera_rot, this.rot ) 137 | 138 | Camera.move = (x,y,z) => { 139 | // XXX does this need to update property values? 140 | camera.move([x,y,z]) 141 | Camera.update() 142 | } 143 | Camera.moveTo = (x,y,z) => { 144 | Camera.pos.x = x 145 | Camera.pos.y = y 146 | Camera.pos.z = z 147 | } 148 | Camera.update = ()=> { 149 | const pos = camera.position 150 | gl.uniform3f( camera_pos, pos[0], pos[1], pos[2] ) 151 | gl.uniformMatrix4fv( ucamera, false, camera.view() ) 152 | } 153 | 154 | // determine an offset from the current camera position based 155 | // on the current camera rotation e.g. to always position a light 156 | // behind the camera. 157 | Camera.offset = (amt=[0,0,3]) => { 158 | const cam = mat4.create() 159 | mat4.rotateY(cam, cam, camera.rotation[1]) 160 | mat4.rotateX(cam, cam, camera.rotation[0]) 161 | vec3.transformMat4(amt, amt, cam) 162 | return amt 163 | } 164 | 165 | let prvx = 0, prvy = 0, x = 0, y = 0 166 | Camera.__mousemovefnc = e => { 167 | prvx = x 168 | prvy = y 169 | x = e.pageX 170 | y = e.pageY 171 | } 172 | 173 | let prevTime = 0 174 | let k = Marching.keys 175 | Camera.__framefnc = t => { 176 | if( k.ArrowLeft ) camera.rotation[1] += camera.rotationSpeed 177 | if( k.ArrowRight ) camera.rotation[1] -= camera.rotationSpeed 178 | if( k.ArrowUp && !k.Shift ) camera.rotation[0] -= camera.rotationSpeed 179 | if( k.ArrowDown && !k.Shift) camera.rotation[0] += camera.rotationSpeed 180 | 181 | if( Marching.cameraEnabled ) { 182 | camera.control( 183 | t*1000 - prevTime, 184 | [k.w,k.s,k.d,k.a,k.ArrowUp && k.Shift, k.ArrowDown && k.Shift], 185 | [x,y], [prvx,prvy] 186 | ) 187 | Camera.update() 188 | prvx = x 189 | prvy = y 190 | prevTime = t*1000 191 | } 192 | } 193 | 194 | Camera.__mousemove = null 195 | Camera.on = ()=> { 196 | if( Camera.__mousemove === null ) { 197 | window.addEventListener( 'mousemove', Camera.__mousemovefnc ) 198 | Camera.__mousemove = true 199 | } 200 | if( Marching.callbacks.indexOf( Camera.__framefnc ) === -1 ) { 201 | Marching.callbacks.push( Camera.__framefnc ) 202 | } 203 | } 204 | 205 | handler( ()=> { 206 | if( this.pos.dirty === true ) { 207 | 208 | //camera.position = [this.pos.x, this.pos.y, this.pos.z ] 209 | 210 | //camera.position = [this.pos.x, this.pos.y, this.pos.z ] 211 | //camera.update() 212 | const pos = camera.position 213 | gl.uniform3f( camera_pos, pos[0], pos[1], pos[2] ) 214 | gl.uniformMatrix4fv( ucamera, false, camera.view() ) 215 | this.pos.dirty = false 216 | 217 | 218 | } 219 | 220 | // XXX this is broken and needs to be fixed 221 | if( this.dir.dirty === true ) { 222 | gl.uniform3f( camera_normal, this.dir.x, this.dir.y, this.dir.z ) 223 | gl.uniformMatrix4fv( ucamera, false, camera.view() ) 224 | this.dir.dirty = false 225 | } 226 | if( this.__rot.dirty === true ) { 227 | gl.uniform1f( camera_rot, this.__rot.value ) 228 | this.__rot.dirty = false 229 | } 230 | if( typeof this.onmove === 'function' ) { 231 | this.onmove( this ) 232 | } 233 | }) 234 | 235 | } 236 | } 237 | 238 | module.exports = Camera 239 | -------------------------------------------------------------------------------- /js/renderFragmentShader.js: -------------------------------------------------------------------------------- 1 | const getMainContinuous = function( steps, minDistance, maxDistance, postprocessing, bg='vec4(0.,0.,0.,1.)' ) { 2 | const out = ` 3 | // adapted from https://www.shadertoy.com/view/ldfSWs 4 | vec3 calcNormal(vec3 pos, float eps) { 5 | const vec3 v1 = vec3( 1.0,-1.0,-1.0); 6 | const vec3 v2 = vec3(-1.0,-1.0, 1.0); 7 | const vec3 v3 = vec3(-1.0, 1.0,-1.0); 8 | const vec3 v4 = vec3( 1.0, 1.0, 1.0); 9 | 10 | return normalize( v1 * scene ( pos + v1*eps ).x+ 11 | v2 * scene ( pos + v2*eps ).x+ 12 | v3 * scene ( pos + v3*eps ).x+ 13 | v4 * scene ( pos + v4*eps ).x); 14 | } 15 | 16 | vec3 calcNormal(vec3 pos) { 17 | return calcNormal(pos, 0.002); 18 | } 19 | 20 | // Adapted from from https://www.shadertoy.com/view/ldfSWs 21 | vec2 calcRayIntersection( vec3 rayOrigin, vec3 rayDir, float maxd, float precis ) { 22 | float latest = precis * 2.0; 23 | float dist = +0.0; 24 | float type = -1.0; 25 | vec2 result; 26 | vec2 res = vec2(-50000., -1.);; 27 | 28 | for (int i = 0; i < ${steps} ; i++) { 29 | if (latest < precis || dist > maxd) break; 30 | 31 | result = scene(rayOrigin + rayDir * dist); 32 | 33 | latest = result.x; 34 | dist += latest; 35 | } 36 | 37 | if( dist < maxd ) { 38 | result.x = dist; 39 | res = result; 40 | } 41 | 42 | return res; 43 | } 44 | 45 | layout(location = 0) out vec4 col; 46 | layout(location = 1) out vec4 depth; 47 | void main() { 48 | vec2 uv = gl_FragCoord.xy / resolution; 49 | vec2 pos = uv * 2.0 - 1.0; 50 | 51 | // not sure why I need the -y axis but without it 52 | // everything is flipped using perspective-camera 53 | pos.x *= ( resolution.x / -resolution.y ); 54 | 55 | vec4 color = bg; 56 | vec3 ro = camera_pos; 57 | vec3 rd = normalize( mat3(camera) * vec3( pos, 2. ) ); 58 | 59 | vec2 t = calcRayIntersection( ro, rd, ${maxDistance}, ${minDistance} ); 60 | 61 | vec3 samplePos = vec3(100.f); 62 | if( t.x > -0.5 ) { 63 | samplePos = ro + rd * t.x; 64 | vec3 nor = calcNormal( samplePos ); 65 | 66 | color = vec4( lighting( samplePos, nor, ro, rd, t.y, true ), 1. ); 67 | ${postprocessing} 68 | } 69 | 70 | 71 | col = clamp( vec4( color ), 0., 1. ); 72 | 73 | float normalizedDepth = t.x / ${maxDistance}; 74 | depth = abs(samplePos.z - ro.z ) < ${maxDistance} ? vec4( vec3( 1.-normalizedDepth ), 1. ) : vec4(0.); 75 | }` 76 | 77 | return out 78 | } 79 | 80 | const getMainVoxels = function( steps, postprocessing, voxelSize = .05 ) { 81 | const out = ` 82 | struct VoxelDistance { 83 | bvec3 mask; 84 | vec3 distance; 85 | float fogCoeff; 86 | int id; 87 | }; 88 | 89 | VoxelDistance calcRayIntersection( vec3 rayOrigin, vec3 rayDir ) { 90 | vec2 result; 91 | 92 | float m = ${voxelSize.toFixed(2)}; 93 | rayOrigin *= 1./m; 94 | vec3 mapPos = vec3(floor(rayOrigin)); 95 | vec3 diff = mapPos - rayOrigin; 96 | 97 | vec3 deltaDist = abs(vec3(length(rayDir)) / rayDir); 98 | vec3 rayStep = vec3(sign(rayDir)); 99 | vec3 sideDist = (sign(rayDir) * diff + (sign(rayDir) * 0.5) + 0.5) * deltaDist; 100 | 101 | bvec3 mask; 102 | vec3 d = vec3(-100000.); 103 | float fogCoeff = 0.; 104 | 105 | for (int i = 0; i < ${Math.round(steps*1/voxelSize)} ; i++) { 106 | result = scene(mapPos*m); 107 | if( result.x <= 0. ) { 108 | d = mapPos*m+result.x; 109 | break; 110 | } 111 | 112 | mask = bvec3( lessThanEqual(sideDist.xyz, min(sideDist.yzx, sideDist.zxy)) ); 113 | sideDist += vec3( mask ) * deltaDist; 114 | mapPos += vec3(mask) * rayStep; 115 | fogCoeff += result.x * m; 116 | } 117 | 118 | VoxelDistance vd = VoxelDistance( mask, d, fogCoeff, int(result.y) ); 119 | return vd; 120 | } 121 | 122 | layout(location = 0) out vec4 col; 123 | layout(location = 1) out vec4 depth; 124 | void main() { 125 | vec2 uv = gl_FragCoord.xy / resolution; 126 | vec2 pos = uv * 2.0 - 1.0; 127 | 128 | // not sure why I need the -y axis but without it 129 | // everything is flipped using perspective-camera 130 | pos.x *= ( resolution.x / -resolution.y ); 131 | 132 | vec4 color = bg; 133 | vec3 ro = camera_pos; 134 | vec3 rd = normalize( mat3(camera) * vec3( pos, 2. ) ); 135 | 136 | VoxelDistance vd = calcRayIntersection( ro, rd ); 137 | bvec3 mask = vd.mask; 138 | 139 | vec3 nor; 140 | if (mask.x) { 141 | color = vec4(vec3(0.5), 1.); 142 | nor = vec3(1.,0.,0.); 143 | } 144 | if (mask.y) { 145 | color = vec4( vec3(1.0), 1. ); 146 | nor = vec3(0.,1.,0.); 147 | } 148 | if (mask.z) { 149 | color = vec4( vec3(0.75), 1. ); 150 | nor = vec3(0.,0.,1.); 151 | } 152 | if( vd.distance.x == -100000. ) { 153 | color = bg; 154 | } 155 | 156 | float modAmount = ${(1./voxelSize).toFixed(2)}; 157 | bool hit = false; 158 | vec3 t = vec3( length(vd.distance-ro) ); 159 | 160 | if( color != bg ) { 161 | vec3 pos = vd.distance; 162 | color.xyz *= lighting( pos * modAmount, nor, ro, rd, float(vd.id), false ); 163 | hit = true; 164 | ${postprocessing}; 165 | } 166 | 167 | col = color; 168 | 169 | float normalizedDepth = length( (vd.distance-ro) * ${voxelSize.toFixed(2)} ); 170 | depth = hit == true ? vec4( vec3(1.-normalizedDepth), 1. ) : vec4(0.); 171 | }` 172 | 173 | return out 174 | } 175 | 176 | module.exports = function( variables, scene, preface, geometries, lighting, postprocessing, steps=90, minDistance=.001, maxDistance=20, ops, useVoxels=false, voxelSize=0 ) { 177 | 178 | const main = useVoxels === false 179 | ? getMainContinuous( steps, minDistance, maxDistance, postprocessing ) 180 | : getMainVoxels( steps, postprocessing, voxelSize ) 181 | 182 | const fs_source = ` #version 300 es 183 | precision highp float; 184 | 185 | float PI = 3.141592653589793; 186 | 187 | struct Light { 188 | vec3 position; 189 | vec3 color; 190 | float attenuation; 191 | }; 192 | 193 | int rotationCount = 1; 194 | 195 | mat4 rotations[4] = mat4[4]( 196 | mat4(0.), mat4(0.), mat4(0.), mat4(0.) 197 | ); 198 | 199 | struct Material { 200 | int mode; 201 | vec3 ambient; 202 | vec3 diffuse; 203 | vec3 specular; 204 | float shininess; 205 | vec3 fresnel; 206 | int textureID; 207 | }; 208 | 209 | struct SDF { 210 | int materialID; 211 | mat4 transform; 212 | int textureID; 213 | vec3 repeat; 214 | mat4 repeatTransform; 215 | }; 216 | 217 | uniform vec3 camera_pos; 218 | uniform vec3 camera_normal; 219 | uniform float camera_rot; 220 | uniform mat4 camera; 221 | 222 | vec2 resolution = vec2( ${ Marching.__scene.width }., ${Marching.__scene.height}. ); 223 | 224 | ${variables} 225 | 226 | // must be before geometries! 227 | float length8( vec2 p ) { 228 | return float( pow( pow(p.x,8.)+pow(p.y,8.), 1./8. ) ); 229 | } 230 | 231 | vec4 opElongate( in vec3 p, in vec3 h ) { 232 | //return vec4( p-clamp(p,-h,h), 0.0 ); // faster, but produces zero in the interior elongated box 233 | 234 | vec3 q = abs(p)-h; 235 | return vec4( max(q,0.0), min(max(q.x,max(q.y,q.z)),0.0) ); 236 | } 237 | ${ops} 238 | 239 | /* GEOMETRIES */ 240 | ${geometries} 241 | 242 | vec2 scene(vec3 p); 243 | 244 | // XXX todo put this in domainOperations.js 245 | vec3 polarRepeat(vec3 p, float repetitions) { 246 | float angle = 2.*PI/repetitions; 247 | float a = atan(p.z, p.x) + angle/2.; 248 | float r = length(p.xz); 249 | float c = floor(a/angle); 250 | a = mod(a,angle) - angle/2.; 251 | vec3 _p = vec3( cos(a) * r, p.y, sin(a) * r ); 252 | // For an odd number of repetitions, fix cell index of the cell in -x direction 253 | // (cell index would be e.g. -5 and 5 in the two halves of the cell): 254 | if (abs(c) >= (repetitions/2.)) c = abs(c); 255 | return _p; 256 | } 257 | 258 | // XXX this shouldn't be here... 259 | float opHalve( in float sdf, vec4 p, in int dir ){ 260 | float _out = 0.; 261 | switch( dir ) { 262 | case 0: 263 | _out = max( sdf, p.y ); 264 | break; 265 | case 1: 266 | _out = max( sdf, -p.y ); 267 | break; 268 | case 2: 269 | _out = max( sdf, p.x ); 270 | break; 271 | case 3: 272 | _out = max( sdf, -p.x ); 273 | break; 274 | } 275 | 276 | return _out; 277 | } 278 | 279 | // taken from https://iquilezles.org/articles/rmshadows/ 280 | float softshadow( in vec3 ro, in vec3 rd, float mint, float maxt, float w ) 281 | { 282 | float res = 1.0; 283 | float t = mint; 284 | for( int i=0; i<256 && tmaxt ) break; 290 | } 291 | res = max(res,-1.0); 292 | return 0.25*(1.0+res)*(1.0+res)*(2.0-res); 293 | } 294 | 295 | ${lighting} 296 | 297 | vec2 scene(vec3 _p ) { 298 | vec4 p = vec4( _p, 1. ); 299 | ${preface} 300 | return ${scene}; 301 | } 302 | 303 | ${main} 304 | ` 305 | 306 | return fs_source 307 | } 308 | -------------------------------------------------------------------------------- /js/scene.js: -------------------------------------------------------------------------------- 1 | const getFog = require( './fog.js' ) 2 | const vignette = require( './vignette.js' ) 3 | const { param_wrap, MaterialID } = require( './utils.js' ) 4 | const __lighting = require( './lighting.js' ) 5 | const { Var, float_var_gen, vec2_var_gen, vec3_var_gen, vec4_var_gen, int_var_gen, VarAlloc } = require('./var.js') 6 | 7 | const getScene = function( SDF ) { 8 | 9 | Scene = function( objs, canvas, steps=100, minDistance=.001, maxDistance=40, size=2, shouldAnimate=false ) { 10 | const scene = Object.create( Scene.prototype ) 11 | 12 | MaterialID.clear() 13 | 14 | SDF.lighting.lights = [] 15 | 16 | scene.__prerender = objs 17 | if( objs.length > 1 ) { 18 | // reduce objects to nested Unions 19 | scene.__prerender = objs.reduce( ( current, next ) => SDF.Union( current, next ) ) 20 | } 21 | 22 | Object.assign( scene, { 23 | objs, 24 | canvas, 25 | postprocessing:[], 26 | __shadow:.2, 27 | __followLight:null, 28 | __postprocessingFlag:false, 29 | __steps:null, 30 | __thresold:null, 31 | __farPlane:null, 32 | __resolution:null, 33 | __voxelSize:.1 34 | }) 35 | 36 | scene.useQuality = true 37 | scene.useVoxels = false 38 | 39 | SDF.__scene = scene 40 | 41 | return scene 42 | } 43 | 44 | Scene.prototype = { 45 | animate( v ) { this.__animate = v; return this }, 46 | setdim( w, h ) { 47 | this.width = w 48 | this.height = h 49 | 50 | this.donotuseresolution = true 51 | return this 52 | }, 53 | resolution( v ) { 54 | this.width = Math.floor( this.canvas.width = window.innerWidth * v ) 55 | this.height = Math.floor( this.canvas.height = window.innerHeight * v ) 56 | 57 | this.__resolution = v; 58 | return this 59 | }, 60 | voxel( v = .1 ) { 61 | this.useVoxels = true 62 | this.__voxelSize = v 63 | return this 64 | }, 65 | threshold( v ) { this.__threshold = v; return this }, 66 | steps( v ) { this.__steps = v; return this }, 67 | farPlane( v ) { this.__farPlane = v; return this }, 68 | camera( x=0, y=0, z=5, speed=1 ) { 69 | SDF.camera.__camera.position[0] = x 70 | SDF.camera.__camera.position[1] = y 71 | SDF.camera.__camera.position[2] = z 72 | SDF.camera.__camera.rotationSpeed = speed * .01 73 | SDF.camera.__camera.positionSpeed = speed * -.25 74 | SDF.camera.update() 75 | return this 76 | }, 77 | shadow( k=0 ) { 78 | this.__shadow = k; 79 | return this; 80 | }, 81 | quality( quality=10 ) { 82 | if( this.__thresold === null ) this.threshold( .1 / (quality * quality * quality ) ) 83 | if( this.__steps === null ) this.steps( quality * 20 ) 84 | if( this.__farPlane === null ) this.farPlane( quality * 5 ) 85 | if( this.donotuseresolution === undefined && this.__resolultion === null ) this.resolution( Math.min( .2 * quality, 2 ) ) 86 | 87 | return this 88 | }, 89 | follow( light, distance=3 ) { 90 | this.__followLight = light 91 | SDF.camera.onmove = function( camera ) { 92 | const offset = SDF.camera.offset() 93 | light.pos.x = SDF.camera.__camera.position[0] - offset[0] 94 | light.pos.y = SDF.camera.__camera.position[1] - offset[1] 95 | light.pos.z = SDF.camera.__camera.position[2] - offset[2] 96 | light.dirty = true 97 | } 98 | SDF.lighting.lights = [light] 99 | return this 100 | }, 101 | light( ...lights ) { 102 | SDF.lighting.lights = SDF.lighting.lights.concat( lights ) 103 | if( this.__followLight !== null ) SDF.lighting.lights.push( this.__followLight ) 104 | return this 105 | }, 106 | fog: getFog( Scene, SDF ), 107 | vignette: vignette( Scene, SDF ), 108 | background: require( './background.js' )( Scene, SDF ), 109 | 110 | applyPreset( presetName ) { 111 | const preset = this.presets[ presetName ] 112 | if( preset === undefined ) { 113 | throw ReferenceError(`The render preset ${presetName} doesn't exist.`) 114 | } 115 | if( preset.farPlane !== undefined ) { 116 | this.farPlane( this.__farPlane || preset.farPlane ) 117 | }else{ 118 | this.__farPlane = 0 119 | } 120 | this.steps( this.__steps || preset.steps ) 121 | if( this.donotuseresolution === undefined ) this.resolution( this.__resolution || preset.resolution ) 122 | this.threshold( this.__threshold || preset.threshold || .001 ) 123 | this.useVoxels = presetName.indexOf( 'voxel' ) !== -1 124 | 125 | return preset.animated 126 | }, 127 | 128 | post( ...fx ) { 129 | this.__postprocessingFlag = true 130 | SDF.fx.clear() 131 | SDF.fx.post( ...fx ) 132 | return this 133 | }, 134 | 135 | render( quality=10, animate=false, useQuality=true, shouldResetTime=false ) { 136 | // adds default if none has been specified 137 | this.background() 138 | if( this.__postprocessingFlag === false ) { SDF.fx.clear() } 139 | 140 | if( typeof quality === 'string' ) { 141 | animate = this.applyPreset( quality ) 142 | }else if( this.useQuality === true ) { 143 | this.quality( quality ) 144 | } 145 | 146 | this.animate( animate ) 147 | 148 | //SDF.distanceOps.__clear() 149 | SDF.alterations.__clear() 150 | SDF.textures.clear() 151 | 152 | // MATERIALS MUST BE GENERATED BEFORE GEOMETRIES, 153 | // SO THAT GEOMETRIES CAN PROPERLY REFERENCE THEM 154 | SDF.materials.generate() 155 | const geometries = SDF.primitives.emit_geometries() 156 | 157 | let [ variablesDeclaration, sceneRendering, postprocessing ] = SDF.generateSDF( this ) 158 | //SDF.materials.generate( Marching.scene ) 159 | 160 | const lighting = SDF.lighting.gen( this.__shadow, geometries ) 161 | variablesDeclaration += SDF.materials.emit_decl() 162 | variablesDeclaration += SDF.textures.emit_decl() 163 | variablesDeclaration += SDF.lighting.emit_decl() 164 | variablesDeclaration += this.__background.emit_decl() 165 | 166 | if( this.width === undefined ) this.width = window.innerWidth 167 | if( this.height === undefined ) this.height = window.innerHeight 168 | 169 | this.fs = SDF.renderFragmentShader( 170 | variablesDeclaration, 171 | sceneRendering.out, 172 | sceneRendering.preface, 173 | SDF.requiredGeometries.join('\n') + SDF.requiredOps.join('\n'), 174 | lighting, 175 | postprocessing, 176 | this.__steps, this.__threshold, this.__farPlane.toFixed(1), 177 | SDF.distanceOps.__getGLSL() + SDF.alterations.__getGLSL(), 178 | this.useVoxels, 179 | this.__voxelSize 180 | ) 181 | 182 | 183 | const time = SDF.render !== null && !shouldResetTime ? SDF.render.time : 0 184 | SDF.start( this.fs, this.width, this.height, this.__animate, time ) 185 | 186 | //SDF.materials.materials.length = 0 187 | 188 | this.useQuality = true 189 | 190 | this.__postprocessingFlag = false 191 | this.__threshold = null 192 | this.__farPlane = null 193 | this.__steps = null 194 | this.__resolution = null 195 | 196 | return this 197 | }, 198 | presets: { 199 | 'fractal.close': { 200 | farPlane:1, 201 | resolution:1, 202 | steps:150, 203 | animated:true, 204 | threshold:.000125 205 | }, 206 | 'fractal.kindaclose': { 207 | farPlane:2, 208 | resolution:1, 209 | steps:250, 210 | animated:true, 211 | threshold:.000125/2 212 | }, 213 | 'fractal.med': { 214 | farPlane:5, 215 | resolution:.75, 216 | steps:80, 217 | animated:true, 218 | threshold:.001, 219 | }, 220 | 'fractal.low': { 221 | farPlane:3.0, 222 | resolution:.5, 223 | animated:true, 224 | steps:50, 225 | threshold:.005, 226 | }, 227 | 'fractal.high': { 228 | farPlane:10, 229 | resolution:1, 230 | animated:true, 231 | steps:100, 232 | threshold:.001, 233 | }, 234 | 'repeat.low': { 235 | farPlane:25, 236 | resolution:.5, 237 | animated:true, 238 | steps:50 239 | }, 240 | 'repeat.med': { 241 | farPlane:35, 242 | resolution:1, 243 | animated:true, 244 | steps:75 245 | }, 246 | 'repeat.high': { 247 | farPlane:40, 248 | resolution:1, 249 | animated:true, 250 | steps:100 251 | }, 252 | 'voxel.high': { 253 | resolution:1, 254 | animated:true, 255 | steps:30 256 | }, 257 | 'voxel.med': { 258 | resolution:1, 259 | animated:true, 260 | steps:20 261 | }, 262 | 'voxel.low': { 263 | resolution:.5, 264 | animated:true, 265 | steps:10 266 | }, 267 | low: { 268 | threshold:.05, 269 | steps:45, 270 | farPlane:12, 271 | resolution:.4, 272 | animated:true 273 | }, 274 | medium: { 275 | threshold:.01, 276 | steps:80, 277 | farPlane:18, 278 | resolution:.5, 279 | animated:true 280 | }, 281 | med: { 282 | threshold:.01, 283 | steps:80, 284 | farPlane:18, 285 | resolution:.5, 286 | animated:true 287 | }, 288 | high: { 289 | threshold:.005, 290 | steps:90, 291 | farPlane:20, 292 | resolution:1, 293 | animated:true 294 | } 295 | }, 296 | 297 | } 298 | 299 | return Scene 300 | 301 | } 302 | 303 | module.exports = getScene 304 | -------------------------------------------------------------------------------- /js/material.js: -------------------------------------------------------------------------------- 1 | const SceneNode = require( './sceneNode.js' ), 2 | { param_wrap, MaterialID } = require( './utils.js' ), 3 | { Var, float_var_gen, vec2_var_gen, vec3_var_gen, vec4_var_gen } = require( './var.js' ), 4 | { Vec2, Vec3, Vec4 } = require( './vec.js' ) 5 | 6 | 7 | const glsl = require( 'glslify' ) 8 | 9 | const __Materials = function( SDF ) { 10 | 11 | const Materials = { 12 | materials:[], 13 | __materials:[], 14 | __clearOnEmit: true, 15 | modeConstants : [ 16 | 'global', 17 | 'normal', 18 | 'phong', 19 | 'orenn', 20 | 'noise' 21 | ], 22 | 23 | default: 'global', 24 | 25 | //defaultMaterials:` 26 | // Material materials[2] = Material[2]( 27 | // Material( 0, vec3( 1. ), vec3(0.,0.,0.), vec3(1.), 8., Fresnel( 0., 1., 2.) ), 28 | // Material( 0, vec3( 1. ), vec3(1.,0.,0.), vec3(1.), 8., Fresnel( 0., 1., 2.) ) 29 | // ); 30 | //`, 31 | 32 | addMaterial( mat ) { 33 | if( mat === undefined ) mat = Materials.material.default 34 | 35 | if( Materials.materials.indexOf( mat ) === -1 ) { 36 | mat.id = MaterialID.alloc() 37 | 38 | // we have to dirty the material so that its data 39 | // will be uploaded to new shaders, otherwise the 40 | // material will only work the first time it's used, when 41 | // it's dirty on initialization. 42 | Materials.dirty( mat ) 43 | 44 | Materials.materials.push( mat ) 45 | }else{ 46 | mat.id = Materials.materials.indexOf( mat ) 47 | } 48 | 49 | return mat 50 | }, 51 | 52 | material( mode='global', __ambient, __diffuse, __specular, __shininess, __fresnel, __texture=null ){ 53 | let modeIdx = Materials.modeConstants.indexOf( mode ) 54 | if( modeIdx === -1 ) { 55 | console.warn( `There is no material type named ${mode}. Using the default material, ${Materials.default}, instead.` ) 56 | mode = Materials.default 57 | modeIdx = Materials.modeConstants.indexOf( mode ) 58 | } 59 | 60 | if( typeof __ambient === 'number' ) __ambient = Vec3( __ambient ) 61 | const ambient = param_wrap( __ambient, vec3_var_gen(.1,.1,.1) ) 62 | if( typeof __diffuse=== 'number' ) __diffuse= Vec3( __diffuse ) 63 | const diffuse = param_wrap( __diffuse, vec3_var_gen(0,0,1) ) 64 | if( typeof __specular === 'number' ) __specular = Vec3( __specular ) 65 | const specular = param_wrap( __specular, vec3_var_gen(1,1,1) ) 66 | const shininess = param_wrap( __shininess, float_var_gen(8) ) 67 | if( typeof __fresnel === 'number' ) __fresnel = Vec3( __fresnel ) 68 | const fresnel = param_wrap( __fresnel, vec3_var_gen(0,1,2) ) 69 | 70 | const mat = { shininess, mode, texture:__texture, type:'material' } 71 | 72 | Object.defineProperty( mat, 'ambient', { 73 | get() { return ambient }, 74 | set(v) { 75 | if( typeof v === 'object' ) { 76 | ambient.set( v ) 77 | }else{ 78 | ambient.value.x = v 79 | ambient.value.y = v 80 | ambient.value.z = v 81 | ambient.dirty = true 82 | } 83 | } 84 | }) 85 | Object.defineProperty( mat, 'diffuse', { 86 | get() { return diffuse }, 87 | set(v) { 88 | if( typeof v === 'object' ) { 89 | diffuse.set( v ) 90 | }else{ 91 | diffuse.value.x = v 92 | diffuse.value.y = v 93 | diffuse.value.z = v 94 | diffuse.dirty = true 95 | } 96 | } 97 | }) 98 | Object.defineProperty( mat, 'specular', { 99 | get() { return specular }, 100 | set(v) { 101 | if( typeof v === 'object' ) { 102 | specular.set( v ) 103 | }else{ 104 | specular.value.x = v 105 | specular.value.y = v 106 | specular.value.z = v 107 | specular.dirty = true 108 | } 109 | } 110 | }) 111 | Object.defineProperty( mat, 'fresnel', { 112 | get() { return fresnel }, 113 | set(v) { 114 | if( typeof v === 'object' ) { 115 | fresnel.set( v ) 116 | }else{ 117 | fresnel.value.x = v 118 | fresnel.value.y = v 119 | fresnel.value.z = v 120 | fresnel.dirty = true 121 | } 122 | } 123 | }) 124 | //Object.defineProperty( mat, 'shininess', { 125 | // get() { return mat.shininess.value }, 126 | // set(v){ 127 | // mat.shininess.value = v 128 | // mat.shininess.dirty = true 129 | // } 130 | //}) // 131 | 132 | return mat 133 | }, 134 | 135 | dirty( mat ) { 136 | mat.ambient.dirty = true 137 | mat.diffuse.dirty = true 138 | mat.specular.dirty = true 139 | mat.shininess.dirty = true 140 | mat.fresnel.dirty = true 141 | if( mat.texture !== null ) mat.texture.dirty = true 142 | }, 143 | 144 | walk( branch ) { 145 | if( branch === null ) return 146 | if( branch.__material ) { 147 | branch.mat = branch.__material = this.addMaterial( branch.__material ) 148 | } 149 | 150 | if( 'a' in branch ) { 151 | // combinators 152 | this.walk( branch.a ) 153 | this.walk( branch.b ) 154 | }else if( 'sdf' in branch ) { 155 | // repeat etc. 156 | this.walk( branch.sdf ) 157 | } 158 | }, 159 | 160 | generate() { 161 | this.materials = [] 162 | MaterialID.clear() 163 | 164 | const head = Array.isArray( SDF.__scene.__prerender ) 165 | ? SDF.__scene.__prerender[0] 166 | : SDF.__scene.__prerender 167 | 168 | this.walk( head ) 169 | }, 170 | 171 | emit_materials() { 172 | let str = `Material materials[${this.materials.length}] = Material[${this.materials.length}](` 173 | 174 | this.materials.sort( (a,b) => a.id > b.id ? 1 : -1 ) 175 | 176 | for( let mat of this.materials ) { 177 | Materials.dirty( mat ) 178 | str += `\n Material( ${this.modeConstants.indexOf( mat.mode )}, ${mat.ambient.emit()}, ${mat.diffuse.emit()}, ${mat.specular.emit()}, ${mat.shininess.emit()}, ${mat.fresnel.emit()}, 0 ),` 179 | } 180 | 181 | str = str.slice(0,-1) // remove trailing comma 182 | 183 | str += '\n );' 184 | 185 | this.__materials = this.materials.slice( 0 ) 186 | this.__str = str 187 | if( this.__clearOnEmit ) { 188 | //this.materials.length = 0 189 | } 190 | 191 | return str 192 | }, 193 | 194 | emit_decl() { 195 | let str = '' 196 | for( let mat of this.__materials ) { 197 | str += mat.ambient.emit_decl() 198 | str += mat.diffuse.emit_decl() 199 | str += mat.specular.emit_decl() 200 | str += mat.shininess.emit_decl() 201 | str += mat.fresnel.emit_decl() 202 | } 203 | 204 | return str 205 | }, 206 | 207 | update_location( gl, program ) { 208 | for( let mat of this.__materials ) { 209 | if( mat.ambient.dirty === true ) mat.ambient.update_location( gl, program ) 210 | if( mat.diffuse.dirty === true ) mat.diffuse.update_location( gl, program ) 211 | if( mat.specular.dirty === true ) mat.specular.update_location( gl, program ) 212 | if( mat.shininess.dirty === true ) mat.shininess.update_location( gl, program ) 213 | if( mat.fresnel.dirty === true ) mat.fresnel.update_location( gl, program ) 214 | } 215 | }, 216 | 217 | upload_data( gl, program='' ) { 218 | for( let mat of this.__materials ) { 219 | if( mat.ambient.dirty === true ) mat.ambient.upload_data( gl, program ) 220 | if( mat.diffuse.dirty === true ) mat.diffuse.upload_data( gl, program ) 221 | if( mat.specular.dirty === true ) mat.specular.upload_data( gl, program ) 222 | if( mat.shininess.dirty === true ) mat.shininess.upload_data( gl, program ) 223 | if( mat.fresnel.dirty === true ) mat.fresnel.upload_data( gl, program ) 224 | } 225 | } 226 | } 227 | 228 | const f = value => value % 1 === 0 ? value.toFixed(1) : value 229 | 230 | Object.assign( Materials.material, { 231 | default : Materials.material( 'global', Vec3( .15 ), Vec3(0), Vec3(1), 8, Vec3( 0 ) ), 232 | red : Materials.material( 'global', Vec3(.25,0,0), Vec3(1,0,0), Vec3(0), 2, Vec3(0) ), 233 | green : Materials.material( 'global', Vec3(0,.25,0), Vec3(0,1,0), Vec3(0), 2, Vec3(0) ), 234 | blue : Materials.material( 'global', Vec3(0,0,.25), Vec3(0,0,1), Vec3(0), 2, Vec3(0) ), 235 | cyan : Materials.material( 'global', Vec3(0,.25,.25), Vec3(0,1,1), Vec3(0), 2, Vec3(0) ), 236 | magenta : Materials.material( 'global', Vec3(.25,0,.25), Vec3(1,0,1), Vec3(0), 2, Vec3(0) ), 237 | yellow : Materials.material( 'global', Vec3(.25,.25,.0), Vec3(1,1,0), Vec3(0), 2, Vec3(0) ), 238 | black : Materials.material( 'global', Vec3(0, 0, 0), Vec3(0,0,0), Vec3(0), 2, Vec3(0) ), 239 | white : Materials.material( 'global', Vec3(.25), Vec3(1), Vec3(1), 2, Vec3(0) ), 240 | grey : Materials.material( 'global', Vec3(.25), Vec3(.33), Vec3(1), 2, Vec3(0) ), 241 | 242 | 'white glow' : Materials.material( 'phong', Vec3(.015), Vec3(1), Vec3(1), 16, Vec3(0,200,5) ), 243 | glue : Materials.material( 'phong', Vec3(.015), Vec3(1), Vec3(1), 16, Vec3(0,15,-.1) ), 244 | inverse : Materials.material( 'phong', 1, .5, 1, 16, Vec3(1,.5,-2) ), 245 | blackhole : Materials.material( 'phong', Vec3(0), Vec3(0), Vec3(0), 32 ), 246 | redp : Materials.material( 'phong', Vec3(1,0,0), Vec3(1,0,0), Vec3(1), 128, Vec3(0) ), 247 | 248 | normal : Materials.material( 'normal' ), 249 | noise : Materials.material( 'noise', Vec3( .15 ), Vec3(1,0,0), Vec3(1), 8, Vec3( 0, 1, .5 )) 250 | }) 251 | 252 | return Materials 253 | } 254 | 255 | module.exports = __Materials 256 | -------------------------------------------------------------------------------- /playground/main.css: -------------------------------------------------------------------------------- 1 | canvas { 2 | width:100%; 3 | height:100%; 4 | position:relative; 5 | } 6 | 7 | body { 8 | margin:0; 9 | background:black; 10 | font-family:monospace 11 | } 12 | 13 | .CodeMirror{ 14 | background:transparent !important 15 | } 16 | .CodeMirror-highlight { 17 | background-color:rgba(89, 151, 198, .85) !important 18 | } 19 | .CodeMirror-selected { 20 | background-color:rgba(89, 151, 198, .85) !important 21 | } 22 | 23 | .Codemirror-activeline-background { 24 | background:rgba(255,255,255,.2) 25 | } 26 | .CodeMirror pre { 27 | background-color: rgba( 0,0,0,.75 ) !important; 28 | float:left !important; 29 | clear:left !important; 30 | font-family:Menlo, "Courier New", monospace !important; 31 | } 32 | 33 | .CodeMirror-linenumber { 34 | float:none !important; 35 | clear:none !important; 36 | } 37 | 38 | .CodeMirror-gutter { 39 | background:black 40 | } 41 | 42 | #menu { 43 | position: fixed; 44 | top: 1em; 45 | right: 1.5em; 46 | z-index: 999; 47 | color: white; 48 | display: inline-block; 49 | line-height: 2em; 50 | vertical-align: top; 51 | font-family: sans-serif; 52 | font-weight: bold; 53 | font-size: 12px; 54 | text-decoration: none; 55 | } 56 | 57 | #menu #help, #cameracontrols { 58 | color: white; 59 | display: inline-block; 60 | line-height: 2em; 61 | vertical-align: top; 62 | font-family: sans-serif; 63 | font-weight: bold; 64 | font-size: 12px; 65 | text-decoration: none; 66 | } 67 | 68 | #cameracontrols { user-select:none } 69 | 70 | #help { margin:0 1em } 71 | 72 | input { 73 | vertical-align:super; 74 | margin-right:1em 75 | } 76 | 77 | #menu #help:hover { 78 | text-decoration: underline; 79 | } 80 | 81 | #menu #source { 82 | width: 2em; 83 | height: 2em; 84 | display: inline-block; 85 | background-image: url(./github.png); 86 | background-size: cover; 87 | background-position: center; 88 | margin-top: -0.2em; 89 | } 90 | 91 | #menu #demo { 92 | vertical-align: top; 93 | margin: 0.4em; 94 | } 95 | 96 | .panel{ 97 | position:absolute; 98 | bottom:0; left:0; 99 | width:100%; 100 | height:20em; 101 | background:gray; 102 | font-family:sans-serif; 103 | padding: 0 5px; 104 | box-sizing:border-box; 105 | font-size:.85em; 106 | overflow:scroll; 107 | } 108 | .remove-panel{ 109 | position:absolute; 110 | right:0; 111 | color:darkred; 112 | } 113 | 114 | .panel ul { 115 | padding:0; 116 | margin:0 117 | } 118 | 119 | .panel ul li { 120 | margin-top:.5em 121 | } 122 | 123 | img { 124 | position:absolute; 125 | right: 0em; 126 | top:0; 127 | width:45px; 128 | height:38px; 129 | z-index:10 130 | } 131 | 132 | .CodeMirror { 133 | font-family: 'Menlo','Source Code Pro', monospace; 134 | font-size: .7em; 135 | background-color:#bbb; 136 | color:#000; 137 | font-weight:500; 138 | } 139 | 140 | .CodeMirror { 141 | font-family:Menlo, monospace; 142 | font-size:.8em; 143 | background:rgba(46,50,53,1); 144 | color:rgb( 153 ) !important; 145 | padding:1.5em; 146 | } 147 | 148 | span.cm-comment { color:rgb(121,121,121) !important } 149 | 150 | span.cm-property, 151 | span.cm-attribute, 152 | span.cm-variable, 153 | span.cm-variable-2, 154 | .cm-def, 155 | .cm-s-default { color: rgba(173,173,173,1) !important } 156 | 157 | span.cm-keyword, 158 | span.cm-number, span.cm-atom { color:rgba(89, 151, 198, 1) !important } 159 | 160 | span.cm-string, span.cm-string-2 {color: rgb(225, 225, 225) !important } 161 | 162 | 163 | .CodeMirror-cursor { 164 | background: rgba(255,255,255,.5); 165 | border: none !important; 166 | width: .5em !important; 167 | display: block; 168 | } 169 | 170 | .tp-rotv_t { background:black !important } 171 | /*.CodeMirror-selected {*/ 172 | /* background:rgba(84,84,84,.5) !important;*/ 173 | /*}*/ 174 | 175 | /* toastr v2.1.3 */ 176 | 177 | .toast-title{font-weight:700}.toast-message{-ms-word-wrap:break-word;word-wrap:break-word}.toast-message a,.toast-message label{color:#FFF}.toast-message a:hover{color:#CCC;text-decoration:none}.toast-close-button{position:relative;right:-.3em;top:-.3em;float:right;font-size:20px;font-weight:700;color:#FFF;-webkit-text-shadow:0 1px 0 #fff;text-shadow:0 1px 0 #fff;opacity:.8;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80);line-height:1}.toast-close-button:focus,.toast-close-button:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.4;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);filter:alpha(opacity=40)}.rtl .toast-close-button{left:-.3em;float:left;right:.3em}button.toast-close-button{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.toast-top-center{top:0;right:0;width:100%}.toast-bottom-center{bottom:0;right:0;width:100%}.toast-top-full-width{top:0;right:0;width:100%}.toast-bottom-full-width{bottom:0;right:0;width:100%}.toast-top-left{top:12px;left:12px}.toast-top-right{top:12px;right:12px}.toast-bottom-right{right:12px;bottom:12px}.toast-bottom-left{bottom:12px;left:12px}#toast-container{position:fixed;z-index:999999;pointer-events:none}#toast-container *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}#toast-container>div{position:relative;pointer-events:auto;overflow:hidden;margin:0 0 6px;padding:15px 15px 15px 50px;width:300px;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;background-position:15px center;background-repeat:no-repeat;-moz-box-shadow:0 0 12px #999;-webkit-box-shadow:0 0 12px #999;box-shadow:0 0 12px #999;color:#FFF;opacity:.8;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80)}#toast-container>div.rtl{direction:rtl;padding:15px 50px 15px 15px;background-position:right 15px center}#toast-container>div:hover{-moz-box-shadow:0 0 12px #000;-webkit-box-shadow:0 0 12px #000;box-shadow:0 0 12px #000;opacity:1;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=100);filter:alpha(opacity=100);cursor:pointer}#toast-container>.toast-info{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=)!important}#toast-container>.toast-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=)!important}#toast-container>.toast-success{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==)!important}#toast-container>.toast-warning{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=)!important}#toast-container.toast-bottom-center>div,#toast-container.toast-top-center>div{width:300px;margin-left:auto;margin-right:auto}#toast-container.toast-bottom-full-width>div,#toast-container.toast-top-full-width>div{width:96%;margin-left:auto;margin-right:auto}.toast{background-color:#030303}.toast-success{background-color:#51A351}.toast-error{background-color:#BD362F}.toast-info{background-color:#2F96B4}.toast-warning{background-color:#F89406}.toast-progress{position:absolute;left:0;bottom:0;height:4px;background-color:#000;opacity:.4;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);filter:alpha(opacity=40)}@media all and (max-width:240px){#toast-container>div{padding:8px 8px 8px 50px;width:11em}#toast-container>div.rtl{padding:8px 50px 8px 8px}#toast-container .toast-close-button{right:-.2em;top:-.2em}#toast-container .rtl .toast-close-button{left:-.2em;right:.2em}}@media all and (min-width:241px) and (max-width:480px){#toast-container>div{padding:8px 8px 8px 50px;width:18em}#toast-container>div.rtl{padding:8px 50px 8px 8px}#toast-container .toast-close-button{right:-.2em;top:-.2em}#toast-container .rtl .toast-close-button{left:-.2em;right:.2em}}@media all and (min-width:481px) and (max-width:768px){#toast-container>div{padding:15px 15px 15px 50px;width:25em}#toast-container>div.rtl{padding:15px 50px 15px 15px}} 178 | -------------------------------------------------------------------------------- /playground/codemirror.css: -------------------------------------------------------------------------------- 1 | /* BASICS */ 2 | 3 | .CodeMirror { 4 | /* Set height, width, borders, and global font properties here */ 5 | font-family: monospace; 6 | height: 300px; 7 | color: black; 8 | direction: ltr; 9 | } 10 | 11 | /* PADDING */ 12 | 13 | .CodeMirror-lines { 14 | padding: 4px 0; /* Vertical padding around content */ 15 | } 16 | .CodeMirror pre { 17 | padding: 0 4px; /* Horizontal padding of content */ 18 | } 19 | 20 | .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 21 | background-color: white; /* The little square between H and V scrollbars */ 22 | } 23 | 24 | /* GUTTER */ 25 | 26 | .CodeMirror-gutters { 27 | border-right: 1px solid #ddd; 28 | background-color: #f7f7f7; 29 | white-space: nowrap; 30 | } 31 | .CodeMirror-linenumbers {} 32 | .CodeMirror-linenumber { 33 | padding: 0 3px 0 5px; 34 | min-width: 20px; 35 | text-align: right; 36 | color: #999; 37 | white-space: nowrap; 38 | } 39 | 40 | .CodeMirror-guttermarker { color: black; } 41 | .CodeMirror-guttermarker-subtle { color: #999; } 42 | 43 | /* CURSOR */ 44 | 45 | .CodeMirror-cursor { 46 | border-left: 1px solid black; 47 | border-right: none; 48 | width: 0; 49 | } 50 | /* Shown when moving in bi-directional text */ 51 | .CodeMirror div.CodeMirror-secondarycursor { 52 | border-left: 1px solid silver; 53 | } 54 | .cm-fat-cursor .CodeMirror-cursor { 55 | width: auto; 56 | border: 0 !important; 57 | background: #7e7; 58 | } 59 | .cm-fat-cursor div.CodeMirror-cursors { 60 | z-index: 1; 61 | } 62 | .cm-fat-cursor-mark { 63 | background-color: rgba(20, 255, 20, 0.5); 64 | -webkit-animation: blink 1.06s steps(1) infinite; 65 | -moz-animation: blink 1.06s steps(1) infinite; 66 | animation: blink 1.06s steps(1) infinite; 67 | } 68 | .cm-animate-fat-cursor { 69 | width: auto; 70 | border: 0; 71 | -webkit-animation: blink 1.06s steps(1) infinite; 72 | -moz-animation: blink 1.06s steps(1) infinite; 73 | animation: blink 1.06s steps(1) infinite; 74 | background-color: #7e7; 75 | } 76 | @-moz-keyframes blink { 77 | 0% {} 78 | 50% { background-color: transparent; } 79 | 100% {} 80 | } 81 | @-webkit-keyframes blink { 82 | 0% {} 83 | 50% { background-color: transparent; } 84 | 100% {} 85 | } 86 | @keyframes blink { 87 | 0% {} 88 | 50% { background-color: transparent; } 89 | 100% {} 90 | } 91 | 92 | /* Can style cursor different in overwrite (non-insert) mode */ 93 | .CodeMirror-overwrite .CodeMirror-cursor {} 94 | 95 | .cm-tab { display: inline-block; text-decoration: inherit; } 96 | 97 | .CodeMirror-rulers { 98 | position: absolute; 99 | left: 0; right: 0; top: -50px; bottom: -20px; 100 | overflow: hidden; 101 | } 102 | .CodeMirror-ruler { 103 | border-left: 1px solid #ccc; 104 | top: 0; bottom: 0; 105 | position: absolute; 106 | } 107 | 108 | /* DEFAULT THEME */ 109 | 110 | .cm-s-default .cm-header {color: blue;} 111 | .cm-s-default .cm-quote {color: #090;} 112 | .cm-negative {color: #d44;} 113 | .cm-positive {color: #292;} 114 | .cm-header, .cm-strong {font-weight: bold;} 115 | .cm-em {font-style: italic;} 116 | .cm-link {text-decoration: underline;} 117 | .cm-strikethrough {text-decoration: line-through;} 118 | 119 | .cm-s-default .cm-keyword {color: #708;} 120 | .cm-s-default .cm-atom {color: #219;} 121 | .cm-s-default .cm-number {color: #164;} 122 | .cm-s-default .cm-def {color: #00f;} 123 | .cm-s-default .cm-variable, 124 | .cm-s-default .cm-punctuation, 125 | .cm-s-default .cm-property, 126 | .cm-s-default .cm-operator {} 127 | .cm-s-default .cm-variable-2 {color: #05a;} 128 | .cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;} 129 | .cm-s-default .cm-comment {color: #a50;} 130 | .cm-s-default .cm-string {color: #a11;} 131 | .cm-s-default .cm-string-2 {color: #f50;} 132 | .cm-s-default .cm-meta {color: #555;} 133 | .cm-s-default .cm-qualifier {color: #555;} 134 | .cm-s-default .cm-builtin {color: #30a;} 135 | .cm-s-default .cm-bracket {color: #997;} 136 | .cm-s-default .cm-tag {color: #170;} 137 | .cm-s-default .cm-attribute {color: #00c;} 138 | .cm-s-default .cm-hr {color: #999;} 139 | .cm-s-default .cm-link {color: #00c;} 140 | 141 | .cm-s-default .cm-error {color: #f00;} 142 | .cm-invalidchar {color: #f00;} 143 | 144 | .CodeMirror-composing { border-bottom: 2px solid; } 145 | 146 | /* Default styles for common addons */ 147 | 148 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;} 149 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;} 150 | .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } 151 | .CodeMirror-activeline-background {background: #e8f2ff;} 152 | 153 | /* STOP */ 154 | 155 | /* The rest of this file contains styles related to the mechanics of 156 | the editor. You probably shouldn't touch them. */ 157 | 158 | .CodeMirror { 159 | position: relative; 160 | overflow: hidden; 161 | background: white; 162 | } 163 | 164 | .CodeMirror-scroll { 165 | overflow: scroll !important; /* Things will break if this is overridden */ 166 | /* 30px is the magic margin used to hide the element's real scrollbars */ 167 | /* See overflow: hidden in .CodeMirror */ 168 | margin-bottom: -30px; margin-right: -30px; 169 | padding-bottom: 30px; 170 | height: 100%; 171 | outline: none; /* Prevent dragging from highlighting the element */ 172 | position: relative; 173 | } 174 | .CodeMirror-sizer { 175 | position: relative; 176 | border-right: 30px solid transparent; 177 | } 178 | 179 | /* The fake, visible scrollbars. Used to force redraw during scrolling 180 | before actual scrolling happens, thus preventing shaking and 181 | flickering artifacts. */ 182 | .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 183 | position: absolute; 184 | z-index: 6; 185 | display: none; 186 | } 187 | .CodeMirror-vscrollbar { 188 | right: 0; top: 0; 189 | overflow-x: hidden; 190 | overflow-y: scroll; 191 | } 192 | .CodeMirror-hscrollbar { 193 | bottom: 0; left: 0; 194 | overflow-y: hidden; 195 | overflow-x: scroll; 196 | } 197 | .CodeMirror-scrollbar-filler { 198 | right: 0; bottom: 0; 199 | } 200 | .CodeMirror-gutter-filler { 201 | left: 0; bottom: 0; 202 | } 203 | 204 | .CodeMirror-gutters { 205 | position: absolute; left: 0; top: 0; 206 | min-height: 100%; 207 | z-index: 3; 208 | } 209 | .CodeMirror-gutter { 210 | white-space: normal; 211 | height: 100%; 212 | display: inline-block; 213 | vertical-align: top; 214 | margin-bottom: -30px; 215 | } 216 | .CodeMirror-gutter-wrapper { 217 | position: absolute; 218 | z-index: 4; 219 | background: none !important; 220 | border: none !important; 221 | } 222 | .CodeMirror-gutter-background { 223 | position: absolute; 224 | top: 0; bottom: 0; 225 | z-index: 4; 226 | } 227 | .CodeMirror-gutter-elt { 228 | position: absolute; 229 | cursor: default; 230 | z-index: 4; 231 | } 232 | .CodeMirror-gutter-wrapper ::selection { background-color: transparent } 233 | .CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent } 234 | 235 | .CodeMirror-lines { 236 | cursor: text; 237 | min-height: 1px; /* prevents collapsing before first draw */ 238 | } 239 | .CodeMirror pre { 240 | /* Reset some styles that the rest of the page might have set */ 241 | -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; 242 | border-width: 0; 243 | background: transparent; 244 | font-family: inherit; 245 | font-size: inherit; 246 | margin: 0; 247 | white-space: pre; 248 | word-wrap: normal; 249 | line-height: inherit; 250 | color: inherit; 251 | z-index: 2; 252 | position: relative; 253 | overflow: visible; 254 | -webkit-tap-highlight-color: transparent; 255 | -webkit-font-variant-ligatures: contextual; 256 | font-variant-ligatures: contextual; 257 | } 258 | .CodeMirror-wrap pre { 259 | word-wrap: break-word; 260 | white-space: pre-wrap; 261 | word-break: normal; 262 | } 263 | 264 | .CodeMirror-linebackground { 265 | position: absolute; 266 | left: 0; right: 0; top: 0; bottom: 0; 267 | z-index: 0; 268 | } 269 | 270 | .CodeMirror-linewidget { 271 | position: relative; 272 | z-index: 2; 273 | padding: 0.1px; /* Force widget margins to stay inside of the container */ 274 | } 275 | 276 | .CodeMirror-widget {} 277 | 278 | .CodeMirror-rtl pre { direction: rtl; } 279 | 280 | .CodeMirror-code { 281 | outline: none; 282 | } 283 | 284 | /* Force content-box sizing for the elements where we expect it */ 285 | .CodeMirror-scroll, 286 | .CodeMirror-sizer, 287 | .CodeMirror-gutter, 288 | .CodeMirror-gutters, 289 | .CodeMirror-linenumber { 290 | -moz-box-sizing: content-box; 291 | box-sizing: content-box; 292 | } 293 | 294 | .CodeMirror-measure { 295 | position: absolute; 296 | width: 100%; 297 | height: 0; 298 | overflow: hidden; 299 | visibility: hidden; 300 | } 301 | 302 | .CodeMirror-cursor { 303 | position: absolute; 304 | pointer-events: none; 305 | } 306 | .CodeMirror-measure pre { position: static; } 307 | 308 | div.CodeMirror-cursors { 309 | visibility: hidden; 310 | position: relative; 311 | z-index: 3; 312 | } 313 | div.CodeMirror-dragcursors { 314 | visibility: visible; 315 | } 316 | 317 | .CodeMirror-focused div.CodeMirror-cursors { 318 | visibility: visible; 319 | } 320 | 321 | .CodeMirror-selected { background: #d9d9d9; } 322 | .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } 323 | .CodeMirror-crosshair { cursor: crosshair; } 324 | .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } 325 | .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } 326 | 327 | .cm-searching { 328 | background-color: #ffa; 329 | background-color: rgba(255, 255, 0, .4); 330 | } 331 | 332 | /* Used to force a border model for a node */ 333 | .cm-force-border { padding-right: .1px; } 334 | 335 | @media print { 336 | /* Hide the cursor when printing */ 337 | .CodeMirror div.CodeMirror-cursors { 338 | visibility: hidden; 339 | } 340 | } 341 | 342 | /* See issue #2901 */ 343 | .cm-tab-wrap-hack:after { content: ''; } 344 | 345 | /* Help users use markselection to safely style text background */ 346 | span.CodeMirror-selectedtext { background: none; } 347 | -------------------------------------------------------------------------------- /js/distanceDeformations.js: -------------------------------------------------------------------------------- 1 | const SceneNode = require( './sceneNode.js' ) 2 | const { param_wrap, MaterialID } = require( './utils.js' ) 3 | const { Var, float_var_gen, vec2_var_gen, vec3_var_gen, vec4_var_gen, int_var_gen, VarAlloc } = require( './var.js' ) 4 | const Transform = require( './transform.js' ) 5 | 6 | const ops = { 7 | // this needs to create an opOut, not return a vec2 8 | Displace( __name ) { 9 | let name = __name === undefined ? 'p' : __name 10 | const sdf = this.sdf.emit( name ); 11 | 12 | const sdfStr = `float d1${this.id} = ${sdf.out}.x;\n` 13 | 14 | let displaceString = `float d2${this.id} = sin( ${this.amount.emit()}.x * ${name}.x ) * ` 15 | displaceString += `sin( ${this.amount.emit()}.y * ${name}.y ) * ` 16 | displaceString += `sin( ${this.amount.emit()}.z * ${name}.z );\n` 17 | displaceString += `${sdf.out}.x = (d1${this.id} + d2${this.id}*${this.size.emit()})*.5;\n` 18 | 19 | const output = { 20 | out: `${sdf.out}`, 21 | preface: sdf.preface + sdfStr + displaceString 22 | } 23 | 24 | return output 25 | }, 26 | Halve( __name ) { 27 | let name = __name === undefined ? 'p' : __name 28 | 29 | // XXX why? 30 | this.transform.invert() 31 | 32 | const bumpString = ` vec4 transformBump${this.id} = ${name} * ${this.transform.emit()};\n` 33 | const pointString = ` vec4 transformBump2${this.id} = (transformBump${this.id} * ${this.sdf.transform.emit()});\n` 34 | const sdf = this.sdf.emit( `transformBump2${this.id}` ); 35 | 36 | let displaceString = `${sdf.out}.x = opHalve( ${sdf.out}.x, transformBump2${this.id}, int(${this.amount.emit()}) );` 37 | 38 | const output = { 39 | out: `${sdf.out}`, 40 | preface: bumpString + pointString + sdf.preface + displaceString 41 | } 42 | 43 | return output 44 | }, 45 | Bend( __name ) { 46 | let name = __name === undefined ? 'p' : __name 47 | const sdf = this.sdf.emit( 'q'+this.id ); 48 | 49 | let preface=` float c${this.id} = cos( ${this.amount.emit()}.x * ${name}.x ); 50 | float s${this.id} = sin( ${this.amount.emit()}.x * ${name}.x ); 51 | mat2 m${this.id} = mat2( c${this.id},-s${this.id},s${this.id},c${this.id} ); 52 | vec4 q${this.id} = vec4( m${this.id} * ${name}.xy, ${name}.z, 1. );\n` 53 | 54 | if( typeof sdf.preface === 'string' ) { 55 | preface += sdf.preface 56 | } 57 | 58 | return { preface, out:sdf.out } 59 | }, 60 | 61 | __Twist( __name ) { 62 | let name = __name === undefined ? 'p' : __name 63 | 64 | const sdf = this.sdf.emit( 'q'+this.id ); 65 | 66 | let preface=` float c${this.id} = cos( ${this.amount.emit()}.x * ${name}.y ); 67 | float s${this.id} = sin( ${this.amount.emit()}.x * ${name}.y ); 68 | mat2 m${this.id} = mat2( c${this.id},-s${this.id},s${this.id},c${this.id} ); 69 | vec4 q${this.id} = vec4( m${this.id} * ${name}.xz, ${name}.y, 1. );\n` 70 | 71 | if( typeof sdf.preface === 'string' ) { 72 | preface += sdf.preface 73 | } 74 | 75 | return { preface, out:sdf.out } 76 | }, 77 | 78 | Twist( __name ) { 79 | let name = __name === undefined ? 'p' : __name 80 | this.transform.invert() 81 | 82 | const bumpString = ` vec4 twist${this.id} = ${name} * ${this.transform.emit()};\n` 83 | const bumpString2 = ` vec4 twist2${this.id} = (twist${this.id} * ${this.sdf.transform.emit()});\n` 84 | //const sdf = this.sdf.emit( 'q'+this.id ); 85 | 86 | //let preface=` float c${this.id} = cos( ${this.amount.emit()}.x * ${name}.y ); 87 | // float s${this.id} = sin( ${this.amount.emit()}.x * ${name}.y ); 88 | // mat2 m${this.id} = mat2( c${this.id},-s${this.id},s${this.id},c${this.id} ); 89 | // vec4 q${this.id} = vec4( m${this.id} * ${name}.xz, ${name}.y, 1. );\n` 90 | 91 | name = `twist2${this.id}` 92 | let preface=` float c${this.id} = cos( ${this.amount.emit()}.x * ${name}.y ); 93 | float s${this.id} = sin( ${this.amount.emit()}.x * ${name}.y ); 94 | mat2 m${this.id} = mat2( c${this.id},-s${this.id},s${this.id},c${this.id} ); 95 | vec4 q${this.id} = vec4( m${this.id} * ${name}.xz, ${name}.y, 1. );\n` 96 | 97 | const sdf = this.sdf.emit( `q${this.id}` ); 98 | if( typeof sdf.preface === 'string' ) { 99 | preface += sdf.preface 100 | } 101 | 102 | return { preface:bumpString + bumpString2 + preface, out:sdf.out } 103 | }, 104 | 105 | __Bump( __name ) { 106 | let name = __name === undefined ? 'p' : __name 107 | 108 | const bumpString = ` vec4 transformBump${this.id} = ${name} * ${this.transform.emit()};\n` 109 | const tex = this.amount.emit( name ) 110 | 111 | const pointString = `(transformBump${this.id} * ${this.sdf.transform.emit()})` 112 | 113 | const sdf = this.sdf.emit( pointString, this.transform, `tex${this.id}` ) 114 | 115 | Marching.textures.addTexture( this.amount.value ) 116 | 117 | let preface=` vec3 tex${this.id} = getTexture( ${this.amount.value.id}, ${pointString}.xyz ) * ${this.size.emit()};\n 118 | //vec4 displaceBump${this.id} = vec4((${pointString} - tex${this.id}), 1.); 119 | ` 120 | //${sdf.out}.x = (tex${this.id}.x + tex${this.id}.y + tex${this.id}.z ) / 3. * .5 + ${sdf.out}.x;\n` 121 | //vec4 ${'p'+this.id} = vec4(${pointString} + tex${this.id}, 1.);\n` 122 | 123 | //sdf.preface += `\n 124 | // ${sdf.out}.x -= min(tex${this.id}.x, min(tex${this.id}.y, tex${this.id}.z));\n` 125 | 126 | if( typeof sdf.preface === 'string' ) { 127 | preface = preface + sdf.preface 128 | } 129 | 130 | preface = bumpString + preface 131 | 132 | return { preface, out:sdf.out } 133 | }, 134 | // XXX todo: something like https://www.shadertoy.com/view/ldSGzR 135 | // https://www.dropbox.com/s/l1yl164jb3rhomq/mm_sfgrad_bump.pdf?dl=0 136 | Bump( __name ) { 137 | let name = __name === undefined ? 'p' : __name 138 | 139 | const bumpString = ` vec4 transformBump${this.id} = ${name} * ${this.transform.emit()};\n` 140 | const tex = this.amount.emit( name ) 141 | 142 | const pointString = `(transformBump${this.id} * ${this.sdf.transform.emit()}).xyz` 143 | 144 | const sdf = this.sdf.emit( `transformBump${this.id}`, this.transform ) 145 | 146 | Marching.textures.addTexture( this.amount.value ) 147 | 148 | let preface=` vec3 tex${this.id} = getTexture( ${this.amount.value.id}, ${pointString}) * ${this.size.emit()}; 149 | ${sdf.out}.x += (tex${this.id}.x + tex${this.id}.y + tex${this.id}.z)/3.;\n` 150 | 151 | if( typeof sdf.preface === 'string' ) { 152 | preface = sdf.preface + preface 153 | } 154 | 155 | preface = bumpString + preface 156 | 157 | return { preface, out:sdf.out } 158 | }, 159 | } 160 | 161 | const DistanceOps = {} 162 | 163 | for( let name in ops ) { 164 | 165 | // get codegen function 166 | let __op = ops[ name ] 167 | 168 | // create constructor 169 | DistanceOps[ name ] = function( a,b,c ) { 170 | const op = Object.create( DistanceOps[ name ].prototype ) 171 | op.sdf = a 172 | op.amount = b 173 | op.emit = __op 174 | op.name = name 175 | op.transform = Transform() 176 | 177 | const defaultValues = [.5,.5,.5] 178 | 179 | op.id = VarAlloc.alloc() 180 | const isArray = true 181 | 182 | if( name === 'Halve' ) { 183 | op.amount = param_wrap( b, float_var_gen( b ) ) 184 | }else{ 185 | if( typeof b === 'number' ) { 186 | b = [b,b,b] 187 | b.type = 'vec3' 188 | } 189 | } 190 | 191 | if( name !== 'Bumpz' ) { 192 | let __var = param_wrap( 193 | b, 194 | vec3_var_gen( ...defaultValues ) 195 | ) 196 | 197 | // for assigning entire new vectors to property 198 | Object.defineProperty( op, 'amount', { 199 | get() { return __var }, 200 | set(v) { 201 | if( typeof v === 'object' ) { 202 | __var.set( v ) 203 | }else{ 204 | __var.value.x = v 205 | __var.value.y = v 206 | __var.value.z = v 207 | __var.value.w = v 208 | __var.dirty = true 209 | } 210 | } 211 | }) 212 | 213 | op.params = [{ name:'amount' }] 214 | }else{ 215 | op.params = [] 216 | op.emit_decl = function() {} 217 | op.emit = function() {} 218 | op.update_data= function() {} 219 | op.upload_location = function() {} 220 | } 221 | op.__setMaterial = function(mat) { 222 | if( typeof mat === 'string' ) mat = Marching.Material[ mat ] 223 | this.__material = this.mat = Marching.materials.addMaterial( mat ) 224 | op.sdf.material( this.__material ) 225 | } 226 | if( name === 'Displace' || name === 'Bump' ) { 227 | let __var2 = param_wrap( 228 | c, 229 | float_var_gen( .03 ) 230 | ) 231 | Object.defineProperty( op, 'size', { 232 | get() { return __var2 }, 233 | set(v) { 234 | __var2.set( v ) 235 | __var2.dirty = true 236 | } 237 | }) 238 | 239 | op.params.push({ name:'size' }) 240 | } 241 | op.__desc = { parameters:op.params } 242 | return op 243 | } 244 | 245 | DistanceOps[ name ].prototype = SceneNode() 246 | 247 | DistanceOps[name].prototype.emit_decl = function () { 248 | let str = this.sdf.emit_decl() + (this.name !== 'Bump' ? this.amount.emit_decl() : '') 249 | str += this.transform.emit_decl() 250 | if( this.name === 'Displace' || this.name === 'Bump' ) str += this.size.emit_decl() 251 | 252 | return str 253 | }; 254 | 255 | DistanceOps[name].prototype.update_location = function(gl, program) { 256 | this.sdf.update_location( gl, program ) 257 | if( this.name !== 'Bump' ) this.amount.update_location( gl, program ) 258 | if( this.name === 'Displace' || this.name === 'Bump') this.size.update_location( gl, program ) 259 | this.transform.update_location( gl, program ) 260 | } 261 | 262 | DistanceOps[name].prototype.upload_data = function(gl) { 263 | this.sdf.upload_data( gl ) 264 | if( this.name !== 'Bump' ) this.amount.upload_data( gl ) 265 | if( this.name === 'Displace' || this.name === 'Bump' ) this.size.upload_data( gl ) 266 | this.transform.upload_data( gl ) 267 | } 268 | } 269 | DistanceOps.Halve.UP = 0 270 | DistanceOps.Halve.DOWN = 1 271 | DistanceOps.Halve.LEFT = 3 272 | DistanceOps.Halve.RIGHT = 2 273 | 274 | module.exports = DistanceOps 275 | 276 | -------------------------------------------------------------------------------- /js/distanceOperations.js: -------------------------------------------------------------------------------- 1 | const SceneNode = require( './sceneNode.js' ) 2 | const { param_wrap, MaterialID } = require( './utils.js' ) 3 | const { Var, float_var_gen, vec2_var_gen, vec3_var_gen, vec4_var_gen, int_var_gen, VarAlloc } = require( './var.js' ) 4 | const Transform = require( './transform.js' ) 5 | const glslops = require( './distanceOperationsGLSL.js' ) 6 | 7 | const opslen = { 8 | Union:2, 9 | Intersection:2, 10 | Difference:2, 11 | ColumnsUnion:4, 12 | ColumnsIntersection:4, 13 | ColumnsDifference:4, 14 | StairsUnion:4, 15 | StairsIntersection:4, 16 | StairsDifference:4, 17 | RoundUnion:3, 18 | RoundDifference:3, 19 | RoundIntersection:3, 20 | ChamferUnion:3, 21 | ChamferDifference:3, 22 | ChamferIntersection:3, 23 | Pipe:3, 24 | Engrave:3, 25 | Groove:4, 26 | Tongue:4, 27 | 28 | // these two do not currently have support for transforms or repeats... 29 | Onion:2, 30 | Switch:2 31 | } 32 | 33 | const ops = { 34 | Union( ...args ) { return `opU( ${args.join(',')} )` }, 35 | SmoothUnion( ...args ) { return `opSmoothUnion( ${args.join(',')} )` }, 36 | Intersection( ...args ) { return `opI( ${args.join(',')} )` }, 37 | SmoothIntersection( ...args ) { return `opSmoothIntersection( ${args.join(',')} )` }, 38 | Difference( ...args ) { return `opS( ${args.join(',')} )` }, 39 | SmoothDifference( ...args ) { return `opSmoothSubtraction( ${args.join(',')} )` }, 40 | StairsUnion( ...args ) { return `fOpUnionStairs( ${args.join(',')} )` }, 41 | StairsIntersection( ...args ) { return `fOpIntersectionStairs( ${args.join(',')} )` }, 42 | StairsDifference( ...args ) { return `fOpSubstractionStairs( ${args.join(',')} )` }, 43 | ColumnsUnion( ...args ) { return `fOpUnionColumns( ${args.join(',')} )` }, 44 | ColumnsIntersection( ...args ) { return `fOpIntersectionColumns( ${args.join(',')} )` }, 45 | ColumnsDifference( ...args ) { return `fOpDifferenceColumns( ${args.join(',')} )` }, 46 | RoundUnion( ...args ) { return `fOpUnionRound( ${args.join(',')} )` }, 47 | RoundDifference( ...args ) { return `fOpDifferenceRound( ${args.join(',')} )` }, 48 | RoundIntersection( ...args ) { return `fOpIntersectionRound( ${args.join(',')} )` }, 49 | ChamferUnion( ...args ) { return `fOpUnionChamfer( ${args.join(',')} )` }, 50 | ChamferDifference( ...args ) { return `fOpDifferenceChamfer( ${args.join(',')} )` }, 51 | ChamferIntersection( ...args ) { return `fOpIntersectionChamfer( ${args.join(',')} )` }, 52 | Pipe( ...args ) { return `fOpPipe( ${args.join(',')} )` }, 53 | Engrave( ...args ) { return `fOpEngrave( ${args.join(',')} )` }, 54 | Groove( ...args ) { return `fOpGroove( ${args.join(',')} )` }, 55 | Tongue( ...args ) { return `fOpTongue( ${args.join(',')} )` }, 56 | 57 | // these two do not currently have support for transforms or repeats... 58 | Onion( ...args ) { return `opOnion( ${args.join(',')} )` }, 59 | Switch( a,b,c,d,e,f ) { return `opSwitch( ${a}, ${b}, ${c} )` } 60 | } 61 | 62 | const emit_float = function( a ) { 63 | if (a % 1 === 0) 64 | return a.toFixed( 1 ) 65 | else 66 | return a 67 | } 68 | 69 | const DistanceOps = { 70 | __glsl:[], 71 | __getGLSL() { 72 | return this.__glsl.join('\n') 73 | }, 74 | __clear() { this.__glsl.length = 0 } 75 | } 76 | 77 | const oneops = ['Onion','Halve'] 78 | 79 | for( let name in ops ) { 80 | 81 | // get codegen function 82 | let op = ops[ name ] 83 | const name2 = name + '2' 84 | 85 | // create constructor 86 | DistanceOps[ name ] = function( a,b,c,d ) { 87 | const op = Object.create( DistanceOps[ name ].prototype ) 88 | op.a = a 89 | op.b = oneops.indexOf( name ) === -1 ? b : param_wrap( b, float_var_gen(.2 ) ) 90 | op.transform = Transform( false ) 91 | op.id = VarAlloc.alloc() 92 | op.type = 'distance_op' 93 | op.name = name 94 | 95 | let __c = param_wrap( c, float_var_gen(.3) ) 96 | 97 | op.__len = opslen[ name ] 98 | if( op.__len > 2 ) { 99 | Object.defineProperty( op, 'c', { 100 | get() { return __c }, 101 | set(v) { 102 | __c.set( v ) 103 | } 104 | }) 105 | 106 | if( op.__len > 3 ) { 107 | let __d = param_wrap( d, float_var_gen(4) ) 108 | 109 | Object.defineProperty( op, 'd', { 110 | get() { return __d }, 111 | set(v) { 112 | __d.set( v ) 113 | } 114 | }) 115 | } 116 | } 117 | 118 | op.__setTexture = function(tex,props) { 119 | if( typeof tex === 'string' ) { 120 | this.texture = op.texture.bind( this ) 121 | this.__textureObj = this.tex = Marching.Texture( tex,props,this.texture ) 122 | this.__textureID = this.__textureObj.id 123 | }else{ 124 | this.__textureObj = this.tex = Object.assign( tex, props ) 125 | this.__textureID = this.__textureObj.id 126 | } 127 | } 128 | op.__setMaterial = function(mat) { 129 | if( typeof mat === 'string' ) mat = Marching.Material[ mat ] 130 | this.__material = this.mat = Marching.materials.addMaterial( mat ) 131 | } 132 | op.__setBump = function(tex,props) { 133 | //this.bump = p.bump.bind( this ) 134 | const b = this.bump = this.__bumpObj = Marching.Bump( this, tex, props ) 135 | this.bump.texture = this.bump.amount.value 136 | this.__bumpID = this.__bumpObj.id 137 | this.rotate = this.bump.rotate 138 | this.translate = this.bump.translate 139 | this.scale = this.bump.scale 140 | Object.defineProperty( this.bump, 'strength', { 141 | get() { return b.size }, 142 | set(v){ b.size = v } 143 | }) 144 | } 145 | Object.assign( op, { 146 | renderingBump : false, 147 | emittingDecl : false, 148 | uploading : false, 149 | updating : false 150 | }) 151 | 152 | let repeat = null 153 | Object.defineProperty( op, 'repeat', { 154 | get() { return repeat }, 155 | set(v){ 156 | repeat = v 157 | this.a.repeat = v 158 | this.b.repeat = v 159 | } 160 | }) 161 | 162 | op.matId = MaterialID.alloc() 163 | 164 | op.params = [{name:'c'},{ name:'d'}] 165 | op.__desc = { parameters: op.params } 166 | 167 | return op 168 | } 169 | 170 | DistanceOps[ name2 ] = function( ...args ) { 171 | // accepts unlimited arguments, but the last one could be a blending coefficient 172 | let blend = .25, coeff=4, u 173 | 174 | if( typeof args[ args.length - 1 ] === 'number' ) { 175 | blend = args.pop() 176 | 177 | // if there are two non-sdf arguments to the function... 178 | if( typeof args[ args.length - 1 ] === 'number' ) { 179 | coeff = blend 180 | blend = args.pop() 181 | } 182 | 183 | u = args.reduce( (state,next) => DistanceOps[ name ]( state, next, blend, coeff ) ) 184 | }else{ 185 | u = args.reduce( (state,next) => DistanceOps[ name ]( state, next ) ) 186 | } 187 | 188 | return u 189 | } 190 | 191 | DistanceOps[ name ].prototype = SceneNode() 192 | 193 | DistanceOps[ name ].prototype.texture = function( ...args ) { 194 | this.__setTexture( ...args ) 195 | this.a.texture( this.__textureObj ) 196 | if( oneops.indexOf( name ) === -1 ) 197 | this.b.texture( this.__textureObj ) 198 | 199 | return this 200 | } 201 | DistanceOps[ name ].prototype.material = function( ...args ) { 202 | this.__setMaterial( ...args ) 203 | this.a.material( this.__material ) 204 | if( oneops.indexOf( name ) === -1 ) 205 | this.b.material( this.__material ) 206 | 207 | return this 208 | } 209 | 210 | const pushString = function( name ) { 211 | const glslobj = glslops[ name ] 212 | 213 | // some definitions are a single string, and not split into 214 | // separate float and opOut functions 215 | if( typeof glslobj === 'string' ) { 216 | if( DistanceOps.__glsl.indexOf( glslobj ) === -1 ) { 217 | DistanceOps.__glsl.push( glslobj ) 218 | } 219 | }else{ 220 | // some distance operations are dependent on other ones... 221 | // if this one has dependencies add them. 222 | // dependencies must be added before adding other functions 223 | // so that they're above them in the final GLSL code. 224 | if( glslobj.dependencies !== undefined ) { 225 | for( let dname of glslobj.dependencies ) { 226 | const d = glslops[ dname ] 227 | if( DistanceOps.__glsl.indexOf( d.float ) === -1 ) { 228 | DistanceOps.__glsl.push( d.float ) 229 | } 230 | } 231 | } 232 | if( DistanceOps.__glsl.indexOf( glslobj.float ) === -1 ) { 233 | DistanceOps.__glsl.push( glslobj.float ) 234 | } 235 | if( DistanceOps.__glsl.indexOf( glslobj.vec2) === -1 ) { 236 | DistanceOps.__glsl.push( glslobj.vec2 ) 237 | } 238 | } 239 | } 240 | 241 | DistanceOps[ name ].prototype.emit = function ( pname='p', transform = null ){ 242 | this.__dirty() 243 | const isNotOneop = oneops.indexOf( name ) === -1 244 | if( this.__bumpObj !== undefined && this.renderingBump === false) { 245 | this.renderingBump = true 246 | return this.__bumpObj.emit( pname, transform ) 247 | } 248 | pushString( name ) 249 | 250 | if( transform !== null ) this.transform.apply( transform, false ) 251 | //this.transform.internal() 252 | 253 | // first two args are fixed, rest are variable 254 | let emitters = [] 255 | const a = this.a.emit( pname, this.transform ) 256 | const b = isNotOneop ? this.b.emit( pname, this.transform ) : this.b.emit() 257 | 258 | emitters[0] = a.out 259 | emitters[1] = isNotOneop ? b.out : b 260 | if( this.__len > 2 ) emitters.push( this.c.emit() ) 261 | if( this.__len > 3 ) emitters.push( this.d.emit() ) 262 | 263 | const body = ` 264 | vec2 do${this.id} = ${op( ...emitters )}; 265 | do${this.id}.x *= ${this.transform.emit()}_scale; 266 | ` 267 | 268 | const output = { 269 | out: 'do'+this.id, 270 | preface: (a.preface || '') + ( isNotOneop ? b.preface || '' : '') + body 271 | } 272 | 273 | this.renderingBump = false 274 | return output 275 | } 276 | 277 | DistanceOps[name].prototype.emit_decl = function () { 278 | if( this.__bumpObj !== undefined && this.emittingDecl === false) { 279 | this.emittingDecl = true 280 | return this.__bumpObj.emit_decl() 281 | } 282 | let str = this.transform.emit_decl() + this.a.emit_decl() + this.b.emit_decl() 283 | if( this.c !== undefined ) str += this.c.emit_decl() 284 | if( this.d !== undefined ) str += this.d.emit_decl() 285 | 286 | if( ops[ name ].code !== undefined ) { 287 | //str += ops[ name ].code 288 | if( Marching.requiredOps.indexOf( ops[ name ].code ) === - 1 ) { 289 | Marching.requiredOps.push( ops[ name ].code ) 290 | } 291 | } 292 | 293 | this.emittingDecl = false 294 | return str 295 | }; 296 | 297 | DistanceOps[name].prototype.update_location = function(gl, program) { 298 | if( this.__bumpObj !== undefined && this.updating === false) { 299 | this.updating = true 300 | return this.__bumpObj.update_location( gl, program ) 301 | } 302 | this.a.update_location( gl, program ) 303 | this.transform.update_location( gl, program ) 304 | this.b.update_location( gl, program ) 305 | if( this.c !== undefined ) this.c.update_location( gl, program ) 306 | if( this.d !== undefined ) this.d.update_location( gl, program ) 307 | 308 | this.updating = false 309 | } 310 | 311 | DistanceOps[name].prototype.upload_data = function(gl) { 312 | if( this.__bumpObj !== undefined && this.uploading === false ) { 313 | this.uploading = true 314 | return this.__bumpObj.upload_data( gl ) 315 | } 316 | this.transform.internal() 317 | this.transform.upload_data( gl ) 318 | this.a.transform.apply( this.transform ) 319 | if( oneops.indexOf( name ) === -1 ) 320 | this.b.transform.apply( this.transform ) 321 | 322 | this.a.upload_data( gl ) 323 | this.b.upload_data( gl ) 324 | if( this.c !== undefined ) this.c.upload_data( gl ) 325 | if( this.d !== undefined ) this.d.upload_data( gl ) 326 | this.uploading = false 327 | 328 | } 329 | } 330 | 331 | module.exports = DistanceOps 332 | --------------------------------------------------------------------------------