glsl-transpiler
Version:
Transpile GLSL to JS
1,543 lines (1,239 loc) • 38.9 kB
JavaScript
'use strict'
/**
* Transform glsl to js.
*
* Dev notes.
* glsl-parser often creates identifiers/other nodes by inheriting them from definition.
* So by writing som additional info into nodes, note that it will be accessible everywhere below, where initial id is referred by.
*
* @module glsl-transpiler/lib/index
*/
import Emitter from 'events'
import inherits from 'inherits'
import assert from 'assert'
import parse from './parse.js'
import builtins from './builtins.js'
import types from './types.js'
import operators from './operators.js'
import stdlib from './stdlib.js'
import Descriptor from './descriptor.js'
import prepr from 'prepr'
var floatRE = /^-?[0-9]*(?:.[0-9]+)?(?:e-?[0-9]+)?$/i
var swizzleRE = /^[xyzwstpdrgba]{1,4}$/
/**
* Create GLSL codegen instance
*
* @constructor
*/
function GLSL(options) {
if (!(this instanceof GLSL)) return new GLSL(options)
Object.assign(this, options)
this.reset()
//return function compiler for convenience
var compile = this.compile.bind(this)
compile.compiler = this
compile.compile = compile
return compile
}
inherits(GLSL, Emitter)
/**
* Basic rendering settings
*/
GLSL.prototype.optimize = true
GLSL.prototype.preprocess = prepr
GLSL.prototype.debug = false
GLSL.prototype.version = '100 es'
/**
* Operator names
*/
GLSL.prototype.operators = operators.operators
/**
* Type constructors
*/
GLSL.prototype.types = types
/**
* Map of builtins with their types
*/
GLSL.prototype.builtins = builtins
/**
* Parse string arg, return ast.
*/
GLSL.prototype.parse = parse
/**
* Stdlib functions
*/
GLSL.prototype.stdlib = stdlib
/**
* Initialize analysing scopes/vars/types
*/
GLSL.prototype.reset = function () {
if (this.descriptors) this.descriptors.clear()
//cache of descriptors associated with nodes
else this.descriptors = new Map()
//scopes analysed. Each scope is named after the function they are contained in
this.scopes = {
global: {
__name: 'global',
__parentScope: null
}
}
//hash of registered structures
this.structs = {
}
//collected uniforms
this.uniforms = {
}
//collected varying-s
this.varyings = {
}
//collected attributes
this.attributes = {
}
//collected functions, with output types
this.functions = {
}
//collected stdlib functions need to be included
if (this.includes == null || this.includes === true) {
this.includes = {
}
}
//current scope of the node processed
this.currentScope = 'global'
}
/**
* Compile whether string or tree to js
*/
GLSL.prototype.compile = function compile(arg) {
//apply preprocessor
if (this.preprocess) {
if (this.preprocess instanceof Function) {
arg = this.preprocess(arg)
}
else {
arg = prepr(arg)
}
}
arg = this.parse(arg)
var result = this.process(arg)
result = this.stringifyStdlib(this.includes) + '\n' + result
return result
}
/**
* Process glsl AST node so that it returns descriptor for a node
* which by default casts to a string
* but contains additional info:
* `component` values, if node operates on array
* `type` which is returned from the node
* `complexity` of the node
*/
GLSL.prototype.process = function (node, arg) {
//we don’t process descriptors
if (node instanceof String) {
return node
}
//return cached descriptor, if already was processed
if (this.descriptors.has(node)) {
return this.descriptors.get(node)
}
//cache simple things as easy descriptors
if (node == null ||
typeof node === 'number' ||
typeof node === 'string' ||
typeof node === 'boolean') {
return this.cache(node, Descriptor(node, { complexity: 0 }))
}
//in some cases glsl-parser returns node object inherited from other node
//which properties exist only in prototype.
//Insofar structures take it’s definition type, so should be ignored.
//See #Structures test for example.
if (!node.hasOwnProperty('type')) return this.cache(node, Descriptor(null))
var t = this.transforms[node.type]
var startCall = false
//wrap unknown node
if (t === undefined) {
console.warn(`Unknown node type '${node.type}'`)
return this.cache(node, null)
}
if (!t) {
return this.cache(node, null)
}
if (typeof t !== 'function') {
return this.cache(node, t)
}
//do start routines on the first call
if (!this.started) {
this.emit('start', node)
this.started = true
startCall = true
}
//apply node serialization
var result = t.call(this, node, arg)
if (this.optimize) {
result = this.optimizeDescriptor(result)
}
this.cache(result)
this.addInclude(result.include)
//invoke end
if (startCall) {
this.started = false
this.emit('end', node)
}
return result
}
/**
* Try to optimize descriptor -
* whether expanding components is more profitable than keeping complex version
*/
GLSL.prototype.optimizeDescriptor = function (descriptor) {
//try to optimize
if (this.optimize && descriptor.optimize !== false && descriptor.components) {
var complexity = descriptor.components.reduce(function (prev, curr) {
return prev + curr.complexity || 0
}, 0)
if (complexity < descriptor.complexity) {
//expand array, if complexity is ok
if (descriptor.components && descriptor.components.length > 1) {
var include = descriptor.components.map(function (c) { return c.include; }, this).filter(Boolean)
return Descriptor(`new Float32Array([${descriptor.components.join(', ')}])`, Object.assign(descriptor, {
include: include,
complexity: complexity
}))
}
}
}
return descriptor
}
/**
* Cache descriptor, return it
*/
GLSL.prototype.cache = function (node, value) {
if (this.descriptors.has(node)) return this.descriptors.get(node)
//force descriptor on save
if (!(value instanceof String)) value = Descriptor(value)
this.descriptors.set(node, value)
return this.descriptors.get(node)
}
/**
* List of transforms for various token types
*/
GLSL.prototype.transforms = {
stmtlist: function (node) {
if (!node.children.length) return Descriptor(null)
var result = node.children.map(this.process, this).join('\n')
return Descriptor(result)
},
stmt: function (node) {
var result = node.children.map(this.process, this).join('')
if (result && result[result.length - 1] !== ';') result += ';'
return Descriptor(result)
},
struct: function (node) {
var structName = node.children[0].data
//get args nodes
var args = node.children.slice(1)
var argTypes = []
//arg names
var argsList = args.map(function (arg) {
if (arg.type !== 'decl') throw Error('Struct statements should be declarations.')
var decllist = arg.children[arg.children.length - 1]
if (decllist.type !== 'decllist') throw Error('Struct statement declaration has wrong structure.')
return decllist.children.map(function (node) {
// { vec3 direction; }
if (node.type === 'ident') return node.data
// { vec3 data[2]; }
if (node.type === 'quantifier') {
// console.log(node)
}
throw Error('Struct statement contains something strange.')
})
}).flat()
var argTypes = args.map(function (arg) {
var type = arg.children[4].token.data
var decllist = arg.children[arg.children.length - 1]
return decllist.children.map(function () {
return type
})
}).flat()
var struct = function struct() {
var args = arguments
var includes = []
var fields = argsList.map(function (argName, i) {
if (args[i]) {
var initValue = this.process(args[i])
}
else {
var initValue = this.types[argTypes[i]].call(this, args[i])
}
initValue = this.optimizeDescriptor(initValue)
includes = includes.concat(initValue.include)
return Descriptor(`${argName}: ${initValue}`, {
type: argTypes[i],
optimize: false,
components: initValue.components
})
}, this)
return Descriptor(`{\n${fields.join(',\n')}\n}`, {
type: structName,
optimize: false,
include: includes.filter(Boolean),
components: fields
})
}.bind(this)
//we should set length to be a compatible type constructor
Object.defineProperty(struct, 'length', { value: argTypes.length })
//register struct constructor, in a fashion of type constructors
this.structs[structName] =
this.types[structName] = struct
return Descriptor(null)
},
function: function (node) {
var result = ''
//if function has no body, that means it is interface for it. We can ignore it.
if (node.children.length < 3) return Descriptor(null)
//add function name - just render ident node
assert.equal(node.children[0].type, 'ident', 'Function should have an identifier.')
var name = this.process(node.children[0])
//add args
assert.equal(node.children[1].type, 'functionargs', 'Function should have arguments.')
var args = this.process(node.children[1])
//get out type of the function in declaration
var outType = node.parent.children[4].token.data
//add argument types suffix to a fn
var argTypesSfx = args.components.map(function (arg) {
return `${arg.type}`
}).join('_')
//sort arguments by qualifier
var inArgs = []
var outArgs = []
args.components.forEach(function (arg, index) {
arg.index = index
if (arg.qualifier.slice(0, 2) === 'in') {
inArgs.push(arg)
}
if (arg.qualifier.slice(-3) === 'out') {
outArgs.push(arg)
}
})
if (outArgs.length === 0) {
outArgs = null
}
//if main name is registered - provide type-scoped name of function
if (this.functions[name] && argTypesSfx) {
name = `${name}_${argTypesSfx}`
}
//add body
assert.equal(node.children[2].type, 'stmtlist', 'Function should have a body.')
//create function body
result += `function ${name} (${args}) {\n`
//guard input parameters from being mutated
inArgs.forEach(function (arg) {
if (/^(vec|mat)/.test(arg.type)) {
result += `${arg} = ${arg}.slice();\n`
}
})
//populate current scope information
//this is used by the `return` transform
var scope = this.scopes[this.currentScope]
scope.callName = name
scope.outArgs = outArgs
result += this.process(node.children[2])
if (outArgs && outType === 'void') {
//the output list is usually created when transforming the `return` statement
//but this function does not have a `return` at the end
result += `\n${name}.__out__ = [${outArgs.join(', ')}];`
}
result = result.replace(/\n/g, '\n\t')
result += '\n}'
//get scope back to the global after fn ended
this.currentScope = this.scopes[this.currentScope].__parentScope.__name
//create descriptor
result = Descriptor(result, {
type: outType,
complexity: 999
})
//save the output arguments list
//this is used by the `call` transform
result.outArgs = outArgs
//register function descriptor
this.functions[name] = result
return result
},
//function arguments are just shown as a list of ids
functionargs: function (node) {
//create new scope - func args are the unique token stream-style detecting a function entry
var lastScope = this.currentScope
var scopeName = (node.parent && node.parent.children[0].data) || 'global'
this.currentScope = scopeName
if (!this.scopes[scopeName]) {
this.scopes[scopeName] = {
__parentScope: this.scopes[lastScope],
__name: scopeName
}
}
var comps = node.children.map(this.process, this)
return Descriptor(comps.join(', '), {
components: comps
})
},
//declarations are mapped to var a = n, b = m
//decl defines it’s inner placeholders rigidly
decl: function (node) {
var result
var typeNode = node.children[4]
var decllist = node.children[5]
//register structure
if (node.token.data === 'struct') {
this.process(typeNode)
if (!decllist) return Descriptor(null)
}
assert(
decllist.type === 'decllist' ||
decllist.type === 'function' ||
decllist.type === 'struct',
'Decl structure is malicious')
//declare function as hoisting one
if (decllist.type === 'function') {
return this.process(decllist)
}
//case of function args - drop var
if (node.parent.type === 'functionargs') {
result = this.process(decllist)
// in/out/inout
var qualifier = node.token.data
if (qualifier === result.type) {
result.qualifier = 'in'
} else {
result.qualifier = qualifier
}
return result
}
//default type, like variable decl etc
else {
result = this.process(decllist)
}
//prevent empty var declaration
if (!result || !result.trim()) return Descriptor(null, {
type: result.type,
components: result.components,
optimize: false
})
return Descriptor(`var ${result}`, {
type: result.type,
components: result.components,
optimize: false
})
},
//decl list is the same as in js, so just merge identifiers, that's it
decllist: function (node) {
var ids = []
var lastId = 0
//get datatype - it is the 4th children of a decl
var dataType = node.parent.children[4].token.data
//unwrap anonymous structure type
if (dataType === 'struct') {
dataType = node.parent.children[4].children[0].data
}
//attribute, uniform, varying etc
var bindingType = node.parent.children[1].token.data
//get dimensions - it is from 5th to the len-1 nodes of a decl
//that’s in case if dimensions are defined first-class like `float[3] c = 1;`
//result is [] or [3] or [1, 2] or [4, 5, 5], etc.
//that is OpenGL 3.0 feature
var dimensions = []
for (var i = 5, l = node.parent.children.length - 1; i < l; i++) {
dimensions.push(parseInt(node.parent.children[i].children[0].children[0].data))
}
for (var i = 0, l = node.children.length; i < l; i++) {
var child = node.children[i]
if (child.type === 'ident') {
var ident = this.process(child)
ident.type = dataType
lastId = ids.push(ident)
//save identifier to the scope
this.variable(ident, {
type: dataType,
binding: bindingType,
node: child,
dimensions: []
})
}
else if (child.type === 'quantifier') {
//with non-first-class array like `const float c[3]`
//dimensions might be undefined, so we have to specify them here
var dimensions = this.variable(ids[lastId - 1]).dimensions
dimensions.push(parseInt(child.children[0].children[0].data))
this.variable(ids[lastId - 1], { dimensions: dimensions })
}
else if (child.type === 'expr') {
var ident = ids[lastId - 1]
//ignore wrapping literals
var value = this.process(child)
//save identifier initial value
this.variable(ident, { value: value })
}
else {
throw Error('Undefined type in decllist: ' + child.type)
}
}
var functionargs = node.parent.parent.type === 'functionargs'
//get binding type fn
var replace = this[bindingType]
var comps = ids.map(function (ident, i) {
if (functionargs) return ident
var result = this.variable(ident).value
//emptyfier, like false or null value
if (replace !== undefined && !replace) {
return ''
}
//function replacer
else if (replace instanceof Function) {
var callResult = replace(ident, this.variable(ident))
//if call result is something sensible - use it
if (callResult != null) {
result = callResult
}
}
//if result is false/null/empty string - ignore variable definition
if (!(result + '') && result !== 0) return ident
return `${ident} = ${result}`
}, this).filter(Boolean)
var res = Descriptor(comps.join(', '), {
type: dataType
})
return res
},
//placeholders are empty objects - ignore them
placeholder: function (node) {
return node.token.data
},
//i++, --i etc
suffix: function (node) {
var str = this.process(node.children[0])
return Descriptor(str + node.data, { type: str.type })
},
//loops are the same as in js
forloop: function (node) {
var init = this.process(node.children[0])
var cond = this.process(node.children[1])
var iter = this.process(node.children[2])
var body = this.process(node.children[3])
return Descriptor(`for (${init}; ${cond}; ${iter}) {\n${body}\n}`, {
})
},
whileloop: function (node) {
var cond = this.process(node.children[0])
var body = this.process(node.children[1])
return Descriptor(`while (${cond}) {\n${body}\n}`, {
})
},
operator: function (node) {
//access operators - expand to arrays
if (node.data === '.') {
// a.x or a().x
var identNode = node.children[0]
var ident = this.process(identNode)
var type = ident.type
var prop = node.children[1].data
//ab.xyz for example
if (swizzleRE.test(prop)) {
return this.unswizzle(node)
}
return Descriptor(`${ident}.${prop}`, {
type: type
})
}
throw Error('Unknown operator ' + node.data)
},
expr: function (node) {
var complexity = 0
var result = node.children.map(function (n) {
var res = this.process(n)
complexity += res.complexity;
return res
}, this).join('')
result = Descriptor(result, { complexity: complexity })
return result
},
precision: function () {
return Descriptor(null)
},
//FIXME: it never creates comments
comment: function (node) {
return Descriptor(null)
},
preprocessor: function (node) {
return Descriptor('/* ' + node.token.data + ' */')
},
keyword: function (node) {
var type
if (node.data === 'true' || node.data === 'false') type = 'bool'
//FIXME: guess every other keyword is a type, isn’t it?
else type = node.data
return Descriptor(node.data, {
type: type,
complexity: 0,
optimize: false
})
},
ident: function (node) {
//get type of registered var, if possible to find it
var id = node.token.data
var scope = this.scopes[this.currentScope]
//find the closest scope with the id
while (scope[id] == null) {
scope = scope.__parentScope
if (!scope) {
// console.warn(`'${id}' is not defined`)
break
}
}
var str = node.data
if (scope) {
var type = scope[id].type
var res = Descriptor(str, {
type: type,
complexity: 0
})
return res
}
//FIXME: guess type more accurately here
return Descriptor(str, {
type: null,
complexity: 0
})
},
return: function (node) {
var expr = this.process(node.children[0])
var result
var scope = this.scopes[this.currentScope]
if (scope.outArgs) {
var outStmt = `${scope.callName}.__out__ = [${scope.outArgs.join(', ')}]`
if (expr.visible) {
// func.__return__ = <expression>;
// func.__out__ = [outArg1, outArg2, ...];
// return func.__return__;
result = `${scope.callName}.__return__ = ${expr};\n${outStmt};\nreturn ${scope.callName}.__return__`
} else {
// func.__out__ = [outArg1, outArg2, ...];
// return;
result = `${outStmt};\nreturn`
}
} else {
// return <expression>;
result = 'return' + (expr.visible ? ' ' + expr : '')
}
return Descriptor(result, { type: expr.type })
},
continue: function () { return Descriptor('continue') },
break: function () { return Descriptor('break') },
discard: function () { return Descriptor('discard()') },
'do-while': function (node) {
var exprs = this.process(node.children[0])
var cond = this.process(node.children[1])
return Descriptor(`do {\n${exprs}\n} while (${cond})`, {
})
},
binary: function (node) {
var leftNode = node.children[0]
var rightNode = node.children[1]
var left = this.process(leftNode)
var right = this.process(rightNode)
var leftType = left.type
var rightType = right.type
var operator = node.data
//data access operator
if (node.data === '[') {
//for case of glsl array access like float[3]
if (this.types[node.type]) {
return Descriptor(`${leftType}[${right}]`, {
type: this.types[leftType].type,
complexity: left.complexity + right.complexity + 1
})
}
//matrix/etc double access a[1][2]
if (leftNode.type === 'binary') {
var matNode = leftNode.children[0]
var matDesc = this.process(matNode)
var vecSize = this.types[leftType].length
var matType = matDesc.type
var matSize = this.types[matType].length
var outerRight = this.process(leftNode.children[1])
var idx = parseFloat(outerRight) | 0
var offset = parseFloat(right) | 0
//if number - try to access component
if (!isNaN(idx) && !isNaN(offset)) {
return Descriptor(matDesc.components[vecSize * idx + offset], {
type: 'float',
complexity: matDesc.complexity + right.complexity + 1
})
}
//if calc - do slice
else {
return Descriptor(`${matDesc}[${outerRight} * ${vecSize} + ${right}]`, {
type: 'float',
complexity: matDesc.complexity + outerRight.complexity + right.complexity + 2
})
}
}
//matrix single access a[0] → vec
if (/mat/.test(leftType)) {
var size = this.types[leftType].length
var start = this.processOperation(right, Descriptor(size), '*')
var end = this.processOperation(start, Descriptor(size), '+')
var comps = floatRE.test(start) && floatRE.test(end) ? left.components.slice(start, end) : undefined
var res = Descriptor(`${left}.subarray(${start}, ${end})`, {
type: this.types[leftType].type,
complexity: left.complexity + size,
components: comps
})
res = this.optimizeDescriptor(res)
return res
}
//detect array access
//FIXME: double array access here will fail
var leftVar = this.variable(left)
var type = leftVar && leftVar.dimensions && leftVar.dimensions.length ? leftType : this.types[leftType].type
//something[N] return as is
return Descriptor(`${left}[${right}]`, {
type: type || null,
complexity: left.complexity + right.complexity + 1
})
}
//default binary operators a × b
return this.processOperation(left, right, operator)
},
assign: function (node) {
var operator = node.data, right = this.process(node.children[1]), left
if (node.children[0].type === 'identifier') {
left = Descriptor(node.children[0].data, {
type: right.type,
optimize: false,
complexity: 0
})
}
else {
left = this.process(node.children[0])
}
var target = left
// here some targets may be unswizzled already, eg.
// [a[0], a[1]], a[0][0], etc.
var isSwizzle = node.children[0].type === 'operator' &&
swizzleRE.test(node.children[0].children[1].data)
//a *= b.x (single-operand)
if (!isSwizzle && this.types[right.type].length == 1 && this.types[target.type].length == 1) {
return Descriptor(`${target} ${operator} ${right}`, {
type: right.type,
complexity: target.complexity + 1 + right.complexity
})
}
//mat3[0] *= vec3; - left can be a structure property set a.prop
if (!isSwizzle && this.types[right.type].length > 1 && this.types[target.type].length > 1) {
let rcomp = right.components,
compComplexity = right.components.reduce((total, comp) => (
comp.complexity + total
), 0),
reduceComplexity = 3 + target.complexity + this.types[right.type].length + right.complexity,
components
if (rcomp?.length == target.components?.length) {
components = target.components.map((comp, i) => (
Descriptor(`${comp} ${operator} ${rcomp[i]}`, { type: rcomp[i].type, complexity: rcomp[i].complexity + 1 })
))
// optimize for individual components
if (compComplexity < reduceComplexity) return Descriptor(`(${components.join(', ')}, ${target})`, {
type: right.type,
complexity: compComplexity,
components
})
}
return Descriptor(`${right}.reduce((res,el,i)=>(res[i] ${operator} el, res), ${target})`, {
type: right.type,
complexity: reduceComplexity,
components
})
}
//in cases of setting swizzle - we have to place left unswizzle to the right
if (isSwizzle) {
var positions = this.swizzlePositions(node.children[0].children[1].data)
var len = this.types[this.process(node.children[0].children[0]).type].length
var ids = Array(len).fill('null')
for (var i = 0; i < positions.length; i++) {
ids[positions[i]] = i
}
var targetType = node.children[0].children[0].type
// a.x = ...
if ((targetType === 'ident' || targetType === 'builtin')) {
target = Descriptor(node.children[0].children[0].data, {
type: right.type,
optimize: false
})
}
//a.wy *= a.zx →
//a = [null, 1, null, 0].map(function (idx, i) {
// return idx == null ? gl_position[i] : this[idx]
//}, a.wy * a.zx)
if (positions.length > 1) {
//*=
if (operator.length > 1) {
var subOperator = operator.slice(0, -1)
right = this.processOperation(this.unswizzle(node.children[0]), right, subOperator)
right = this.optimizeDescriptor(right)
}
var comps = Array(len)
for (var i = 0; i < len; i++) {
comps[i] = Descriptor(`${target}[${i}]`, {
type: 'float',
complexity: 1
})
}
for (var i = 0; i < positions.length; i++) {
comps[positions[i]] = right.components[i]
}
right = Descriptor(
`[${ids.join(', ')}].map(function (idx, i) { return idx == null ? ${target}[i] : this[idx]; }, ${right})`, {
type: right.type,
complexity: len * 4 + right.complexity,
include: right.include,
components: comps
})
right = this.optimizeDescriptor(right)
return Descriptor(`${target} = ${right}`, {
type: right.type,
optimize: false,
include: right.include
})
}
//a.x *= b → a[0] *= b
else {
if (targetType === 'builtin' || targetType === 'ident') {
return Descriptor(`${target}[${positions[0]}] ${operator} ${right}`, {
type: right.type,
optimize: false
})
} else {
return Descriptor(`${target} ${operator} ${right}`, {
type: right.type,
optimize: false
})
}
}
}
//`a *= x` → `a = a * x`
else if (operator.length > 1) {
var subOperator = operator.slice(0, -1)
right = this.processOperation(left, right, subOperator)
right = this.optimizeDescriptor(right)
}
//simple assign, =
return Descriptor(`${target} = ${right}`, {
type: right.type,
complexity: 1,
include: right.include
})
},
ternary: function (node) {
var cond = this.process(node.children[0])
var a = this.process(node.children[1])
var b = this.process(node.children[2])
return Descriptor(`${cond} ? ${a} : ${b}`, { type: a.type })
},
unary: function (node) {
var str = this.process(node.children[0])
var complexity = str.complexity + 1
//ignore + operator, we dont need to cast data
if (node.data === '+') {
//++x
if (node.children[0].type === 'unary') {
return Descriptor(node.data + str, { type: str.type, complexity: complexity })
}
else if (node.children[0].parent.type === 'unary') {
return Descriptor(node.data + str, { type: str.type, complexity: complexity })
}
//+x
return Descriptor(str)
}
return this.processOperation(null, str, node.data)
},
//gl_Position, gl_FragColor, gl_FragPosition etc
builtin: function (node) {
return Descriptor(node.data, {
type: this.builtins[node.data],
complexity: 0
})
},
call: function (node) {
var args = node.children.slice(1)
var argValues = args.map(this.process, this)
var argTypes = argValues.map(function (arg) {
return arg.type
}, this)
//if first node is an access, like a.b() - treat special access-call case
if (node.children[0].data === '.') {
var methodNode = node.children[0].children[1]
var holderNode = node.children[0].children[0]
var methodName = this.process(methodNode)
var holderName = this.process(holderNode)
var type = holderName.type
//if length call - return length of a vector
//vecN.length → N
if (methodName == 'length' && this.types[type].length > 1) {
return Descriptor(this.types[type].length, {
type: 'int',
complexity: 0
})
}
var callName = Descriptor(`${holderName}.${methodName}`, {
type: methodName.type,
complexity: holderName.complexity + methodName.complexity
})
}
//first node is caller: float(), float[2](), vec4[1][3][4]() etc.
else {
var callName = this.process(node.children[0])
}
//if first child of the call is array call - expand array
//FIXME: in cases of anonymously created arrays of arrays, outside of declarations, there might be an issue: `vec4[3][3](0,1)`
if (node.children[0].data === '[') {
var dimensions = []
var keywordNode = node.children[0]
while (keywordNode.type != 'keyword') {
dimensions.push(parseInt(keywordNode.children[1].data))
keywordNode = keywordNode.children[0]
}
//if nested type is primitive - expand literals without wrapping
var value = ''
if (this.types[callName]) {
value += args.map(this.process, this).join(', ')
} else {
value += callName + '('
value += args.map(this.process, this).join(', ')
value += ')'
}
//wrap array init expression
return Descriptor(this.wrapDimensions(argValues, dimensions.reverse()), {
type: callName.type,
complexity: 999
})
}
//else treat as function/constructor call
else {
if (this.debug) {
if (callName == 'print') {
var args = argValues.map(function (a) {
return a + ':' + a.type
})
console.log.apply(console, args)
return Descriptor(null)
}
if (callName == 'show') {
console.log.apply(console, argValues.map(function (a) {
return a
}))
return Descriptor(null)
}
}
//struct(), vec2(), float()
if (this.types[callName]) {
return this.types[callName].apply(this, args)
}
//someFn()
else {
var type, optimize = true, outArgs = null
//registered fn()
var fn = this.functions[callName]
if (fn) {
var sfx = argTypes.join('_')
if (sfx && this.functions[`${callName}_${sfx}`]) {
fn = this.functions[`${callName}_${sfx}`]
type = fn.type
outArgs = fn.outArgs
callName = Descriptor(`${callName}_${sfx}`, {
complexity: callName.complexity
})
}
else {
type = fn.type
outArgs = fn.outArgs
}
}
//stdlib()
else if (this.stdlib[callName]) {
this.addInclude(callName)
//if callname is other than included name - redirect call name
if (this.stdlib[callName].name) {
callName = this.stdlib[callName].name
}
//add other includes if any
this.addInclude(this.stdlib[callName].include)
type = this.stdlib[callName].type
if (typeof type === 'function') {
type = type.call(this, node)
}
}
if (!type) {
// Unable to guess the type of '${callName}'
// keep type as null, meaning that can be any
type = null
optimize = false
}
var res = `${callName}(${argValues.join(', ')})`
if (outArgs) {
// calling func(in a, out b, out c):
// (func(a, b, c), [b, c] = func.__out__, func.__return__)
var outList = outArgs.map(function (arg) {
return argValues[arg.index]
})
res = `(${res}, [${outList.join(', ')}] = ${callName}.__out__, ${callName}.__return__)`
}
return Descriptor(res, {
type: type || callName.type,
complexity: 999 /* argValues.reduce(function (prev, curr) {
return curr.complexity+prev
}, callName.complexity||999) */,
optimize: optimize
})
}
}
},
literal: function (node) {
//convert 023 → 0o23
if (/^0[0-9]+/.test(node.data)) {
node.data = '0o' + node.data.slice(1)
}
//if special format - parse it as int, else - return unchanged
var result = /^[0-9][xob]/.test(node.data) ? Number(node.data) : node.data
//guess type - as far in js any number tends to be a float, give priority to it
//in order to avoid unnecessary types alignment
var type
if (/true|false/i.test(node.data)) type = 'bool'
else if (/^[0-9]+$/.test(node.data) > 0) type = 'int'
else if (floatRE.test(node.data)) type = 'float'
return Descriptor(result, { type: type, complexity: 0 })
},
//ifs are the same as js
if: function (node) {
var cond = this.process(node.children[0])
var ifBody = this.process(node.children[1])
var result = `if (${cond}) {\n${ifBody}\n}`
if (node.children.length > 1) {
var elseBody = this.process(node.children[2])
if (elseBody.visible) result += ` else {\n${elseBody}\n}`
}
return Descriptor(result, {
type: 'float'
})
},
//grouped expression like a = (a - 1)
group: function (node) {
//children are like (1, 2, 3) - does not make a big sense
//the last one is always taken as a result
var children = node.children.map(this.process, this)
var result = '(' + children.join(', ') + ')'
var last = children[children.length - 1]
//each component therefore should be wrapped to group as well
//FIXME: single-multip location ops like (x*34.) + 1. are possible to be unwrapped, providing that they are of the most precedence.
if (last.components) {
last.components = last.components.map(function (comp) {
//if component contains no operations (we not smartly guess that each op adds to complexity) - keep component as is.
if (comp.complexity === 1) return comp
//otherwise wrap it, as it may contain precedences etc.
return Descriptor('(' + comp + ')', comp)
})
}
return Descriptor(result, {
type: last.type,
components: last.components,
complexity: children.reduce(function (prev, curr) { return prev + curr.complexity || 0 }, 0)
})
}
// switch: function () {
//FIXME: not implemented in glsl-parser
// }
}
/**
* Return list if ids for swizzle letters
*/
GLSL.prototype.swizzlePositions = function (prop) {
var swizzles = 'xyzwstpdrgba'
var positions = []
for (var i = 0, l = prop.length; i < l; i++) {
var letter = prop[i]
var position = swizzles.indexOf(letter) % 4
positions.push(position)
}
return positions
}
/**
* Transform access node to a swizzle construct
* ab.xyz → [ab[0], ab[1], ab[2]]
*/
GLSL.prototype.unswizzle = function (node) {
var identNode = node.children[0]
var ident = this.process(identNode)
var type = ident.type
var prop = node.children[1].data
var positions = this.swizzlePositions(prop)
var args = positions.map(function (position) {
//[0, 1].yx → [1, 0]
// a.yx → [a[1], a[0]]
return ident.components && ident.components[position] || position
})
//a.x → a[0]
if (args.length === 1) {
var result
// unknown identifiers or calls often have undefined components
// a.z → a[2]
if (typeof args[0] === 'number') {
result = Descriptor(`${ident}[${args[0]}]`, { type: null, complexity: 999 })
}
else {
if (args[0] == null) console.warn(`Cannot unswizzle '${ident.type}(${ident}).${prop}': ${prop} is outside the type range.`)
result = Descriptor(args[0] || `undefined`, {
type: 'float',
complexity: 1
})
}
return result
}
//vec2 a.xy → a
if (type && args.length === this.types[type].length && positions.every(function (position, i) { return position === i })) {
return ident
}
var complexity = args.length * ident.complexity
//a.yz → [1, 2].map(function(x) { return this[x]; }, a)
var result = Descriptor(`new Float32Array([${positions.join(', ')}].map(function (x, i) { return this[x]}, ${ident}))`, {
complexity: ident.components ? args.length * 2 : 999,
type: `vec${args.length}`,
components: ident.components && args
})
result = this.optimizeDescriptor(result)
return result
}
/**
* Get/set variable from/to a [current] scope
*/
GLSL.prototype.variable = function (ident, data, scope) {
if (!scope) scope = this.currentScope
//set/update variable
if (data) {
//create variable
if (!this.scopes[scope][ident]) {
this.scopes[scope][ident] = {}
}
var variable = Object.assign(this.scopes[scope][ident], data)
//preset default value for a variable, if undefined
if (data.value == null) {
if (this.types[variable.type]) {
//for sampler types pass name as arg
if (/sampler|image/.test(variable.type)) {
variable.value = this.types[variable.type].call(this, ident)
}
else {
variable.value = this.types[variable.type].call(this)
}
}
//some unknown types
else {
variable.value = variable.type + `()`
}
variable.value = this.optimizeDescriptor(variable.value)
variable.value = this.wrapDimensions(variable.value, variable.dimensions)
}
//if value is passed - we guess that variable knows how to init itself
//usually it is `call` node rendered
// else {
// }
//just set an id
if (variable.id == null) variable.id = ident
//save scope
if (variable.scope == null) variable.scope = this.scopes[scope]
//save variable to the collections
if (variable.binding === 'uniform') {
this.uniforms[ident] = variable
}
if (variable.binding === 'attribute') {
this.attributes[ident] = variable
}
if (variable.binding === 'varying') {
this.varyings[ident] = variable
}
return variable
}
//get varialbe
return this.scopes[scope][ident]
}
/**
* Return value wrapped to the proper number of dimensions
*/
GLSL.prototype.wrapDimensions = function (value, dimensions) {
//wrap value to dimensions
if (dimensions.length) {
if (!Array.isArray(value)) value = [value]
value = dimensions.reduceRight(function (value, curr) {
var result = []
//for each dimension number - wrap result n times
var prevVal, val
for (var i = 0; i < curr; i++) {
val = value[i] == null ? prevVal : value[i]
prevVal = val
result.push(val)
}
return `[${result.join(', ')}]`
}, value)
}
return value
}
/**
* Operator renderer
*/
GLSL.prototype.processOperation = operators
/**
* Add include, pass optional prop object
* For example addInclude('vec3', 'add') will include `vec3` class
* with its `add` method
*/
GLSL.prototype.addInclude = function (name, prop) {
if (!name || !this.includes) return
if (Array.isArray(name)) {
return name.forEach(function (i) {
this.addInclude(i)
}, this)
}
if (!(name instanceof String) && typeof name === 'object') {
for (var subName in name) {
this.addInclude(subName, name[subName])
}
return
}
if (!prop) {
if (this.includes[name] == null) this.includes[name] = true
}
else {
if (this.includes[name] == null || this.includes[name] === true) this.includes[name] = {}
this.includes[name][prop] = true
}
}
/**
* Get stdlib source for includes
*/
GLSL.prototype.stringifyStdlib = function (includes) {
if (!includes) includes = this.includes
var methods = []
for (var meth in includes) {
if (!includes[meth]) continue
//eg vecN
var result = this.stdlib[meth].toString()
methods.push(result)
//eg vecN.operation
if (includes[meth]) {
for (var prop in includes[meth]) {
if (!this.stdlib[meth][prop]) {
console.warn(`Cannot find '${meth}.${prop}' in stdlib`)
continue
}
methods.push(`${meth}.${prop} = ${this.stdlib[meth][prop].toString()}`)
}
}
}
return methods.join('\n')
}
export default GLSL