├── .gitignore ├── .npmignore ├── README.md ├── example ├── example.js └── index.html ├── images ├── 1.gif ├── full.gif └── spector.gif ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .cache 4 | dist/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | images 2 | example 3 | .cache 4 | dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🔮 MagicShader 2 | 3 | > ⚠️ probably won't work with modern version of threejs, last time I tested was with r114 4 | > pr is welcome 5 | 6 | A thin wrapper on top of `RawShaderMaterial`, that allows to easily create new uniforms and live-edit them via `dat.gui`. 7 | 8 | No need to create the uniforms manually and bind them with `dat.gui`. 9 | Just write some comments in your GLSL, and everything will work magically ✨ 10 | 11 | 12 | ## 🕵️‍♂️ How to use 13 | Install via npm 14 | 15 | ``` 16 | npm i -D magicshader 17 | ``` 18 | 19 | and just use it instead of `RawShaderMaterial`: 20 | 21 | ```javascript 22 | import MagicShader from 'magicshader'; 23 | 24 | const material = new MagicShader({...}) 25 | ``` 26 | 27 | The parameters are exactly the same. 28 | 29 | ## 🤷‍♀️ Ok...where the magic is? 30 | 31 | Now you can add the `// ms({})` magic comment after your uniforms. 32 | 33 | 34 | Example: 35 | 36 | ```javascript 37 | const material = new MagicShader({ 38 | vertexShader: ` 39 | precision highp float; 40 | 41 | attribute vec3 position; 42 | uniform mat4 modelViewMatrix; 43 | uniform mat4 projectionMatrix; 44 | 45 | void main() { 46 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 47 | } 48 | `, 49 | fragmentShader: ` 50 | precision highp float; 51 | 52 | uniform vec3 color; // ms({ value: '#ff0000' }) 53 | 54 | void main() { 55 | gl_FragColor = vec4(color, 1.0); 56 | } 57 | ` 58 | }); 59 | ``` 60 | 61 | This will give you: 62 | ![1](./images/1.gif "1") 63 | 64 | No need to init your uniform or bind `dat.gui`. 65 | You can just work on your GLSL files. 66 | 67 | 68 | ## 👨‍💻 What else? 69 | ```javascript 70 | const material = new MagicShader({ 71 | name: 'Cube Shader!', 72 | vertexShader: ` 73 | precision highp float; 74 | 75 | attribute vec3 position; 76 | uniform mat4 modelViewMatrix; 77 | uniform mat4 projectionMatrix; 78 | 79 | uniform vec3 translate; // ms({ value: [0, 0, 0], step: 0.01 }) 80 | uniform float scale; // ms({ value: 0.5, options: { small: 0.5, medium: 1, big: 2 } }) 81 | uniform mat4 aMatrix4; // ms({ value: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }) 82 | 83 | void main() { 84 | vec3 pos = position + translate; 85 | pos *= scale; 86 | 87 | gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); 88 | } 89 | `, 90 | fragmentShader: ` 91 | precision highp float; 92 | 93 | uniform vec3 color; // ms({ value: '#ff0000' }) 94 | uniform float brightness; // ms({ value: 0, range: [0, 0.5], step: 0.1 }) 95 | uniform vec2 dummyValue; // ms({ value: [1024, 768], range: [[0, 2000], [0, 1500]] }) 96 | uniform bool visible; // ms({ value: 1, name: 'Visibility' }) 97 | uniform int test; // ms({ value: 0 }) 98 | 99 | void main() { 100 | gl_FragColor = vec4(color + brightness, 1.0); 101 | } 102 | ` 103 | }); 104 | ``` 105 | 106 | Will result in: 107 | ![full](./images/full.gif "full") 108 | 109 | ## 🕵️‍ SpectorJS 110 | 111 | With the [SpectorJS extension](https://github.com/BabylonJS/Spector.js/blob/master/documentation/extension.md) enabled, you can live-edit the shaders. You can even add and modify "magic" uniforms on the fly. 112 | 113 | ![full](./images/spector.gif "full") 114 | 115 | ## 💅 Ok, cool. Just finished my app and I'm ready to deploy 116 | 117 | Then you can hide the `dat.gui` UI 118 | ```javascript 119 | import MagicShader, { gui } from 'magicshader'; 120 | gui.destroy(); 121 | ``` 122 | 123 | 124 | ## 😴 TODO 125 | 126 | - [ ] Do more tests... 127 | - [ ] add support for sampler2D and FBO? 128 | - [ ] check if it works with firefox/safari shader editor 129 | - [ ] inspect/edit threejs default uniforms (like `projectionMatrix`) 130 | -------------------------------------------------------------------------------- /example/example.js: -------------------------------------------------------------------------------- 1 | import { 2 | Scene, 3 | Mesh, 4 | PerspectiveCamera, 5 | WebGLRenderer, 6 | BoxBufferGeometry, 7 | RawShaderMaterial, 8 | } from 'three'; 9 | 10 | import MagicShader from '../index'; 11 | 12 | const renderer = new WebGLRenderer(); 13 | renderer.setSize(window.innerWidth, window.innerHeight); 14 | document.body.appendChild(renderer.domElement); 15 | 16 | const scene = new Scene(); 17 | const camera = new PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 10); 18 | camera.position.z = 1; 19 | 20 | const geometry = new BoxBufferGeometry(1, 1, 1); 21 | const material = new MagicShader({ 22 | name: 'Cube Shader!', 23 | vertexShader: ` 24 | precision highp float; 25 | 26 | attribute vec3 position; 27 | uniform mat4 modelViewMatrix; 28 | uniform mat4 projectionMatrix; 29 | 30 | uniform vec3 translate; // ms({ value: [0, 0, 0], step: 0.01 }) 31 | uniform float scale; // ms({ value: 0.5, options: { small: 0.5, medium: 1, big: 2 } }) 32 | uniform mat4 aMatrix4; // ms({ value: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }) 33 | 34 | void main() { 35 | vec3 pos = position + translate; 36 | pos *= scale; 37 | 38 | gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); 39 | } 40 | `, 41 | fragmentShader: ` 42 | precision highp float; 43 | 44 | uniform vec3 color; // ms({ value: '#ff0000' }) 45 | uniform float brightness; // ms({ value: 0, range: [0, 0.5], step: 0.1 }) 46 | uniform vec2 dummyValue; // ms({ value: [1024, 768], range: [[0, 2000], [0, 1500]] }) 47 | uniform bool visible; // ms({ value: 1, name: 'Visibility' }) 48 | uniform int test; // ms({ value: 0 }) 49 | 50 | void main() { 51 | gl_FragColor = vec4(color + brightness, 1.0); 52 | } 53 | ` 54 | }); 55 | 56 | const mesh = new Mesh(geometry, material); 57 | scene.add(mesh); 58 | 59 | 60 | const geometry2 = new BoxBufferGeometry(1, 1, 1); 61 | const material2 = new MagicShader({ 62 | name: 'Cube Shader2!', 63 | vertexShader: ` 64 | precision highp float; 65 | 66 | attribute vec3 position; 67 | uniform mat4 modelViewMatrix; 68 | uniform mat4 projectionMatrix; 69 | 70 | uniform vec3 translate; // ms({ value: [0, 0, 0], step: 0.01 }) 71 | uniform float scale; // ms({ value: 0.5, options: { small: 0.5, medium: 1, big: 2 } }) 72 | uniform mat4 aMatrix4; // ms({ value: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }) 73 | 74 | void main() { 75 | vec3 pos = position + translate; 76 | pos *= scale; 77 | 78 | gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); 79 | } 80 | `, 81 | fragmentShader: ` 82 | precision highp float; 83 | 84 | uniform vec3 color; // ms({ value: '#ff0000' }) 85 | uniform float brightness; // ms({ value: 0, range: [0, 0.5], step: 0.1 }) 86 | uniform vec2 dummyValue; // ms({ value: [1024, 768], range: [[0, 2000], [0, 1500]] }) 87 | uniform bool visible; // ms({ value: 1, name: 'Visibility' }) 88 | uniform int test; // ms({ value: 0 }) 89 | 90 | void main() { 91 | gl_FragColor = vec4(color + brightness, 1.0); 92 | } 93 | ` 94 | }); 95 | 96 | const mesh2 = new Mesh(geometry2, material2); 97 | scene.add(mesh2); 98 | 99 | function animate() { 100 | requestAnimationFrame(animate); 101 | 102 | mesh.rotation.x += 0.01; 103 | mesh.rotation.y += 0.01; 104 | 105 | renderer.render(scene, camera); 106 | } 107 | 108 | animate(); -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /images/1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luruke/magicshader/25ba98d9c8e6c737ffeb83ffe1da9811b3c81770/images/1.gif -------------------------------------------------------------------------------- /images/full.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luruke/magicshader/25ba98d9c8e6c737ffeb83ffe1da9811b3c81770/images/full.gif -------------------------------------------------------------------------------- /images/spector.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luruke/magicshader/25ba98d9c8e6c737ffeb83ffe1da9811b3c81770/images/spector.gif -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { RawShaderMaterial, Color } from 'three'; 2 | import * as dat from 'dat.gui'; 3 | 4 | const magicUniformsToThree = uniforms => { 5 | const r = {}; 6 | 7 | for (const k in uniforms) { 8 | const uniform = uniforms[k]; 9 | let value = uniforms[k].json.value; 10 | 11 | if (uniform.type === 'bool') { 12 | value = !!value; 13 | } 14 | 15 | if (typeof value === 'string') { 16 | value = new Color(value); 17 | } 18 | 19 | r[k] = { value }; 20 | } 21 | 22 | return r; 23 | }; 24 | 25 | const parseShaders = (vertex, fragment) => { 26 | const vertexUniforms = parseGLSL(vertex); 27 | const fragmentUniforms = parseGLSL(fragment); 28 | 29 | return { 30 | ...vertexUniforms, 31 | ...fragmentUniforms, 32 | }; 33 | }; 34 | 35 | const parseGLSL = source => { 36 | const lines = source.match(/uniform (.+?) (.+?);.+\/\/.+ms\((.+?)\)/gm); 37 | 38 | if (!lines) { 39 | return {}; 40 | } 41 | 42 | const uniforms = {}; 43 | 44 | lines.forEach(line => { 45 | const [, type, name, jsonString] = line.match(/uniform (.+?) (.+?);.+\/\/.+ms\((.+?)\)/); 46 | let json = {}; 47 | 48 | try { 49 | eval(`json = ${jsonString}`); 50 | } catch (e) { 51 | throw new Error(e); 52 | } 53 | 54 | if (!json.value) { 55 | json.value = 0; 56 | } 57 | 58 | uniforms[name] = { 59 | name, 60 | type, 61 | json, 62 | }; 63 | }); 64 | 65 | return uniforms; 66 | }; 67 | 68 | const gui = new dat.GUI({ 69 | name: 'MagicShader', 70 | }); 71 | 72 | let spectorGui; 73 | let id = 0; 74 | 75 | class MagicShader extends RawShaderMaterial { 76 | constructor(params) { 77 | const originalParams = params; 78 | const magicUniforms = parseShaders(params.vertexShader, params.fragmentShader); 79 | 80 | params.uniforms = { 81 | ...magicUniformsToThree(magicUniforms), 82 | ...params.uniforms, 83 | }; 84 | 85 | super(params); 86 | 87 | this.originalParams = originalParams; 88 | this.params = params; 89 | this.magicUniforms = magicUniforms; 90 | this.displayName = this.params.name || `Shader n. ${++id}`; 91 | 92 | this.spector(); 93 | this.bindUI(); 94 | } 95 | 96 | bindUI() { 97 | if (this.gui) { 98 | gui.removeFolder(this.gui); 99 | } 100 | 101 | this.gui = gui.addFolder(this.displayName); 102 | 103 | Object.keys(this.magicUniforms).forEach(key => { 104 | const magicUniform = this.magicUniforms[key]; 105 | const magicJson = magicUniform.json; 106 | const uniform = this.uniforms[key]; 107 | const folder = this.gui.addFolder(magicJson.name || `🔮 ${magicUniform.type} - ${key}`); 108 | 109 | if (uniform.value instanceof Color) { 110 | const add = folder.addColor(magicJson, 'value').onChange(res => { 111 | uniform.value.set(res); 112 | }); 113 | 114 | add.listen(); 115 | } else if (Array.isArray(magicJson.value)) { 116 | Object.keys(uniform.value).forEach(index => { 117 | const add = folder.add(uniform.value, index); 118 | 119 | magicJson.step && add.step(magicJson.step); 120 | 121 | if (magicJson.range && magicJson.range[index] && magicJson.range[index].length === 2) { 122 | add.min(magicJson.range[index][0]); 123 | add.max(magicJson.range[index][1]); 124 | } 125 | 126 | add.listen(); 127 | }); 128 | } else { 129 | const add = folder.add(uniform, 'value'); 130 | 131 | magicJson.step && add.step(magicJson.step); 132 | magicJson.options && add.options(magicJson.options); 133 | 134 | if (magicJson.range && magicJson.range.length === 2) { 135 | add.min(magicJson.range[0]); 136 | add.max(magicJson.range[1]); 137 | } 138 | 139 | add.listen(); 140 | } 141 | }); 142 | } 143 | 144 | // Spector.js stuff 145 | spector() { 146 | if (!window.spector) { 147 | return; 148 | } 149 | 150 | if (!spectorGui) { 151 | // Just once, for the whole context. 152 | spectorGui = gui.addFolder('📈 Spector'); 153 | 154 | this.spectorFPS = 0; 155 | setInterval(() => { 156 | this.spectorFPS = spector.getFps(); 157 | }, 200); 158 | 159 | spectorGui.add(this, 'spectorFPS').name('FPS').listen(); 160 | spectorGui.add(this, 'capture'); 161 | } 162 | 163 | this.checkProgram = this.checkProgram.bind(this); 164 | this.checkProgramInterval = setInterval(this.checkProgram, 200); 165 | } 166 | 167 | capture() { 168 | const instance = document.querySelector('canvas'); 169 | spector.captureNextFrame(instance); 170 | } 171 | 172 | checkProgram() { 173 | if (this.program && this.program.program) { 174 | this.program.program.__SPECTOR_Object_TAG.displayText = this.displayName; 175 | this.program.vertexShader.__SPECTOR_Object_TAG.displayText = `Vertex - ${this.displayName}`; 176 | this.program.fragmentShader.__SPECTOR_Object_TAG.displayText = `Fragment - ${this.displayName}`; 177 | 178 | this.program.program.__SPECTOR_rebuildProgram = this.rebuildShader.bind(this); 179 | clearInterval(this.checkProgramInterval); 180 | } 181 | } 182 | 183 | rebuildShader(vertex, fragment, onCompile, onError) { 184 | this.vertexShader = vertex; 185 | this.fragmentShader = fragment; 186 | 187 | this.magicUniforms = parseShaders(vertex, fragment); 188 | this.uniforms = { 189 | ...this.uniforms, 190 | ...magicUniformsToThree(this.magicUniforms), 191 | }; 192 | 193 | this.needsUpdate = true; 194 | this.bindUI(); 195 | 196 | onCompile(this.program.program); 197 | this.checkProgramInterval = setInterval(this.checkProgram, 200); 198 | } 199 | 200 | clone() { 201 | return new this.constructor(this.originalParams).copy(this); 202 | } 203 | } 204 | 205 | export { gui }; 206 | export default MagicShader; 207 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magicshader", 3 | "version": "0.1.5", 4 | "description": "🔮 Tiny helper for three.js to debug and write shaders", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "parcel example/index.html" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/luruke/magicshader.git" 12 | }, 13 | "author": "luruke", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/luruke/magicshader/issues" 17 | }, 18 | "homepage": "https://github.com/luruke/magicshader#readme", 19 | "dependencies": { 20 | "dat.gui": "^0.7.6", 21 | "three": "^0.114.0" 22 | }, 23 | "devDependencies": { 24 | "parcel": "^1.12.4" 25 | }, 26 | "peerDependencies": { 27 | "three": ">= 0.111.0" 28 | } 29 | } 30 | --------------------------------------------------------------------------------