├── .eslintignore ├── .eslintrc.json ├── .github ├── codecov.yml └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── .nycrc ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── bin ├── webpack-glsl-minify └── wgm ├── package-lock.json ├── package.json ├── src ├── __tests__ │ ├── cases.test.ts │ ├── cli.test.ts │ ├── internals.ts │ ├── minify.test.ts │ ├── preprocessor.test.ts │ └── webpack.test.ts ├── cli.ts ├── fsAsync.ts ├── index.ts ├── minify.ts ├── node.ts └── webpack.ts ├── tests ├── cases │ ├── commas-uniforms.glsl │ ├── commas-uniforms.json │ ├── commas-variables.glsl │ ├── commas-variables.json │ ├── comments.glsl │ ├── comments.json │ ├── complex-include.glsl │ ├── complex.glsl │ ├── complex.json │ ├── const-multiple.glsl │ ├── const-multiple.json │ ├── const.glsl │ ├── const.json │ ├── define.glsl │ ├── define.json │ ├── fragment.glsl │ ├── fragment.json │ ├── hello.glsl │ ├── hello.json │ ├── include1.glsl │ ├── include1.json │ ├── include2.glsl │ ├── include2.json │ ├── layout1.glsl │ ├── layout1.json │ ├── layout2.glsl │ ├── layout2.json │ ├── precision.glsl │ ├── precision.json │ ├── uniform-array-of-const-2.glsl │ ├── uniform-array-of-const-2.json │ ├── uniform-array-of-const.glsl │ ├── uniform-array-of-const.json │ ├── vertex.glsl │ └── vertex.json └── webpack │ ├── glsl-include │ └── YCbCr.glsl │ ├── glsl │ └── test.glsl │ ├── index-es.js │ ├── index.js │ ├── webpack.test1.js │ ├── webpack.test2.js │ ├── webpack.test3.js │ ├── webpack.test4.js │ ├── webpack.test5.js │ ├── webpack.test6.js │ ├── webpack.test7.js │ └── webpack.test8.js ├── tsconfig.eslint.json └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | **/build/** -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint-config-leosingleton" 4 | ], 5 | "env": { 6 | "node": true, 7 | "es6": true 8 | }, 9 | "parserOptions": { 10 | "project": ["./tsconfig.json", "./tsconfig.eslint.json"] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | informational: true 6 | patch: 7 | default: 8 | informational: true 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | build: 7 | name: Build 8 | runs-on: ubuntu-latest 9 | timeout-minutes: 5 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v1 14 | 15 | - name: Install Node 16.x 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: 16.x 19 | 20 | - name: Install NPM Packages 21 | run: npm install 22 | 23 | - name: Lint 24 | run: npm run lint 25 | 26 | - name: Compile 27 | run: npm run build 28 | 29 | - name: Execute unit tests 30 | run: npm run test 31 | 32 | - name: Publish Code Coverage 33 | uses: codecov/codecov-action@v1 34 | with: 35 | file: ./build/cobertura-coverage.xml 36 | 37 | - name: Upload Build Artifact 38 | uses: actions/upload-artifact@v2 39 | with: 40 | name: webpack-glsl-minify-${{github.run_number}} 41 | path: | 42 | ** 43 | !**/node_modules/** 44 | 45 | - name: Publish NPM Package 46 | if: github.ref == 'refs/heads/master' 47 | uses: JS-DevTools/npm-publish@v1 48 | with: 49 | token: ${{ secrets.NPM_TOKEN }} 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules/* 3 | /**/build/* 4 | .nyc_output 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | build/__tests__ -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@istanbuljs/nyc-config-typescript", 3 | "report-dir": "./build" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug Unit Tests", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeArgs": [ 9 | "--inspect-brk", 10 | "${workspaceRoot}/node_modules/jasmine-xml-reporter/bin/jasmine.js", 11 | "./build/__tests__/*.test.js", 12 | "--output=build/" 13 | ], 14 | "console": "integratedTerminal", 15 | "internalConsoleOptions": "neverOpen", 16 | "port": 9229, 17 | "skipFiles": [ 18 | "/**/*.js" 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2022 Leo C. Singleton IV 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 | # GLSL Preprocessor, Minifier, and Webpack Loader 2 | ![CI](https://github.com/leosingleton/webpack-glsl-minify/workflows/CI/badge.svg) 3 | [![npm version](https://badge.fury.io/js/webpack-glsl-minify.svg)](https://badge.fury.io/js/webpack-glsl-minify) 4 | [![codecov](https://codecov.io/gh/leosingleton/webpack-glsl-minify/branch/master/graph/badge.svg)](https://codecov.io/gh/leosingleton/webpack-glsl-minify) 5 | 6 | webpack-glsl-minify is a loader for Webpack that handles GLSL files. In addition to simply loading the GLSL program 7 | into a JavaScript string, it also has a preprocessor which executes at compile time, and a minifier which shrinks the 8 | GLSL program before embedding it in JavaScript. 9 | 10 | ## Install 11 | ``` 12 | npm install --save-dev webpack-glsl-minify 13 | ``` 14 | 15 | ## Usage 16 | ### Webpack Configuration 17 | To use, add the following to your webpack.js config file: 18 | 19 | ```javascript 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.glsl$/, 24 | use: 'webpack-glsl-minify' 25 | } 26 | ] 27 | }, 28 | resolve: { 29 | extensions: [ '.glsl' ] 30 | } 31 | ``` 32 | 33 | ### GLSL Preprocessor 34 | The preprocessor uses an `@` symbol to distinguish commands executed at Webpack compile time versus shader compile time. 35 | The following commands are supported: 36 | 37 | #### include Directive 38 | Includes another GLSL file. Example: 39 | ``` 40 | @include "../path/another-file.glsl" 41 | ``` 42 | 43 | #### define Directive 44 | Defines a macro which will be substituted elsewhere in the code. Example: 45 | ``` 46 | @define PI 3.1415 47 | // .... 48 | float angle = 2.0 * PI; 49 | ``` 50 | 51 | Note that `@define` is not a replacement for `#define`. For minification purposes, it is often better to let the shader 52 | compiler do the macro substitution instead of the Webpack compiler. 53 | 54 | #### nomangle Directive 55 | Disables name mangling on one or more symbols. Example: 56 | ``` 57 | @nomangle symbol1 symbol2 58 | ``` 59 | 60 | #### const Directive 61 | Defines a constant variable with a unique substitution value that can be used to search-and-replace to initialize the 62 | constant. Example: 63 | ``` 64 | @const int my_int 65 | ``` 66 | will produce 67 | ```glsl 68 | const int A=$0$; 69 | ``` 70 | and the mapping from `my_int` to the substitution value `$0$` will be returned in the output. 71 | 72 | ### Minification 73 | The following minification optimizations are performed: 74 | 75 | * Removal of comments and whitespace. Both C-style `/* Comment */` and C++-style `// Comment` are supported. 76 | * Shortening floating point numbers. `1.0` becomes `1.` 77 | * Mangling symbol names. All functions, variables, parameters, and uniforms are renamed to short names. Built-in GLSL 78 | functions and variables begining with `gl_` are automatically excluded. Attributes and varying variables are also 79 | excluded, as the names must be consistent across multiple shaders. Additional symbols may be excluded with the 80 | `@nomangle` directive. 81 | 82 | ### Output 83 | By default, an object is exported via JavaScript containing the source code and a map of the mangled uniforms and 84 | constants: 85 | ```javascript 86 | module.exports = { 87 | sourceCode: "uniform vec3 A;uniform float B;/* ... More minified GLSL code here */", 88 | uniforms: { // Map of minified uniform variables 89 | uniform1: { // Unminified uniform name 90 | variableName: "A", // Minified uniform name 91 | variableType: "vec3" // Type of the uniform 92 | }, 93 | uniform2: { 94 | variableName: "B", 95 | variableType: "float" 96 | } // ... 97 | }, 98 | consts: { // Map of minified const variables 99 | const1: { // Unminified const name 100 | variableName: "$0$", // Substitution value to replace to initialize the const 101 | variableType: "vec2" // Type of the const 102 | } // ... 103 | } 104 | }; 105 | ``` 106 | 107 | The map of uniforms is included to make it easy for the JavaScript code compiling and executing the WebGL shader to 108 | set the uniform values, even after minification. 109 | 110 | TypeScript type definitions are included in the webpack-glsl-minify package for the output object. Simply cast to a 111 | `GlslShader` object when including GLSL code: 112 | ```javascript 113 | import { GlslShader, GlslVariable, GlslVariableMap } from 'webpack-glsl-minify'; 114 | 115 | let shader = require('./myshader.glsl') as GlslShader; 116 | ``` 117 | 118 | ## Loader Options 119 | 120 | ```javascript 121 | module: { 122 | rules: [ 123 | { 124 | test: /\.glsl$/, 125 | use: { 126 | loader: 'webpack-glsl-minify', 127 | options: { 128 | output: 'object', 129 | esModule: false, 130 | stripVersion: false, 131 | preserveDefines: false, 132 | preserveUniforms: false, 133 | preserveVariables: false, 134 | disableMangle: false, 135 | nomangle: [ 'variable1', 'variable2' ], 136 | includesOnly: false, 137 | } 138 | } 139 | } 140 | ] 141 | }, 142 | resolve: { 143 | extensions: [ '.glsl' ] 144 | } 145 | ``` 146 | 147 | This loader also supports the following loader-specific options: 148 | 149 | * `output`: Default `'object'`, which outputs JavaScript code which exports an object described in the section above. 150 | Alternatively, `'source'` may be specified which exports only a string containing the source code instead. 151 | Selecting `'source'` automatically disables mangling of uniforms as there is no output map of the mangled names. 152 | * `esModule`: Default `false`. Uses ES modules syntax instead of CommonJS. Applies to the "object" and "source" 153 | output formats. 154 | * `stripVersion`: Default `false`. Strips any `#version` directives. 155 | * `preserveDefines`: Default `false`. Disables name mangling of `#define`s. 156 | * `preserveUniforms`: Default `false`. Disables name mangling of uniforms. 157 | * `preserveVariables`: Default `false`. Disables name mangling of variables. 158 | * `preserveAll`: Default `false`. Disables all mangling. 159 | * `disableMangle`: Default `false`. Disables name mangling. This is useful for development purpose. 160 | * `nomangle`: Specifies an array of additional variable names or keywords to explicitly disable name mangling. 161 | * `includesOnly`: Default `false`. Disables all processing except include files. Useful for development! 162 | 163 | ## Using Without Webpack 164 | 165 | Additionally, webpack-glsl-minify provides a command-line tool which can be used as a build step without Webpack. By 166 | default, it produces `.js` files for each of the input `.glsl` files specified, output in the same directory as the 167 | source `.glsl`. Alternatively, the `-outDir` parameter may be used to produce output in a separate output directory 168 | mirroring the input directory layout. 169 | 170 | ```console 171 | $ npx webpack-glsl-minify --help 172 | webpack-glsl-minify [options] 173 | 174 | Minifies one or more GLSL files. Input files may be specified in glob syntax. 175 | 176 | Options: 177 | --version Show version number [boolean] 178 | --ext, -e Extension for output files [string] [default: ".js"] 179 | --outDir, -o Output base directory. By default, files are output to 180 | the same directory as the input .glsl file. [string] 181 | --output Output format 182 | [choices: "object", "source", "sourceOnly"] [default: "object"] 183 | --esModule Uses ES modules syntax. Applies to the "object" and 184 | "source" output formats. [boolean] 185 | --stripVersion Strips any #version directives [boolean] 186 | --preserveDefines Disables name mangling of #defines [boolean] 187 | --preserveUniforms Disables name mangling of uniforms [boolean] 188 | --preserveVariables Disables name mangling of variables [boolean] 189 | --preserveAll Disables all mangling [boolean] 190 | --nomangle Disables name mangling for a set of keywords [array] 191 | --includesOnly Only process include files [boolean] 192 | --help Show help [boolean] 193 | ``` 194 | 195 | ## Compiling From Source 196 | The source code is written in TypeScript. The build script supports two commands: 197 | 198 | * `npm run build` - Compiles the output to `build/` 199 | * `npm run test` - Runs unit tests 200 | 201 | ## License 202 | Copyright (c) 2018-2022 [Leo C. Singleton IV](https://www.leosingleton.com/). 203 | This software is licensed under the MIT License. 204 | -------------------------------------------------------------------------------- /bin/webpack-glsl-minify: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../build/cli.js') 3 | -------------------------------------------------------------------------------- /bin/wgm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../build/cli.js') 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-glsl-minify", 3 | "version": "1.5.0", 4 | "author": "Leo C. Singleton IV ", 5 | "description": "GLSL Loader, Preprocessor, and Minifier for Webpack", 6 | "homepage": "https://github.com/leosingleton/webpack-glsl-minify", 7 | "license": "MIT", 8 | "private": false, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/leosingleton/webpack-glsl-minify.git" 12 | }, 13 | "dependencies": { 14 | "glob": "^7.2.0", 15 | "yargs": "^17.3.1" 16 | }, 17 | "devDependencies": { 18 | "@istanbuljs/nyc-config-typescript": "^1.0.2", 19 | "@types/glob": "^7.2.0", 20 | "@types/jasmine-node": "^1.14.36", 21 | "@types/webpack": "^5.28.0", 22 | "@types/yargs": "^17.0.8", 23 | "@typescript-eslint/eslint-plugin": "^5.12.0", 24 | "@typescript-eslint/parser": "^5.12.0", 25 | "eslint": "^8.9.0", 26 | "eslint-config-leosingleton": "github:leosingleton/eslint-config-leosingleton", 27 | "eslint-plugin-github": "^4.3.5", 28 | "eslint-plugin-import": "^2.25.4", 29 | "eslint-plugin-jsdoc": "^37.9.2", 30 | "jasmine": "^4.0.2", 31 | "nyc": "^15.1.0", 32 | "source-map-support": "^0.5.21", 33 | "ts-node": "^10.5.0", 34 | "typescript": "^4.5.5", 35 | "webpack": "^5.69.1", 36 | "webpack-cli": "^4.9.2" 37 | }, 38 | "scripts": { 39 | "build": "npx tsc", 40 | "clean": "rm -rf ./build", 41 | "lint": "npx eslint \"**/*.ts\" \"**/*.js\"", 42 | "test": "npx nyc -r cobertura -r text ./node_modules/jasmine/bin/jasmine.js ./build/__tests__/*.test.js --output=build/" 43 | }, 44 | "files": [ 45 | "bin/*", 46 | "build/*.d.ts", 47 | "build/*.js" 48 | ], 49 | "bin": { 50 | "webpack-glsl-minify": "bin/webpack-glsl-minify", 51 | "wgm": "bin/wgm" 52 | }, 53 | "main": "build/index.js", 54 | "types": "build/index.d.ts" 55 | } 56 | -------------------------------------------------------------------------------- /src/__tests__/cases.test.ts: -------------------------------------------------------------------------------- 1 | // src/__tests__/cases.test.ts 2 | // Copyright 2018-2020 Leo C. Singleton IV 3 | 4 | import { GlslMinifyInternal } from './internals'; 5 | import { readFile } from '../fsAsync'; 6 | import * as path from 'path'; 7 | import glob = require('glob'); 8 | 9 | describe('Execute GLSL test cases against expected outputs', () => { 10 | // Discover test cases. They exist as .glsl files with the expected output stored as a .json file of the same name. 11 | const testPath = path.resolve(__dirname, '../../tests/cases/*.json'); 12 | const matches = glob.sync(testPath); 13 | 14 | for (const expectedFile of matches) { 15 | // From the file path of the expected output, calculate the file path of the source file 16 | const basename = path.basename(expectedFile); 17 | const extname = path.extname(basename); 18 | const sourceFile = expectedFile.substr(0, expectedFile.length - extname.length) + '.glsl'; 19 | 20 | it(basename, async () => { 21 | // Read and minify the source file 22 | const glsl = new GlslMinifyInternal(); 23 | const output = await glsl.readAndExecuteFile(sourceFile); 24 | 25 | // Read and compare the output against the expected output 26 | const expected = JSON.parse(await readFile(expectedFile, 'utf-8')); 27 | expect(output).toEqual(expected); 28 | }); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /src/__tests__/cli.test.ts: -------------------------------------------------------------------------------- 1 | // src/__tests__/cli.test.ts 2 | // Copyright 2018-2020 Leo C. Singleton IV 3 | 4 | import * as fsAsync from '../fsAsync'; 5 | import * as path from 'path'; 6 | 7 | async function runCli(inputFile: string, params: string): Promise { 8 | // Launch CLI app 9 | const workingDir = path.resolve(__dirname, '../..'); // git repo root 10 | const outDir = 'build/__tests__/cli'; 11 | const cmdline = `npx nyc --silent --no-clean bin/webpack-glsl-minify ${inputFile} ${params} --outDir ${outDir}`; 12 | await fsAsync.exec(cmdline, workingDir); 13 | 14 | // Read the output file produced by Webpack and return it 15 | const outputFile = path.resolve(__dirname, workingDir, outDir, inputFile + '.js'); 16 | const data = await fsAsync.readFile(outputFile, 'utf-8'); 17 | return data; 18 | } 19 | 20 | describe('CLI app', () => { 21 | it('Executes with default options', async () => { 22 | const output = await runCli('tests/webpack/glsl/test.glsl', ''); 23 | expect(output).toContain('gl_FragColor=vec4'); 24 | expect(output.indexOf('u_cb;')).toEqual(-1); // Uniforms are minified by default 25 | expect(output.indexOf('mat3 transform')).toEqual(-1); // Variables are minified by default 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/__tests__/internals.ts: -------------------------------------------------------------------------------- 1 | // src/__tests__/internals.ts 2 | // Copyright 2018-2020 Leo C. Singleton IV 3 | 4 | import { GlslFile, GlslMinify, GlslMinifyOptions, GlslShader, TokenType } from '../minify'; 5 | import { nodeReadFile, nodeDirname } from '../node'; 6 | 7 | /** Wrapper around GlslMinify to expose protected members to unit tests */ 8 | export class GlslMinifyInternal extends GlslMinify { 9 | public constructor(options: GlslMinifyOptions = {}) { 10 | super(options, nodeReadFile, nodeDirname); 11 | } 12 | 13 | public async processIncludes(content: GlslFile): Promise { 14 | return super.processIncludes(content); 15 | } 16 | 17 | public preprocessPass1(content: string): string { 18 | return super.preprocessPass1(content); 19 | } 20 | 21 | public preprocessPass2(content: string): string { 22 | return super.preprocessPass2(content); 23 | } 24 | 25 | public static getTokenType(token: string): TokenType { 26 | return super.getTokenType(token); 27 | } 28 | 29 | public readFile = nodeReadFile; 30 | 31 | public async readAndExecuteFile(filename: string): Promise { 32 | const file = await this.readFile(filename); 33 | return this.executeFile(file); 34 | } 35 | 36 | public async readAndTrimFile(filename: string): Promise { 37 | const file = await this.readFile(filename); 38 | return GlslMinifyInternal.trim(file.contents); 39 | } 40 | 41 | /** Removes whitespace and empty lines from a string */ 42 | public static trim(content: string): string { 43 | const lines = content.split('\n'); 44 | 45 | let output = ''; 46 | for (const line of lines) { 47 | if (line.length > 0) { 48 | if (output !== '') { 49 | output += '\n'; 50 | } 51 | output += line; 52 | } 53 | } 54 | 55 | return output; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/__tests__/minify.test.ts: -------------------------------------------------------------------------------- 1 | // src/__tests__/minify.test.ts 2 | // Copyright 2018-2020 Leo C. Singleton IV 3 | 4 | import { GlslMinifyInternal } from './internals'; 5 | import { GlslMinify, GlslVariableMap, TokenMap, TokenType } from '../minify'; 6 | 7 | /** Counts the number of properties in the `consts` or `uniforms` output of the minifier */ 8 | function countProperties(map: GlslVariableMap): number { 9 | return Object.getOwnPropertyNames(map).length; 10 | } 11 | 12 | describe('GlslMinify', () => { 13 | it('Calculates unique minified names', () => { 14 | expect(TokenMap.getMinifiedName(0)).toEqual('A'); 15 | expect(TokenMap.getMinifiedName(1)).toEqual('B'); 16 | expect(TokenMap.getMinifiedName(25)).toEqual('Z'); 17 | expect(TokenMap.getMinifiedName(26)).toEqual('a'); 18 | expect(TokenMap.getMinifiedName(27)).toEqual('b'); 19 | expect(TokenMap.getMinifiedName(52)).toEqual('AA'); 20 | expect(TokenMap.getMinifiedName(104)).toEqual('BA'); 21 | }); 22 | 23 | it('Allocates minified names', () => { 24 | const map = new TokenMap({}); 25 | expect(map.minifyToken('token1')).toEqual('A'); 26 | expect(map.minifyToken('token2')).toEqual('B'); 27 | expect(map.minifyToken('token1')).toEqual('A'); 28 | expect(map.minifyToken('gl_FragColor')).toEqual('gl_FragColor'); 29 | expect(map.minifyToken('int')).toEqual('int'); 30 | expect(map.minifyToken('token3')).toEqual('C'); 31 | }); 32 | 33 | it('Determines token type', () => { 34 | expect(GlslMinifyInternal.getTokenType('attribute')).toEqual(TokenType.ttAttribute); 35 | expect(GlslMinifyInternal.getTokenType('.')).toEqual(TokenType.ttDot); 36 | expect(GlslMinifyInternal.getTokenType('12345')).toEqual(TokenType.ttNumeric); 37 | expect(GlslMinifyInternal.getTokenType('+=')).toEqual(TokenType.ttOperator); 38 | expect(GlslMinifyInternal.getTokenType('#version 150')).toEqual(TokenType.ttPreprocessor); 39 | expect(GlslMinifyInternal.getTokenType('gl_FragColor')).toEqual(TokenType.ttToken); 40 | expect(GlslMinifyInternal.getTokenType('uniform')).toEqual(TokenType.ttUniform); 41 | expect(GlslMinifyInternal.getTokenType('varying')).toEqual(TokenType.ttVarying); 42 | }); 43 | 44 | it('Detects all valid number formats', () => { 45 | expect(GlslMinifyInternal.getTokenType('176')).toEqual(TokenType.ttNumeric); // Base 10 46 | expect(GlslMinifyInternal.getTokenType('0176')).toEqual(TokenType.ttNumeric); // Base 8 47 | expect(GlslMinifyInternal.getTokenType('0x176')).toEqual(TokenType.ttNumeric); // Base 16 48 | expect(GlslMinifyInternal.getTokenType('176u')).toEqual(TokenType.ttNumeric); // Unsigned Base 10 49 | expect(GlslMinifyInternal.getTokenType('176U')).toEqual(TokenType.ttNumeric); // Unsigned Base 10 50 | }); 51 | 52 | it('Stringifies an object', () => { 53 | const myobj = { 54 | prop1: 'hello', 55 | prop2: { 56 | vals: [0, 1, 2], 57 | num: 1.23 58 | } 59 | }; 60 | 61 | const str = GlslMinify.stringify(myobj); 62 | const expected = '{prop1:"hello",prop2:{vals:[0,1,2],num:1.23}}'; 63 | expect(str).toEqual(expected); 64 | }); 65 | 66 | it('Supports preserveUniforms', async () => { 67 | const glsl = new GlslMinifyInternal({ preserveUniforms: true }); 68 | const output = await glsl.readAndExecuteFile('tests/cases/commas-uniforms.glsl'); 69 | 70 | // Uniforms are not minified 71 | expect(output.uniforms.uRed.variableName).toEqual('uRed'); 72 | expect(output.uniforms.uRed.variableType).toEqual('float'); 73 | expect(output.uniforms.uGreen.variableName).toEqual('uGreen'); 74 | expect(output.uniforms.uGreen.variableType).toEqual('float'); 75 | expect(output.uniforms.uBlue.variableName).toEqual('uBlue'); 76 | expect(output.uniforms.uBlue.variableType).toEqual('float'); 77 | expect(countProperties(output.consts)).toEqual(0); 78 | expect(countProperties(output.uniforms)).toEqual(3); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /src/__tests__/preprocessor.test.ts: -------------------------------------------------------------------------------- 1 | // src/__tests__/preprocessor.test.ts 2 | // Copyright 2018-2020 Leo C. Singleton IV 3 | 4 | import { GlslMinifyInternal } from './internals'; 5 | 6 | describe('GlslMinify Preprocessor', () => { 7 | it('Reads files', async () => { 8 | const glsl = new GlslMinifyInternal(); 9 | const file = await glsl.readFile('tests/cases/hello.glsl'); 10 | expect(file.contents).toEqual('// Hello World!'); 11 | }); 12 | 13 | it('Preprocessor removes comments', async () => { 14 | const glsl = new GlslMinifyInternal(); 15 | const file = await glsl.readFile('tests/cases/comments.glsl'); 16 | const output = glsl.preprocessPass1(await glsl.processIncludes(file)); 17 | expect(output).toEqual('void main() {}\n'); 18 | }); 19 | 20 | it('Preprocessor handles @include directives', async () => { 21 | const glsl = new GlslMinifyInternal(); 22 | const file = await glsl.readFile('tests/cases/include1.glsl'); 23 | const output = await glsl.processIncludes(file); 24 | 25 | // Expect an additional newline for the // comment after the @include 26 | expect(output).toEqual( 27 | 'void /* C-style comment */main() {/**\n * Multi-line comments too\n */}// C++-style comment'); 28 | }); 29 | 30 | it('Preprocessor handles @define directives', async () => { 31 | const glsl = new GlslMinifyInternal(); 32 | const file = await glsl.readFile('tests/cases/define.glsl'); 33 | const output = GlslMinifyInternal.trim(glsl.preprocessPass2(file.contents)); 34 | 35 | const expected = 'void main() { gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); }'; 36 | expect(output.length).toEqual(expected.length); 37 | expect(output).toEqual(expected); 38 | }); 39 | 40 | it('Preprocessor handles @const directives', async () => { 41 | const glsl = new GlslMinifyInternal(); 42 | const file = await glsl.readFile('tests/cases/const.glsl'); 43 | const output = GlslMinifyInternal.trim(glsl.preprocessPass2(file.contents)); 44 | 45 | const expected = 'const float color=$0$;\nvoid main() { gl_FragColor = vec4(vec3(color), 1.0); }'; 46 | expect(output.length).toEqual(expected.length); 47 | expect(output).toEqual(expected); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/__tests__/webpack.test.ts: -------------------------------------------------------------------------------- 1 | // src/__tests__/webpack.test.ts 2 | // Copyright 2018-2022 Leo C. Singleton IV 3 | 4 | import * as fsAsync from '../fsAsync'; 5 | import * as path from 'path'; 6 | 7 | async function runWebpack(configFile: string): Promise { 8 | // Find test directory 9 | const workingDir = path.resolve(__dirname, '../../tests/webpack'); 10 | if (!(await fsAsync.exists(workingDir))) { 11 | throw new Error(`Failed to find test directory: ${workingDir}`); 12 | } 13 | 14 | // Launch Webpack 15 | await fsAsync.exec(`npx nyc --silent --no-clean npx webpack --mode=production --config=${configFile}`, workingDir); 16 | 17 | // Run the output JavaScript file in NodeJS to ensure it is valid 18 | const node = process.argv0; 19 | const outputJS = path.resolve(workingDir, '../../build/__tests__/webpack/index.js'); 20 | await fsAsync.exec(`${node} ${outputJS}`, workingDir); 21 | 22 | // Read the output file produced by Webpack and return it 23 | const outputFile = path.resolve(workingDir, '../../build/__tests__/webpack/index.js'); 24 | const data = await fsAsync.readFile(outputFile, 'utf-8'); 25 | return data; 26 | } 27 | 28 | describe('Webpack Loader', () => { 29 | it('Executes with default options', async () => { 30 | const output = await runWebpack('webpack.test1.js'); 31 | expect(output).toContain('gl_FragColor=vec4'); 32 | expect(output.indexOf('u_cb;')).toEqual(-1); // Uniforms are minified by default 33 | expect(output.indexOf('mat3 transform')).toEqual(-1); // Variables are minified by default 34 | expect(output).toContain('#version'); // #version directives are preserved by default 35 | }, 15000); 36 | 37 | it('Executes with mangling disabled', async () => { 38 | const output = await runWebpack('webpack.test2.js'); 39 | expect(output).toContain('gl_FragColor=vec4'); 40 | expect(output).toContain('u_cb;'); 41 | expect(output).toContain('mat3 transform'); 42 | }, 15000); 43 | 44 | it('Executes with output = source', async () => { 45 | const output = await runWebpack('webpack.test3.js'); 46 | expect(output).toContain('gl_FragColor=vec4'); 47 | expect(output).toContain('u_cb;'); // Uniform mangling is disables 48 | expect(output.indexOf('mat3 transform')).toEqual(-1); // Variables are still minified 49 | }, 15000); 50 | 51 | it('Executes with specific nomangle keywords', async () => { 52 | const output = await runWebpack('webpack.test4.js'); 53 | expect(output).toContain('gl_FragColor=vec4'); 54 | expect(output.indexOf('u_cb;')).toEqual(-1); // Uniforms are minified by default 55 | expect(output).toContain('u_cr;'); // u_cr is in the nomangle list 56 | expect(output.indexOf('mat3 transform')).toEqual(-1); // Variables are still minified 57 | expect(output).toContain('vec3 offset'); // offset is in the nomangle list 58 | }, 15000); 59 | 60 | it('Strips #version directives', async () => { 61 | const output = await runWebpack('webpack.test5.js'); 62 | expect(output).toContain('gl_FragColor=vec4'); 63 | expect(output.indexOf('u_cb;')).toEqual(-1); // Uniforms are minified by default 64 | expect(output.indexOf('mat3 transform')).toEqual(-1); // Variables are minified by default 65 | expect(output.indexOf('#version')).toEqual(-1); // #version directives are stripped in this test case 66 | }, 15000); 67 | 68 | it('Executes with preserveAll', async () => { 69 | const output = await runWebpack('webpack.test6.js'); 70 | expect(output).toContain('gl_FragColor=vec4'); 71 | expect(output).toContain('u_cb;'); 72 | expect(output).toContain('mat3 transform'); 73 | expect(output).toContain('YCbCr2RGB('); // Function names are not mangled with preserveAll 74 | }, 15000); 75 | 76 | it('Outputs an ES module', async () => { 77 | const output = await runWebpack('webpack.test7.js'); 78 | expect(output).toContain('gl_FragColor=vec4'); 79 | expect(output.indexOf('u_cb;')).toEqual(-1); // Uniforms are minified by default 80 | expect(output.indexOf('mat3 transform')).toEqual(-1); // Variables are minified by default 81 | expect(output).toContain('#version'); // #version directives are preserved by default 82 | }, 15000); 83 | 84 | it('Executes with include only and keeps content', async () => { 85 | const output = await runWebpack('webpack.test8.js'); 86 | expect(output).toContain('// Add the texture coordinates from the vertex shader'); 87 | expect(output).toContain('* Convert YCbCr colorspace to RGB'); 88 | expect(output).toContain('gl_FragColor = vec4'); // Perserve whitespace 89 | expect(output).toContain('u_cb;'); // Uniform mangling is disabled 90 | expect(output).toContain('mat3 transform'); // Variables are not minified 91 | expect(output).toContain('#version'); // #version directives are preserved by default 92 | }, 15000); 93 | }); 94 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | // src/cli.ts 2 | // Copyright 2018-2020 Leo C. Singleton IV 3 | // Entry point when running "npx webpack-glsl-minify ..." on the command line 4 | 5 | import * as fsAsync from './fsAsync'; 6 | import { GlslMinify, GlslOutputFormat } from './minify'; 7 | import { nodeDirname, nodeReadFile } from './node'; 8 | import glob = require('glob'); 9 | import * as path from 'path'; 10 | import * as yargs from 'yargs'; 11 | 12 | interface Arguments { 13 | files: string | string[]; 14 | ext: string; 15 | outDir?: string; 16 | output?: GlslOutputFormat; 17 | esModule?: boolean; 18 | stripVersion?: boolean; 19 | preserveDefines?: boolean; 20 | preserveUniforms?: boolean; 21 | preserveVariables?: boolean; 22 | preserveAll?: boolean; 23 | nomangle?: string[]; 24 | includesOnly?: boolean; 25 | } 26 | 27 | const outputFormats: GlslOutputFormat[] = [ 'object', 'source', 'sourceOnly' ]; 28 | 29 | // Validate and parse command line arguments. yargs exits and displays help on invalid arguments. 30 | const argv = yargs 31 | .command('$0 [options]', 'Minifies one or more GLSL files. Input files may be specified in glob syntax.') 32 | .demandCommand() 33 | .options({ 34 | ext: { 35 | alias: 'e', 36 | default: '.js', 37 | describe: 'Extension for output files', 38 | type: 'string' 39 | }, 40 | outDir: { 41 | alias: 'o', 42 | describe: 'Output base directory. By default, files are output to the same directory as the input .glsl file.', 43 | type: 'string' 44 | }, 45 | output: { 46 | choices: outputFormats, 47 | describe: 'Output format', 48 | default: 'object' 49 | }, 50 | esModule: { 51 | describe: 'Uses ES modules syntax. Applies to the "object" and "source" output formats.', 52 | type: 'boolean' 53 | }, 54 | stripVersion: { 55 | describe: 'Strips any #version directives', 56 | type: 'boolean' 57 | }, 58 | preserveDefines: { 59 | describe: 'Disables name mangling of #defines', 60 | type: 'boolean' 61 | }, 62 | preserveUniforms: { 63 | describe: 'Disables name mangling of uniforms', 64 | type: 'boolean' 65 | }, 66 | preserveVariables: { 67 | describe: 'Disables name mangling of variables', 68 | type: 'boolean' 69 | }, 70 | preserveAll: { 71 | describe: 'Disables all mangling', 72 | type: 'boolean' 73 | }, 74 | nomangle: { 75 | describe: 'Disables name mangling for a set of keywords', 76 | type: 'array' 77 | }, 78 | includesOnly: { 79 | describe: 'Only processes include directives', 80 | type: 'boolean' 81 | } 82 | }) 83 | .help() 84 | .argv as any as Arguments; 85 | 86 | // Create minifier 87 | const glsl = new GlslMinify({ 88 | output: argv.output, 89 | esModule: argv.esModule, 90 | stripVersion: argv.stripVersion, 91 | preserveDefines: argv.preserveDefines, 92 | preserveUniforms: argv.preserveUniforms, 93 | preserveVariables: argv.preserveVariables, 94 | nomangle: argv.nomangle, 95 | includesOnly: argv.includesOnly, 96 | }, nodeReadFile, nodeDirname); 97 | 98 | // Process input files 99 | if (Array.isArray(argv.files)) { 100 | for (const pattern of argv.files) { 101 | processGlob(pattern); 102 | } 103 | } else { 104 | processGlob(argv.files); 105 | } 106 | 107 | function processGlob(pattern: string): void { 108 | glob(pattern, (err, matches) => { 109 | if (err) { 110 | console.log(err); 111 | process.exit(-1); 112 | } 113 | 114 | for (const file of matches) { 115 | processFile(file).then(() => {}, err => { 116 | console.log(err); 117 | process.exit(-1); 118 | }); 119 | } 120 | }); 121 | } 122 | 123 | async function processFile(file: string): Promise { 124 | // Determine the output file path 125 | const filename = path.basename(file); 126 | const outfile = path.resolve(argv.outDir || '', path.dirname(file), filename + argv.ext); 127 | console.log(`${file} => ${outfile}`); 128 | 129 | // Read the input file and minify it 130 | const rawGlsl = await nodeReadFile(file); 131 | const minifiedGlsl = await glsl.executeFileAndStringify(rawGlsl); 132 | 133 | // Write output file, ensuring output directory exists first 134 | await fsAsync.mkdirp(outfile); 135 | await fsAsync.writeFile(outfile, minifiedGlsl); 136 | } 137 | -------------------------------------------------------------------------------- /src/fsAsync.ts: -------------------------------------------------------------------------------- 1 | // src/fsAsync.ts 2 | // Copyright 2018-2020 Leo C. Singleton IV 3 | // Wrappers around Node's filesystem functions to make them use async patterns 4 | 5 | import * as cp from 'child_process'; 6 | import * as fs from 'fs'; 7 | import * as path from 'path'; 8 | 9 | export function exists(p: fs.PathLike): Promise { 10 | return new Promise(resolve => { 11 | fs.exists(p, exists => { 12 | resolve(exists); 13 | }); 14 | }); 15 | } 16 | 17 | export function readFile(p: fs.PathLike, options: BufferEncoding): Promise { 18 | return new Promise((resolve, reject) => { 19 | fs.readFile(p, options, (err, data) => { 20 | if (err) { 21 | reject(err); 22 | } 23 | resolve(data); 24 | }); 25 | }); 26 | } 27 | 28 | export function writeFile(p: fs.PathLike, data: any): Promise { 29 | return new Promise((resolve, reject) => { 30 | fs.writeFile(p, data, err => { 31 | if (err) { 32 | reject(err); 33 | } 34 | resolve(); 35 | }); 36 | }); 37 | } 38 | 39 | export function mkdir(p: fs.PathLike): Promise { 40 | return new Promise((resolve, reject) => { 41 | fs.mkdir(p, err => { 42 | if (err) { 43 | reject(err); 44 | } 45 | resolve(); 46 | }); 47 | }); 48 | } 49 | 50 | export async function mkdirp(p: string): Promise { 51 | const dirname = path.dirname(p); 52 | if (await exists(dirname)) { 53 | return; 54 | } 55 | 56 | await mkdirp(dirname); 57 | await mkdir(dirname); 58 | } 59 | 60 | export function exec(command: string, cwd: string): Promise { 61 | return new Promise((resolve, reject) => { 62 | cp.exec(command, { cwd }, (err, stdout, stderr) => { 63 | if (err) { 64 | reject(`${err}\n${stdout}\n${stderr}`); 65 | } 66 | resolve(); 67 | }); 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * webpack-glsl-minify 3 | * Copyright 2018-2020 Leo C. Singleton IV 4 | * Released under the MIT license 5 | */ 6 | 7 | import { webpackLoader } from './webpack'; 8 | export default webpackLoader; 9 | 10 | export { GlslVariable, GlslVariableMap, GlslShader } from './minify'; 11 | -------------------------------------------------------------------------------- /src/minify.ts: -------------------------------------------------------------------------------- 1 | // src/minify.ts 2 | // Copyright 2018-2020 Leo C. Singleton IV 3 | 4 | export interface GlslVariable { 5 | /** Variable type, e.g. 'vec3' or 'float' */ 6 | variableType: string; 7 | 8 | /** Minified variable name */ 9 | variableName: string; 10 | } 11 | 12 | /** Map of original unminified names to their minified details */ 13 | export interface GlslVariableMap { [original: string]: GlslVariable } 14 | 15 | /** A minified shader output by webpack-glsl-minify */ 16 | export interface GlslShader { 17 | /** Minified GLSL code */ 18 | sourceCode: string; 19 | 20 | /** Uniform variable names. Maps the original unminified name to its minified details. */ 21 | uniforms: GlslVariableMap; 22 | 23 | /** Constant variables. Maps the orignal unminified name to the substitution value. */ 24 | consts: GlslVariableMap; 25 | } 26 | 27 | export interface GlslFile { 28 | /** Full path of the file (for resolving further @include directives) */ 29 | path?: string; 30 | 31 | /** Unparsed file contents */ 32 | contents: string; 33 | } 34 | 35 | const glslTypes = [ 36 | // Basic types 37 | 'bool', 'double', 'float', 'int', 'uint', 38 | 39 | // Vector types 40 | 'vec2', 'vec3', 'vec4', 41 | 'bvec2', 'bvec3', 'bvec4', 42 | 'dvec2', 'dvec3', 'dvec4', 43 | 'ivec2', 'ivec3', 'ivec4', 44 | 'uvec2', 'uvec3', 'uvec4', 45 | 46 | // Matrix types 47 | 'mat2', 'mat2x2', 'mat2x3', 'mat2x4', 48 | 'mat3', 'mat3x2', 'mat3x3', 'mat3x4', 49 | 'mat4', 'mat4x2', 'mat4x3', 'mat4x4', 50 | 51 | // Sampler types 52 | 'sampler1D', 'sampler2D', 'sampler3D', 'samplerCube', 'sampler2DRect', 53 | 'isampler1D', 'isampler2D', 'isampler3D', 'isamplerCube', 'isampler2DRect', 54 | 'usampler1D', 'usampler2D', 'usampler3D', 'usamplerCube', 'usampler2DRect', 55 | 56 | 'sampler1DArray', 'sampler2DArray', 'samplerCubeArray', 57 | 'isampler1DArray', 'isampler2DArray', 'isamplerCubeArray', 58 | 'usampler1DArray', 'usampler2DArray', 'usamplerCubeArray', 59 | 60 | 'samplerBuffer', 'sampler2DMS', 'sampler2DMSArray', 61 | 'isamplerBuffer', 'isampler2DMS', 'isampler2DMSArray', 62 | 'usamplerBuffer', 'usampler2DMS', 'usampler2DMSArray', 63 | 64 | 'sampler1DShadow', 'sampler2DShadow', 'samplerCubeShadow', 'sampler2DRectShadow', 'sampler1DArrayShadow', 65 | 'sampler2DArrayShadow', 'samplerCubeArrayShadow', 66 | 67 | 'void' 68 | ]; 69 | 70 | const glslTypeQualifiers = [ 71 | // Other type-related keywords 72 | 'attribute', 'const', 'invariant', 'struct', 'uniform', 'varying', 73 | 74 | // Precision keywords 75 | 'highp', 'lowp', 'mediump', 'precision', 76 | 77 | // Input/output keywords 78 | 'in', 'inout', 'out', 79 | 80 | // Interpolation qualifiers 81 | 'flat', 'noperspective', 'smooth', 'centroid', 'sample', 82 | 83 | // Memory qualifiers 84 | 'coherent', 'volatile', 'restrict', 'readonly', 'writeonly', 85 | 86 | // Layout qualifiers 87 | 'layout', 'location' 88 | ]; 89 | 90 | const glslConstantValues = [ 91 | 'false', 'true', 92 | 93 | // Built-in macros 94 | '__FILE__', '__LINE__', '__VERSION__', 'GL_ES', 'GL_FRAGMENT_PRECISION_HIGH' 95 | ]; 96 | 97 | const glslControlKeywords = [ 98 | // Control keywords 99 | 'break', 'continue', 'do', 'else', 'for', 'if', 'main', 'return', 'while', 100 | 101 | 'discard' 102 | ]; 103 | 104 | const glslBuiltinFunctions = [ 105 | // Trig functions 106 | 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atanh', 'cos', 'cosh', 'degrees', 'radians', 'sin', 'sinh', 'tan', 'tanh', 107 | 108 | // Exponents and logarithms 109 | 'exp', 'exp2', 'inversesqrt', 'log', 'log2', 'pow', 'sqrt', 110 | 111 | // Clamping and modulus-related funcions 112 | 'abs', 'ceil', 'clamp', 'floor', 'fract', 'max', 'min', 'mod', 'modf', 'round', 'roundEven', 'sign', 'trunc', 113 | 114 | // Floating point functions 115 | 'isinf', 'isnan', 116 | 117 | // Boolean functions 118 | 'all', 'any', 'equal','greaterThan', 'greaterThanEqual', 'lessThan', 'lessThanEqual', 'not', 'notEqual', 119 | 120 | // Vector functions 121 | 'cross', 'distance', 'dot', 'faceforward', 'length', 'outerProduct', 'normalize', 'reflect', 'refract', 122 | 123 | // Matrix functions 124 | 'determinant', 'inverse', 'matrixCompMult', 125 | 126 | // Interpolation functions 127 | 'mix', 'step', 'smoothstep', 128 | 129 | // Texture functions 130 | 'texture2D', 'texture2DProj', 'textureCube', 'textureSize', 131 | 132 | // Noise functions 133 | 'noise1', 'noise2', 'noise3', 'noise4', 134 | 135 | // Derivative functions 136 | 'dFdx', 'dFdxCoarse', 'dFdxFine', 137 | 'dFdy', 'dFdyCoarse', 'dFdyFine', 138 | 'fwidth', 'fwidthCoarse', 'fwidthFine' 139 | ]; 140 | 141 | /** List of GLSL reserved keywords to avoid mangling. We automatically include any gl_ variables. */ 142 | const glslReservedKeywords = [].concat(glslTypes, glslTypeQualifiers, glslConstantValues, glslControlKeywords, 143 | glslBuiltinFunctions); 144 | 145 | // Function to test whether a given string is a swizzle identifier 146 | const isSwizzle = (token: string) => 147 | token.match(/^[rgba]{1,4}$/) !== null 148 | || token.match(/^[xyzw]{1,4}$/) !== null 149 | || token.match(/^[stpq]{1,4}$/) !== null; 150 | 151 | /** 152 | * Helper class to minify tokens and track reserved ones 153 | */ 154 | export class TokenMap { 155 | public constructor(private options: GlslMinifyOptions) { 156 | // GLSL has many reserved keywords. In order to not minify them, we add them to the token map now. 157 | this.reserveKeywords(glslReservedKeywords); 158 | } 159 | 160 | /** 161 | * The underlying token map itself. Although the data type is GlslUniform, it is used for all tokens, not just 162 | * uniforms. The type property of GlslUniform is only set for uniforms, however. 163 | */ 164 | private tokens: GlslVariableMap = {}; 165 | 166 | /** 167 | * Adds keywords to the reserved list to prevent minifying them. 168 | * @param keywords Array of strings containing keywords to preserve 169 | */ 170 | public reserveKeywords(keywords: string[]): void { 171 | for (const keyword of keywords) { 172 | if (!this.tokens[keyword]) { 173 | this.tokens[keyword] = { variableType: undefined, variableName: keyword }; 174 | } 175 | } 176 | } 177 | 178 | /** 179 | * Number of tokens minified. Used to generate unique names. Although we could be more sophisticated, and count 180 | * usage, we simply assign names in order. Few shaders have more than 52 variables (the number of single-letter 181 | * variable names), so simple is good enough. 182 | */ 183 | private minifiedTokenCount = 0; 184 | 185 | /** 186 | * Converts a token number to a name 187 | */ 188 | public static getMinifiedName(tokenCount: number): string { 189 | const num = tokenCount % 52; 190 | const offset = (num < 26) ? (num + 65) : (num + 71); // 65 = 'A'; 71 = ('a' - 26) 191 | const c = String.fromCharCode(offset); 192 | 193 | // For tokens over 52, recursively add characters 194 | const recurse = Math.floor(tokenCount / 52); 195 | return (recurse === 0) ? c : (this.getMinifiedName(recurse - 1) + c); 196 | } 197 | 198 | /** 199 | * Minifies a token 200 | * @param name Token name 201 | * @param uniformType If the token is a uniform, the data type 202 | * @returns Minified token name 203 | */ 204 | public minifyToken(name: string, uniformType?: string): string { 205 | // Check whether the token already has an existing minified value 206 | const existing = this.tokens[name]; 207 | if (existing) { 208 | // In the case of a uniform with mangling explicitly disabled, we may already have an entry from the @nomangle 209 | // directive. But still store the type. 210 | if (uniformType) { 211 | existing.variableType = uniformType; 212 | } 213 | return existing.variableName; 214 | } 215 | 216 | // Mangle the name. Special-case any tokens starting with "gl_". They should never be minified. Likewise, never 217 | // mangle substitution values, which start and end with "$". 218 | let min = name; 219 | if (!this.options.preserveAll && !name.startsWith('gl_') && name.indexOf('$') === -1) { 220 | min = TokenMap.getMinifiedName(this.minifiedTokenCount++); 221 | } 222 | 223 | // Allocate a new value 224 | this.tokens[name] = { 225 | variableName: min, 226 | variableType: uniformType 227 | }; 228 | 229 | return min; 230 | } 231 | 232 | /** 233 | * Returns the uniforms and their associated data types 234 | */ 235 | public getUniforms(): GlslVariableMap { 236 | // Filter only the tokens that have the type field set 237 | const result: GlslVariableMap = {}; 238 | for (const original in this.tokens) { 239 | const token = this.tokens[original]; 240 | if (token.variableType) { 241 | result[original] = token; 242 | } 243 | } 244 | 245 | return result; 246 | } 247 | } 248 | 249 | export enum TokenType { 250 | /** A user-created token such as a custom function or variable name */ 251 | ttToken, 252 | 253 | /** 254 | * A built-in token in the GLSL language, such as a function or reserved keyword. (Note: types and type qualifiers 255 | * are handled specially below) 256 | */ 257 | ttReservedToken, 258 | 259 | /** A variable type: int, float, vec2, etc. */ 260 | ttType, 261 | 262 | /** The attribute keyword */ 263 | ttAttribute, 264 | 265 | /** The uniform keyword */ 266 | ttUniform, 267 | 268 | /** The varying keyword */ 269 | ttVarying, 270 | 271 | /** The layout keyword */ 272 | ttLayout, 273 | 274 | /** The in and out keywords */ 275 | ttInOut, 276 | 277 | /** An operator, excluding parentheses, dots, braces, brackets, and semicolons */ 278 | ttOperator, 279 | 280 | /** ( */ 281 | ttOpenParenthesis, 282 | 283 | /** ) */ 284 | ttCloseParenthesis, 285 | 286 | /** { */ 287 | ttOpenBrace, 288 | 289 | /** } */ 290 | ttCloseBrace, 291 | 292 | /** [ */ 293 | ttOpenBracket, 294 | 295 | /** ] */ 296 | ttCloseBracket, 297 | 298 | /** A semicolon */ 299 | ttSemicolon, 300 | 301 | /** The dot operator. This operator has special meaning in GLSL due to vector swizzle masks. */ 302 | ttDot, 303 | 304 | /** A numeric value */ 305 | ttNumeric, 306 | 307 | /** A GLSL preprocessor directive */ 308 | ttPreprocessor, 309 | 310 | /** Special value used in the parser when there is no token */ 311 | ttNone 312 | } 313 | 314 | /** Implementation of NodeJS's readFile() API. */ 315 | export type ReadFileImpl = (filename: string, directory?: string) => Promise; 316 | 317 | /** Stub implementation of NodeJS's readFile() API to work in browsers and non-NodeJS environments */ 318 | function nullReadFile(_filename: string, _directory?: string): Promise { 319 | return new Promise((_resolve, reject) => { 320 | reject(new Error('Not Supported')); 321 | }); 322 | } 323 | 324 | /** Implementation of NodeJS's dirname() API */ 325 | export type DirnameImpl = (p: string) => string; 326 | 327 | /** Stub implementation of NodeJS's dirname() API to work in browsers and non-NodeJS environments */ 328 | export function nullDirname(_p: string): string { 329 | return undefined; 330 | } 331 | 332 | /** Options for the GLSL shader minifier */ 333 | export interface GlslMinifyOptions { 334 | /** Output format. Default = 'object'. */ 335 | output?: GlslOutputFormat; 336 | 337 | /** Uses ES modules syntax. Applies to the "object" and "source" output formats. Default = false. */ 338 | esModule?: boolean; 339 | 340 | /** Strips any #version directives. Default = false. */ 341 | stripVersion?: boolean; 342 | 343 | /** Disables name mangling of #defines. Default = false. */ 344 | preserveDefines?: boolean; 345 | 346 | /** Disables name mangling of uniforms. Default = false. */ 347 | preserveUniforms?: boolean; 348 | 349 | /** Disables name mangling of variables. Default = false. */ 350 | preserveVariables?: boolean; 351 | 352 | /** Disables all mangling. Default = false. */ 353 | preserveAll?: boolean; 354 | 355 | /** Additional variable names or keywords to explicitly disable name mangling */ 356 | nomangle?: string[]; 357 | 358 | /** Only process includes, leave the rest of the file as-is (for development) Default = false. */ 359 | includesOnly?: boolean; 360 | } 361 | 362 | /** 363 | * Output format. Default is 'object'. 364 | * 365 | * 'object': Outputs a JavaScript file exporting an object. The object contains the source code and map of mangled 366 | * uniforms and consts. 367 | * 368 | * 'source': Outputs a JavaScript file exporting the source code as a string. Automatically disables mangling. 369 | * 370 | * 'sourceOnly': Outputs a GLSL file without the JavaScript wrapper. Automatically disables mangling. Only supported 371 | * in the CLI app, not the Webpack loader. 372 | */ 373 | export type GlslOutputFormat = 'object' | 'source' | 'sourceOnly'; 374 | 375 | /** GLSL shader minifier */ 376 | export class GlslMinify { 377 | 378 | /** List of tokens minified by the parser */ 379 | private tokens: TokenMap; 380 | 381 | /** 382 | * Constructor 383 | * @param options Minifier options. See GlslMinifyOptions for details. 384 | * @param readFile Implementation of NodeJS's readFile() API. Three variations are included with the 385 | * webpack-glsl-minify package: nodeReadFile() for NodeJS apps, webpackReadFile() for the Webpack plugin, and 386 | * nullReadFile() for browsers and other environments that don't support reading files from the local disk. 387 | * @param dirname Implementation of NodeJS's dirname() API. Two variations are included with the webpack-glsl-minify 388 | * package: nodeDirname() for NodeJS and Webpack and nullDirname() for browsers and other environments that don't 389 | * support reading files from the local disk. 390 | */ 391 | public constructor(options?: GlslMinifyOptions, readFile = nullReadFile, dirname = nullDirname) { 392 | // If output type is not object, disable mangling as we have no way of returning the map of the mangled names of 393 | // uniforms. 394 | options = options || {}; 395 | if (options.output && options.output !== 'object') { 396 | options.preserveUniforms = true; 397 | } 398 | 399 | this.options = options; 400 | this.readFile = readFile; 401 | this.dirname = dirname; 402 | this.tokens = new TokenMap(options); 403 | } 404 | 405 | 406 | public execute(content: string): Promise { 407 | const input: GlslFile = { contents: content }; 408 | return this.executeFile(input); 409 | } 410 | 411 | public async executeFile(input: GlslFile): Promise { 412 | // Perform the minification. This takes three separate passes over the input. 413 | let sourceCode = await this.processIncludes(input); 414 | if (!this.options.includesOnly) { 415 | const pass1 = this.preprocessPass1(sourceCode); 416 | const pass2 = this.preprocessPass2(pass1); 417 | sourceCode = this.minifier(pass2); 418 | } 419 | 420 | return { 421 | sourceCode, 422 | uniforms: this.tokens.getUniforms(), 423 | consts: this.constValues 424 | }; 425 | } 426 | 427 | protected async processIncludes(content: GlslFile): Promise { 428 | let output = content.contents; 429 | // Process @include directive 430 | const includeRegex = /@include\s+(.*)/; 431 | while (true) { 432 | // Find the next @include directive 433 | const match = includeRegex.exec(output); 434 | if (!match) { 435 | break; 436 | } 437 | // Remove potential comments that exist in this line. 438 | const includeFilename = JSON.parse(this.preprocessPass1(match[1])); 439 | 440 | // Read the file to include 441 | const currentPath = content.path ? this.dirname(content.path) : undefined; 442 | const includeFile = await this.readFile(includeFilename, currentPath); 443 | 444 | // Parse recursively, as the included file may also have @include directives 445 | const includeContent = await this.processIncludes(includeFile); 446 | 447 | // Replace the @include directive with the file contents 448 | output = output.replace(includeRegex, includeContent); 449 | } 450 | 451 | return output; 452 | } 453 | 454 | /** 455 | * The first pass of the preprocessor removes comments 456 | */ 457 | protected preprocessPass1(content: string): string { 458 | let output = content; 459 | 460 | // Remove carriage returns. Use newlines only. 461 | output = output.replace('\r', ''); 462 | 463 | // Strip any #version directives 464 | if (this.options.stripVersion) { 465 | output = output.replace(/#version.+/, ''); 466 | } 467 | 468 | // Remove C style comments 469 | const cStyleRegex = /\/\*[\s\S]*?\*\//g; 470 | output = output.replace(cStyleRegex, ''); 471 | 472 | // Remove C++ style comments 473 | const cppStyleRegex = /\/\/[^\n]*/g; 474 | output = output.replace(cppStyleRegex, '\n'); 475 | 476 | return output; 477 | } 478 | 479 | private constValues: GlslVariableMap = {}; 480 | 481 | /** 482 | * Substitution values are of the form "$0$" 483 | */ 484 | private substitutionValueCount = 0; 485 | 486 | private assignSubstitionValue(constName: string, constType: string): string { 487 | const substitutionValue = `$${this.substitutionValueCount++}$`; 488 | this.tokens.reserveKeywords([substitutionValue]); 489 | 490 | this.constValues[constName] = { 491 | variableName: substitutionValue, 492 | variableType: constType 493 | }; 494 | 495 | return substitutionValue; 496 | } 497 | 498 | /** 499 | * The second pass of the preprocessor handles nomange and define directives 500 | */ 501 | protected preprocessPass2(content: string): string { 502 | let output = content; 503 | 504 | // Disable name mangling for keywords provided via options 505 | if (this.options.nomangle) { 506 | this.tokens.reserveKeywords(this.options.nomangle); 507 | } 508 | 509 | // Process @nomangle directives 510 | const nomangleRegex = /@nomangle\s+(.*)/; 511 | while (true) { 512 | // Find the next @nomangle directive 513 | const match = nomangleRegex.exec(output); 514 | if (!match) { 515 | break; 516 | } 517 | 518 | // Record the keywords 519 | const keywords = match[1].split(/\s/); 520 | this.tokens.reserveKeywords(keywords); 521 | 522 | // Remove the @nomangle line 523 | output = output.replace(nomangleRegex, ''); 524 | } 525 | 526 | // Process @define directives 527 | const defineRegex = /@define\s+(\S+)\s+(.*)/; 528 | while (true) { 529 | // Find the next @define directive 530 | const match = defineRegex.exec(output); 531 | if (!match) { 532 | break; 533 | } 534 | const defineMacro = match[1]; 535 | const replaceValue = match[2]; 536 | 537 | // Remove the @define line 538 | output = output.replace(defineRegex, ''); 539 | 540 | // Replace all instances of the macro with its value 541 | // 542 | // BUGBUG: We start at the beginning of the file, which means we could do replacements prior to the @define 543 | // directive. This is unlikely to happen in real code but will cause some weird behaviors if it does. 544 | let offset = output.indexOf(defineMacro); 545 | while (offset >= 0 && offset < output.length) { 546 | // Ensure that the macro isn't appearing within a larger token 547 | let nextOffset = offset + defineMacro.length; 548 | let nextChar = output[nextOffset]; 549 | if (/\w/.test(nextChar)) { 550 | // Ignore. Part of a larger token. Begin searching again at the next non-word. 551 | do { 552 | nextChar = output[++nextOffset]; 553 | } while (nextChar && /\w/.test(nextChar)); 554 | offset = nextOffset; 555 | } else { 556 | // Replace 557 | const begin = output.substring(0, offset); 558 | const end = output.substring(nextOffset); 559 | output = begin + replaceValue + end; 560 | offset += replaceValue.length; 561 | } 562 | 563 | // Advance the offset 564 | offset = output.indexOf(defineMacro, offset); 565 | } 566 | } 567 | 568 | // Process @const directives 569 | const constRegex = /@const\s+(.*)/; 570 | while (true) { 571 | // Find the next @const directive 572 | const match = constRegex.exec(output); 573 | if (!match) { 574 | break; 575 | } 576 | 577 | // Parse the tokens 578 | const parts = match[1].split(/\s/); 579 | if (parts.length !== 2) { 580 | throw new Error('@const directives require two parameters'); 581 | } 582 | const constType = parts[0]; 583 | const constName = parts[1]; 584 | 585 | // Assign a substitution value 586 | const substitutionValue = this.assignSubstitionValue(constName, constType); 587 | 588 | // Replace the directive with a constant declaration. Note that `String.replace()` has special treatment of $ in 589 | // the replacement string parameter, so we provide a lambda to the replacement string to disable that. 590 | const newCode = `const ${constType} ${constName}=${substitutionValue};`; 591 | output = output.replace(constRegex, () => newCode); 592 | } 593 | 594 | return output; 595 | } 596 | 597 | /** Determines the token type of a token string */ 598 | protected static getTokenType(token: string): TokenType { 599 | if (token === 'attribute') { 600 | return TokenType.ttAttribute; 601 | } else if (token === 'uniform') { 602 | return TokenType.ttUniform; 603 | } else if (token === 'varying') { 604 | return TokenType.ttVarying; 605 | } else if (token === 'layout') { 606 | return TokenType.ttLayout; 607 | } else if (token === 'in' || token === 'out') { 608 | return TokenType.ttInOut; 609 | } else if (glslTypes.indexOf(token) > -1) { 610 | return TokenType.ttType; 611 | } else if (glslReservedKeywords.indexOf(token) > -1) { 612 | return TokenType.ttReservedToken; 613 | } else if (token === ';') { 614 | return TokenType.ttSemicolon; 615 | } else if (token === '.') { 616 | return TokenType.ttDot; 617 | } else if (token === '(') { 618 | return TokenType.ttOpenParenthesis; 619 | } else if (token === ')') { 620 | return TokenType.ttCloseParenthesis; 621 | } else if (token === '{') { 622 | return TokenType.ttOpenBrace; 623 | } else if (token === '}') { 624 | return TokenType.ttCloseBrace; 625 | } else if (token === '[') { 626 | return TokenType.ttOpenBracket; 627 | } else if (token === ']') { 628 | return TokenType.ttCloseBracket; 629 | } else if (token[0] === '#') { 630 | return TokenType.ttPreprocessor; 631 | } else if (/[0-9]/.test(token[0])) { 632 | return TokenType.ttNumeric; 633 | } else if (/\w/.test(token[0])) { 634 | return TokenType.ttToken; 635 | } else { 636 | return TokenType.ttOperator; 637 | } 638 | } 639 | 640 | /** 641 | * The final pass consists of the actual minifier itself 642 | */ 643 | protected minifier(content: string): string { 644 | // Unlike the previous passes, on this one, we start with an empty output and build it up 645 | let output = ''; 646 | 647 | // The token regex looks for any of four items: 648 | // 1) An alphanumeric token (\w+), which may include underscores (or $ for substitution values) 649 | // 2) An operators (non-alphanumeric, non-dot). May consist of 2 characters if it ends in an =, e.g. <=, +=, or ==. 650 | // 3) A dot operator 651 | // 4) GLSL preprocessor directive beginning with # 652 | const tokenRegex = /[\w$]+|[^\s\w#.]=?|\.|#.*/g; 653 | 654 | // 655 | // Minifying uses a simple state machine which tracks the following four state variables: 656 | // 657 | 658 | /** Set when the state machine is within a uniform, attribute, varying, or global in/out declaration */ 659 | let declarationType = TokenType.ttNone; 660 | 661 | /** The last seen variable type (one of the glslTypes string values) */ 662 | let variableType: string; 663 | 664 | /** Set when the previous token may require whitespace separating it from the next token */ 665 | let mayRequireTrailingSpace = false; 666 | 667 | /** Counts the number of open parenthesis ( minus matching close ) */ 668 | let parenthesesDepth = 0; 669 | 670 | /** Counts the number of open brace { minus matching close } */ 671 | let bracesDepth = 0; 672 | 673 | /** Counts the number of open bracket [ minus matching close ] */ 674 | let bracketsDepth = 0; 675 | 676 | /** 677 | * Indicates the current statement has a `layout(location = X)` qualifer. In this case, linking is handled by 678 | * position, not name, meaning we can mangle the variable being defined. 679 | */ 680 | let hasLayout = false; 681 | 682 | /** Token type of the immediately preceding token */ 683 | let prevType = TokenType.ttNone; 684 | 685 | let match: string[]; 686 | while ((match = tokenRegex.exec(content))) { 687 | const token = match[0]; 688 | const type = GlslMinify.getTokenType(token); 689 | 690 | /** Helper function to concatenate `token` to `output` */ 691 | function writeToken(mayRequirePrecedingSpace: boolean, outputToken = token): void { 692 | if (mayRequirePrecedingSpace && mayRequireTrailingSpace) { 693 | output += ' '; 694 | } 695 | output += outputToken; 696 | } 697 | 698 | // Update depth counters 699 | switch (type) { 700 | case TokenType.ttOpenBrace: 701 | bracesDepth++; 702 | break; 703 | 704 | case TokenType.ttOpenBracket: 705 | bracketsDepth++; 706 | break; 707 | 708 | case TokenType.ttOpenParenthesis: 709 | parenthesesDepth++; 710 | break; 711 | 712 | case TokenType.ttCloseBrace: 713 | if (--bracesDepth < 0) { 714 | throw Error('Invalid GLSL. Unmatched close brace.'); 715 | } 716 | break; 717 | 718 | case TokenType.ttCloseBracket: 719 | if (--bracketsDepth < 0) { 720 | throw Error('Invalid GLSL. Unmatched close bracket.'); 721 | } 722 | break; 723 | 724 | case TokenType.ttCloseParenthesis: 725 | if (--parenthesesDepth < 0) { 726 | throw Error('Invalid GLSL. Unmatched close parenthesis.'); 727 | } 728 | break; 729 | } 730 | 731 | switch (type) { 732 | case TokenType.ttPreprocessor: { 733 | // Preprocessor directives must always begin on a new line 734 | if (output !== '' && !output.endsWith('\n')) { 735 | output += '\n'; 736 | } 737 | 738 | // Special case for #define: we want to minify the value being defined 739 | const defineRegex = /#define\s(\w+)\b(.*)/; 740 | const subMatch = defineRegex.exec(token); 741 | if (subMatch) { 742 | if (this.options.preserveDefines) { 743 | this.tokens.reserveKeywords([subMatch[1]]); 744 | } 745 | const minToken = this.tokens.minifyToken(subMatch[1]); 746 | if (subMatch[2]?.[0] === '(') { // This is a function 747 | output += '#define ' + minToken + this.minifier(subMatch[2]) + '\n'; 748 | } else { 749 | output += '#define ' + minToken + ' ' + subMatch[2] + '\n'; 750 | } 751 | break; 752 | } 753 | 754 | // Preprocessor directives are special in that they require the newline 755 | output += token + '\n'; 756 | 757 | // They also end any variable declaration 758 | declarationType = TokenType.ttNone; 759 | variableType = undefined; 760 | mayRequireTrailingSpace = false; 761 | break; 762 | } 763 | 764 | case TokenType.ttNumeric: { 765 | // Special case for numerics: we can omit a zero following a dot (e.g. "1." is the same as "1.0") in GLSL 766 | if (token === '0' && prevType === TokenType.ttDot) { 767 | break; 768 | } 769 | 770 | writeToken(true); 771 | 772 | mayRequireTrailingSpace = true; 773 | break; 774 | } 775 | 776 | case TokenType.ttSemicolon: { 777 | writeToken(false); 778 | 779 | // A semicolon ends a variable declaration 780 | declarationType = TokenType.ttNone; 781 | variableType = undefined; 782 | hasLayout = false; 783 | mayRequireTrailingSpace = false; 784 | break; 785 | } 786 | 787 | case TokenType.ttOpenBrace: 788 | case TokenType.ttOpenBracket: 789 | case TokenType.ttOpenParenthesis: 790 | case TokenType.ttCloseBrace: 791 | case TokenType.ttCloseBracket: 792 | case TokenType.ttCloseParenthesis: 793 | case TokenType.ttOperator: 794 | case TokenType.ttDot: { 795 | writeToken(false); 796 | 797 | mayRequireTrailingSpace = false; 798 | break; 799 | } 800 | 801 | case TokenType.ttInOut: 802 | case TokenType.ttAttribute: 803 | case TokenType.ttUniform: 804 | case TokenType.ttVarying: { 805 | writeToken(true); 806 | 807 | if (parenthesesDepth === 0 && bracesDepth === 0) { 808 | declarationType = type; 809 | } else { 810 | declarationType = TokenType.ttNone; 811 | } 812 | 813 | mayRequireTrailingSpace = true; 814 | break; 815 | } 816 | 817 | case TokenType.ttType: { 818 | writeToken(true); 819 | 820 | variableType = token; 821 | mayRequireTrailingSpace = true; 822 | break; 823 | } 824 | 825 | case TokenType.ttLayout: { 826 | writeToken(true); 827 | 828 | hasLayout = true; 829 | mayRequireTrailingSpace = true; 830 | break; 831 | } 832 | 833 | case TokenType.ttReservedToken: { 834 | writeToken(true); 835 | 836 | mayRequireTrailingSpace = true; 837 | break; 838 | } 839 | 840 | case TokenType.ttToken: { 841 | // Special case: a token following a dot is a swizzle mask. Leave it as-is. 842 | if (prevType === TokenType.ttDot && isSwizzle(token)) { 843 | writeToken(false); 844 | mayRequireTrailingSpace = true; 845 | break; 846 | } 847 | 848 | // Another special case: if the token follows a bracket, it is an array size not a variable/uniform 849 | // declaration, e.g. `uniform vec3 u[ARRAY_SIZE]`. 850 | let realDeclarationType = declarationType; 851 | if (bracketsDepth > 0) { 852 | realDeclarationType = TokenType.ttNone; 853 | } 854 | 855 | // Another special case: variable declarations inside parentheses are parameters and can safely be mangled. 856 | if (parenthesesDepth > 0) { 857 | realDeclarationType = TokenType.ttNone; 858 | } 859 | 860 | // Another special case: variable declarations inside braces are either locals, struct or interface block 861 | // members, and can safely be mangled. 862 | if (bracesDepth > 0) { 863 | realDeclarationType = TokenType.ttNone; 864 | } 865 | 866 | switch (realDeclarationType) { 867 | case TokenType.ttAttribute: 868 | case TokenType.ttVarying: 869 | case TokenType.ttInOut: 870 | // For attribute, varying, and in/out declarations, turn off minification. Unless the statement has a 871 | // layout qualifier, in which case binding is done by position instead of name, so we can safely mangle 872 | // the variable. 873 | if (!hasLayout) { 874 | this.tokens.reserveKeywords([token]); 875 | } 876 | writeToken(true, this.tokens.minifyToken(token)); 877 | break; 878 | 879 | case TokenType.ttUniform: 880 | if (this.options.preserveUniforms) { 881 | this.tokens.reserveKeywords([token]); 882 | } 883 | writeToken(true, this.tokens.minifyToken(token, variableType)); 884 | break; 885 | 886 | default: 887 | if (this.options.preserveVariables) { 888 | this.tokens.reserveKeywords([token]); 889 | } 890 | writeToken(true, this.tokens.minifyToken(token)); 891 | break; 892 | } 893 | 894 | mayRequireTrailingSpace = true; 895 | break; 896 | } 897 | } 898 | 899 | // Advance to the next token 900 | prevType = type; 901 | } 902 | 903 | return output; 904 | } 905 | 906 | public executeAndStringify(content: string): Promise { 907 | const input: GlslFile = { contents: content }; 908 | return this.executeFileAndStringify(input); 909 | } 910 | 911 | public async executeFileAndStringify(input: GlslFile): Promise { 912 | const program = await this.executeFile(input); 913 | const esModule = this.options.esModule; 914 | 915 | switch (this.options.output) { 916 | case 'sourceOnly': 917 | return program.sourceCode; 918 | 919 | case 'source': 920 | return `${esModule ? 'export default' : 'module.exports ='} ${GlslMinify.stringify(program.sourceCode)}`; 921 | 922 | case 'object': 923 | default: 924 | return `${esModule ? 'export default' : 'module.exports ='} ${GlslMinify.stringify(program)}`; 925 | } 926 | } 927 | 928 | /** Similar to JSON.stringify(), except without double-quotes around property names */ 929 | public static stringify(obj: any): string { 930 | if (Array.isArray(obj)) { 931 | let output = '['; 932 | let isFirst = true; 933 | for (const value of obj) { 934 | if (!isFirst) { 935 | output += ','; 936 | } 937 | output += this.stringify(value); 938 | isFirst = false; 939 | } 940 | output += ']'; 941 | return output; 942 | } else if (typeof(obj) === 'object') { 943 | let output = '{'; 944 | let isFirst = true; 945 | for (const prop in obj) { 946 | const value = obj[prop]; 947 | if (!isFirst) { 948 | output += ','; 949 | } 950 | output += prop + ':' + this.stringify(value); 951 | isFirst = false; 952 | } 953 | output += '}'; 954 | return output; 955 | } else { 956 | return JSON.stringify(obj); 957 | } 958 | } 959 | 960 | protected options: GlslMinifyOptions; 961 | protected readFile: ReadFileImpl; 962 | protected dirname: DirnameImpl; 963 | } 964 | -------------------------------------------------------------------------------- /src/node.ts: -------------------------------------------------------------------------------- 1 | // src/node.ts 2 | // Copyright 2018-2020 Leo C. Singleton IV 3 | 4 | import * as fsAsync from './fsAsync'; 5 | import { GlslFile } from './minify'; 6 | import * as path from 'path'; 7 | 8 | /** Implementation of ReadFileImpl for NodeJS */ 9 | export async function nodeReadFile(filename: string, directory?: string): Promise { 10 | // Resolve the full file path 11 | const filePath = path.resolve(directory || '', filename); 12 | 13 | // Read the file 14 | const data = await fsAsync.readFile(filePath, 'utf-8'); 15 | return { path: filePath, contents: data }; 16 | } 17 | 18 | /** Implementation of DirnameImpl for NodeJS and Webpack */ 19 | export function nodeDirname(p: string): string { 20 | return path.dirname(p); 21 | } 22 | -------------------------------------------------------------------------------- /src/webpack.ts: -------------------------------------------------------------------------------- 1 | // src/webpack.ts 2 | // Copyright 2018-2022 Leo C. Singleton IV 3 | 4 | import { GlslMinify, GlslMinifyOptions, GlslFile } from './minify'; 5 | 6 | import { readFile } from 'fs'; 7 | import { LoaderContext } from 'webpack'; 8 | import { nodeDirname } from './node'; 9 | 10 | /** Implementation of readFile for Webpack loaders */ 11 | export function webpackReadFile(loader: LoaderContext, filename: string, directory?: string): 12 | Promise { 13 | return new Promise((resolve, reject) => { 14 | // If no directory was provided, use the root GLSL file being included 15 | directory = directory || loader.context; 16 | 17 | // Resolve the file path 18 | loader.resolve(directory, filename, (err: Error, path: string) => { 19 | if (err) { 20 | return reject(err); 21 | } 22 | 23 | loader.addDependency(path); 24 | readFile(path, 'utf-8', (err, data) => { 25 | if (!err) { 26 | // Success 27 | resolve({ path, contents: data }); 28 | } else { 29 | reject(err); 30 | } 31 | }); 32 | }); 33 | }); 34 | } 35 | 36 | export async function webpackLoader(content: string): Promise { 37 | const loader = this as LoaderContext; 38 | const callback = loader.async(); 39 | const options = loader.getOptions() as GlslMinifyOptions; 40 | 41 | try { 42 | const glsl = new GlslMinify(options, (filename, directory) => webpackReadFile(loader, filename, directory), 43 | nodeDirname); 44 | const code = await glsl.executeAndStringify(content); 45 | 46 | callback(null, code); 47 | } catch (err) { 48 | callback(err); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/cases/commas-uniforms.glsl: -------------------------------------------------------------------------------- 1 | // Instantiate multiple uniforms on a single line, comma-separated 2 | uniform float uRed, uGreen, uBlue; 3 | 4 | void main() 5 | { 6 | gl_FragColor = vec4(uRed, uGreen, uBlue, 1.0); 7 | } 8 | -------------------------------------------------------------------------------- /tests/cases/commas-uniforms.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceCode": "uniform float A,B,C;void main(){gl_FragColor=vec4(A,B,C,1.);}", 3 | "consts": {}, 4 | "uniforms": { 5 | "uRed": { 6 | "variableName": "A", 7 | "variableType": "float" 8 | }, 9 | "uGreen": { 10 | "variableName": "B", 11 | "variableType": "float" 12 | }, 13 | "uBlue": { 14 | "variableName": "C", 15 | "variableType": "float" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /tests/cases/commas-variables.glsl: -------------------------------------------------------------------------------- 1 | void main() 2 | { 3 | // Multiple variables can be defined on a single line, separated by commas 4 | float red, green, blue = 0.5, alpha = 1.0; 5 | 6 | // Multiple variables can be initialized on a single line, separated by commas 7 | red = 0.5, blue = 0.5; 8 | 9 | gl_FragColor = vec4(red, green, blue, alpha); 10 | } 11 | -------------------------------------------------------------------------------- /tests/cases/commas-variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceCode": "void main(){float A,B,C=0.5,D=1.;A=0.5,C=0.5;gl_FragColor=vec4(A,B,C,D);}", 3 | "consts": {}, 4 | "uniforms": {} 5 | } -------------------------------------------------------------------------------- /tests/cases/comments.glsl: -------------------------------------------------------------------------------- 1 | void /* C-style comment */main() {/** 2 | * Multi-line comments too 3 | */}// C++-style comment -------------------------------------------------------------------------------- /tests/cases/comments.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceCode": "void main(){}", 3 | "consts": {}, 4 | "uniforms": {} 5 | } -------------------------------------------------------------------------------- /tests/cases/complex-include.glsl: -------------------------------------------------------------------------------- 1 | 2 | vec3 RGB2YUV(in vec3 color) 3 | { 4 | // Constants are the same as JPEG encoding (from Wikipedia on YCbCr) 5 | mat3 transform = mat3( 6 | 0.299, 0.587, 0.114, 7 | -0.168736, -0.331264, 0.5, 8 | 0.5, -0.418668, -0.081312); 9 | vec3 offset = vec3(0, 0.5, 0.5); 10 | 11 | return color * transform + offset; 12 | } 13 | 14 | vec3 YUV2RGB(in vec3 color) 15 | { 16 | // Constants are the same as JPEG encoding (from Wikipedia on YCbCr) 17 | vec3 offset = vec3(0, -0.5, -0.5); 18 | mat3 transform = mat3( 19 | 1.0, 0.0, 1.402, 20 | 1.0, -0.344136, -0.714136, 21 | 1.0, 1.772, 0.0); 22 | 23 | return (color + offset) * transform; 24 | } 25 | 26 | vec3 tex(in sampler2D sampler, in vec2 fragCoord, in vec3 resolution) 27 | { 28 | vec2 uv = fragCoord / resolution.xy; 29 | vec3 col = texture2D(sampler, uv).rgb; 30 | return RGB2YUV(col); 31 | } 32 | -------------------------------------------------------------------------------- /tests/cases/complex.glsl: -------------------------------------------------------------------------------- 1 | #version 100 2 | 3 | uniform vec3 iResolution; 4 | uniform sampler2D iChannel0; 5 | 6 | #define RADIUS 20 7 | 8 | @include "complex-include.glsl" 9 | 10 | // Input is in YUV format 11 | 12 | void mainImage( out vec4 fragColor, in vec2 fragCoord ) 13 | { 14 | float minB = 1.0; 15 | float maxB = 0.0; 16 | for (int x = -RADIUS; x <= RADIUS; x++) { 17 | for (int y = -RADIUS; y <= RADIUS; y++) { 18 | vec2 xy = vec2(x, y); 19 | vec3 c = tex(iChannel0, fragCoord + xy, iResolution); 20 | 21 | float scoreX = 1.0 - (float(abs(x)) / float(RADIUS)); 22 | float scoreY = 1.0 - (float(abs(y)) / float(RADIUS)); 23 | scoreX = smoothstep(-1., 1., scoreX); 24 | scoreY = smoothstep(-1., 1., scoreX); 25 | float score = scoreX * scoreY; 26 | 27 | maxB = max(maxB, c.x * score); 28 | minB = min(minB, c.x * score); 29 | } 30 | } 31 | //vec3 col = vec3(minB, 0.5, 0.5); 32 | 33 | vec3 orig = tex(iChannel0, fragCoord, iResolution); 34 | 35 | minB = min(minB, maxB - 0.5); 36 | //minB = maxB - 0.5; 37 | 38 | float b = smoothstep(minB, maxB, orig.x); 39 | vec3 col = vec3(b, orig.yz); 40 | 41 | // Whiten whites 42 | col.yz = mix(col.yz, vec2(0.5), smoothstep(0.9, 1.0, col.x)); 43 | 44 | 45 | //col = orig; 46 | 47 | // Output to screen 48 | fragColor = vec4(YUV2RGB(col), 1.0); 49 | } 50 | -------------------------------------------------------------------------------- /tests/cases/complex.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceCode": "#version 100\nuniform vec3 A;uniform sampler2D B;\n#define C 20\nvec3 D(in vec3 E){mat3 F=mat3(0.299,0.587,0.114,-0.168736,-0.331264,0.5,0.5,-0.418668,-0.081312);vec3 G=vec3(0,0.5,0.5);return E*F+G;}vec3 H(in vec3 E){vec3 G=vec3(0,-0.5,-0.5);mat3 F=mat3(1.,0.,1.402,1.,-0.344136,-0.714136,1.,1.772,0.);return(E+G)*F;}vec3 I(in sampler2D J,in vec2 K,in vec3 L){vec2 M=K/L.xy;vec3 N=texture2D(J,M).rgb;return D(N);}void O(out vec4 P,in vec2 K){float Q=1.;float R=0.;for(int S=-C;S<=C;S++){for(int T=-C;T<=C;T++){vec2 U=vec2(S,T);vec3 V=I(B,K+U,A);float W=1.-(float(abs(S))/float(C));float X=1.-(float(abs(T))/float(C));W=smoothstep(-1.,1.,W);X=smoothstep(-1.,1.,W);float Y=W*X;R=max(R,V.x*Y);Q=min(Q,V.x*Y);}}vec3 Z=I(B,K,A);Q=min(Q,R-0.5);float a=smoothstep(Q,R,Z.x);vec3 N=vec3(a,Z.yz);N.yz=mix(N.yz,vec2(0.5),smoothstep(0.9,1.,N.x));P=vec4(H(N),1.);}", 3 | "consts": {}, 4 | "uniforms": { 5 | "iResolution": { 6 | "variableName": "A", 7 | "variableType": "vec3" 8 | }, 9 | "iChannel0": { 10 | "variableName": "B", 11 | "variableType": "sampler2D" 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /tests/cases/const-multiple.glsl: -------------------------------------------------------------------------------- 1 | @const int sizeOfSliceInsideAtlas 2 | @const int zSlicesCount 3 | @const int zSlicesNormalTexCount -------------------------------------------------------------------------------- /tests/cases/const-multiple.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceCode": "const int A=$0$;const int B=$1$;const int C=$2$;", 3 | "consts": { 4 | "sizeOfSliceInsideAtlas": { 5 | "variableName": "$0$", 6 | "variableType": "int" 7 | }, 8 | "zSlicesCount": { 9 | "variableName": "$1$", 10 | "variableType": "int" 11 | }, 12 | "zSlicesNormalTexCount": { 13 | "variableName": "$2$", 14 | "variableType": "int" 15 | } 16 | }, 17 | "uniforms": {} 18 | } -------------------------------------------------------------------------------- /tests/cases/const.glsl: -------------------------------------------------------------------------------- 1 | @const float color 2 | void main() { gl_FragColor = vec4(vec3(color), 1.0); } -------------------------------------------------------------------------------- /tests/cases/const.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceCode": "const float A=$0$;void main(){gl_FragColor=vec4(vec3(A),1.);}", 3 | "consts": { 4 | "color": { 5 | "variableName": "$0$", 6 | "variableType": "float" 7 | } 8 | }, 9 | "uniforms": {} 10 | } -------------------------------------------------------------------------------- /tests/cases/define.glsl: -------------------------------------------------------------------------------- 1 | @define MY_OUT gl_FragColor 2 | @define MY_OUT_RESULT void 3 | @define MY_OUTFN main 4 | @define MY_VEC4 vec4 5 | @define MY_VAL 1.0 6 | 7 | MY_OUT_RESULT MY_OUTFN() { MY_OUT = MY_VEC4(MY_VAL, MY_VAL, MY_VAL, MY_VAL); } -------------------------------------------------------------------------------- /tests/cases/define.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceCode": "void main(){gl_FragColor=vec4(1.,1.,1.,1.);}", 3 | "consts": {}, 4 | "uniforms": {} 5 | } -------------------------------------------------------------------------------- /tests/cases/fragment.glsl: -------------------------------------------------------------------------------- 1 | #version 100 2 | 3 | precision mediump float; 4 | 5 | // Add the texture coordinates from the vertex shader 6 | varying vec2 v_texCoord; 7 | 8 | uniform sampler2D u_y; 9 | uniform sampler2D u_cb; 10 | uniform sampler2D u_cr; 11 | 12 | /** 13 | * Convert YCbCr colorspace to RGB 14 | */ 15 | vec3 YCbCr2RGB(in vec3 color) 16 | { 17 | vec3 offset = vec3(0, -0.5, -0.5); 18 | mat3 transform = mat3( 19 | 1.0, 0.0, 1.402, 20 | 1.0, -0.344136, -0.714136, 21 | 1.0, 1.772, 0.0); 22 | 23 | return (color + offset) * transform; 24 | } 25 | 26 | void main() 27 | { 28 | vec3 y = texture2D(u_y, v_texCoord).rgb; 29 | vec3 cb = texture2D(u_cb, v_texCoord).rgb; 30 | vec3 cr = texture2D(u_cr, v_texCoord).rgb; 31 | 32 | vec3 in0 = vec3(y.x, cb.x, cr.x); 33 | gl_FragColor = vec4(YCbCr2RGB(in0), 1.0); 34 | } 35 | -------------------------------------------------------------------------------- /tests/cases/fragment.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceCode": "#version 100\nprecision mediump float;varying vec2 v_texCoord;uniform sampler2D A;uniform sampler2D B;uniform sampler2D C;vec3 D(in vec3 E){vec3 F=vec3(0,-0.5,-0.5);mat3 G=mat3(1.,0.,1.402,1.,-0.344136,-0.714136,1.,1.772,0.);return(E+F)*G;}void main(){vec3 H=texture2D(A,v_texCoord).rgb;vec3 I=texture2D(B,v_texCoord).rgb;vec3 J=texture2D(C,v_texCoord).rgb;vec3 K=vec3(H.x,I.x,J.x);gl_FragColor=vec4(D(K),1.);}", 3 | "consts": {}, 4 | "uniforms": { 5 | "u_y": { 6 | "variableName": "A", 7 | "variableType": "sampler2D" 8 | }, 9 | "u_cb": { 10 | "variableName": "B", 11 | "variableType": "sampler2D" 12 | }, 13 | "u_cr": { 14 | "variableName": "C", 15 | "variableType": "sampler2D" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /tests/cases/hello.glsl: -------------------------------------------------------------------------------- 1 | // Hello World! -------------------------------------------------------------------------------- /tests/cases/hello.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceCode": "", 3 | "consts": {}, 4 | "uniforms": {} 5 | } -------------------------------------------------------------------------------- /tests/cases/include1.glsl: -------------------------------------------------------------------------------- 1 | @include "../cases/comments.glsl" // Add some spaces and a C++ comment for good measure -------------------------------------------------------------------------------- /tests/cases/include1.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceCode": "void main(){}", 3 | "consts": {}, 4 | "uniforms": {} 5 | } -------------------------------------------------------------------------------- /tests/cases/include2.glsl: -------------------------------------------------------------------------------- 1 | uniform vec2 myUniform; 2 | @include "comments.glsl" -------------------------------------------------------------------------------- /tests/cases/include2.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceCode": "uniform vec2 A;void main(){}", 3 | "consts": {}, 4 | "uniforms": { 5 | "myUniform": { 6 | "variableName": "A", 7 | "variableType": "vec2" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /tests/cases/layout1.glsl: -------------------------------------------------------------------------------- 1 | #version 330 es 2 | layout (location = 0) in vec3 aPos; 3 | layout (location = 1) in vec2 aTexCoords; 4 | 5 | uniform mat4 model; 6 | uniform mat4 view; 7 | uniform mat4 projection; 8 | 9 | out VS_OUT 10 | { 11 | vec2 TexCoords; 12 | } vs_out; 13 | 14 | void main() 15 | { 16 | gl_Position = projection * view * model * vec4(aPos, 1.0); 17 | vs_out.TexCoords = aTexCoords; 18 | } 19 | -------------------------------------------------------------------------------- /tests/cases/layout1.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceCode": "#version 330 es\nlayout(location=0)in vec3 A;layout(location=1)in vec2 B;uniform mat4 C;uniform mat4 D;uniform mat4 E;out VS_OUT{vec2 F;}G;void main(){gl_Position=E*D*C*vec4(A,1.);G.F=B;}", 3 | "consts": {}, 4 | "uniforms": { 5 | "model": { 6 | "variableName": "C", 7 | "variableType": "mat4" 8 | }, 9 | "view": { 10 | "variableName": "D", 11 | "variableType": "mat4" 12 | }, 13 | "projection": { 14 | "variableName": "E", 15 | "variableType": "mat4" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /tests/cases/layout2.glsl: -------------------------------------------------------------------------------- 1 | #version 330 core 2 | out vec4 FragColor; 3 | 4 | in VS_OUT 5 | { 6 | vec2 TexCoords; 7 | } fs_in; 8 | 9 | uniform sampler2D texture; 10 | 11 | void main() 12 | { 13 | FragColor = texture2D(texture, fs_in.TexCoords); 14 | } 15 | -------------------------------------------------------------------------------- /tests/cases/layout2.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceCode": "#version 330 core\nout vec4 FragColor;in VS_OUT{vec2 A;}B;uniform sampler2D C;void main(){FragColor=texture2D(C,B.A);}", 3 | "consts": {}, 4 | "uniforms": { 5 | "texture": { 6 | "variableName": "C", 7 | "variableType": "sampler2D" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /tests/cases/precision.glsl: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | uniform lowp float uMultiplier; 3 | varying highp vec2 vTexCoord; 4 | 5 | highp vec4 toVec4(in lowp float c) 6 | { 7 | return vec4(c); 8 | } 9 | 10 | void main() 11 | { 12 | highp vec4 one = toVec4(1.0); 13 | gl_FragColor = one * uMultiplier; 14 | } 15 | -------------------------------------------------------------------------------- /tests/cases/precision.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceCode": "precision mediump float;uniform lowp float A;varying highp vec2 vTexCoord;highp vec4 B(in lowp float C){return vec4(C);}void main(){highp vec4 D=B(1.);gl_FragColor=D*A;}", 3 | "consts": {}, 4 | "uniforms": { 5 | "uMultiplier": { 6 | "variableName": "A", 7 | "variableType": "float" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /tests/cases/uniform-array-of-const-2.glsl: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | varying vec2 vCoord; 3 | 4 | /** Input image */ 5 | uniform sampler2D uInput; 6 | 7 | /** Number of pixels to sample */ 8 | @const int PIXELS 9 | 10 | /** Pixels to sample. The X and Y in the vector are the offset. The Z is the weight. */ 11 | uniform vec3 uPixels[PIXELS]; 12 | 13 | void main() 14 | { 15 | vec4 sum = vec4(0.0); 16 | for (int n = 0; n < PIXELS; n++) { 17 | vec3 pixel = uPixels[n]; 18 | sum += texture2D(uInput, vCoord + pixel.xy) * pixel.z; 19 | } 20 | 21 | gl_FragColor = sum; 22 | } 23 | -------------------------------------------------------------------------------- /tests/cases/uniform-array-of-const-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceCode": "precision mediump float;varying vec2 vCoord;uniform sampler2D A;const int B=$0$;uniform vec3 C[B];void main(){vec4 D=vec4(0.);for(int E=0;E1 to 0->2 9 | vec2 zeroToTwo = a_position * 2.0; 10 | 11 | // Convert from 0->2 to -1->+1 (clipspace) 12 | vec2 clipSpace = zeroToTwo - 1.0; 13 | 14 | // gl_Position is a special variable a vertex shader is responsible for setting 15 | gl_Position = vec4(clipSpace * vec2(1, u_flipY), 0, 1); 16 | 17 | // Pass the texCoord to the fragment shader. The GPU will interpolate this value between points. 18 | v_texCoord = a_position; 19 | } 20 | -------------------------------------------------------------------------------- /tests/cases/vertex.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceCode": "#version 150\nattribute vec2 a_position;uniform float A;varying vec2 v_texCoord;void main(){vec2 B=a_position*2.;vec2 C=B-1.;gl_Position=vec4(C*vec2(1,A),0,1);v_texCoord=a_position;}", 3 | "consts": {}, 4 | "uniforms": { 5 | "u_flipY": { 6 | "variableName": "A", 7 | "variableType": "float" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /tests/webpack/glsl-include/YCbCr.glsl: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert YCbCr colorspace to RGB 3 | */ 4 | vec3 YCbCr2RGB(in vec3 color) 5 | { 6 | vec3 offset = vec3(0, -0.5, -0.5); 7 | mat3 transform = mat3( 8 | 1.0, 0.0, 1.402, 9 | 1.0, -0.344136, -0.714136, 10 | 1.0, 1.772, 0.0); 11 | 12 | return (color + offset) * transform; 13 | } 14 | -------------------------------------------------------------------------------- /tests/webpack/glsl/test.glsl: -------------------------------------------------------------------------------- 1 | #version 100 2 | 3 | precision mediump float; 4 | 5 | @include "../glsl-include/YCbCr.glsl" 6 | 7 | // Add the texture coordinates from the vertex shader 8 | varying vec2 v_texCoord; 9 | 10 | uniform sampler2D u_y; 11 | uniform sampler2D u_cb; 12 | uniform sampler2D u_cr; 13 | 14 | void main() 15 | { 16 | vec3 y = texture2D(u_y, v_texCoord).rgb; 17 | vec3 cb = texture2D(u_cb, v_texCoord).rgb; 18 | vec3 cr = texture2D(u_cr, v_texCoord).rgb; 19 | 20 | vec3 in0 = vec3(y.x, cb.x, cr.x); 21 | gl_FragColor = vec4(YCbCr2RGB(in0), 1.0); 22 | } 23 | -------------------------------------------------------------------------------- /tests/webpack/index-es.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Embed a GLSL file 4 | import glsl from './glsl/test.glsl'; 5 | 6 | // We must do something with the GlslShader object to avoid it getting optimized away 7 | console.log(JSON.stringify(glsl)); 8 | -------------------------------------------------------------------------------- /tests/webpack/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Embed a GLSL file 4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 5 | const glsl = require('./glsl/test.glsl'); 6 | -------------------------------------------------------------------------------- /tests/webpack/webpack.test1.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './index.js', 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.glsl$/, 9 | use: '../../' 10 | } 11 | ] 12 | }, 13 | resolve: { 14 | extensions: [ '.glsl' ] 15 | }, 16 | output: { 17 | path: path.resolve(__dirname, '../../build/__tests__/webpack'), 18 | filename: 'index.js' 19 | } 20 | }; -------------------------------------------------------------------------------- /tests/webpack/webpack.test2.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './index.js', 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.glsl$/, 9 | use: { 10 | loader: '../../', 11 | options: { 12 | preserveDefines: true, 13 | preserveUniforms: true, 14 | preserveVariables: true 15 | } 16 | } 17 | } 18 | ] 19 | }, 20 | resolve: { 21 | extensions: [ '.glsl' ] 22 | }, 23 | output: { 24 | path: path.resolve(__dirname, '../../build/__tests__/webpack'), 25 | filename: 'index.js' 26 | } 27 | }; -------------------------------------------------------------------------------- /tests/webpack/webpack.test3.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './index.js', 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.glsl$/, 9 | use: { 10 | loader: '../../', 11 | options: { 12 | output: 'source' 13 | } 14 | } 15 | } 16 | ] 17 | }, 18 | resolve: { 19 | extensions: [ '.glsl' ] 20 | }, 21 | output: { 22 | path: path.resolve(__dirname, '../../build/__tests__/webpack'), 23 | filename: 'index.js' 24 | } 25 | }; -------------------------------------------------------------------------------- /tests/webpack/webpack.test4.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './index.js', 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.glsl$/, 9 | use: { 10 | loader: '../../', 11 | options: { 12 | nomangle: [ 'u_cr', 'offset' ] 13 | } 14 | } 15 | } 16 | ] 17 | }, 18 | resolve: { 19 | extensions: [ '.glsl' ] 20 | }, 21 | output: { 22 | path: path.resolve(__dirname, '../../build/__tests__/webpack'), 23 | filename: 'index.js' 24 | } 25 | }; -------------------------------------------------------------------------------- /tests/webpack/webpack.test5.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './index.js', 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.glsl$/, 9 | use: { 10 | loader: '../../', 11 | options: { 12 | stripVersion: true 13 | } 14 | } 15 | } 16 | ] 17 | }, 18 | resolve: { 19 | extensions: [ '.glsl' ] 20 | }, 21 | output: { 22 | path: path.resolve(__dirname, '../../build/__tests__/webpack'), 23 | filename: 'index.js' 24 | } 25 | }; -------------------------------------------------------------------------------- /tests/webpack/webpack.test6.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './index.js', 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.glsl$/, 9 | use: { 10 | loader: '../../', 11 | options: { 12 | preserveAll: true 13 | } 14 | } 15 | } 16 | ] 17 | }, 18 | resolve: { 19 | extensions: [ '.glsl' ] 20 | }, 21 | output: { 22 | path: path.resolve(__dirname, '../../build/__tests__/webpack'), 23 | filename: 'index.js' 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /tests/webpack/webpack.test7.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './index-es.js', 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.glsl$/, 9 | use: { 10 | loader: '../../', 11 | options: { 12 | esModule: true 13 | } 14 | } 15 | } 16 | ] 17 | }, 18 | resolve: { 19 | extensions: [ '.glsl' ] 20 | }, 21 | output: { 22 | path: path.resolve(__dirname, '../../build/__tests__/webpack'), 23 | filename: 'index.js' 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /tests/webpack/webpack.test8.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './index-es.js', 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.glsl$/, 9 | use: { 10 | loader: '../../', 11 | options: { 12 | esModule: true, 13 | includesOnly: true, 14 | } 15 | } 16 | } 17 | ] 18 | }, 19 | resolve: { 20 | extensions: [ '.glsl' ] 21 | }, 22 | output: { 23 | path: path.resolve(__dirname, '../../build/__tests__/webpack'), 24 | filename: 'index.js' 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | 4 | // Include all files that aren't part of the normal TypeScript build to keep eslint happy... 5 | "include": [ 6 | "**/__tests__/**/*.ts", 7 | "tests/webpack/**/*.js", 8 | "*.js" 9 | ], 10 | "exclude": ["node_modules"] 11 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "allowJs": false, 7 | "noImplicitAny": true, 8 | "preserveConstEnums": true, 9 | "sourceMap": true, 10 | "target": "es6", 11 | "removeComments": false, 12 | "declaration": true 13 | }, 14 | "include": [ 15 | "src/**/*" 16 | ], 17 | "types": [] 18 | } 19 | --------------------------------------------------------------------------------