├── .gitignore ├── LICENSE ├── README.md ├── dist ├── index.bundle.html ├── index.bundle.js ├── mode-glsl.js └── theme-monokai.js ├── docs └── example.gif ├── electron └── app.js ├── elements ├── ace-editor.html ├── image-magnifier.html ├── shader-editor.html ├── shader-preview.html ├── zoomable-canvas.html ├── zoomable-element.html └── zoomable-image.html ├── empty-template.ejs ├── images └── cube-icon.png ├── index.html ├── package.json ├── scripts └── DebugShaders.js ├── textures ├── checkerboard.png ├── clouds.png └── random.png └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Garrett Johnson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webgl-shader-editor 2 | Realtime editor for creating webgl shaders with debugging tools to allow for inspecting local variable definitions in a fragment shader. 3 | 4 | [Demo](https://gkjohnson.github.io/webgl-shader-editor/dist/index.bundle.html) 5 | 6 | ![Example](/docs/example.gif) 7 | 8 | ## Goal 9 | After lot of frustration with developing shaders and not being able to easily track the calculations within a shader, I wanted explore what the possibilities for enabling a rich shader debugger might be light while still affording the direct shader code editing. 10 | 11 | ## Features 12 | #### Real Time Compilation and Render 13 | As the vertex and fragment shader are updated, the render display is updated and errors are displayed as annotations. 14 | 15 | #### Pixel Zoom 16 | Zooming in on the image zooms in to a pixel view of the image. 17 | 18 | #### Local Variable Value Inspection 19 | Hovering over a pixel in the preview pane will display the values of all the local variables used for that pixel. 20 | 21 | #### Local Variable Color Display 22 | Every local variable will be displayed as a rendered image in the preview bar as though that variable were used to output the color for that fragment. 23 | 24 | #### LocalStorage Saving 25 | The vertex and fragment shader being written are saved and reloaded on refresh 26 | 27 | ## Caveats 28 | - Because colors are stored as 4 8bit values, there is a severe loss of precision when reading out floating point values. [Issue 1](https://github.com/gkjohnson/webgl-shader-editor/issues/2), [Issue 2](https://github.com/gkjohnson/webgl-shader-editor/issues/1) 29 | 30 | ## TODO 31 | - [ ] Add pause button for animated variables 32 | - [ ] Texture upload 33 | - [ ] Uniform variable edit UI 34 | - [ ] Multi-pass shaders, stencil buffer, and screen post-effects 35 | - [ ] Add mouse position into shader uniforms 36 | -------------------------------------------------------------------------------- /dist/index.bundle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /dist/mode-glsl.js: -------------------------------------------------------------------------------- 1 | define('ace/mode/doc_comment_highlight_rules', ['require', 'exports', 'module', 'ace/lib/oop', 'ace/mode/text_highlight_rules'], function (require, exports, module) { 2 | 3 | 'use strict'; 4 | 5 | var oop = require('../lib/oop'); 6 | var TextHighlightRules = require('./text_highlight_rules').TextHighlightRules; 7 | 8 | var DocCommentHighlightRules = function () { 9 | 10 | this.$rules = { 11 | 'start': [ { 12 | token: 'comment.doc.tag', 13 | regex: '@[\\w\\d_]+' // TODO: fix email addresses 14 | }, 15 | DocCommentHighlightRules.getTagRule(), 16 | { 17 | defaultToken: 'comment.doc', 18 | caseInsensitive: true 19 | }] 20 | }; 21 | 22 | }; 23 | 24 | oop.inherits(DocCommentHighlightRules, TextHighlightRules); 25 | 26 | DocCommentHighlightRules.getTagRule = function (start) { 27 | 28 | return { 29 | token: 'comment.doc.tag.storage.type', 30 | regex: '\\b(?:TODO|FIXME|XXX|HACK)\\b' 31 | }; 32 | 33 | }; 34 | 35 | DocCommentHighlightRules.getStartRule = function (start) { 36 | 37 | return { 38 | token: 'comment.doc', // doc comment 39 | regex: '\\/\\*(?=\\*)', 40 | next: start 41 | }; 42 | 43 | }; 44 | 45 | DocCommentHighlightRules.getEndRule = function (start) { 46 | 47 | return { 48 | token: 'comment.doc', // closing comment 49 | regex: '\\*\\/', 50 | next: start 51 | }; 52 | 53 | }; 54 | 55 | exports.DocCommentHighlightRules = DocCommentHighlightRules; 56 | 57 | }); 58 | 59 | define('ace/mode/c_cpp_highlight_rules', ['require', 'exports', 'module', 'ace/lib/oop', 'ace/mode/doc_comment_highlight_rules', 'ace/mode/text_highlight_rules'], function (require, exports, module) { 60 | 61 | 'use strict'; 62 | 63 | var oop = require('../lib/oop'); 64 | var DocCommentHighlightRules = require('./doc_comment_highlight_rules').DocCommentHighlightRules; 65 | var TextHighlightRules = require('./text_highlight_rules').TextHighlightRules; 66 | var cFunctions = exports.cFunctions = '\\b(?:hypot(?:f|l)?|s(?:scanf|ystem|nprintf|ca(?:nf|lb(?:n(?:f|l)?|ln(?:f|l)?))|i(?:n(?:h(?:f|l)?|f|l)?|gn(?:al|bit))|tr(?:s(?:tr|pn)|nc(?:py|at|mp)|c(?:spn|hr|oll|py|at|mp)|to(?:imax|d|u(?:l(?:l)?|max)|k|f|l(?:d|l)?)|error|pbrk|ftime|len|rchr|xfrm)|printf|et(?:jmp|vbuf|locale|buf)|qrt(?:f|l)?|w(?:scanf|printf)|rand)|n(?:e(?:arbyint(?:f|l)?|xt(?:toward(?:f|l)?|after(?:f|l)?))|an(?:f|l)?)|c(?:s(?:in(?:h(?:f|l)?|f|l)?|qrt(?:f|l)?)|cos(?:h(?:f)?|f|l)?|imag(?:f|l)?|t(?:ime|an(?:h(?:f|l)?|f|l)?)|o(?:s(?:h(?:f|l)?|f|l)?|nj(?:f|l)?|pysign(?:f|l)?)|p(?:ow(?:f|l)?|roj(?:f|l)?)|e(?:il(?:f|l)?|xp(?:f|l)?)|l(?:o(?:ck|g(?:f|l)?)|earerr)|a(?:sin(?:h(?:f|l)?|f|l)?|cos(?:h(?:f|l)?|f|l)?|tan(?:h(?:f|l)?|f|l)?|lloc|rg(?:f|l)?|bs(?:f|l)?)|real(?:f|l)?|brt(?:f|l)?)|t(?:ime|o(?:upper|lower)|an(?:h(?:f|l)?|f|l)?|runc(?:f|l)?|gamma(?:f|l)?|mp(?:nam|file))|i(?:s(?:space|n(?:ormal|an)|cntrl|inf|digit|u(?:nordered|pper)|p(?:unct|rint)|finite|w(?:space|c(?:ntrl|type)|digit|upper|p(?:unct|rint)|lower|al(?:num|pha)|graph|xdigit|blank)|l(?:ower|ess(?:equal|greater)?)|al(?:num|pha)|gr(?:eater(?:equal)?|aph)|xdigit|blank)|logb(?:f|l)?|max(?:div|abs))|di(?:v|fftime)|_Exit|unget(?:c|wc)|p(?:ow(?:f|l)?|ut(?:s|c(?:har)?|wc(?:har)?)|error|rintf)|e(?:rf(?:c(?:f|l)?|f|l)?|x(?:it|p(?:2(?:f|l)?|f|l|m1(?:f|l)?)?))|v(?:s(?:scanf|nprintf|canf|printf|w(?:scanf|printf))|printf|f(?:scanf|printf|w(?:scanf|printf))|w(?:scanf|printf)|a_(?:start|copy|end|arg))|qsort|f(?:s(?:canf|e(?:tpos|ek))|close|tell|open|dim(?:f|l)?|p(?:classify|ut(?:s|c|w(?:s|c))|rintf)|e(?:holdexcept|set(?:e(?:nv|xceptflag)|round)|clearexcept|testexcept|of|updateenv|r(?:aiseexcept|ror)|get(?:e(?:nv|xceptflag)|round))|flush|w(?:scanf|ide|printf|rite)|loor(?:f|l)?|abs(?:f|l)?|get(?:s|c|pos|w(?:s|c))|re(?:open|e|ad|xp(?:f|l)?)|m(?:in(?:f|l)?|od(?:f|l)?|a(?:f|l|x(?:f|l)?)?))|l(?:d(?:iv|exp(?:f|l)?)|o(?:ngjmp|cal(?:time|econv)|g(?:1(?:p(?:f|l)?|0(?:f|l)?)|2(?:f|l)?|f|l|b(?:f|l)?)?)|abs|l(?:div|abs|r(?:int(?:f|l)?|ound(?:f|l)?))|r(?:int(?:f|l)?|ound(?:f|l)?)|gamma(?:f|l)?)|w(?:scanf|c(?:s(?:s(?:tr|pn)|nc(?:py|at|mp)|c(?:spn|hr|oll|py|at|mp)|to(?:imax|d|u(?:l(?:l)?|max)|k|f|l(?:d|l)?|mbs)|pbrk|ftime|len|r(?:chr|tombs)|xfrm)|to(?:b|mb)|rtomb)|printf|mem(?:set|c(?:hr|py|mp)|move))|a(?:s(?:sert|ctime|in(?:h(?:f|l)?|f|l)?)|cos(?:h(?:f|l)?|f|l)?|t(?:o(?:i|f|l(?:l)?)|exit|an(?:h(?:f|l)?|2(?:f|l)?|f|l)?)|b(?:s|ort))|g(?:et(?:s|c(?:har)?|env|wc(?:har)?)|mtime)|r(?:int(?:f|l)?|ound(?:f|l)?|e(?:name|alloc|wind|m(?:ove|quo(?:f|l)?|ainder(?:f|l)?))|a(?:nd|ise))|b(?:search|towc)|m(?:odf(?:f|l)?|em(?:set|c(?:hr|py|mp)|move)|ktime|alloc|b(?:s(?:init|towcs|rtowcs)|towc|len|r(?:towc|len))))\\b'; 67 | 68 | var c_cppHighlightRules = function () { 69 | 70 | var keywordControls = ( 71 | 'break|case|continue|default|do|else|for|goto|if|_Pragma|' + 72 | 'return|switch|while|catch|operator|try|throw|using' 73 | ); 74 | 75 | var storageType = ( 76 | 'asm|__asm__|auto|bool|_Bool|char|_Complex|double|enum|float|' + 77 | '_Imaginary|int|long|short|signed|struct|typedef|union|unsigned|void|' + 78 | 'class|wchar_t|template|char16_t|char32_t' 79 | ); 80 | 81 | var storageModifiers = ( 82 | 'const|extern|register|restrict|static|volatile|inline|private|' + 83 | 'protected|public|friend|explicit|virtual|export|mutable|typename|' + 84 | 'constexpr|new|delete|alignas|alignof|decltype|noexcept|thread_local' 85 | ); 86 | 87 | var keywordOperators = ( 88 | 'and|and_eq|bitand|bitor|compl|not|not_eq|or|or_eq|typeid|xor|xor_eq' + 89 | 'const_cast|dynamic_cast|reinterpret_cast|static_cast|sizeof|namespace' 90 | ); 91 | 92 | var builtinConstants = ( 93 | 'NULL|true|false|TRUE|FALSE|nullptr' 94 | ); 95 | 96 | var keywordMapper = this.$keywords = this.createKeywordMapper({ 97 | 'keyword.control': keywordControls, 98 | 'storage.type': storageType, 99 | 'storage.modifier': storageModifiers, 100 | 'keyword.operator': keywordOperators, 101 | 'variable.language': 'this', 102 | 'constant.language': builtinConstants 103 | }, 'identifier'); 104 | 105 | var identifierRe = '[a-zA-Z\\$_\u00a1-\uffff][a-zA-Z\\d\\$_\u00a1-\uffff]*\\b'; 106 | var escapeRe = /\\(?:['"?\\abfnrtv]|[0-7]{1,3}|x[a-fA-F\d]{2}|u[a-fA-F\d]{4}U[a-fA-F\d]{8}|.)/.source; 107 | 108 | this.$rules = { 109 | 'start': [ 110 | { 111 | token: 'comment', 112 | regex: '//$', 113 | next: 'start' 114 | }, { 115 | token: 'comment', 116 | regex: '//', 117 | next: 'singleLineComment' 118 | }, 119 | DocCommentHighlightRules.getStartRule('doc-start'), 120 | { 121 | token: 'comment', // multi line comment 122 | regex: '\\/\\*', 123 | next: 'comment' 124 | }, { 125 | token: 'string', // character 126 | regex: "'(?:" + escapeRe + "|.)?'" 127 | }, { 128 | token: 'string.start', 129 | regex: '"', 130 | stateName: 'qqstring', 131 | next: [ 132 | { token: 'string', regex: /\\\s*$/, next: 'qqstring' }, 133 | { token: 'constant.language.escape', regex: escapeRe }, 134 | { token: 'constant.language.escape', regex: /%[^'"\\]/ }, 135 | { token: 'string.end', regex: '"|$', next: 'start' }, 136 | { defaultToken: 'string'} 137 | ] 138 | }, { 139 | token: 'string.start', 140 | regex: 'R"\\(', 141 | stateName: 'rawString', 142 | next: [ 143 | { token: 'string.end', regex: '\\)"', next: 'start' }, 144 | { defaultToken: 'string'} 145 | ] 146 | }, { 147 | token: 'constant.numeric', // hex 148 | regex: '0[xX][0-9a-fA-F]+(L|l|UL|ul|u|U|F|f|ll|LL|ull|ULL)?\\b' 149 | }, { 150 | token: 'constant.numeric', // float 151 | regex: '[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?(L|l|UL|ul|u|U|F|f|ll|LL|ull|ULL)?\\b' 152 | }, { 153 | token: 'keyword', // pre-compiler directives 154 | regex: '#\\s*(?:include|import|pragma|line|define|undef)\\b', 155 | next: 'directive' 156 | }, { 157 | token: 'keyword', // special case pre-compiler directive 158 | regex: '#\\s*(?:endif|if|ifdef|else|elif|ifndef)\\b' 159 | }, { 160 | token: 'support.function.C99.c', 161 | regex: cFunctions 162 | }, { 163 | token: keywordMapper, 164 | regex: '[a-zA-Z_$][a-zA-Z0-9_$]*' 165 | }, { 166 | token: 'keyword.operator', 167 | regex: /--|\+\+|<<=|>>=|>>>=|<>|&&|\|\||\?:|[*%\/+\-&\^|~!<>=]=?/ 168 | }, { 169 | token: 'punctuation.operator', 170 | regex: '\\?|\\:|\\,|\\;|\\.' 171 | }, { 172 | token: 'paren.lparen', 173 | regex: '[[({]' 174 | }, { 175 | token: 'paren.rparen', 176 | regex: '[\\])}]' 177 | }, { 178 | token: 'text', 179 | regex: '\\s+' 180 | } 181 | ], 182 | 'comment': [ 183 | { 184 | token: 'comment', // closing comment 185 | regex: '\\*\\/', 186 | next: 'start' 187 | }, { 188 | defaultToken: 'comment' 189 | } 190 | ], 191 | 'singleLineComment': [ 192 | { 193 | token: 'comment', 194 | regex: /\\$/, 195 | next: 'singleLineComment' 196 | }, { 197 | token: 'comment', 198 | regex: /$/, 199 | next: 'start' 200 | }, { 201 | defaultToken: 'comment' 202 | } 203 | ], 204 | 'directive': [ 205 | { 206 | token: 'constant.other.multiline', 207 | regex: /\\/ 208 | }, 209 | { 210 | token: 'constant.other.multiline', 211 | regex: /.*\\/ 212 | }, 213 | { 214 | token: 'constant.other', 215 | regex: '\\s*<.+?>', 216 | next: 'start' 217 | }, 218 | { 219 | token: 'constant.other', // single line 220 | regex: '\\s*["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]', 221 | next: 'start' 222 | }, 223 | { 224 | token: 'constant.other', // single line 225 | regex: "\\s*['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']", 226 | next: 'start' 227 | }, 228 | { 229 | token: 'constant.other', 230 | regex: /[^\\\/]+/, 231 | next: 'start' 232 | } 233 | ] 234 | }; 235 | 236 | this.embedRules(DocCommentHighlightRules, 'doc-', 237 | [ DocCommentHighlightRules.getEndRule('start') ]); 238 | this.normalizeRules(); 239 | 240 | }; 241 | 242 | oop.inherits(c_cppHighlightRules, TextHighlightRules); 243 | 244 | exports.c_cppHighlightRules = c_cppHighlightRules; 245 | 246 | }); 247 | 248 | define('ace/mode/matching_brace_outdent', ['require', 'exports', 'module', 'ace/range'], function (require, exports, module) { 249 | 250 | 'use strict'; 251 | 252 | var Range = require('../range').Range; 253 | 254 | var MatchingBraceOutdent = function () {}; 255 | 256 | (function () { 257 | 258 | this.checkOutdent = function (line, input) { 259 | 260 | if (!/^\s+$/.test(line)) { 261 | 262 | return false; 263 | 264 | } 265 | 266 | return /^\s*\}/.test(input); 267 | 268 | }; 269 | 270 | this.autoOutdent = function (doc, row) { 271 | 272 | var line = doc.getLine(row); 273 | var match = line.match(/^(\s*\})/); 274 | 275 | if (!match) return 0; 276 | 277 | var column = match[1].length; 278 | var openBracePos = doc.findMatchingBracket({row: row, column: column}); 279 | 280 | if (!openBracePos || openBracePos.row == row) return 0; 281 | 282 | var indent = this.$getIndent(doc.getLine(openBracePos.row)); 283 | doc.replace(new Range(row, 0, row, column - 1), indent); 284 | 285 | }; 286 | 287 | this.$getIndent = function (line) { 288 | 289 | return line.match(/^\s*/)[0]; 290 | 291 | }; 292 | 293 | }).call(MatchingBraceOutdent.prototype); 294 | 295 | exports.MatchingBraceOutdent = MatchingBraceOutdent; 296 | 297 | }); 298 | 299 | define('ace/mode/folding/cstyle', ['require', 'exports', 'module', 'ace/lib/oop', 'ace/range', 'ace/mode/folding/fold_mode'], function (require, exports, module) { 300 | 301 | 'use strict'; 302 | 303 | var oop = require('../../lib/oop'); 304 | var Range = require('../../range').Range; 305 | var BaseFoldMode = require('./fold_mode').FoldMode; 306 | 307 | var FoldMode = exports.FoldMode = function (commentRegex) { 308 | 309 | if (commentRegex) { 310 | 311 | this.foldingStartMarker = new RegExp( 312 | this.foldingStartMarker.source.replace(/\|[^|]*?$/, '|' + commentRegex.start) 313 | ); 314 | this.foldingStopMarker = new RegExp( 315 | this.foldingStopMarker.source.replace(/\|[^|]*?$/, '|' + commentRegex.end) 316 | ); 317 | 318 | } 319 | 320 | }; 321 | oop.inherits(FoldMode, BaseFoldMode); 322 | 323 | (function () { 324 | 325 | this.foldingStartMarker = /(\{|\[)[^\}\]]*$|^\s*(\/\*)/; 326 | this.foldingStopMarker = /^[^\[\{]*(\}|\])|^[\s\*]*(\*\/)/; 327 | this.singleLineBlockCommentRe = /^\s*(\/\*).*\*\/\s*$/; 328 | this.tripleStarBlockCommentRe = /^\s*(\/\*\*\*).*\*\/\s*$/; 329 | this.startRegionRe = /^\s*(\/\*|\/\/)#?region\b/; 330 | this._getFoldWidgetBase = this.getFoldWidget; 331 | this.getFoldWidget = function (session, foldStyle, row) { 332 | 333 | var line = session.getLine(row); 334 | 335 | if (this.singleLineBlockCommentRe.test(line)) { 336 | 337 | if (!this.startRegionRe.test(line) && !this.tripleStarBlockCommentRe.test(line)) { 338 | 339 | return ''; 340 | 341 | } 342 | 343 | } 344 | 345 | var fw = this._getFoldWidgetBase(session, foldStyle, row); 346 | 347 | if (!fw && this.startRegionRe.test(line)) { 348 | 349 | return 'start'; 350 | 351 | } // lineCommentRegionStart 352 | 353 | return fw; 354 | 355 | }; 356 | 357 | this.getFoldWidgetRange = function (session, foldStyle, row, forceMultiline) { 358 | 359 | var line = session.getLine(row); 360 | 361 | if (this.startRegionRe.test(line)) { 362 | 363 | return this.getCommentRegionBlock(session, line, row); 364 | 365 | } 366 | 367 | var match = line.match(this.foldingStartMarker); 368 | if (match) { 369 | 370 | var i = match.index; 371 | 372 | if (match[1]) { 373 | 374 | return this.openingBracketBlock(session, match[1], row, i); 375 | 376 | } 377 | 378 | var range = session.getCommentFoldRange(row, i + match[0].length, 1); 379 | 380 | if (range && !range.isMultiLine()) { 381 | 382 | if (forceMultiline) { 383 | 384 | range = this.getSectionRange(session, row); 385 | 386 | } else if (foldStyle != 'all') { 387 | 388 | range = null; 389 | 390 | } 391 | 392 | } 393 | 394 | return range; 395 | 396 | } 397 | 398 | if (foldStyle === 'markbegin') { 399 | 400 | return; 401 | 402 | } 403 | 404 | var match = line.match(this.foldingStopMarker); 405 | if (match) { 406 | 407 | var i = match.index + match[0].length; 408 | 409 | if (match[1]) { 410 | 411 | return this.closingBracketBlock(session, match[1], row, i); 412 | 413 | } 414 | 415 | return session.getCommentFoldRange(row, i, -1); 416 | 417 | } 418 | 419 | }; 420 | 421 | this.getSectionRange = function (session, row) { 422 | 423 | var line = session.getLine(row); 424 | var startIndent = line.search(/\S/); 425 | var startRow = row; 426 | var startColumn = line.length; 427 | row = row + 1; 428 | var endRow = row; 429 | var maxRow = session.getLength(); 430 | while (++row < maxRow) { 431 | 432 | line = session.getLine(row); 433 | var indent = line.search(/\S/); 434 | if (indent === -1) { 435 | 436 | continue; 437 | 438 | } 439 | if (startIndent > indent) { 440 | 441 | break; 442 | 443 | } 444 | var subRange = this.getFoldWidgetRange(session, 'all', row); 445 | 446 | if (subRange) { 447 | 448 | if (subRange.start.row <= startRow) { 449 | 450 | break; 451 | 452 | } else if (subRange.isMultiLine()) { 453 | 454 | row = subRange.end.row; 455 | 456 | } else if (startIndent == indent) { 457 | 458 | break; 459 | 460 | } 461 | 462 | } 463 | endRow = row; 464 | 465 | } 466 | 467 | return new Range(startRow, startColumn, endRow, session.getLine(endRow).length); 468 | 469 | }; 470 | this.getCommentRegionBlock = function (session, line, row) { 471 | 472 | var startColumn = line.search(/\s*$/); 473 | var maxRow = session.getLength(); 474 | var startRow = row; 475 | 476 | var re = /^\s*(?:\/\*|\/\/|--)#?(end)?region\b/; 477 | var depth = 1; 478 | while (++row < maxRow) { 479 | 480 | line = session.getLine(row); 481 | var m = re.exec(line); 482 | if (!m) continue; 483 | if (m[1]) depth--; 484 | else depth++; 485 | 486 | if (!depth) break; 487 | 488 | } 489 | 490 | var endRow = row; 491 | if (endRow > startRow) { 492 | 493 | return new Range(startRow, startColumn, endRow, line.length); 494 | 495 | } 496 | 497 | }; 498 | 499 | }).call(FoldMode.prototype); 500 | 501 | }); 502 | 503 | define('ace/mode/c_cpp', ['require', 'exports', 'module', 'ace/lib/oop', 'ace/mode/text', 'ace/mode/c_cpp_highlight_rules', 'ace/mode/matching_brace_outdent', 'ace/range', 'ace/mode/behaviour/cstyle', 'ace/mode/folding/cstyle'], function (require, exports, module) { 504 | 505 | 'use strict'; 506 | 507 | var oop = require('../lib/oop'); 508 | var TextMode = require('./text').Mode; 509 | var c_cppHighlightRules = require('./c_cpp_highlight_rules').c_cppHighlightRules; 510 | var MatchingBraceOutdent = require('./matching_brace_outdent').MatchingBraceOutdent; 511 | var Range = require('../range').Range; 512 | var CstyleBehaviour = require('./behaviour/cstyle').CstyleBehaviour; 513 | var CStyleFoldMode = require('./folding/cstyle').FoldMode; 514 | 515 | var Mode = function () { 516 | 517 | this.HighlightRules = c_cppHighlightRules; 518 | 519 | this.$outdent = new MatchingBraceOutdent(); 520 | this.$behaviour = new CstyleBehaviour(); 521 | 522 | this.foldingRules = new CStyleFoldMode(); 523 | 524 | }; 525 | oop.inherits(Mode, TextMode); 526 | 527 | (function () { 528 | 529 | this.lineCommentStart = '//'; 530 | this.blockComment = {start: '/*', end: '*/'}; 531 | 532 | this.getNextLineIndent = function (state, line, tab) { 533 | 534 | var indent = this.$getIndent(line); 535 | 536 | var tokenizedLine = this.getTokenizer().getLineTokens(line, state); 537 | var tokens = tokenizedLine.tokens; 538 | var endState = tokenizedLine.state; 539 | 540 | if (tokens.length && tokens[tokens.length - 1].type == 'comment') { 541 | 542 | return indent; 543 | 544 | } 545 | 546 | if (state == 'start') { 547 | 548 | var match = line.match(/^.*[\{\(\[]\s*$/); 549 | if (match) { 550 | 551 | indent += tab; 552 | 553 | } 554 | 555 | } else if (state == 'doc-start') { 556 | 557 | if (endState == 'start') { 558 | 559 | return ''; 560 | 561 | } 562 | var match = line.match(/^\s*(\/?)\*/); 563 | if (match) { 564 | 565 | if (match[1]) { 566 | 567 | indent += ' '; 568 | 569 | } 570 | indent += '* '; 571 | 572 | } 573 | 574 | } 575 | 576 | return indent; 577 | 578 | }; 579 | 580 | this.checkOutdent = function (state, line, input) { 581 | 582 | return this.$outdent.checkOutdent(line, input); 583 | 584 | }; 585 | 586 | this.autoOutdent = function (state, doc, row) { 587 | 588 | this.$outdent.autoOutdent(doc, row); 589 | 590 | }; 591 | 592 | this.$id = 'ace/mode/c_cpp'; 593 | 594 | }).call(Mode.prototype); 595 | 596 | exports.Mode = Mode; 597 | 598 | }); 599 | 600 | define('ace/mode/glsl_highlight_rules', ['require', 'exports', 'module', 'ace/lib/oop', 'ace/mode/c_cpp_highlight_rules'], function (require, exports, module) { 601 | 602 | 'use strict'; 603 | 604 | var oop = require('../lib/oop'); 605 | var c_cppHighlightRules = require('./c_cpp_highlight_rules').c_cppHighlightRules; 606 | 607 | var glslHighlightRules = function () { 608 | 609 | var keywords = ( 610 | 'attribute|const|uniform|varying|break|continue|do|for|while|' + 611 | 'if|else|in|out|inout|float|int|void|bool|true|false|' + 612 | 'lowp|mediump|highp|precision|invariant|discard|return|mat2|mat3|' + 613 | 'mat4|vec2|vec3|vec4|ivec2|ivec3|ivec4|bvec2|bvec3|bvec4|sampler2D|' + 614 | 'samplerCube|struct' 615 | ); 616 | 617 | var buildinConstants = ( 618 | 'radians|degrees|sin|cos|tan|asin|acos|atan|pow|' + 619 | 'exp|log|exp2|log2|sqrt|inversesqrt|abs|sign|floor|ceil|fract|mod|' + 620 | 'min|max|clamp|mix|step|smoothstep|length|distance|dot|cross|' + 621 | 'normalize|faceforward|reflect|refract|matrixCompMult|lessThan|' + 622 | 'lessThanEqual|greaterThan|greaterThanEqual|equal|notEqual|any|all|' + 623 | 'not|dFdx|dFdy|fwidth|texture2D|texture2DProj|texture2DLod|' + 624 | 'texture2DProjLod|textureCube|textureCubeLod|' + 625 | 'gl_MaxVertexAttribs|gl_MaxVertexUniformVectors|gl_MaxVaryingVectors|' + 626 | 'gl_MaxVertexTextureImageUnits|gl_MaxCombinedTextureImageUnits|' + 627 | 'gl_MaxTextureImageUnits|gl_MaxFragmentUniformVectors|gl_MaxDrawBuffers|' + 628 | 'gl_DepthRangeParameters|gl_DepthRange|' + 629 | 'gl_Position|gl_PointSize|' + 630 | 'gl_FragCoord|gl_FrontFacing|gl_PointCoord|gl_FragColor|gl_FragData' 631 | ); 632 | 633 | var keywordMapper = this.createKeywordMapper({ 634 | 'variable.language': 'this', 635 | 'keyword': keywords, 636 | 'constant.language': buildinConstants 637 | }, 'identifier'); 638 | 639 | this.$rules = new c_cppHighlightRules().$rules; 640 | this.$rules.start.forEach(function (rule) { 641 | 642 | if (typeof rule.token === 'function') { 643 | 644 | rule.token = keywordMapper; 645 | 646 | } 647 | 648 | }); 649 | 650 | }; 651 | 652 | oop.inherits(glslHighlightRules, c_cppHighlightRules); 653 | 654 | exports.glslHighlightRules = glslHighlightRules; 655 | 656 | }); 657 | 658 | define('ace/mode/glsl', ['require', 'exports', 'module', 'ace/lib/oop', 'ace/mode/c_cpp', 'ace/mode/glsl_highlight_rules', 'ace/mode/matching_brace_outdent', 'ace/range', 'ace/mode/behaviour/cstyle', 'ace/mode/folding/cstyle'], function (require, exports, module) { 659 | 660 | 'use strict'; 661 | 662 | var oop = require('../lib/oop'); 663 | var CMode = require('./c_cpp').Mode; 664 | var glslHighlightRules = require('./glsl_highlight_rules').glslHighlightRules; 665 | var MatchingBraceOutdent = require('./matching_brace_outdent').MatchingBraceOutdent; 666 | var Range = require('../range').Range; 667 | var CstyleBehaviour = require('./behaviour/cstyle').CstyleBehaviour; 668 | var CStyleFoldMode = require('./folding/cstyle').FoldMode; 669 | 670 | var Mode = function () { 671 | 672 | this.HighlightRules = glslHighlightRules; 673 | 674 | this.$outdent = new MatchingBraceOutdent(); 675 | this.$behaviour = new CstyleBehaviour(); 676 | this.foldingRules = new CStyleFoldMode(); 677 | 678 | }; 679 | oop.inherits(Mode, CMode); 680 | 681 | (function () { 682 | 683 | this.$id = 'ace/mode/glsl'; 684 | 685 | }).call(Mode.prototype); 686 | 687 | exports.Mode = Mode; 688 | 689 | }); 690 | -------------------------------------------------------------------------------- /dist/theme-monokai.js: -------------------------------------------------------------------------------- 1 | define('ace/theme/monokai', ['require', 'exports', 'module', 'ace/lib/dom'], function (require, exports, module) { 2 | 3 | exports.isDark = true; 4 | exports.cssClass = 'ace-monokai'; 5 | exports.cssText = '.ace-monokai .ace_gutter {\ 6 | background: #2F3129;\ 7 | color: #8F908A\ 8 | }\ 9 | .ace-monokai .ace_print-margin {\ 10 | width: 1px;\ 11 | background: #555651\ 12 | }\ 13 | .ace-monokai {\ 14 | background-color: #272822;\ 15 | color: #F8F8F2\ 16 | }\ 17 | .ace-monokai .ace_cursor {\ 18 | color: #F8F8F0\ 19 | }\ 20 | .ace-monokai .ace_marker-layer .ace_selection {\ 21 | background: #49483E\ 22 | }\ 23 | .ace-monokai.ace_multiselect .ace_selection.ace_start {\ 24 | box-shadow: 0 0 3px 0px #272822;\ 25 | }\ 26 | .ace-monokai .ace_marker-layer .ace_step {\ 27 | background: rgb(102, 82, 0)\ 28 | }\ 29 | .ace-monokai .ace_marker-layer .ace_bracket {\ 30 | margin: -1px 0 0 -1px;\ 31 | border: 1px solid #49483E\ 32 | }\ 33 | .ace-monokai .ace_marker-layer .ace_active-line {\ 34 | background: #202020\ 35 | }\ 36 | .ace-monokai .ace_gutter-active-line {\ 37 | background-color: #272727\ 38 | }\ 39 | .ace-monokai .ace_marker-layer .ace_selected-word {\ 40 | border: 1px solid #49483E\ 41 | }\ 42 | .ace-monokai .ace_invisible {\ 43 | color: #52524d\ 44 | }\ 45 | .ace-monokai .ace_entity.ace_name.ace_tag,\ 46 | .ace-monokai .ace_keyword,\ 47 | .ace-monokai .ace_meta.ace_tag,\ 48 | .ace-monokai .ace_storage {\ 49 | color: #F92672\ 50 | }\ 51 | .ace-monokai .ace_punctuation,\ 52 | .ace-monokai .ace_punctuation.ace_tag {\ 53 | color: #fff\ 54 | }\ 55 | .ace-monokai .ace_constant.ace_character,\ 56 | .ace-monokai .ace_constant.ace_language,\ 57 | .ace-monokai .ace_constant.ace_numeric,\ 58 | .ace-monokai .ace_constant.ace_other {\ 59 | color: #AE81FF\ 60 | }\ 61 | .ace-monokai .ace_invalid {\ 62 | color: #F8F8F0;\ 63 | background-color: #F92672\ 64 | }\ 65 | .ace-monokai .ace_invalid.ace_deprecated {\ 66 | color: #F8F8F0;\ 67 | background-color: #AE81FF\ 68 | }\ 69 | .ace-monokai .ace_support.ace_constant,\ 70 | .ace-monokai .ace_support.ace_function {\ 71 | color: #66D9EF\ 72 | }\ 73 | .ace-monokai .ace_fold {\ 74 | background-color: #A6E22E;\ 75 | border-color: #F8F8F2\ 76 | }\ 77 | .ace-monokai .ace_storage.ace_type,\ 78 | .ace-monokai .ace_support.ace_class,\ 79 | .ace-monokai .ace_support.ace_type {\ 80 | font-style: italic;\ 81 | color: #66D9EF\ 82 | }\ 83 | .ace-monokai .ace_entity.ace_name.ace_function,\ 84 | .ace-monokai .ace_entity.ace_other,\ 85 | .ace-monokai .ace_entity.ace_other.ace_attribute-name,\ 86 | .ace-monokai .ace_variable {\ 87 | color: #A6E22E\ 88 | }\ 89 | .ace-monokai .ace_variable.ace_parameter {\ 90 | font-style: italic;\ 91 | color: #FD971F\ 92 | }\ 93 | .ace-monokai .ace_string {\ 94 | color: #E6DB74\ 95 | }\ 96 | .ace-monokai .ace_comment {\ 97 | color: #75715E\ 98 | }\ 99 | .ace-monokai .ace_indent-guide {\ 100 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAEklEQVQImWPQ0FD0ZXBzd/wPAAjVAoxeSgNeAAAAAElFTkSuQmCC) right repeat-y\ 101 | }'; 102 | 103 | var dom = require('../lib/dom'); 104 | dom.importCssString(exports.cssText, exports.cssClass); 105 | 106 | }); 107 | -------------------------------------------------------------------------------- /docs/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkjohnson/webgl-shader-editor/4bcfb53aec5ad59b1c7fc180c892169964914eca/docs/example.gif -------------------------------------------------------------------------------- /electron/app.js: -------------------------------------------------------------------------------- 1 | const { 2 | app, 3 | BrowserWindow, 4 | Menu 5 | } = require('electron'); 6 | 7 | app.on('ready', () => { 8 | 9 | const main = new BrowserWindow(); 10 | main.loadURL(`${__dirname}/../index.html`); 11 | main.setMenu(null); 12 | 13 | }); 14 | 15 | app.on('window-all-closed', () => app.quit()); 16 | 17 | Menu.setApplicationMenu(new Menu()); 18 | -------------------------------------------------------------------------------- /elements/ace-editor.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 99 | -------------------------------------------------------------------------------- /elements/image-magnifier.html: -------------------------------------------------------------------------------- 1 | 2 | 87 | 88 | 89 | 183 | -------------------------------------------------------------------------------- /elements/shader-editor.html: -------------------------------------------------------------------------------- 1 | 2 | 73 | 74 | 163 | -------------------------------------------------------------------------------- /elements/shader-preview.html: -------------------------------------------------------------------------------- 1 | 2 | 193 | 194 | 870 | -------------------------------------------------------------------------------- /elements/zoomable-canvas.html: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 123 | -------------------------------------------------------------------------------- /elements/zoomable-element.html: -------------------------------------------------------------------------------- 1 | 2 | 53 | 54 | 55 | 311 | -------------------------------------------------------------------------------- /elements/zoomable-image.html: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 22 | 80 | -------------------------------------------------------------------------------- /empty-template.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /images/cube-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkjohnson/webgl-shader-editor/4bcfb53aec5ad59b1c7fc180c892169964914eca/images/cube-icon.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebGL Shader Editor 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 61 | 62 | 63 | // Lighting 64 | struct DirLight { 65 | vec3 color; 66 | vec3 direction; 67 | }; 68 | 69 | uniform float time; 70 | uniform sampler2D texture0; 71 | uniform sampler2D texture1; 72 | uniform sampler2D texture2; 73 | uniform DirLight directionalLights[NUM_DIR_LIGHTS]; 74 | uniform vec3 ambientLightColor; 75 | 76 | varying vec3 lighting; 77 | varying vec2 texcoord; 78 | 79 | void main() 80 | { 81 | vec3 worldNorm = (modelViewMatrix * vec4(normal, 0)).xyz; 82 | 83 | texcoord = uv; 84 | lighting = ambientLightColor; 85 | for(int i = 0; i < NUM_DIR_LIGHTS; i ++) { 86 | DirLight dl = directionalLights[i]; 87 | lighting += clamp(dot(worldNorm, dl.direction), 0.0, 1.0) * dl.color; 88 | } 89 | 90 | vec3 sample = texture2D(texture1, uv).xyz; 91 | vec3 pos = position + normal * sample * sin(time * 0.001); 92 | 93 | gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); 94 | } 95 | 96 | 97 | 98 | varying vec3 lighting; 99 | varying vec2 texcoord; 100 | 101 | uniform sampler2D texture0; // checkerboard 102 | uniform sampler2D texture1; // clouds 103 | uniform sampler2D texture2; // random noise 104 | uniform vec4 screenSize; 105 | 106 | void main() { 107 | vec3 rgb = texture2D(texture1, texcoord).xyz; 108 | vec2 st; 109 | st.x = gl_FragCoord.x / 2.0; 110 | st.x -= floor(st.x); 111 | 112 | st.y = gl_FragCoord.y / 2.0; 113 | st.y -= floor(st.y); 114 | 115 | if (rgb.r < 0.5) { 116 | float offset = 0.5; 117 | offset += floor(st.y + 0.5) == 0.0 ? -1.0 : 0.0; 118 | if (floor(st.x + offset) == 0.0) discard; 119 | } 120 | gl_FragColor = vec4(rgb, 1) * vec4(rgb * lighting, 1); 121 | } 122 | 123 | 124 | 125 | 126 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webgl-shader-editor", 3 | "version": "1.0.0", 4 | "description": "Realtime editor for creating webgl shaders", 5 | "main": "electron/app.js", 6 | "scripts": { 7 | "start": "static-server", 8 | "electron": "electron .", 9 | "build-electron": "electron-packager . webgl-shader-editor", 10 | "build": "webpack", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/gkjohnson/webgl-shader-editor.git" 16 | }, 17 | "author": "Garrett Johnson ", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/gkjohnson/webgl-shader-editor/issues" 21 | }, 22 | "homepage": "https://github.com/gkjohnson/webgl-shader-editor#readme", 23 | "dependencies": { 24 | "@gkjohnson/javascript-utils": "0.0.3", 25 | "@polymer/polymer": "^2.0.2", 26 | "@webcomponents/webcomponentsjs": "^1.0.10", 27 | "ace-builds": "^1.2.8", 28 | "three": "^0.87.1" 29 | }, 30 | "devDependencies": { 31 | "electron": "^1.7.8", 32 | "electron-packager": "^9.1.0", 33 | "html-webpack-plugin": "^2.30.1", 34 | "script-loader": "^0.7.1", 35 | "static-server": "^2.0.5", 36 | "url-loader": "^0.6.2", 37 | "wc-loader": "^1.1.12", 38 | "webpack": "^3.6.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /scripts/DebugShaders.js: -------------------------------------------------------------------------------- 1 | window.DebugShaders = {}; 2 | const DebugShaders = window.DebugShaders; 3 | 4 | (function () { 5 | 6 | const NEGATE_UNIFORM = '_negate_'; 7 | const MAIN_SIG = 'void main() {'; 8 | 9 | const variableRegex = /((((precision|varying|uniform|attribute)\s+)?)((highp|mediump|lowp)\s+)?)(vec4|vec3|vec2|float|int|uint|bool)\s+([A-Za-z0-9]+)/; 10 | 11 | // Clean up the shader to make it easer to parse 12 | const normalize = shader => { 13 | 14 | return shader 15 | .replace(/\/\/[^\n]*\n/g, '') // remove comment line 16 | .replace(/\/\*(\*(?!\/)|[^*])*\*\//g, '') // remove block comment 17 | .replace(/(\n|\s)+/g, ' ') // replace newlines or whitespace with a single space 18 | .replace(/\s*({|})\s*/g, '\n$1\n') // compress the area around braces 19 | .replace(/\s*;\s*/g, ';\n') // replace newlines after semicolons 20 | .replace(/(return|discard)\s*;/g, ';') // remove any early discards or returns 21 | .replace(/void\s+main\s*\(\)(\s|\n)*{/, MAIN_SIG); // clean up the main function signature 22 | 23 | }; 24 | 25 | // Find the line range between which the main function lives 26 | const getMainFuncRange = shader => { 27 | 28 | const st = shader.indexOf(MAIN_SIG); 29 | const bef = shader.substr(0, st); 30 | const aft = shader.substr(st); 31 | 32 | const befLines = bef.replace(/[^\n]/g, '').length; 33 | const aftBraces = aft.replace(/[^{}\n]*/g, ''); 34 | 35 | // count the braces to the end of the main function body 36 | let started = false; 37 | let braceCount = 0; 38 | let lines = 0; 39 | for (let i = 0; i < aftBraces.length; i++) { 40 | 41 | const ch = aftBraces[i]; 42 | if (ch === '{') braceCount++; 43 | if (ch === '}') braceCount--; 44 | if (ch === '\n') lines++; 45 | 46 | if (braceCount > 0) started = true; 47 | if (started && braceCount === 0) break; 48 | 49 | } 50 | 51 | return { start: befLines, end: befLines + lines }; 52 | 53 | }; 54 | 55 | // get a line that outputs the given variable of type 56 | // as a fragment color 57 | const toGlFragColorLine = (type, name) => { 58 | 59 | let r = 0; 60 | let g = 0; 61 | let b = 0; 62 | let a = 1; 63 | 64 | const neg = `(${NEGATE_UNIFORM} ? -1.0 : 1.0)`; 65 | 66 | if (/^vec/.test(type)) { 67 | 68 | // TODO: Pack these more so more of 69 | // the data can be read back out, otherwise 70 | // they're clamped from 0 to 1.0 71 | r = `${name}.r * ${neg}`; 72 | g = `${name}.g * ${neg}`; 73 | if (/^vec(3|4)/.test(type)) b = `${name}.b * ${neg}`; 74 | if (/^vec4/.test(type)) a = `${name}.a * ${neg}`; 75 | 76 | } else if (type === 'bool') { 77 | 78 | r = `${name} ? 1 : 0`; 79 | g = r; 80 | b = r; 81 | a = r; 82 | 83 | } else if (/^(int|uint)/.test(type)) { 84 | 85 | r = `float(((${name} * int(${neg})) << 0 ) & 0xFF) / 0xFF`; 86 | g = `float(((${name} * int(${neg})) << 8 ) & 0xFF) / 0xFF`; 87 | b = `float(((${name} * int(${neg})) << 16) & 0xFF) / 0xFF`; 88 | a = `float(((${name} * int(${neg})) << 24) & 0xFF) / 0xFF`; 89 | 90 | } else if (type === 'float') { 91 | 92 | // TODO : Pack this into bytes so we can 93 | // read it back out as a larger float 94 | r = g = b = `${name}`; 95 | 96 | } 97 | 98 | return `gl_FragColor = vec4(${r},${g},${b},${a});`; 99 | 100 | }; 101 | 102 | DebugShaders.enumerate = (vs, fs, negate = false) => { 103 | 104 | vs = normalize(vs); 105 | fs = normalize(fs); 106 | 107 | const vsRange = getMainFuncRange(vs); 108 | const fsRange = getMainFuncRange(fs); 109 | 110 | const shaders = []; 111 | const fsVarying = []; 112 | 113 | // generate shaders for outputing colors for each variable 114 | // in the vertex shader 115 | let lines = vs.split('\n'); 116 | lines.forEach((line, i) => { 117 | 118 | if (i < vsRange.start || i > vsRange.end) return; 119 | if (/for|while/g.test(line)) return; 120 | 121 | const matches = line.match(variableRegex); 122 | if (matches) { 123 | 124 | const prefix = (matches[1] || '').trim(); 125 | const type = matches[7].trim(); 126 | const name = matches[8].trim(); 127 | 128 | if (prefix) return; 129 | if (/(int|uint)/g.test(type)) return; 130 | 131 | const newVarName = `_out_${name}_`; 132 | const varyingLine = `varying ${type} ${newVarName};`; 133 | const newLines = [varyingLine].concat(lines); 134 | newLines[vsRange.end] = `\n${newVarName} = ${name};\n` + newLines[vsRange.end]; 135 | 136 | const newvs = newLines.join('\n'); 137 | const newfs = `${varyingLine}\n${fs}`.replace(MAIN_SIG, MAIN_SIG + '\n' + toGlFragColorLine(type, newVarName) + '\nreturn;\n'); 138 | 139 | shaders.push({ 140 | type, 141 | name, 142 | vertexShader: newvs, 143 | fragmentShader: newfs, 144 | line: i, 145 | fromShader: 'vertex' 146 | }); 147 | 148 | } 149 | 150 | }); 151 | 152 | // generate shaders for outputing colors for each variable 153 | // in the fragmnet shader 154 | lines = fs.split('\n'); 155 | lines.forEach((line, i) => { 156 | 157 | if (/for|while/g.test(line)) return; 158 | 159 | const matches = line.match(variableRegex); 160 | if (matches) { 161 | 162 | const prefix = (matches[1] || '').trim(); 163 | const type = matches[7].trim(); 164 | const name = matches[8].trim(); 165 | 166 | if (prefix) { 167 | 168 | if (prefix === 'varying') fsVarying.push({ type, name, line: i }); 169 | return; 170 | 171 | } 172 | if (/(int|uint)/g.test(type)) return; 173 | if (i < fsRange.start || i > fsRange.end) return; 174 | 175 | const newlines = [].concat(lines); 176 | newlines[fsRange.end] = '\n' + toGlFragColorLine(type, name) + '\nreturn;\n' + newlines[fsRange.end]; 177 | 178 | shaders.push({ 179 | type, 180 | name, 181 | vertexShader: vs, 182 | fragmentShader: newlines.join('\n'), 183 | line: i, 184 | fromShader: 'fragment' 185 | }); 186 | 187 | } 188 | 189 | }); 190 | 191 | // output color for each varying variable in the frag shader 192 | fsVarying 193 | .forEach(it => { 194 | 195 | const res = fs.replace(MAIN_SIG, MAIN_SIG + '\n' + toGlFragColorLine(it.type, it.name) + '\nreturn;\n'); 196 | shaders.push({ 197 | type: it.type, 198 | name: it.name, 199 | vertexShader: vs, 200 | fragmentShader: res, 201 | line: it.line, 202 | fromShader: 'fragment' 203 | }); 204 | 205 | }); 206 | 207 | // prefix the _negate_ uniform on each shader which is 208 | // used to invert the color of each output 209 | for (let i in shaders) { 210 | 211 | shaders[i].fragmentShader = ` 212 | uniform bool ${NEGATE_UNIFORM}; 213 | ${shaders[i].fragmentShader} 214 | `; 215 | 216 | } 217 | 218 | return shaders; 219 | 220 | }; 221 | 222 | DebugShaders.readPixel = (ctx, x, y) => { 223 | 224 | const data = ctx.getImageData(x, y, 1, 1).data; 225 | 226 | return { 227 | r: data[0], 228 | g: data[1], 229 | b: data[2], 230 | a: data[3] 231 | }; 232 | 233 | }; 234 | 235 | DebugShaders.pixelToArray = (px, type, prec = 5) => { 236 | 237 | const cv = f => parseFloat((f / 255.0).toPrecision(prec)); 238 | 239 | if (type === 'vec2') return [cv(px.r), cv(px.g)]; 240 | if (type === 'vec3') return [cv(px.r), cv(px.g), cv(px.b)]; 241 | if (type === 'vec4') return [cv(px.r), cv(px.g), cv(px.b), cv(px.a)]; 242 | if (type === 'bool') return [!!px.r]; 243 | if (type === 'int'); // TODO 244 | if (type === 'uint'); // TODO 245 | if (type === 'float') return [cv(px.r)]; 246 | 247 | }; 248 | 249 | })(); 250 | -------------------------------------------------------------------------------- /textures/checkerboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkjohnson/webgl-shader-editor/4bcfb53aec5ad59b1c7fc180c892169964914eca/textures/checkerboard.png -------------------------------------------------------------------------------- /textures/clouds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkjohnson/webgl-shader-editor/4bcfb53aec5ad59b1c7fc180c892169964914eca/textures/clouds.png -------------------------------------------------------------------------------- /textures/random.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gkjohnson/webgl-shader-editor/4bcfb53aec5ad59b1c7fc180c892169964914eca/textures/random.png -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | module.exports = { 3 | entry: __dirname + '/index.html', 4 | output: { 5 | path: __dirname + '/dist', 6 | filename: 'index.bundle.js' 7 | }, 8 | 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.html$/, 13 | use: [{ loader: 'wc-loader' }] 14 | }, 15 | { 16 | test: /\.js$/, 17 | use: [{ loader: 'script-loader' }] 18 | }, 19 | { 20 | test: /\.(jpg|jpeg|png)/, 21 | use: [{ loader: 'url-loader' }] 22 | } 23 | ] 24 | }, 25 | 26 | plugins: [ 27 | new HtmlWebpackPlugin({ 28 | template: './empty-template.ejs', 29 | filename: 'index.bundle.html' 30 | }) 31 | ] 32 | }; 33 | --------------------------------------------------------------------------------