diff --git a/README.md b/README.md index 76152ec..751eccd 100644 --- a/README.md +++ b/README.md @@ -4,35 +4,24 @@ ![img](http://i.imgur.com/bROGMVq.png) -A helper module for unit testing shaders and comparing the result of `gl_FragColor` from a 1x1 WebGL canvas. See [glsl-hsl2rgb](https://github.com/Jam3/glsl-hsl2rgb) for a practical example. +A helper module for processing rectangular shaders and obtaining the reslut. Can be used for unit testing, audio processing etc. See [glsl-hsl2rgb](https://github.com/Jam3/glsl-hsl2rgb) or [audio-shader](https://github.com/audio-lab/audio-shader) for practical examples. -Example: +Example: ```js var ShaderOutput = require('gl-shader-output') -//your shader, could be a simple glsl-shader-core object -var glslify = require('glslify') -var shader = glslify({ - vertex: [ - 'attribute vec2 position;', - 'void main() {', - 'gl_Position = vec4(position, 1.0, 1.0);', - '}' - ].join('\n') - fragment: [ - 'precision mediump float;', - 'uniform float green;', - 'void main() {', - 'gl_FragColor = vec4(0.0, green, 0.0, 1.0);', - '}' - ].join('\n') -}) - //get a draw function for our test -var draw = ShaderOutput({ - shader: shader -}) +var draw = ShaderOutput(` + precision mediump float; + uniform float green; + void main() { + gl_FragColor = vec4(0.0, green, 0.0, 1.0); + } +`, { + width: 1, + height: 1 +}); //returns the frag color as [R, G, B, A] var color = draw() @@ -46,19 +35,21 @@ var almostEqual = require('array-almost-equal') almostEqual(color2, [0.0, 0.5, 0.0, 1.0], epsilon) ``` -You can use this with tools like [smokestack](https://github.com/hughsk/smokestack) for test-driven GLSL development. +You can use this with tools like [smokestack](https://github.com/hughsk/smokestack) for test-driven GLSL development. ## Usage [![NPM](https://nodei.co/npm/gl-shader-output.png)](https://www.npmjs.com/package/gl-shader-output) -#### `draw = ShaderOutput(opt)` +#### `draw = ShaderOutput(source?, options?)` -Takes the following options, and returns a `draw` function. +Takes a shader object/source and options object, and returns a `draw` function. Possible options: -- `shader` the shader (required), can be a function that accepts `gl` or an instance of gl-shader -- `gl` the gl state to re-use, expected to hold a 1x1 canvas (creates a new one if not specified) -- [webgl-context](https://www.npmjs.com/package/webgl-context) options such as `alpha` and `premultipliedAlpha` +- `shader` the shader, can be a source of fragment shader, a function that accepts `gl` or an instance of gl-shader. Same as passing shader as the only argument. +- `gl` the gl state to re-use, expected to hold a canvas (creates a new one if not specified, or uses nogl fallback if there is no webgl in environment). Set `null` to force nogl rendering. +- `width` the width of a gl context, if undefined +- `height` the height of a gl context, if undefined +- other [webgl-context](https://www.npmjs.com/package/webgl-context) options such as `alpha` and `premultipliedAlpha` The draw function has the following signature: @@ -72,4 +63,4 @@ The return value is the gl_FragColor RGBA of the canvas, in floats, such as `[0. ## License -MIT, see [LICENSE.md](http://github.com/Jam3/gl-shader-output/blob/master/LICENSE.md) for details. +MIT, see [LICENSE.md](http://github.com/Jam3/gl-shader-output/blob/master/LICENSE.md) for details. \ No newline at end of file diff --git a/index.js b/index.js index 9a8d376..f40d9d3 100644 --- a/index.js +++ b/index.js @@ -1,41 +1,143 @@ -var create = require('webgl-context') -var getPixels = require('canvas-pixels').get3d -var triangle = require('a-big-triangle') -var xtend = require('xtend') -var assign = require('xtend/mutable') - -module.exports = function(opt) { - opt = xtend({ - width: 1, +/** + * @module gl-shader-output + */ + +var create = require('webgl-context'); +var getPixels = require('canvas-pixels').get3d; +var xtend = require('xtend'); +var assign = require('xtend/mutable'); +var noGl = require('./nogl'); +var Shader = require('gl-shader'); +var glExt = require('webglew'); +var Framebuffer = require('gl-fbo'); + + +module.exports = function (shader, opt) { + //resolve incomplete args + if (!opt) { + //just options + if (typeof shader === 'object' && !shader.fragShader) { + opt = shader; + } + //just a shader object + else { + opt = { + shader: shader + }; + } + } + else { + opt.shader = shader; + } + + //take over passed shader object opts + if (opt.shader && opt.shader.fragShader) { + if (opt.gl === undefined) opt.gl = opt.shader.gl; + } + + //extend default options + opt = xtend({ + width: 1, height: 1, - preserveDrawingBuffer: true - }, opt) - - var gl = opt.gl || create(opt) - if (!opt.shader) - throw new Error('no shader supplied to gl-shader-output') - - var shader = typeof opt.shader === 'function' - ? opt.shader(gl) - : opt.shader - - function process(uniforms) { - gl.clearColor(0, 0, 0, 0) - gl.clear(gl.COLOR_BUFFER_BIT) - - shader.bind() + preserveDrawingBuffer: true, + shader: '' + }, opt); + + //redefine shader + shader = opt.shader; + + //try to obtain veritable gl + var gl = opt.gl === undefined ? create(opt) : opt.gl; + + //if gl is null - use noGL version of renderer + if (!gl) return noGl(shader, opt); + + + //check WebGL extensions to support floats + var glExtensions = glExt(gl); + if ( !glExtensions.OES_texture_float ){ + console.warn("Available webgl does not support OES_texture_float extension. Using noGL instead."); + return noGL(shader, opt); + } + if ( !glExtensions.OES_texture_float_linear ) { + console.warn("Available webgl does not support OES_texture_float_linear extension. Using noGL instead."); + return noGL(shader, opt); + } + + + //ensure shader is created + if (!shader) { + throw new Error('No shader supplied to gl-shader-output'); + } + else if (typeof shader === 'function') { + shader = shader(gl); + } + + //create gl-shader, if only fragment shader is passed + if (typeof shader === 'string') { + shader = Shader(gl, '\ + attribute vec2 position;\ + void main() {\ + gl_Position = vec4(position, 1.0, 1.0);\ + }\ + ' , shader); + } + + + //set gl context dims + gl.canvas.width = opt.width; + gl.canvas.height = opt.height; + + //as far we process 2d rect + gl.disable(gl.DEPTH_TEST); + gl.disable(gl.BLEND); + gl.disable(gl.CULL_FACE); + gl.disable(gl.DITHER); + gl.disable(gl.POLYGON_OFFSET_FILL); + // gl.disable(gl.SAMPLE_ALPHA_COVERAGE); + // gl.disable(gl.SAMPLE_COVERAGE); + // gl.disable(gl.SCISSOR_TEST); + // gl.disable(gl.STENCIL_TEST); + + + //create rendering data + var buffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, -1, 3, 3, -1]), gl.STATIC_DRAW); + shader.attributes.position.pointer(); + + //set framebuffer as a main target + var framebuffer = new Framebuffer(gl, [opt.width, opt.height], { + preferFloat: true, + // float: true, + depth: false, + color: 1 + }); + framebuffer.bind(); + + shader.bind(); + + + function process (uniforms) { + var w = gl.drawingBufferWidth, h = gl.drawingBufferHeight; + + gl.clearColor(0, 0, 0, 0); + gl.clear(gl.COLOR_BUFFER_BIT); //if user specifies some uniforms - if (uniforms) - assign(shader.uniforms, uniforms) + if (uniforms) { + shader.bind(); + assign(shader.uniforms, uniforms); + } //full-screen quad - triangle(gl) + gl.drawArrays(gl.TRIANGLES, 0, 3); - var pixels = Array.prototype.slice.call(getPixels(gl)) - return pixels.map(function(p) { - return p / 255 - }) + var result = new Float32Array(w * h * 4); + gl.readPixels(0, 0, w, h, gl.RGBA, gl.FLOAT, result); + + return result; } - return process -} \ No newline at end of file + + return process; +}; diff --git a/nogl.js b/nogl.js new file mode 100644 index 0000000..27099e7 --- /dev/null +++ b/nogl.js @@ -0,0 +1,54 @@ +/** + * Nogl implementation + * + * @module gl-shader-output/nogl + */ +var GLSL = require('glsl-js'); + + +function create (shader, options) { + //reset gl-shader object + if (shader && shader.fragShader) { + shader = shader._fref.src; + }; + + var width = options.width, height = options.height; + + var compiler = new GLSL({ + replaceUniform: shaderVar, + replaceAttribute: shaderVar, + replaceVarying: shaderVar + }); + + function shaderVar (name) { + return `__data.${name}`; + }; + + var source = compiler.compile(shader); + + var process = new Function('__data', ` + ${source} + + var result = [], gl_FragColor = [0, 0, 0, 0], gl_FragCoord = [0, 0, 0, 0]; + + for (var j = 0; j < ${height}; j++) { + for (var i = 0; i < ${width}; i++) { + main(); + result.push(gl_FragColor[0]); + result.push(gl_FragColor[1]); + result.push(gl_FragColor[2]); + result.push(gl_FragColor[3]); + } + } + + return result; + `); + + function draw (uniforms) { + return process(uniforms); + } + + return draw; +}; + +module.exports = create; \ No newline at end of file diff --git a/package.json b/package.json index 24846fe..eefdfb1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gl-shader-output", "version": "1.0.2", - "description": "test a shader's gl_FragColor output on a 1x1 canvas", + "description": "Process fragment shader on an rectangular canvas", "main": "index.js", "license": "MIT", "author": { @@ -9,23 +9,33 @@ "email": "dave.des@gmail.com", "url": "https://github.com/mattdesl" }, + "contributors": [ + "Dmitry Ivanov " + ], "dependencies": { - "a-big-triangle": "^1.0.0", - "canvas-pixels": "0.0.0", + "gl-fbo": "^2.0.5", + "gl-shader": "^4.2.0", + "glsl-js": "^2.1.1", "webgl-context": "^2.1.1", + "webglew": "^1.0.5", "xtend": "^4.0.0" }, "devDependencies": { + "array-almost-equal": "^1.0.0", "faucet": "0.0.1", - "glslify": "^1.6.0", + "glslify": "^2.0.0", "smokestack": "^3.2.0", "tap-closer": "^1.0.0", - "tape": "^3.4.0", - "test-fuzzy-array": "^1.0.1", - "wzrd": "^1.2.1" + "tst": "^1.3.1" + }, + "browserify": { + "transform": [ + "glslify" + ] }, "scripts": { - "dev": "wzrd test/test.js -- -t glslify", + "test:browser": "budo test/test.js", + "test:node": "node test/test.js", "test": "browserify test/test.js -t glslify | tap-closer | smokestack | faucet" }, "keywords": [ diff --git a/test/test.js b/test/test.js index 53d3575..2061783 100644 --- a/test/test.js +++ b/test/test.js @@ -1,52 +1,131 @@ -var test = require('tape') - +var test = require('tst') var create = require('../') -var glslify = require('glslify') -var FuzzyArray = require('test-fuzzy-array') - -test('should return the color blue', function(t) { - var shader = glslify({ - vertex: './shaders/test.vert', - fragment: './shaders/blue.frag' - }) - - var draw = create({ - shader: shader - }) - - t.deepEqual(draw(), [0, 0, 1, 1]) - t.end() -}) - -test('should be able to handle alpha', function(t) { - var shader = glslify({ - vertex: './shaders/test.vert', - fragment: './shaders/alpha.frag' - }) - - var draw = create({ - shader: shader - }) - - t.deepEqual(draw(), [0, 0, 1, 0]) - t.end() -}) - -test('should accept uniforms', function(t) { - var shader = glslify({ - vertex: './shaders/test.vert', - fragment: './shaders/uniforms.frag' - }) - - var draw = create({ - shader: shader - }) +var glslify = require('glslify'); +var almost = require('array-almost-equal'); +var assert = require('assert'); +var Shader = require('gl-shader'); +var createGl = require('webgl-context'); + + +test('should process single point', function() { + var vShader = glslify('./shaders/test.vert'); + var fShader = glslify('./shaders/blue.frag'); + + var max = 10e2; + + test('webgl', function () { + var draw = create(fShader); + assert.deepEqual(draw(), [0, 0, 1, 1]); + }); + + test('nogl', function () { + var draw = create(fShader, { + gl: null + }); + assert.deepEqual(draw(), [0, 0, 1, 1]); + }); +}); + + +test('gl vs nogl performance', function() { + var vShader = glslify('./shaders/test.vert'); + var fShader = glslify('./shaders/blue.frag'); + + var max = 10e2; + + var drawGl = create(fShader); + var drawNogl = create(fShader, { + gl: null + }); + + test('webgl', function () { + for (var i = 0; i < max; i++) { + drawGl(); + } + }); + test('nogl', function () { + for (var i = 0; i < max; i++) { + drawNogl(); + } + }); +}); + + +test('should process more-than-one dimension input', function() { + var shader = Shader(createGl(), + glslify('./shaders/test.vert'), + glslify('./shaders/blue.frag') + ); + + test('webgl', function () { + var draw = create({ + shader: shader, + width: 2, + height: 2 + }); + assert.deepEqual(draw(), [0,0,1,1, 0,0,1,1, 0,0,1,1, 0,0,1,1]) + }); + + test('nogl', function () { + var draw = create({ + shader: shader, + width: 2, + height: 2, + gl: null + }); + assert.deepEqual(draw(), [0,0,1,1, 0,0,1,1, 0,0,1,1, 0,0,1,1]) + }); +}); + +test('should be able to handle alpha', function() { + var shader = Shader(createGl(), + glslify('./shaders/test.vert'), + glslify('./shaders/alpha.frag') + ); + + test('webgl', function () { + var draw = create({ + shader: shader + }); + assert.deepEqual(draw(), [0, 0, 1, 0]) + }); + + test('nogl', function () { + var draw = create({ + shader: shader, + gl: null + }); + assert.deepEqual(draw(), [0, 0, 1, 0]) + }); +}); + + +test('should accept uniforms', function() { + var shader = Shader(createGl(), + glslify('./shaders/test.vert'), + glslify('./shaders/uniforms.frag') + ); var input = [0, 0.25, 0.5, 1.0] - var reversed = input.slice().reverse() - - var almost = FuzzyArray(t, 0.01) - almost(draw({ u_value: input, multiplier: 1.0 }), reversed) - almost(draw({ u_value: input, multiplier: 3.0 }), [ 1, 1, 0.75, 0 ]) - t.end() -}) + var reversed = input.slice().reverse(); + + test('webgl', function () { + var draw = create({ + shader: shader + }) + + almost(draw({ u_value: input, multiplier: 1.0 }), reversed, 0.01) + almost(draw({ u_value: input, multiplier: 3.0 }), [ 1, 1, 0.75, 0 ], 0.01) + }); + + test('nogl', function () { + var draw = create({ + shader: shader, + gl: null + }) + + almost(draw({ u_value: input, multiplier: 1.0 }), reversed, 0.01) + almost(draw({ u_value: input, multiplier: 3.0 }), [ 1, 1, 0.75, 0 ], 0.01) + }); + +});