marching
Version:
Marching.js is a JavaScript library that compiles GLSL ray marchers.
325 lines (278 loc) • 10.4 kB
JavaScript
const SceneNode = require( './sceneNode.js' )
const { param_wrap, MaterialID } = require( './utils.js' )
const { Var, float_var_gen, vec2_var_gen, vec3_var_gen, vec4_var_gen, int_var_gen, VarAlloc } = require( './var.js' )
const Transform = require( './transform.js' )
const glslops = require( './distanceOperationsGLSL.js' )
const opslen = {
Union:2,
Intersection:2,
Difference:2,
StairsUnion:4,
StairsIntersection:4,
StairsDifference:4,
RoundUnion:3,
RoundDifference:3,
RoundIntersection:3,
ChamferUnion:3,
ChamferDifference:3,
ChamferIntersection:3,
Pipe:3,
Engrave:3,
Groove:4,
Tongue:4,
// these two do not currently have support for transforms or repeats...
Onion:2,
Switch:2
}
const ops = {
Union( ...args ) { return `opU( ${args.join(',')} )` },
SmoothUnion( ...args ) { return `opSmoothUnion( ${args.join(',')} )` },
Intersection( ...args ) { return `opI( ${args.join(',')} )` },
SmoothIntersection( ...args ) { return `opSmoothIntersection( ${args.join(',')} )` },
Difference( ...args ) { return `opS( ${args.join(',')} )` },
SmoothDifference( ...args ) { return `opSmoothSubtraction( ${args.join(',')} )` },
StairsUnion( ...args ) { return `fOpUnionStairs( ${args.join(',')} )` },
StairsIntersection( ...args ) { return `fOpIntersectionStairs( ${args.join(',')} )` },
StairsDifference( ...args ) { return `fOpSubstractionStairs( ${args.join(',')} )` },
RoundUnion( ...args ) { return `fOpUnionRound( ${args.join(',')} )` },
RoundDifference( ...args ) { return `fOpDifferenceRound( ${args.join(',')} )` },
RoundIntersection( ...args ) { return `fOpIntersectionRound( ${args.join(',')} )` },
ChamferUnion( ...args ) { return `fOpUnionChamfer( ${args.join(',')} )` },
ChamferDifference( ...args ) { return `fOpDifferenceChamfer( ${args.join(',')} )` },
ChamferIntersection( ...args ) { return `fOpIntersectionChamfer( ${args.join(',')} )` },
Pipe( ...args ) { return `fOpPipe( ${args.join(',')} )` },
Engrave( ...args ) { return `fOpEngrave( ${args.join(',')} )` },
Groove( ...args ) { return `fOpGroove( ${args.join(',')} )` },
Tongue( ...args ) { return `fOpTongue( ${args.join(',')} )` },
// these two do not currently have support for transforms or repeats...
Onion( ...args ) { return `opOnion( ${args.join(',')} )` },
Switch( a,b,c,d,e,f ) { return `opSwitch( ${a}, ${b}, ${c} )` }
}
const emit_float = function( a ) {
if (a % 1 === 0)
return a.toFixed( 1 )
else
return a
}
const DistanceOps = {
__glsl:[],
__getGLSL() {
return this.__glsl.join('\n')
},
__clear() { this.__glsl.length = 0 }
}
const oneops = ['Onion','Halve']
for( let name in ops ) {
// get codegen function
let op = ops[ name ]
const name2 = name + '2'
// create constructor
DistanceOps[ name ] = function( a,b,c,d ) {
const op = Object.create( DistanceOps[ name ].prototype )
op.a = a
op.b = oneops.indexOf( name ) === -1 ? b : param_wrap( b, float_var_gen(.2 ) )
op.transform = Transform( false )
op.id = VarAlloc.alloc()
op.type = 'distance_op'
op.name = name
let __c = param_wrap( c, float_var_gen(.3) )
op.__len = opslen[ name ]
if( op.__len > 2 ) {
Object.defineProperty( op, 'c', {
get() { return __c },
set(v) {
__c.set( v )
}
})
if( op.__len > 3 ) {
let __d = param_wrap( d, float_var_gen(4) )
Object.defineProperty( op, 'd', {
get() { return __d },
set(v) {
__d.set( v )
}
})
}
}
op.__setTexture = function(tex,props) {
if( typeof tex === 'string' ) {
this.texture = op.texture.bind( this )
this.__textureObj = this.tex = Marching.Texture( tex,props,this.texture )
this.__textureID = this.__textureObj.id
}else{
this.__textureObj = this.tex = Object.assign( tex, props )
this.__textureID = this.__textureObj.id
}
}
op.__setMaterial = function(mat) {
if( typeof mat === 'string' ) mat = Marching.Material[ mat ]
this.__material = this.mat = Marching.materials.addMaterial( mat )
}
op.__setBump = function(tex,props) {
//this.bump = p.bump.bind( this )
const b = this.bump = this.__bumpObj = Marching.Bump( this, tex, props )
this.bump.texture = this.bump.amount.value
this.__bumpID = this.__bumpObj.id
this.rotate = this.bump.rotate
this.translate = this.bump.translate
this.scale = this.bump.scale
Object.defineProperty( this.bump, 'strength', {
get() { return b.size },
set(v){ b.size = v }
})
}
Object.assign( op, {
renderingBump : false,
emittingDecl : false,
uploading : false,
updating : false
})
let repeat = null
Object.defineProperty( op, 'repeat', {
get() { return repeat },
set(v){
repeat = v
this.a.repeat = v
this.b.repeat = v
}
})
op.matId = MaterialID.alloc()
op.params = [{name:'c'},{ name:'d'}]
op.__desc = { parameters: op.params }
return op
}
DistanceOps[ name2 ] = function( ...args ) {
// accepts unlimited arguments, but the last one could be a blending coefficient
let blend = .25, coeff=4, u
if( typeof args[ args.length - 1 ] === 'number' ) {
blend = args.pop()
// if there are two non-sdf arguments to the function...
if( typeof args[ args.length - 1 ] === 'number' ) {
coeff = blend
blend = args.pop()
}
u = args.reduce( (state,next) => DistanceOps[ name ]( state, next, blend, coeff ) )
}else{
u = args.reduce( (state,next) => DistanceOps[ name ]( state, next ) )
}
return u
}
DistanceOps[ name ].prototype = SceneNode()
DistanceOps[ name ].prototype.texture = function( ...args ) {
this.__setTexture( ...args )
this.a.texture( this.__textureObj )
if( oneops.indexOf( name ) === -1 )
this.b.texture( this.__textureObj )
return this
}
DistanceOps[ name ].prototype.material = function( ...args ) {
this.__setMaterial( ...args )
this.a.material( this.__material )
if( oneops.indexOf( name ) === -1 )
this.b.material( this.__material )
return this
}
const pushString = function( name ) {
const glslobj = glslops[ name ]
// some definitions are a single string, and not split into
// separate float and opOut functions
if( typeof glslobj === 'string' ) {
if( DistanceOps.__glsl.indexOf( glslobj ) === -1 ) {
DistanceOps.__glsl.push( glslobj )
}
}else{
// some distance operations are dependent on other ones...
// if this one has dependencies add them.
// dependencies must be added before adding other functions
// so that they're above them in the final GLSL code.
if( glslobj.dependencies !== undefined ) {
for( let dname of glslobj.dependencies ) {
const d = glslops[ dname ]
if( DistanceOps.__glsl.indexOf( d.float ) === -1 ) {
DistanceOps.__glsl.push( d.float )
}
}
}
if( DistanceOps.__glsl.indexOf( glslobj.float ) === -1 ) {
DistanceOps.__glsl.push( glslobj.float )
}
if( DistanceOps.__glsl.indexOf( glslobj.vec2) === -1 ) {
DistanceOps.__glsl.push( glslobj.vec2 )
}
}
}
DistanceOps[ name ].prototype.emit = function ( pname='p', transform = null ){
const isNotOneop = oneops.indexOf( name ) === -1
if( this.__bumpObj !== undefined && this.renderingBump === false) {
this.renderingBump = true
return this.__bumpObj.emit( pname, transform )
}
pushString( name )
if( transform !== null ) this.transform.apply( transform, false )
//this.transform.internal()
// first two args are fixed, rest are variable
let emitters = []
const a = this.a.emit( pname, this.transform )
const b = isNotOneop ? this.b.emit( pname, this.transform ) : this.b.emit()
emitters[0] = a.out
emitters[1] = isNotOneop ? b.out : b
if( this.__len > 2 ) emitters.push( this.c.emit() )
if( this.__len > 3 ) emitters.push( this.d.emit() )
const body = `
vec2 do${this.id} = ${op( ...emitters )};
do${this.id}.x *= ${this.transform.emit()}_scale;
`
const output = {
out: 'do'+this.id,
preface: (a.preface || '') + ( isNotOneop ? b.preface || '' : '') + body
}
this.renderingBump = false
return output
}
DistanceOps[name].prototype.emit_decl = function () {
if( this.__bumpObj !== undefined && this.emittingDecl === false) {
this.emittingDecl = true
return this.__bumpObj.emit_decl()
}
let str = this.transform.emit_decl() + this.a.emit_decl() + this.b.emit_decl()
if( this.c !== undefined ) str += this.c.emit_decl()
if( this.d !== undefined ) str += this.d.emit_decl()
if( ops[ name ].code !== undefined ) {
//str += ops[ name ].code
if( Marching.requiredOps.indexOf( ops[ name ].code ) === - 1 ) {
Marching.requiredOps.push( ops[ name ].code )
}
}
this.emittingDecl = false
return str
};
DistanceOps[name].prototype.update_location = function(gl, program) {
if( this.__bumpObj !== undefined && this.updating === false) {
this.updating = true
return this.__bumpObj.update_location( gl, program )
}
this.a.update_location( gl, program )
this.transform.update_location( gl, program )
this.b.update_location( gl, program )
if( this.c !== undefined ) this.c.update_location( gl, program )
if( this.d !== undefined ) this.d.update_location( gl, program )
this.updating = false
}
DistanceOps[name].prototype.upload_data = function(gl) {
if( this.__bumpObj !== undefined && this.uploading === false ) {
this.uploading = true
return this.__bumpObj.upload_data( gl )
}
this.transform.internal()
this.transform.upload_data( gl )
this.a.transform.apply( this.transform )
if( oneops.indexOf( name ) === -1 )
this.b.transform.apply( this.transform )
this.a.upload_data( gl )
this.b.upload_data( gl )
if( this.c !== undefined ) this.c.upload_data( gl )
if( this.d !== undefined ) this.d.upload_data( gl )
this.uploading = false
}
}
module.exports = DistanceOps