├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── example └── index.js ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules/* 16 | *.DS_Store 17 | 18 | *.mp4 19 | *.jpg 20 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules/* 16 | *.DS_Store 17 | example/* 18 | 19 | *.mp4 20 | *.jpg 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 Eric Arnebäck 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # regl-recorder 2 | 3 | [![Circle CI](https://circleci.com/gh/Erkaman/regl-recorder.svg?style=shield)](https://circleci.com/gh/Erkaman/regl-recorder) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 4 | [![npm version](https://badge.fury.io/js/regl-recorder.svg)](https://badge.fury.io/js/regl-recorder) 5 | 6 | 7 | A small utility that can be used for recording videos in the 8 | WebGL framework [regl](https://github.com/mikolalysenko/regl). 9 | 10 | [![Result](http://img.youtube.com/vi/1lB319WdSoU/0.jpg)](https://youtu.be/1lB319WdSoU) 11 | 12 | # Example 13 | 14 | A small example for recording a rotating bunny is shown below 15 | 16 | ```javascript 17 | var createReglRecorder = require('../index') 18 | const normals = require('angle-normals') 19 | const mat4 = require('gl-mat4') 20 | const bunny = require('bunny') 21 | 22 | const VIDEO_WIDTH = 3840 * 0.1 23 | const VIDEO_HEIGHT = 2160 * 0.1 24 | 25 | const regl = require('regl')(require('gl')(VIDEO_WIDTH, VIDEO_HEIGHT, {preserveDrawingBuffer: true})) 26 | var recorder = createReglRecorder(regl, 150) 27 | 28 | const drawBunny = regl({ 29 | vert: ` 30 | precision mediump float; 31 | 32 | attribute vec3 position; 33 | attribute vec3 normal; 34 | 35 | varying vec3 vNormal; 36 | 37 | uniform mat4 view, projection; 38 | 39 | void main() { 40 | vNormal = normal; 41 | gl_Position = projection * view * vec4(position, 1); 42 | }`, 43 | 44 | frag: ` 45 | precision mediump float; 46 | 47 | varying vec3 vNormal; 48 | 49 | void main() { 50 | vec3 color = vec3(0.6, 0.0, 0.0); 51 | vec3 lightDir = vec3(0.39, 0.87, 0.29); 52 | vec3 ambient = 0.3 * color; 53 | vec3 diffuse = 0.7 * color * clamp( dot(vNormal, lightDir), 0.0, 1.0 ); 54 | gl_FragColor = vec4(ambient + diffuse, 1.0); 55 | }`, 56 | 57 | attributes: { 58 | position: bunny.positions, 59 | normal: normals(bunny.cells, bunny.positions) 60 | }, 61 | 62 | elements: bunny.cells, 63 | 64 | uniforms: { 65 | view: ({tick}) => { 66 | const t = 0.01 * (tick + 100) 67 | return mat4.lookAt([], 68 | [30 * Math.cos(t), 10.5, 30 * Math.sin(t)], 69 | [0, 2.5, 0], 70 | [0, 1, 0]) 71 | }, 72 | projection: ({viewportWidth, viewportHeight}) => 73 | mat4.perspective([], 74 | Math.PI / 4, 75 | viewportWidth / viewportHeight, 76 | 0.01, 77 | 1000) 78 | } 79 | }) 80 | 81 | regl.frame(({viewportWidth, viewportHeight}) => { 82 | regl.clear({ 83 | depth: 1, 84 | color: [0, 0, 0, 1] 85 | }) 86 | drawBunny() 87 | 88 | recorder.frame(viewportWidth, viewportHeight) 89 | }) 90 | ``` 91 | 92 | Notice from the above that we have to do two things to use 93 | `regl-recorder`. Firstly, we must create a 94 | [headless WebGL context](https://github.com/stackgl/headless-gl), and 95 | give it to the `regl`: 96 | 97 | ```javascript 98 | const regl = require('regl')(require('gl')(VIDEO_WIDTH, VIDEO_HEIGHT, {preserveDrawingBuffer: true})) 99 | var recorder = createReglRecorder(regl, 150) 100 | ``` 101 | 102 | Secondly, at the end of a frame, we must insert 103 | 104 | ```javascript 105 | recorder.frame(viewportWidth, viewportHeight) 106 | ``` 107 | 108 | And then you start the recording by running the program in node with 109 | 110 | ``` javascript 111 | node example/index.js 112 | ``` 113 | 114 | If you do the above, `regl-recorder` will record 150 frames of a bunny 115 | animation. Since the tool records at 30FPS, this will result in a 116 | 5 minutes long video. 117 | 118 | This tool will not output a video file, but only the recorded 119 | frames. They are put into a folder named something like 120 | `video-71f41ec69ea844df11b0`. You can convert these frames into a 121 | video file with `ffmpeg`, by doing 122 | 123 | ``` sh 124 | ffmpeg -y -framerate 30 -i video-71f41ec69ea844df11b0/frame%08d.jpg -b 10000k -vf "vflip" -c:v libx264 -r 30 out.mp4 125 | ``` 126 | 127 | And you will thus obtain a video like the below: 128 | 129 | [![Result](http://img.youtube.com/vi/1lB319WdSoU/0.jpg)](https://youtu.be/1lB319WdSoU) 130 | 131 | Note that since the recorder uses headless, you can only record regl 132 | programs that uses extensions supported by headless. So you can only 133 | use [these extensions](https://github.com/stackgl/headless-gl#what-extensions-are-supported) 134 | 135 | # API 136 | 137 | #### `var recorder = require('regl-recorder')(regl, frames)` 138 | 139 | This creates a new recorder instance. 140 | 141 | * `regl` a regl context 142 | * `frames` the number of frames to record. The recording rate is 143 | 30FPS, so the resulting video will be `frames/30` seconds long. 144 | 145 | #### `recorder.frame(viewportWidth, viewportHeight)` 146 | 147 | Records the current frame. Should be called at the end of every frame. 148 | 149 | * `viewportWidth, viewportHeight` the current dimensions of the viewport. 150 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | var createReglRecorder = require('../index') 2 | const normals = require('angle-normals') 3 | const mat4 = require('gl-mat4') 4 | const bunny = require('bunny') 5 | 6 | const VIDEO_WIDTH = 3840 * 0.1 7 | const VIDEO_HEIGHT = 2160 * 0.1 8 | 9 | const regl = require('regl')(require('gl')(VIDEO_WIDTH, VIDEO_HEIGHT, {preserveDrawingBuffer: true})) 10 | var recorder = createReglRecorder(regl, 150) 11 | 12 | const drawBunny = regl({ 13 | vert: ` 14 | precision mediump float; 15 | 16 | attribute vec3 position; 17 | attribute vec3 normal; 18 | 19 | varying vec3 vNormal; 20 | 21 | uniform mat4 view, projection; 22 | 23 | void main() { 24 | vNormal = normal; 25 | gl_Position = projection * view * vec4(position, 1); 26 | }`, 27 | 28 | frag: ` 29 | precision mediump float; 30 | 31 | varying vec3 vNormal; 32 | 33 | void main() { 34 | vec3 color = vec3(0.6, 0.0, 0.0); 35 | vec3 lightDir = vec3(0.39, 0.87, 0.29); 36 | vec3 ambient = 0.3 * color; 37 | vec3 diffuse = 0.7 * color * clamp( dot(vNormal, lightDir), 0.0, 1.0 ); 38 | gl_FragColor = vec4(ambient + diffuse, 1.0); 39 | }`, 40 | 41 | attributes: { 42 | position: bunny.positions, 43 | normal: normals(bunny.cells, bunny.positions) 44 | }, 45 | 46 | elements: bunny.cells, 47 | 48 | uniforms: { 49 | view: ({tick}) => { 50 | const t = 0.01 * (tick + 100) 51 | return mat4.lookAt([], 52 | [30 * Math.cos(t), 10.5, 30 * Math.sin(t)], 53 | [0, 2.5, 0], 54 | [0, 1, 0]) 55 | }, 56 | projection: ({viewportWidth, viewportHeight}) => 57 | mat4.perspective([], 58 | Math.PI / 4, 59 | viewportWidth / viewportHeight, 60 | 0.01, 61 | 1000) 62 | } 63 | }) 64 | 65 | regl.frame(({viewportWidth, viewportHeight}) => { 66 | regl.clear({ 67 | depth: 1, 68 | color: [0, 0, 0, 1] 69 | }) 70 | drawBunny() 71 | 72 | recorder.frame(viewportWidth, viewportHeight) 73 | }) 74 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var jpeg = require('jpeg-js') 2 | var fs = require('fs') 3 | const crypto = require('crypto') 4 | 5 | module.exports = function (regl, frames) { 6 | if (!regl) { 7 | throw new Error('regl-recorder: you must specify a regl context') 8 | } 9 | 10 | if (typeof frames !== 'number' || frames <= 0) { 11 | throw new Error('regl-recorder: `frames` must be a positive integer') 12 | } 13 | var frameCount = 0 14 | 15 | // output directory name is generated randomly 16 | var rand = crypto.randomBytes(10).toString('hex') 17 | var outputDir = 'video-' + rand 18 | 19 | // we want to record at 30FPS, so we skip every other frame 20 | // (since regl by default is 60FPS) 21 | var skipFrame = 0 22 | 23 | // create output directory. 24 | try { 25 | fs.mkdirSync(outputDir) 26 | } catch (e) { 27 | if (e.code !== 'EEXIST') throw e 28 | } 29 | 30 | function pad (number, digits) { 31 | return Array(Math.max(digits - String(number).length + 1, 0)).join(0) + number 32 | } 33 | 34 | function frame (viewportWidth, viewportHeight) { 35 | if (frameCount === frames) { 36 | console.log('Done! Output frames to directory ' + outputDir) 37 | process.exit(0) 38 | } else if (skipFrame === 0) { 39 | var pixelBuffer = new Buffer(regl.read()) 40 | var rawImageData = { 41 | data: pixelBuffer, 42 | width: viewportWidth, 43 | height: viewportHeight 44 | } 45 | var jpegImageData = jpeg.encode(rawImageData, 100) 46 | 47 | const PAD = 8 48 | fs.writeFileSync(outputDir + '/frame' + pad(frameCount, PAD) + '.jpg', jpegImageData.data) 49 | console.log('frame: ', pad(frameCount, PAD) + '/' + pad(frames, PAD)) 50 | 51 | frameCount++ 52 | } 53 | skipFrame = (skipFrame + 1) % 2 54 | } 55 | 56 | return { 57 | frame: frame 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "regl-recorder", 3 | "version": "0.2.0", 4 | "description": "Small utility for recording videos in regl.", 5 | "main": "index.js", 6 | "directories": { 7 | "example": "example" 8 | }, 9 | "dependencies": { 10 | "jpeg-js": "^0.2.0" 11 | }, 12 | "devDependencies": { 13 | "angle-normals": "^1.0.0", 14 | "bunny": "^1.0.1", 15 | "gl": "^4.0.2", 16 | "gl-mat4": "^1.1.4", 17 | "regl": "^0.10.0", 18 | "standard": "^7.1.2" 19 | }, 20 | "scripts": { 21 | "example": "node example/index.js", 22 | "test": "standard" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/Erkaman/regl-recorder.git" 27 | }, 28 | "keywords": [ 29 | "movie", 30 | "recorder", 31 | "animation", 32 | "regl" 33 | ], 34 | "author": "Eric Arnebäck", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/Erkaman/regl-recorder/issues" 38 | }, 39 | "homepage": "https://github.com/Erkaman/regl-recorder" 40 | } 41 | --------------------------------------------------------------------------------