├── gulpfile.ts ├── samples ├── oidos-synthclipse │ ├── music.xrns │ ├── config.yml │ ├── shader.preset │ └── shader.stoy ├── audio-shader │ ├── config.yml │ ├── shader.frag │ └── hooks.cpp └── multipass │ ├── config.yml │ ├── shader.frag │ └── hooks.cpp ├── types └── nconf-yaml │ └── index.d.ts ├── .gitignore ├── demo ├── config.yml ├── static │ └── Readme.md └── shader.frag ├── .prettierrc.yml ├── engine ├── server.hpp ├── window.hpp ├── audio-synthesizer-hooks │ ├── oidos.cpp │ ├── realtime.cpp │ └── 4klang.cpp ├── demo.hpp ├── debug.hpp ├── capture-hooks.cpp ├── debug.cpp ├── main-template.cpp └── server.cpp ├── scripts ├── audio-synthesizers │ ├── realtime.ts │ ├── 4klang.ts │ ├── 8klang.ts │ └── oidos.ts ├── variables.ts ├── zip.ts ├── hooks.ts ├── monitor.ts ├── lib.ts ├── capture.ts ├── hot-reload.ts ├── definitions.ts ├── tasks.ts ├── compilation.ts ├── shader-providers │ ├── simple.ts │ └── synthclipse.ts ├── shader-minifiers │ └── shader-minifier.ts ├── context.ts ├── demo.ts └── generate-source-codes.ts ├── tsconfig.json ├── tslint.json ├── package.json └── Readme.md /gulpfile.ts: -------------------------------------------------------------------------------- 1 | export * from './scripts/tasks'; 2 | -------------------------------------------------------------------------------- /samples/oidos-synthclipse/music.xrns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CookieCollective/4k-Demo-Oven/HEAD/samples/oidos-synthclipse/music.xrns -------------------------------------------------------------------------------- /types/nconf-yaml/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'nconf-yaml' { 2 | import { IFormat } from 'nconf'; 3 | const format: IFormat; 4 | export = format; 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /dist/ 3 | /node_modules/ 4 | /dump*.dmp 5 | 6 | config.local.yml 7 | 8 | # Synthclipse 9 | .settings/ 10 | .cproject 11 | .project 12 | -------------------------------------------------------------------------------- /samples/oidos-synthclipse/config.yml: -------------------------------------------------------------------------------- 1 | demo: 2 | audio-synthesizer: 3 | tool: oidos 4 | name: oidos-synthclipse 5 | shader-provider: 6 | tool: synthclipse 7 | -------------------------------------------------------------------------------- /demo/config.yml: -------------------------------------------------------------------------------- 1 | # Use this file to customize your demo. 2 | # Write computer specific settings in config.local.yml, which won't be versioned. 3 | demo: 4 | name: Write Your Demo Title Here 5 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | overrides: 2 | - files: 3 | - "*.js" 4 | - "*.ts" 5 | options: 6 | arrowParens: always 7 | singleQuote: true 8 | trailingComma: es5 9 | useTabs: true 10 | -------------------------------------------------------------------------------- /engine/server.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "demo.hpp" 4 | 5 | struct StartServerOptions 6 | { 7 | int port; 8 | GLint *programs; 9 | }; 10 | 11 | void serverStart(const StartServerOptions &options); 12 | void serverStop(); 13 | 14 | void serverUpdate(); 15 | -------------------------------------------------------------------------------- /demo/static/Readme.md: -------------------------------------------------------------------------------- 1 | # Title 2 | 3 | Authors: 4 | 5 | - **Alice**: code. 6 | - **Bob**: graphics. 7 | 8 | Released at _Release party year_ in competition _compo_. 9 | 10 | Baked with [Cookie Collective's 4k Demo Oven](https://github.com/CookieCollective/4k-Demo-Oven). 11 | -------------------------------------------------------------------------------- /samples/audio-shader/config.yml: -------------------------------------------------------------------------------- 1 | demo: 2 | audio-synthesizer: 3 | tool: none 4 | gl: 5 | constants: 6 | - GL_COLOR_ATTACHMENT0 7 | - GL_FRAMEBUFFER 8 | - GL_RGBA32F 9 | - GL_TEXTURE0 10 | functions: 11 | - glActiveTexture 12 | - glBindFramebuffer 13 | - glFramebufferTexture2D 14 | - glGenFramebuffers 15 | - glUniform1i 16 | name: audio-shader 17 | -------------------------------------------------------------------------------- /engine/window.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | static const PIXELFORMATDESCRIPTOR pfd = { 4 | sizeof(PIXELFORMATDESCRIPTOR), 5 | 1, 6 | PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER, 7 | PFD_TYPE_RGBA, 8 | 32, 9 | 0, 10 | 0, 11 | 0, 12 | 0, 13 | 0, 14 | 0, 15 | 8, 16 | 0, 17 | 0, 18 | 0, 19 | 0, 20 | 0, 21 | 0, 22 | 32, 23 | 0, 24 | 0, 25 | PFD_MAIN_PLANE, 26 | 0, 27 | 0, 28 | 0, 29 | 0, 30 | }; 31 | -------------------------------------------------------------------------------- /engine/audio-synthesizer-hooks/oidos.cpp: -------------------------------------------------------------------------------- 1 | #pragma hook declarations 2 | 3 | #include 4 | 5 | #pragma hook audio_start 6 | 7 | Oidos_FillRandomData(); 8 | Oidos_GenerateMusic(); 9 | Oidos_StartMusic(); 10 | 11 | #pragma hook audio_time 12 | 13 | float time = Oidos_GetPosition() / Oidos_TicksPerSecond; 14 | 15 | #pragma hook audio_duration 16 | 17 | Oidos_MusicLength / Oidos_TicksPerSecond 18 | 19 | #pragma hook audio_is_playing 20 | 21 | Oidos_GetPosition() < 22 | Oidos_MusicLength 23 | -------------------------------------------------------------------------------- /demo/shader.frag: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform float time; 4 | uniform float resolutionWidth; 5 | uniform float resolutionHeight; 6 | 7 | #pragma fragment 0 8 | 9 | // This is the template at https://www.shadertoy.com/new. 10 | void main() { 11 | // Normalized pixel coordinates (from 0 to 1). 12 | vec2 uv = gl_FragCoord.xy / vec2(resolutionWidth, resolutionHeight); 13 | 14 | // Time varying pixel color. 15 | vec3 col = 0.5 + 0.5 * cos(time + uv.xyx + vec3(0, 2, 4)); 16 | 17 | // Output to screen. 18 | gl_FragColor = vec4(col, 1.0); 19 | } 20 | -------------------------------------------------------------------------------- /scripts/audio-synthesizers/realtime.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | import { IAudioSynthesizer, ICompilationDefinition } from '../definitions'; 4 | import { addHooks } from '../hooks'; 5 | 6 | export class RealtimeAudioSynthesizer implements IAudioSynthesizer { 7 | getDefaultConfig() { 8 | return {}; 9 | } 10 | 11 | checkConfig() { 12 | // Fine. 13 | } 14 | 15 | async addToCompilation(compilation: ICompilationDefinition) { 16 | await addHooks( 17 | compilation.cpp.hooks, 18 | join('engine', 'audio-synthesizer-hooks', 'realtime.cpp') 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "lib": ["es2017", "esnext.asynciterable"], 6 | "sourceMap": true, 7 | "outDir": "dist-scripts", 8 | "moduleResolution": "node", 9 | "noFallthroughCasesInSwitch": true, 10 | "noImplicitReturns": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "removeComments": true, 14 | "strict": true, 15 | "strictNullChecks": true, 16 | "typeRoots": ["./types", "./node_modules/@types"] 17 | }, 18 | "include": ["scripts"] 19 | } 20 | -------------------------------------------------------------------------------- /engine/audio-synthesizer-hooks/realtime.cpp: -------------------------------------------------------------------------------- 1 | #pragma hook declarations 2 | 3 | #include 4 | 5 | #pragma hook audio_start 6 | 7 | LARGE_INTEGER frequency; 8 | LARGE_INTEGER startCounter; 9 | 10 | QueryPerformanceFrequency(&frequency); 11 | QueryPerformanceCounter(&startCounter); 12 | 13 | #pragma hook audio_time 14 | 15 | LARGE_INTEGER counter; 16 | QueryPerformanceCounter(&counter); 17 | 18 | float time = (float)(counter.QuadPart - startCounter.QuadPart) / frequency.QuadPart; 19 | 20 | #pragma hook audio_duration 21 | 22 | std::numeric_limits::infinity() 23 | 24 | #pragma hook audio_is_playing 25 | 26 | true 27 | -------------------------------------------------------------------------------- /samples/multipass/config.yml: -------------------------------------------------------------------------------- 1 | demo: 2 | gl: 3 | constants: 4 | - GL_ARRAY_BUFFER 5 | - GL_COLOR_ATTACHMENT0 6 | - GL_FRAMEBUFFER 7 | - GL_RGBA32F 8 | - GL_TEXTURE0 9 | functions: 10 | - glActiveTexture 11 | - glBindBuffer 12 | - glBindFramebuffer 13 | - glBindVertexArray 14 | - glCreateBuffers 15 | - glCreateVertexArrays 16 | - glEnableVertexAttribArray 17 | - glFramebufferTexture2D 18 | - glGenFramebuffers 19 | - glNamedBufferStorage 20 | - glNamedBufferSubData 21 | - glUniform1i 22 | - glVertexAttribPointer 23 | name: multipass 24 | -------------------------------------------------------------------------------- /engine/demo.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../build/demo-data.hpp" 4 | 5 | #ifdef DEBUG 6 | 7 | #define GLEW_STATIC 8 | #include 9 | #include 10 | 11 | #define loadGLFunctions() \ 12 | { \ 13 | GLenum err = glewInit(); \ 14 | if (GLEW_OK != err) \ 15 | { \ 16 | std::cerr << "Error: " << glewGetErrorString(err) << std::endl; \ 17 | return; \ 18 | } \ 19 | } 20 | 21 | #else 22 | 23 | #include "../build/demo-gl.hpp" 24 | 25 | #define loadGLFunctions() \ 26 | for (auto i = 0; i < GL_EXT_FUNCTION_COUNT; ++i) \ 27 | { \ 28 | glExtFunctions[i] = wglGetProcAddress(glExtFunctionNames[i]); \ 29 | } 30 | 31 | #endif 32 | -------------------------------------------------------------------------------- /samples/oidos-synthclipse/shader.preset: -------------------------------------------------------------------------------- 1 | /*! 2 | * 3 | * BPM = <1.0|10.0|200.0> 90.0 4 | * Direction = <0.1, 0.1, 0.1> 0.0, 0.0, 1.0 5 | * DistanceEpsilon = <1.0|0.0|1.0> 0.01 6 | * EquiRectangular = false 7 | * Eye = <1.0, 1.0, 1.0|-50.0, -50.0, -50.0|50.0, 50.0, 50.0> 0.0, 0.0, -2.0 8 | * FOV = <0.1|0.0|2.0> 0.5 9 | * OverrideCamera = false 10 | * SphereColor0 = <0.004, 0.004, 0.004> 0.019607844, 0.22352941, 0.36862746 11 | * SphereColor1 = <0.004, 0.004, 0.004> 0.03137255, 1.0, 0.83137256 12 | * Steps = <1.0|5.0|50.0> 30.0 13 | * Up = <0.1, 0.1, 0.1> 0.0, 1.0, 0.0 14 | * 15 | */ 16 | -------------------------------------------------------------------------------- /scripts/audio-synthesizers/4klang.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from 'nconf'; 2 | import { join } from 'path'; 3 | 4 | import { IAudioSynthesizer, ICompilationDefinition } from '../definitions'; 5 | import { addHooks } from '../hooks'; 6 | 7 | export class VierKlangAudioSynthesizer implements IAudioSynthesizer { 8 | private config: Provider; 9 | 10 | constructor(config: Provider) { 11 | this.config = config; 12 | } 13 | 14 | getDefaultConfig() { 15 | return {}; 16 | } 17 | 18 | checkConfig() { 19 | this.config.required(['tools:4klang']); 20 | } 21 | 22 | async addToCompilation(compilation: ICompilationDefinition) { 23 | await addHooks( 24 | compilation.cpp.hooks, 25 | join('engine', 'audio-synthesizer-hooks', '4klang.cpp') 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /scripts/audio-synthesizers/8klang.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from 'nconf'; 2 | import { join } from 'path'; 3 | 4 | import { IAudioSynthesizer, ICompilationDefinition } from '../definitions'; 5 | import { addHooks } from '../hooks'; 6 | 7 | export class AchtKlangAudioSynthesizer implements IAudioSynthesizer { 8 | private config: Provider; 9 | 10 | constructor(config: Provider) { 11 | this.config = config; 12 | } 13 | 14 | getDefaultConfig() { 15 | return {}; 16 | } 17 | 18 | checkConfig() { 19 | this.config.required(['tools:8klang']); 20 | } 21 | 22 | async addToCompilation(compilation: ICompilationDefinition) { 23 | await addHooks( 24 | compilation.cpp.hooks, 25 | join('engine', 'audio-synthesizer-hooks', '8klang.cpp') 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended", 5 | "tslint-config-prettier", 6 | "tslint-plugin-prettier" 7 | ], 8 | "rules": { 9 | "prettier": true, 10 | "arrow-parens": true, 11 | "member-access": [true, "no-public"], 12 | "member-ordering": [ 13 | true, 14 | { 15 | "order": "fields-first" 16 | } 17 | ], 18 | "no-bitwise": false, 19 | "no-console": false, 20 | "no-empty": [true, "allow-empty-catch"], 21 | "no-var-requires": false, 22 | "no-unused-expression": [true, "allow-fast-null-checks"], 23 | "object-literal-sort-keys": true, 24 | "object-literal-key-quotes": [true, "as-needed"], 25 | "ordered-imports": true, 26 | "variable-name": [true, "allow-leading-underscore"] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /scripts/variables.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IConstVariable, 3 | IRegularVariable, 4 | IUniformVariable, 5 | Variable, 6 | } from './definitions'; 7 | 8 | export function addConstant( 9 | variables: Variable[], 10 | type: string, 11 | name: string, 12 | value: string 13 | ) { 14 | const variable: IConstVariable = { 15 | active: true, 16 | kind: 'const', 17 | name, 18 | type, 19 | value, 20 | }; 21 | variables.push(variable); 22 | } 23 | 24 | export function addRegular(variables: Variable[], type: string, name: string) { 25 | const variable: IRegularVariable = { 26 | active: true, 27 | kind: 'regular', 28 | name, 29 | type, 30 | }; 31 | variables.push(variable); 32 | } 33 | 34 | export function addUniform(variables: Variable[], type: string, name: string) { 35 | const variable: IUniformVariable = { 36 | active: true, 37 | kind: 'uniform', 38 | name, 39 | type, 40 | }; 41 | variables.push(variable); 42 | } 43 | -------------------------------------------------------------------------------- /engine/debug.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifdef DEBUG 4 | 5 | #include 6 | 7 | extern char debugBuffer[4096]; 8 | 9 | extern HWND debugHwnd; 10 | 11 | extern GLint debugFragmentShaders[PASS_COUNT]; 12 | extern GLint debugVertexShaders[PASS_COUNT]; 13 | 14 | void APIENTRY showDebugMessageFromOpenGL( 15 | GLenum source, 16 | GLenum type, 17 | GLuint id, 18 | GLenum severity, 19 | GLsizei length, 20 | const GLchar *message, 21 | const void *userParam); 22 | 23 | void APIENTRY showDebugMessage(const char *message); 24 | 25 | void _checkGLError(const char *filename, int lineNumber); 26 | 27 | // This function can be called after a GL function to check whether an error has been raised. 28 | #define checkGLError() _checkGLError(__FILE__, __LINE__) 29 | 30 | void checkShaderCompilation(GLint shader); 31 | 32 | #else 33 | 34 | #define checkGLError() 35 | #define checkShaderCompilation(shader) 36 | #define showDebugMessage(...) 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /scripts/zip.ts: -------------------------------------------------------------------------------- 1 | import { pathExists, remove, stat } from 'fs-extra'; 2 | import { join, resolve } from 'path'; 3 | 4 | import { IContext } from './definitions'; 5 | import { spawn } from './lib'; 6 | 7 | export async function zip(context: IContext) { 8 | const { config } = context; 9 | const szip = config.get('tools:7z'); 10 | const distDirectory = config.get('paths:dist'); 11 | const zipPath = resolve(distDirectory, config.get('demo:name') + '.zip'); 12 | try { 13 | await remove(zipPath); 14 | 15 | await spawn(szip, ['a', zipPath, resolve(config.get('paths:exe'))], { 16 | cwd: distDirectory, 17 | }); 18 | 19 | const staticDirectory = join(config.get('directory'), 'static'); 20 | if (await pathExists(staticDirectory)) { 21 | const stats = await stat(staticDirectory); 22 | if (stats.isDirectory()) { 23 | await spawn(szip, ['a', zipPath, '*', '-r'], { 24 | cwd: staticDirectory, 25 | }); 26 | } 27 | } 28 | } catch (err) { 29 | console.error('Unable to zip the demo.'); 30 | console.error(err); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /samples/audio-shader/shader.frag: -------------------------------------------------------------------------------- 1 | #version 450 2 | precision mediump float; 3 | 4 | uniform float time; 5 | uniform float resolutionWidth; 6 | uniform float resolutionHeight; 7 | 8 | uniform sampler2D audioTexture; 9 | 10 | #pragma fragment 0 11 | 12 | vec2 generateAudioSample(float t) { 13 | vec2 results = vec2(0, 0); 14 | results.x = sin(t * 1200.0 + 1000.0 * sin(t * 0.2)); 15 | results.y = sin(t * 1500.0 + 1000.0 * sin(t * 0.2)); 16 | results *= sin(t * 400.0 + 20.0 * sin(t * 11.5)); 17 | return results; 18 | } 19 | 20 | void mainF0() { 21 | float t = 2.0 * ((gl_FragCoord.x - 0.5) + (gl_FragCoord.y - 0.5) * 2048.0) / 44100.0; 22 | gl_FragColor.xy = generateAudioSample(t); 23 | gl_FragColor.zw = generateAudioSample(t + (1.0 / 44100.0)); 24 | } 25 | 26 | #pragma fragment 1 27 | 28 | void mainF1() { 29 | vec2 uv = gl_FragCoord.xy / vec2(resolutionWidth, resolutionHeight) - 0.5; 30 | uv.y += sin(uv.x * 100.0 + time) * 0.02; 31 | float t = time * 0.1 + uv.x; 32 | vec4 audioSample = texture(audioTexture, vec2(mod(t, 2048.0), floor(t / 2048.0))); 33 | float diff = abs(audioSample.x - 2.5 * uv.y); 34 | float color = step(diff, 0.1); 35 | gl_FragColor = vec4(color); 36 | } 37 | -------------------------------------------------------------------------------- /scripts/hooks.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'fs-extra'; 2 | 3 | import { IHooks } from './definitions'; 4 | import { forEachMatch } from './lib'; 5 | 6 | export async function addHooks(hooks: IHooks, path: string) { 7 | const contents = await readFile(path, 'utf8'); 8 | 9 | let partStartIndex = 0; 10 | let hookName: string | undefined; 11 | 12 | function takePart(partEndIndex?: number) { 13 | if (hookName) { 14 | const code = contents.substring(partStartIndex, partEndIndex); 15 | 16 | if (hooks[hookName]) { 17 | hooks[hookName] += code; 18 | } else { 19 | hooks[hookName] = code; 20 | } 21 | } 22 | } 23 | 24 | const partsRegExp = /^#pragma\s+hook\s+(.+)\s+$/gm; 25 | forEachMatch(partsRegExp, contents, (match) => { 26 | takePart(match.index); 27 | partStartIndex = partsRegExp.lastIndex; 28 | hookName = match[1]; 29 | }); 30 | 31 | takePart(); 32 | } 33 | 34 | export function replaceHooks(hooks: IHooks, str: string): string { 35 | Object.keys(hooks).forEach((hookName) => { 36 | str = str.replace( 37 | new RegExp(`\\bREPLACE_HOOK_${hookName.toUpperCase()}`, 'g'), 38 | () => replaceHooks(hooks, hooks[hookName]) 39 | ); 40 | }); 41 | return str; 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "4k-demo-oven", 4 | "version": "1.4.0", 5 | "description": "Demo framework.", 6 | "dependencies": { 7 | "@types/fs-extra": "^8.0.0", 8 | "@types/gulp": "^4.0.6", 9 | "@types/nconf": "^0.10.0", 10 | "@types/node-notifier": "^5.4.0", 11 | "@types/request": "^2.48.1", 12 | "@types/request-promise-native": "^1.0.16", 13 | "fs-extra": "^8.1.0", 14 | "gulp": "^4.0.2", 15 | "gulp-cli": "^2.2.0", 16 | "nconf": "^0.10.0", 17 | "nconf-yaml": "^1.0.2", 18 | "node-notifier": "^5.4.0", 19 | "request": "^2.88.0", 20 | "request-promise-native": "^1.0.7", 21 | "ts-node": "^8.3.0", 22 | "typescript": "^3.5.3", 23 | "yargs": "^13.3.0" 24 | }, 25 | "devDependencies": { 26 | "husky": "^3.0.1", 27 | "lint-staged": "^9.2.1", 28 | "prettier": "^1.18.2", 29 | "tslint": "^5.18.0", 30 | "tslint-config-prettier": "^1.18.0", 31 | "tslint-plugin-prettier": "^2.0.1" 32 | }, 33 | "husky": { 34 | "hooks": { 35 | "pre-commit": "lint-staged" 36 | } 37 | }, 38 | "lint-staged": { 39 | "*.ts": [ 40 | "tslint --fix", 41 | "git add" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /engine/capture-hooks.cpp: -------------------------------------------------------------------------------- 1 | #pragma hook declarations 2 | 3 | static int frameNumber; 4 | static HANDLE frameFile; 5 | static DWORD frameBytesWritten; 6 | static char *frameBuffer; 7 | 8 | static char *frameFilename(int n) 9 | { 10 | static char *name = "00000.raw"; 11 | char *ptr = name + 4; 12 | 13 | while (n > 0) 14 | { 15 | *ptr-- = (n - (n / 10) * 10) + '0'; 16 | n /= 10; 17 | } 18 | 19 | return name; 20 | } 21 | 22 | #pragma hook initialize 23 | 24 | frameNumber = 0; 25 | frameBuffer = (char *)HeapAlloc(GetProcessHeap(), 0, resolutionWidth *resolutionHeight * 3 /* RGB8 */); 26 | 27 | #pragma hook capture_time 28 | 29 | float time = (float)frameNumber / CAPTURE_FPS; 30 | 31 | #pragma hook capture_is_playing 32 | 33 | ((float)frameNumber / CAPTURE_FPS) < REPLACE_HOOK_AUDIO_DURATION 34 | 35 | #pragma hook capture_frame 36 | 37 | glReadPixels(0, 0, resolutionWidth, resolutionHeight, GL_RGB, GL_UNSIGNED_BYTE, frameBuffer); 38 | frameFile = CreateFile(frameFilename(frameNumber), GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL); 39 | if (frameFile) 40 | { 41 | WriteFile(frameFile, frameBuffer, resolutionWidth * resolutionHeight * 3, &frameBytesWritten, NULL); 42 | CloseHandle(frameFile); 43 | } 44 | frameNumber++; 45 | -------------------------------------------------------------------------------- /scripts/monitor.ts: -------------------------------------------------------------------------------- 1 | import { stat } from 'fs-extra'; 2 | import * as notifier from 'node-notifier'; 3 | import { dirname, resolve } from 'path'; 4 | 5 | import { IContext } from './definitions'; 6 | import { spawn } from './lib'; 7 | 8 | export class Monitor { 9 | private context: IContext; 10 | private size: number = -1; 11 | 12 | constructor(context: IContext) { 13 | this.context = context; 14 | } 15 | 16 | async run(callback: () => Promise) { 17 | try { 18 | const startTime = Date.now(); 19 | 20 | await callback(); 21 | 22 | const endTime = Date.now(); 23 | 24 | console.log('Build successful.'); 25 | console.log( 26 | `Build duration: ${((endTime - startTime) * 1e-3).toFixed(1)} seconds.` 27 | ); 28 | console.log(`Demo size: ${this.size} bytes.`); 29 | } catch (err) { 30 | console.error('Build failed.'); 31 | console.error(err); 32 | 33 | if (this.context.config.get('notify')) { 34 | notifier.notify({ 35 | message: err.toString(), 36 | title: 'Build failed.', 37 | }); 38 | } 39 | } 40 | } 41 | 42 | async notifySuccess() { 43 | const exePath = this.context.config.get('paths:exe'); 44 | 45 | const stats = await stat(exePath); 46 | this.size = stats.size; 47 | 48 | if (this.context.config.get('notify')) { 49 | notifier.notify({ 50 | message: this.size + ' bytes.', 51 | title: 'Build successful.', 52 | wait: true, 53 | }); 54 | 55 | notifier.on('click', () => { 56 | spawn(resolve(exePath), [], { 57 | cwd: dirname(exePath), 58 | }); 59 | }); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /scripts/lib.ts: -------------------------------------------------------------------------------- 1 | import { 2 | spawn as originalSpawn, 3 | SpawnOptionsWithoutStdio, 4 | } from 'child_process'; 5 | import { emptyDir } from 'fs-extra'; 6 | 7 | import { IContext } from './definitions'; 8 | 9 | export function emptyDirectories(context: IContext) { 10 | return Promise.all( 11 | [context.config.get('paths:build'), context.config.get('paths:dist')].map( 12 | (path) => emptyDir(path) 13 | ) 14 | ); 15 | } 16 | 17 | export function forEachMatch( 18 | regexp: RegExp, 19 | string: string, 20 | callback: (match: RegExpExecArray) => void 21 | ) { 22 | let match: RegExpExecArray | null = regexp.exec(string); 23 | while (match !== null) { 24 | callback(match); 25 | match = regexp.exec(string); 26 | } 27 | } 28 | 29 | export function spawn( 30 | command: string, 31 | args: readonly string[], 32 | options?: SpawnOptionsWithoutStdio 33 | ): Promise { 34 | return new Promise((resolve, reject) => { 35 | console.log(`Executing ${command} ${args.join(' ')}`); 36 | const cp = originalSpawn(command, args, options); 37 | 38 | cp.stdout.on('data', (data) => { 39 | console.log(data.toString()); 40 | }); 41 | 42 | cp.stderr.on('data', (data) => { 43 | console.error(data.toString()); 44 | }); 45 | 46 | cp.on('close', (code, signal) => { 47 | if (code) { 48 | return reject(new Error(command + ' exited with code ' + code + '.')); 49 | } else if (signal) { 50 | return reject( 51 | new Error(command + ' was stopped by signal ' + signal + '.') 52 | ); 53 | } else { 54 | return resolve(); 55 | } 56 | }); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /engine/audio-synthesizer-hooks/4klang.cpp: -------------------------------------------------------------------------------- 1 | #pragma hook declarations 2 | 3 | #include <4klang.h> 4 | 5 | #include 6 | #include 7 | 8 | #pragma data_seg(".audio") 9 | static SAMPLE_TYPE soundBuffer[MAX_SAMPLES * 2]; 10 | static HWAVEOUT waveOut; 11 | 12 | static const WAVEFORMATEX waveFormat = { 13 | #ifdef FLOAT_32BIT 14 | WAVE_FORMAT_IEEE_FLOAT, 15 | #else 16 | WAVE_FORMAT_PCM, 17 | #endif 18 | 2, // channels 19 | SAMPLE_RATE, // samples per sec 20 | SAMPLE_RATE * sizeof(SAMPLE_TYPE) * 2, // bytes per sec 21 | sizeof(SAMPLE_TYPE) * 2, // block alignment; 22 | sizeof(SAMPLE_TYPE) * 8, // bits per sample 23 | 0, // extension not needed 24 | }; 25 | 26 | static WAVEHDR waveHDR = { 27 | (LPSTR)soundBuffer, 28 | MAX_SAMPLES * sizeof(SAMPLE_TYPE) * 2, // MAX_SAMPLES*sizeof(float)*2(stereo) 29 | 0, 30 | 0, 31 | 0, 32 | 0, 33 | 0, 34 | 0, 35 | }; 36 | 37 | static MMTIME mmTime = { 38 | TIME_SAMPLES, 39 | 0, 40 | }; 41 | 42 | #pragma hook audio_start 43 | 44 | CreateThread(0, 0, (LPTHREAD_START_ROUTINE)_4klang_render, soundBuffer, 0, 0); 45 | waveOutOpen(&waveOut, WAVE_MAPPER, &waveFormat, NULL, 0, CALLBACK_NULL); 46 | waveOutPrepareHeader(waveOut, &waveHDR, sizeof(waveHDR)); 47 | waveOutWrite(waveOut, &waveHDR, sizeof(waveHDR)); 48 | 49 | #pragma hook audio_time 50 | 51 | waveOutGetPosition(waveOut, &mmTime, sizeof(MMTIME)); 52 | float time = (float)mmTime.u.sample / (float)SAMPLE_RATE; 53 | 54 | #pragma hook audio_duration 55 | 56 | return MAX_SAMPLES / (float)SAMPLE_RATE; 57 | 58 | #pragma hook audio_is_playing 59 | 60 | mmTime.u.sample < MAX_SAMPLES 61 | -------------------------------------------------------------------------------- /samples/multipass/shader.frag: -------------------------------------------------------------------------------- 1 | #version 450 2 | precision mediump float; 3 | 4 | uniform float time; 5 | uniform float resolutionWidth; 6 | uniform float resolutionHeight; 7 | 8 | uniform sampler2D firstPassTexture; 9 | 10 | const float PI = 3.14;//! replace 11 | 12 | vec4 _gl_Position; 13 | #define gl_Position _gl_Position 14 | 15 | #pragma attributes 16 | 17 | vec3 aPosition; 18 | vec2 aUV; 19 | 20 | #pragma varyings 21 | 22 | vec3 vColor; 23 | 24 | #pragma outputs 25 | 26 | vec4 color; 27 | 28 | #pragma common 29 | 30 | mat2 rot(float a) { float c = cos(a), s = sin(a); return mat2(c, - s, s, c); } 31 | 32 | vec3 curve(float ratio) { 33 | ratio += time; 34 | vec3 position = vec3(0.5 + 0.3 * sin(ratio), 0, 0); 35 | position.xz *= rot(ratio); 36 | position.yz *= rot(ratio * 0.58); 37 | position.yx *= rot(ratio * 1.5); 38 | return position; 39 | } 40 | 41 | #pragma vertex 0 42 | 43 | void mainV0() { 44 | vec3 position = aPosition; 45 | float ratio = aUV.x * 0.5 + position.x * 2.0; 46 | float i = position.x; 47 | position = curve(ratio); 48 | vec3 next = curve(ratio + 0.01); 49 | vec2 y = normalize(next.xy - position.xy); 50 | vec2 x = vec2(y.y, - y.x); 51 | position.xy += x * aUV.y * (0.01 + 0.01 * position.z); 52 | gl_Position = vec4(position.xy, 0.0, 1.0); 53 | vColor = vec3(aUV.xy * 0.5 + 0.5, 0); 54 | } 55 | 56 | #pragma fragment 0 57 | 58 | void mainF0() { 59 | float fade = smoothstep(0.0, 0.5, vColor.x); 60 | fade *= (1.0 - abs(vColor.x * 2.0 - 1.0)); 61 | color = vec4(1, 1, 1, fade); 62 | } 63 | 64 | #pragma fragment 1 65 | 66 | void mainF1() { 67 | vec2 uv = gl_FragCoord.xy / vec2(resolutionWidth, resolutionHeight); 68 | color = 1.0 - texture(firstPassTexture, uv); 69 | } 70 | -------------------------------------------------------------------------------- /scripts/capture.ts: -------------------------------------------------------------------------------- 1 | import { emptyDir } from 'fs-extra'; 2 | import { join, resolve } from 'path'; 3 | 4 | import { IContext } from './definitions'; 5 | import { spawn } from './lib'; 6 | 7 | export async function spawnCapture(context: IContext) { 8 | const { config } = context; 9 | const framesDirectory = config.get('paths:frames'); 10 | 11 | await emptyDir(framesDirectory); 12 | 13 | await spawn(resolve(config.get('paths:exe')), [], { 14 | cwd: framesDirectory, 15 | }); 16 | } 17 | 18 | export async function encode(context: IContext) { 19 | const { config } = context; 20 | const framesDirectory = config.get('paths:frames'); 21 | 22 | const args = [ 23 | '-y', 24 | '-f', 25 | 'image2', 26 | '-r', 27 | config.get('capture:fps'), 28 | '-s', 29 | config.get('capture:width') + 'x' + config.get('capture:height'), 30 | '-pix_fmt', 31 | 'rgb24', 32 | '-start_number', 33 | '0', 34 | '-i', 35 | join(framesDirectory, '%05d.raw'), 36 | ]; 37 | 38 | if (config.get('capture:audioFilename')) { 39 | args.push( 40 | '-i', 41 | join(config.get('directory'), config.get('capture:audioFilename')) 42 | ); 43 | } else { 44 | console.warn( 45 | 'capture:audioFilename has not been set, video will be silent.' 46 | ); 47 | } 48 | 49 | args.push( 50 | '-vf', 51 | 'vflip', 52 | '-codec:v', 53 | 'libx264', 54 | '-crf', 55 | '18', 56 | '-bf', 57 | '2', 58 | '-flags', 59 | '+cgop', 60 | '-pix_fmt', 61 | 'yuv420p', 62 | '-codec:a', 63 | 'aac', 64 | '-strict', 65 | '-2', 66 | '-b:a', 67 | '384k', 68 | '-movflags', 69 | 'faststart', 70 | join(config.get('paths:dist'), config.get('demo:name') + '.mp4') 71 | ); 72 | 73 | await spawn(config.get('tools:ffmpeg'), args); 74 | } 75 | -------------------------------------------------------------------------------- /scripts/audio-synthesizers/oidos.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from 'nconf'; 2 | import { join } from 'path'; 3 | 4 | import { IAudioSynthesizer, ICompilationDefinition } from '../definitions'; 5 | import { addHooks } from '../hooks'; 6 | import { spawn } from '../lib'; 7 | 8 | export class OidosAudioSynthesizer implements IAudioSynthesizer { 9 | private config: Provider; 10 | 11 | constructor(config: Provider) { 12 | this.config = config; 13 | } 14 | 15 | getDefaultConfig() { 16 | return { 17 | filename: 'music.xrns', 18 | }; 19 | } 20 | 21 | checkConfig() { 22 | this.config.required([ 23 | 'demo:audio-synthesizer:filename', 24 | 'tools:oidos', 25 | 'tools:python2', 26 | ]); 27 | } 28 | 29 | async addToCompilation(compilation: ICompilationDefinition) { 30 | const buildDirectory: string = this.config.get('paths:build'); 31 | const demoDirectory: string = this.config.get('directory'); 32 | 33 | await addHooks( 34 | compilation.cpp.hooks, 35 | join('engine', 'audio-synthesizer-hooks', 'oidos.cpp') 36 | ); 37 | 38 | compilation.cpp.clArgs.push( 39 | '/I' + join(this.config.get('tools:oidos'), 'player') 40 | ); 41 | 42 | await spawn(this.config.get('tools:python2'), [ 43 | join(this.config.get('tools:oidos'), 'convert', 'OidosConvert.py'), 44 | join(demoDirectory, this.config.get('demo:audio-synthesizer:filename')), 45 | join(buildDirectory, 'music.asm'), 46 | ]); 47 | 48 | compilation.asm.sources[join(buildDirectory, 'oidos.obj')] = { 49 | dependencies: [join(buildDirectory, 'music.asm')], 50 | source: join(this.config.get('tools:oidos'), 'player', 'oidos.asm'), 51 | }; 52 | 53 | compilation.asm.sources[join(buildDirectory, 'random.obj')] = { 54 | source: join(this.config.get('tools:oidos'), 'player', 'random.asm'), 55 | }; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /scripts/hot-reload.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'request-promise-native'; 2 | 3 | import { IContext, IDemoDefinition } from './definitions'; 4 | import { forEachMatch } from './lib'; 5 | 6 | export async function updateDemo(context: IContext, demo: IDemoDefinition) { 7 | const baseUrl = `http://localhost:${context.config.get('server:port')}/`; 8 | 9 | const requests: request.RequestPromise[] = []; 10 | 11 | function addRequest(passIndex: number, stage: string, code: string) { 12 | requests.push( 13 | request(`${baseUrl}passes/${passIndex}/${stage}`, { 14 | body: code, 15 | headers: { 16 | 'Content-Type': 'text/plain', 17 | }, 18 | method: 'POST', 19 | }) 20 | ); 21 | } 22 | 23 | const stageVariableRegExp = /\w+ [\w,]+;/g; 24 | let vertexSpecificCode = ''; 25 | let fragmentSpecificCode = ''; 26 | 27 | if (demo.shader.attributesCode) { 28 | forEachMatch(stageVariableRegExp, demo.shader.attributesCode, (match) => { 29 | vertexSpecificCode += 'in ' + match[0]; 30 | }); 31 | } 32 | 33 | if (demo.shader.varyingsCode) { 34 | forEachMatch(stageVariableRegExp, demo.shader.varyingsCode, (match) => { 35 | vertexSpecificCode += 'out ' + match[0]; 36 | fragmentSpecificCode += 'in ' + match[0]; 37 | }); 38 | } 39 | 40 | if (demo.shader.outputsCode) { 41 | forEachMatch(stageVariableRegExp, demo.shader.outputsCode, (match) => { 42 | fragmentSpecificCode += 'out ' + match[0]; 43 | }); 44 | } 45 | 46 | demo.shader.passes.forEach((pass, passIndex) => { 47 | if (pass.vertexCode) { 48 | let code = ''; 49 | 50 | if (demo.shader.prologCode) { 51 | code += demo.shader.prologCode; 52 | } 53 | 54 | code += vertexSpecificCode; 55 | code += demo.shader.commonCode; 56 | code += pass.vertexCode; 57 | 58 | addRequest(passIndex, 'vertex', code); 59 | } 60 | 61 | if (pass.fragmentCode) { 62 | let code = ''; 63 | 64 | if (demo.shader.prologCode) { 65 | code += demo.shader.prologCode; 66 | } 67 | 68 | code += fragmentSpecificCode; 69 | code += demo.shader.commonCode; 70 | code += pass.fragmentCode; 71 | 72 | addRequest(passIndex, 'fragment', code); 73 | } 74 | }); 75 | 76 | await Promise.all(requests); 77 | } 78 | -------------------------------------------------------------------------------- /samples/oidos-synthclipse/shader.stoy: -------------------------------------------------------------------------------- 1 | uniform float time; 2 | uniform vec2 synth_Resolution; 3 | 4 | //! 5 | uniform bool OverrideCamera;//! checkbox[false] 6 | #include 7 | 8 | //! 9 | uniform float BPM;//! slider[10, 100, 200] 10 | 11 | //! 12 | uniform float Steps;//! slider[5, 30, 50] 13 | uniform float DistanceEpsilon;//! slider[0, 0.01, 1] 14 | 15 | //! 16 | uniform vec3 SphereColor0;//! color[1, 1, 1] 17 | uniform vec3 SphereColor1;//! color[1, 1, 1] 18 | 19 | const float TAU=6.283; 20 | 21 | // START 22 | 23 | const float beat = time * BPM/60.; 24 | 25 | struct D 26 | { 27 | float distance; 28 | vec3 color; 29 | }; 30 | 31 | mat2 rotate(float angle) 32 | { 33 | float c=cos(angle), 34 | s=sin(angle); 35 | return mat2(c,s,-s,c); 36 | } 37 | 38 | vec3 camera(vec3 ray) 39 | { 40 | // Rotate camera. 41 | ray.xy*=rotate(beat); 42 | ray.yz*=rotate(beat); 43 | 44 | return ray; 45 | } 46 | 47 | D map(vec3 ray) 48 | { 49 | D data; 50 | 51 | // Repeat spheres. 52 | vec3 index=floor((ray+2.)/4.); 53 | ray=mod(ray+2.,4.)-2.; 54 | 55 | // A bouncing ball. 56 | float bounce=fract(beat)*(1-fract(beat))*4.; 57 | float radius=.2+.5*bounce; 58 | data.distance=length(ray)-radius; 59 | data.color=mix(SphereColor0,SphereColor1,fract(sin(dot(index,vec3(3,5,8)))*1e4)); 60 | 61 | return data; 62 | } 63 | 64 | void main(){ 65 | vec2 uv=gl_FragCoord.xy/synth_Resolution; 66 | uv-=.5; 67 | uv.x*=synth_Resolution.x/synth_Resolution.y; 68 | 69 | vec3 rayOrigin=camera(vec3(0.,0.,-2.)); 70 | vec3 rayDirection=camera(normalize(vec3(uv,1.-length(uv)*.5))); 71 | 72 | #ifdef SYNTHCLIPSE_ONLY 73 | if(OverrideCamera){ 74 | getCamera(rayOrigin,rayDirection); 75 | } 76 | #endif 77 | 78 | vec3 ray=rayOrigin; 79 | D data; 80 | 81 | // Simple raymarching algorithm. 82 | float stepIndex; 83 | for(stepIndex=0.;stepIndex 97 | -------------------------------------------------------------------------------- /engine/debug.cpp: -------------------------------------------------------------------------------- 1 | #define UNICODE 2 | #define _WIN32_WINNT 0x0600 3 | #define WIN32_LEAN_AND_MEAN 4 | 5 | #include 6 | 7 | #include "demo.hpp" 8 | 9 | #include "debug.hpp" 10 | 11 | char debugBuffer[4096]; 12 | 13 | HWND debugHwnd; 14 | 15 | GLint debugFragmentShaders[PASS_COUNT]; 16 | GLint debugVertexShaders[PASS_COUNT]; 17 | 18 | void APIENTRY showDebugMessageFromOpenGL( 19 | GLenum source, 20 | GLenum type, 21 | GLuint id, 22 | GLenum severity, 23 | GLsizei length, 24 | const GLchar *message, 25 | const void *userParam) 26 | { 27 | #ifdef DEBUG_MESSAGE_BOX 28 | // TODO : find better. This disable fullscreen, this is the only solution I found to display message box on top... 29 | SetWindowPos( 30 | debugHwnd, NULL, 1, 0, 31 | width, height, 32 | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); 33 | MessageBox(debugHwnd, message, "Error", MB_OK | MB_TOPMOST | MB_SETFOREGROUND | MB_SYSTEMMODAL); 34 | #endif 35 | 36 | std::cerr << "OpenGL debug message: "; 37 | if (type == GL_DEBUG_TYPE_ERROR) 38 | { 39 | std::cerr << "** GL ERROR **"; 40 | } 41 | std::cerr << "type = 0x" << std::hex << type 42 | << ", severity = 0x" << std::hex << severity 43 | << ", message = " << message << std::endl; 44 | } 45 | 46 | void APIENTRY showDebugMessage(const char *message) 47 | { 48 | std::cerr << message << std::endl; 49 | } 50 | 51 | void _checkGLError(const char *filename, int lineNumber) 52 | { 53 | GLenum err = glGetError(); 54 | if (err != GL_NO_ERROR) 55 | { 56 | char *error = nullptr; 57 | 58 | #define ERROR_CASE(ERR) \ 59 | case ERR: \ 60 | error = #ERR; \ 61 | break; 62 | 63 | switch (err) 64 | { 65 | ERROR_CASE(GL_INVALID_ENUM) 66 | ERROR_CASE(GL_INVALID_OPERATION) 67 | ERROR_CASE(GL_INVALID_VALUE) 68 | ERROR_CASE(GL_INVALID_FRAMEBUFFER_OPERATION_EXT) 69 | ERROR_CASE(GL_OUT_OF_MEMORY) 70 | } 71 | 72 | #undef ERROR_CASE 73 | 74 | std::cerr << "OpenGL error at " << filename << "@" << lineNumber << ": "; 75 | if (error != nullptr) 76 | { 77 | std::cerr << error; 78 | } 79 | else 80 | { 81 | std::cerr << "0x" << std::hex << err; 82 | } 83 | std::cerr << std::endl; 84 | } 85 | 86 | // ExitProcess(1); 87 | } 88 | 89 | void checkShaderCompilation(GLint shader) 90 | { 91 | glGetShaderInfoLog(shader, sizeof(debugBuffer), NULL, debugBuffer); 92 | if (debugBuffer[0] != '\0') 93 | { 94 | showDebugMessage(debugBuffer); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /scripts/definitions.ts: -------------------------------------------------------------------------------- 1 | export interface IContextOptions { 2 | capture?: boolean; 3 | debug?: boolean; 4 | } 5 | 6 | export interface IPass { 7 | fragmentCode?: string; 8 | vertexCode?: string; 9 | } 10 | 11 | export interface IAnnotations { 12 | [key: string]: string | boolean; 13 | } 14 | 15 | export interface IVariable { 16 | kind?: string; 17 | 18 | active: boolean; 19 | minifiedName?: string; 20 | name: string; 21 | type: string; 22 | } 23 | 24 | export interface IConstVariable extends IVariable { 25 | kind: 'const'; 26 | 27 | value: string; 28 | } 29 | 30 | export interface IRegularVariable extends IVariable { 31 | kind: 'regular'; 32 | } 33 | 34 | export interface IUniformVariable extends IVariable { 35 | kind: 'uniform'; 36 | } 37 | 38 | export type Variable = IConstVariable | IRegularVariable | IUniformVariable; 39 | 40 | export interface IUniformArray { 41 | name: string; 42 | minifiedName?: string; 43 | variables: IUniformVariable[]; 44 | } 45 | 46 | export interface IUniformArrays { 47 | [type: string]: IUniformArray; 48 | } 49 | 50 | export interface IHooks { 51 | [type: string]: string; 52 | } 53 | 54 | export interface IShaderDefinition { 55 | prologCode?: string; 56 | attributesCode?: string; 57 | varyingsCode?: string; 58 | outputsCode?: string; 59 | commonCode: string; 60 | passes: IPass[]; 61 | 62 | glslVersion?: string; 63 | uniformArrays: IUniformArrays; 64 | variables: Variable[]; 65 | } 66 | 67 | export interface ISource { 68 | dependencies?: string[]; 69 | includes?: string[]; 70 | source: string; 71 | } 72 | 73 | export interface ISources { 74 | [objPath: string]: ISource; 75 | } 76 | 77 | export interface IAsmCompilationDefinition { 78 | nasmArgs: string[]; 79 | includePaths: string[]; 80 | 81 | sources: ISources; 82 | } 83 | 84 | export interface ICppCompilationDefinition { 85 | clArgs: string[]; 86 | 87 | hooks: IHooks; 88 | 89 | sources: ISources; 90 | } 91 | 92 | export interface ICompilationDefinition { 93 | asm: IAsmCompilationDefinition; 94 | cpp: ICppCompilationDefinition; 95 | 96 | crinklerArgs: string[]; 97 | linkArgs: string[]; 98 | } 99 | 100 | export interface IAudioSynthesizer { 101 | getDefaultConfig(): object; 102 | checkConfig(): void; 103 | addToCompilation(compilation: ICompilationDefinition): Promise; 104 | } 105 | 106 | export interface IShaderProvider { 107 | getDefaultConfig(): object; 108 | checkConfig(): void; 109 | provide(definition: IShaderDefinition): Promise; 110 | } 111 | 112 | export interface IShaderMinifier { 113 | getDefaultConfig(): object; 114 | checkConfig(): void; 115 | minify(definition: IShaderDefinition): Promise; 116 | } 117 | 118 | export interface IDemoDefinition { 119 | shader: IShaderDefinition; 120 | compilation: ICompilationDefinition; 121 | } 122 | 123 | export interface IConfig { 124 | get(key?: string): any; 125 | } 126 | 127 | export interface IContext { 128 | config: IConfig; 129 | 130 | audioSynthesizer?: IAudioSynthesizer; 131 | shaderProvider: IShaderProvider; 132 | shaderMinifier?: IShaderMinifier; 133 | } 134 | -------------------------------------------------------------------------------- /samples/audio-shader/hooks.cpp: -------------------------------------------------------------------------------- 1 | #pragma hook declarations 2 | 3 | #define SOUND_TEXTURE_SIZE 2048 4 | #define SAMPLE_RATE 44100 5 | #define MAX_SAMPLES (SOUND_TEXTURE_SIZE * SOUND_TEXTURE_SIZE * 2) 6 | #define SAMPLE_TYPE float 7 | #define FLOAT_32BIT 8 | 9 | #include 10 | #include 11 | 12 | #pragma data_seg(".var") 13 | 14 | static SAMPLE_TYPE soundBuffer[MAX_SAMPLES * 2]; 15 | static HWAVEOUT waveOut; 16 | 17 | static const WAVEFORMATEX waveFormat = { 18 | #ifdef FLOAT_32BIT 19 | WAVE_FORMAT_IEEE_FLOAT, 20 | #else 21 | WAVE_FORMAT_PCM, 22 | #endif 23 | 2, // channels 24 | SAMPLE_RATE, // samples per sec 25 | SAMPLE_RATE * sizeof(SAMPLE_TYPE) * 2, // bytes per sec 26 | sizeof(SAMPLE_TYPE) * 2, // block alignment; 27 | sizeof(SAMPLE_TYPE) * 8, // bits per sample 28 | 0, // extension not needed 29 | }; 30 | 31 | static WAVEHDR waveHDR = { 32 | (LPSTR)soundBuffer, 33 | MAX_SAMPLES * sizeof(SAMPLE_TYPE) * 2, // MAX_SAMPLES*sizeof(float)*2(stereo) 34 | 0, 35 | 0, 36 | 0, 37 | 0, 38 | 0, 39 | 0, 40 | }; 41 | 42 | static MMTIME mmTime = { 43 | TIME_SAMPLES, 44 | 0, 45 | }; 46 | 47 | static GLuint audioTextureId; 48 | 49 | static unsigned int fbo; 50 | 51 | #pragma hook initialize 52 | 53 | glGenTextures(1, &audioTextureId); 54 | checkGLError(); 55 | 56 | glGenFramebuffers(1, &fbo); 57 | checkGLError(); 58 | 59 | glViewport(0, 0, SOUND_TEXTURE_SIZE, SOUND_TEXTURE_SIZE); 60 | checkGLError(); 61 | 62 | glBindFramebuffer(GL_FRAMEBUFFER, fbo); 63 | checkGLError(); 64 | 65 | glUseProgram(programs[0]); 66 | checkGLError(); 67 | 68 | glBindTexture(GL_TEXTURE_2D, audioTextureId); 69 | checkGLError(); 70 | 71 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, SOUND_TEXTURE_SIZE, SOUND_TEXTURE_SIZE, 0, GL_RGBA, GL_FLOAT, NULL); 72 | checkGLError(); 73 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 74 | checkGLError(); 75 | glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, audioTextureId, 0); 76 | checkGLError(); 77 | 78 | glRects(-1, -1, 1, 1); 79 | checkGLError(); 80 | 81 | glReadPixels(0, 0, SOUND_TEXTURE_SIZE, SOUND_TEXTURE_SIZE, GL_RGBA, GL_FLOAT, soundBuffer); 82 | checkGLError(); 83 | 84 | glViewport(0, 0, resolutionWidth, resolutionHeight); 85 | checkGLError(); 86 | 87 | glFinish(); 88 | checkGLError(); 89 | 90 | glBindFramebuffer(GL_FRAMEBUFFER, 0); 91 | checkGLError(); 92 | 93 | glUseProgram(programs[1]); 94 | checkGLError(); 95 | 96 | glActiveTexture(GL_TEXTURE0 + 0); 97 | checkGLError(); 98 | 99 | glBindTexture(GL_TEXTURE_2D, audioTextureId); 100 | checkGLError(); 101 | 102 | glUniform1i(3, 0); 103 | checkGLError(); 104 | 105 | waveOutOpen(&waveOut, WAVE_MAPPER, &waveFormat, NULL, 0, CALLBACK_NULL); 106 | waveOutPrepareHeader(waveOut, &waveHDR, sizeof(waveHDR)); 107 | waveOutWrite(waveOut, &waveHDR, sizeof(waveHDR)); 108 | 109 | #pragma hook render 110 | 111 | waveOutGetPosition(waveOut, &mmTime, sizeof(MMTIME)); 112 | float time = (float)mmTime.u.sample / (float)SAMPLE_RATE; 113 | 114 | uniformTime = time; 115 | 116 | glUniform1fv(0, FLOAT_UNIFORM_COUNT, floatUniforms); 117 | checkGLError(); 118 | 119 | glRects(-1, -1, 1, 1); 120 | checkGLError(); 121 | 122 | #pragma hook audio_duration 123 | 124 | return MAX_SAMPLES / (float)SAMPLE_RATE; 125 | 126 | #pragma hook audio_is_playing 127 | 128 | mmTime.u.sample < MAX_SAMPLES 129 | -------------------------------------------------------------------------------- /scripts/tasks.ts: -------------------------------------------------------------------------------- 1 | import { watch as originalWatch } from 'gulp'; 2 | import { join, resolve } from 'path'; 3 | 4 | import { encode as originalEncode, spawnCapture } from './capture'; 5 | import { compile } from './compilation'; 6 | import { provideContext } from './context'; 7 | import { IContext } from './definitions'; 8 | import { provideDemo } from './demo'; 9 | import { 10 | writeDemoData, 11 | writeDemoGl, 12 | writeDemoMain, 13 | } from './generate-source-codes'; 14 | import { updateDemo as originalUpdateDemo } from './hot-reload'; 15 | import { emptyDirectories, spawn } from './lib'; 16 | import { Monitor } from './monitor'; 17 | import { zip } from './zip'; 18 | 19 | async function buildDemo(context: IContext) { 20 | await emptyDirectories(context); 21 | 22 | const demo = await provideDemo(context); 23 | 24 | await writeDemoData(context, demo); 25 | await writeDemoGl(context); 26 | await writeDemoMain(context, demo); 27 | 28 | await compile(context, demo); 29 | } 30 | 31 | async function buildWithContext(context: IContext) { 32 | const monitor = new Monitor(context); 33 | 34 | await monitor.run(async () => { 35 | await buildDemo(context); 36 | 37 | await monitor.notifySuccess(); 38 | 39 | if (context.config.get('zip')) { 40 | await zip(context); 41 | } 42 | }); 43 | } 44 | 45 | function executeWithContext(context: IContext) { 46 | return spawn(resolve(context.config.get('paths:exe')), []); 47 | } 48 | 49 | async function updateDemoWithContext(context: IContext) { 50 | const demo = await provideDemo(context); 51 | 52 | await originalUpdateDemo(context, demo); 53 | } 54 | 55 | function watchWithContext(context: IContext) { 56 | const updateDemo = () => updateDemoWithContext(context); 57 | 58 | return originalWatch( 59 | [join(context.config.get('directory'), '**', '*').replace(/\\/g, '/')], 60 | updateDemo 61 | ); 62 | } 63 | 64 | export function build() { 65 | const context = provideContext({}); 66 | 67 | return buildWithContext(context); 68 | } 69 | 70 | export async function capture() { 71 | const context = provideContext({ 72 | capture: true, 73 | }); 74 | 75 | await buildDemo(context); 76 | 77 | await spawnCapture(context); 78 | 79 | await originalEncode(context); 80 | } 81 | 82 | export async function clean() { 83 | const context = provideContext({ 84 | capture: true, 85 | }); 86 | 87 | await emptyDirectories(context); 88 | } 89 | 90 | export async function dev() { 91 | const context = provideContext({ 92 | debug: true, 93 | }); 94 | 95 | await buildWithContext(context); 96 | 97 | const watcher = watchWithContext(context); 98 | 99 | await executeWithContext(context); 100 | 101 | watcher.close(); 102 | } 103 | 104 | export async function encode() { 105 | const context = provideContext({ 106 | capture: true, 107 | }); 108 | 109 | await originalEncode(context); 110 | } 111 | 112 | export function execute() { 113 | const context = provideContext({}); 114 | 115 | return executeWithContext(context); 116 | } 117 | 118 | export async function showConfig() { 119 | const context = provideContext({}); 120 | 121 | console.log(context.config.get()); 122 | } 123 | 124 | export function watch() { 125 | const context = provideContext({}); 126 | 127 | return watchWithContext(context); 128 | } 129 | 130 | export default async function() { 131 | const context = provideContext({}); 132 | 133 | await buildWithContext(context); 134 | await executeWithContext(context); 135 | } 136 | -------------------------------------------------------------------------------- /samples/multipass/hooks.cpp: -------------------------------------------------------------------------------- 1 | #pragma hook declarations 2 | 3 | #pragma data_seg(".var") 4 | static GLuint vbo, vao; 5 | 6 | static const constexpr int count = 100; 7 | static const constexpr int sliceX = 100; 8 | static const constexpr int sliceY = 1; 9 | static const constexpr int faceX = sliceX + 1; 10 | static const constexpr int faceY = sliceY + 1; 11 | static const constexpr int indiceCount = count * ((faceX + 2) * faceY); // count * line (+2 obfuscated triangle) * row 12 | static const constexpr int vertexCount = count * (faceX * faceY) * (3 + 2); // count * line * row * (x,y,z + u,v) 13 | 14 | static GLfloat vertices[vertexCount]; 15 | static int indices[indiceCount]; 16 | 17 | static const constexpr int textureCount = 1; 18 | static GLuint textureIds[textureCount]; 19 | 20 | static unsigned int fbo; 21 | 22 | #pragma hook initialize 23 | 24 | int i = 0; 25 | for (int index = 0; index < count; ++index) 26 | { 27 | int xx = 0; 28 | for (int y = 0; y < faceY; ++y) 29 | { 30 | for (int x = 0; x < faceX; ++x) 31 | { 32 | vertices[index * (faceX * faceY) * (3 + 2) + xx * (3 + 2) + 0] = (float)index; 33 | vertices[index * (faceX * faceY) * (3 + 2) + xx * (3 + 2) + 1] = (float)index; 34 | vertices[index * (faceX * faceY) * (3 + 2) + xx * (3 + 2) + 2] = (float)index; 35 | vertices[index * (faceX * faceY) * (3 + 2) + xx * (3 + 2) + 3 + 0] = ((float)x / (float)sliceX) * 2.f - 1.f; 36 | vertices[index * (faceX * faceY) * (3 + 2) + xx * (3 + 2) + 3 + 1] = ((float)y / (float)sliceY) * 2.f - 1.f; 37 | xx++; 38 | } 39 | } 40 | for (int r = 0; r < faceY - 1; ++r) 41 | { 42 | indices[i++] = index * (faceX * faceY) + r * faceX; 43 | for (int c = 0; c < faceX; ++c) 44 | { 45 | indices[i++] = index * (faceX * faceY) + r * faceX + c; 46 | indices[i++] = index * (faceX * faceY) + (r + 1) * faceX + c; 47 | } 48 | indices[i++] = index * (faceX * faceY) + (r + 1) * faceX + (faceX - 1); 49 | } 50 | } 51 | 52 | glCreateBuffers(1, &vbo); 53 | checkGLError(); 54 | 55 | glNamedBufferStorage(vbo, sizeof(vertices), vertices, 0); 56 | checkGLError(); 57 | glCreateVertexArrays(1, &vao); 58 | checkGLError(); 59 | glBindVertexArray(vao); 60 | checkGLError(); 61 | 62 | glBindBuffer(GL_ARRAY_BUFFER, vbo); 63 | checkGLError(); 64 | 65 | glEnableVertexAttribArray(0); 66 | checkGLError(); 67 | glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), 0); 68 | checkGLError(); 69 | 70 | glEnableVertexAttribArray(1); 71 | checkGLError(); 72 | glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid *)(3 * sizeof(GLfloat))); 73 | checkGLError(); 74 | 75 | glGenTextures(textureCount, textureIds); 76 | checkGLError(); 77 | 78 | for (i = 0; i < textureCount; i++) 79 | { 80 | glBindTexture(GL_TEXTURE_2D, textureIds[i]); 81 | checkGLError(); 82 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, resolutionWidth, resolutionHeight, 0, GL_RGBA, GL_FLOAT, NULL); 83 | checkGLError(); 84 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 85 | checkGLError(); 86 | //glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 87 | //checkGLError(); 88 | } 89 | 90 | glGenFramebuffers(1, &fbo); 91 | checkGLError(); 92 | 93 | #pragma hook render 94 | 95 | uniformTime = time; 96 | 97 | // Pass 0 98 | 99 | glBindFramebuffer(GL_FRAMEBUFFER, fbo); 100 | checkGLError(); 101 | 102 | glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureIds[0], 0); 103 | checkGLError(); 104 | 105 | glUseProgram(programs[0]); 106 | checkGLError(); 107 | 108 | glUniform1fv(0, FLOAT_UNIFORM_COUNT, floatUniforms); 109 | checkGLError(); 110 | 111 | glClear(GL_COLOR_BUFFER_BIT); // | GL_DEPTH_BUFFER_BIT); 112 | checkGLError(); 113 | 114 | glEnable(GL_BLEND); 115 | checkGLError(); 116 | 117 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 118 | checkGLError(); 119 | 120 | glBindVertexArray(vao); 121 | checkGLError(); 122 | 123 | glDrawElements(GL_TRIANGLE_STRIP, indiceCount, GL_UNSIGNED_INT, indices); 124 | checkGLError(); 125 | 126 | // Pass 1 127 | 128 | glBindFramebuffer(GL_FRAMEBUFFER, 0); 129 | checkGLError(); 130 | 131 | glUseProgram(programs[1]); 132 | checkGLError(); 133 | 134 | glUniform1fv(0, FLOAT_UNIFORM_COUNT, floatUniforms); 135 | checkGLError(); 136 | 137 | glActiveTexture(GL_TEXTURE0 + 0); 138 | checkGLError(); 139 | 140 | glBindTexture(GL_TEXTURE_2D, textureIds[0]); 141 | checkGLError(); 142 | 143 | glUniform1i(3, 0); 144 | checkGLError(); 145 | 146 | glRects(-1, -1, 1, 1); 147 | checkGLError(); 148 | -------------------------------------------------------------------------------- /scripts/compilation.ts: -------------------------------------------------------------------------------- 1 | import { join, sep } from 'path'; 2 | 3 | import { IContext, IDemoDefinition } from './definitions'; 4 | import { spawn } from './lib'; 5 | 6 | export async function compile(context: IContext, demo: IDemoDefinition) { 7 | const { config } = context; 8 | const { compilation } = demo; 9 | const buildDirectory: string = config.get('paths:build'); 10 | const demoDirectory: string = config.get('directory'); 11 | 12 | let outArgs = ['/OUT:' + config.get('paths:exe')]; 13 | 14 | /* 15 | switch (config.get('demo:audio-synthesizer:tool') || 'none') { 16 | case '4klang': { 17 | compilation.asm.sources[join(buildDirectory, '4klang.obj')] = { 18 | dependencies: [join(demoDirectory, '4klang.inc')], 19 | source: join(config.get('tools:4klang'), '4klang.asm'), 20 | }; 21 | 22 | compilation.cpp.sources[join(buildDirectory, 'audio-4klang.obj')] = { 23 | dependencies: [ 24 | join(demoDirectory, '4klang.inc'), 25 | join(demoDirectory, 'config.yml'), 26 | join(demoDirectory, 'config.local.yml'), 27 | ], 28 | source: join('engine', 'audio-4klang.cpp'), 29 | }; 30 | 31 | break; 32 | } 33 | 34 | case '8klang': { 35 | compilation.asm.sources[join(buildDirectory, '8klang.obj')] = { 36 | dependencies: [join(demoDirectory, '4klang.inc')], 37 | source: join(config.get('tools:8klang'), '4klang.asm'), 38 | }; 39 | 40 | compilation.cpp.sources[join(buildDirectory, 'audio-8klang.obj')] = { 41 | dependencies: [ 42 | join(demoDirectory, '4klang.inc'), 43 | join(demoDirectory, 'config.yml'), 44 | join(demoDirectory, 'config.local.yml'), 45 | ], 46 | source: join('engine', 'audio-4klang.cpp'), 47 | }; 48 | 49 | break; 50 | } 51 | 52 | case 'oidos': { 53 | await spawn(config.get('tools:python2'), [ 54 | join(config.get('tools:oidos'), 'convert', 'OidosConvert.py'), 55 | join(demoDirectory, config.get('demo:audio-synthesizer:filename')), 56 | join(buildDirectory, 'music.asm'), 57 | ]); 58 | 59 | compilation.asm.sources[join(buildDirectory, 'oidos.obj')] = { 60 | dependencies: [join(buildDirectory, 'music.asm')], 61 | source: join(config.get('tools:oidos'), 'player', 'oidos.asm'), 62 | }; 63 | 64 | compilation.asm.sources[join(buildDirectory, 'random.obj')] = { 65 | source: join(config.get('tools:oidos'), 'player', 'random.asm'), 66 | }; 67 | 68 | compilation.cpp.sources[join(buildDirectory, 'audio-oidos.obj')] = { 69 | dependencies: [ 70 | join(demoDirectory, 'config.yml'), 71 | join(demoDirectory, 'config.local.yml'), 72 | ], 73 | includes: [join(config.get('tools:oidos'), 'player')], 74 | source: join('engine', 'audio-oidos.cpp'), 75 | }; 76 | 77 | break; 78 | } 79 | } 80 | */ 81 | 82 | await Promise.all([ 83 | Promise.all( 84 | Object.keys(compilation.asm.sources).map((obj) => { 85 | const asmSource = compilation.asm.sources[obj]; 86 | 87 | let args = compilation.asm.nasmArgs; 88 | 89 | if (asmSource.includes) { 90 | args = args.concat(asmSource.includes); 91 | } 92 | 93 | args = args.concat([ 94 | '-f', 95 | 'win32', 96 | '-i', 97 | buildDirectory + sep, 98 | '-i', 99 | demoDirectory + sep, 100 | '-o', 101 | obj, 102 | asmSource.source, 103 | ]); 104 | 105 | return spawn(config.get('tools:nasm'), args); 106 | }) 107 | ), 108 | Promise.all( 109 | Object.keys(compilation.cpp.sources).map((obj) => { 110 | const cppSource = compilation.cpp.sources[obj]; 111 | 112 | let args = compilation.cpp.clArgs; 113 | 114 | if (cppSource.includes) { 115 | args = args.concat( 116 | cppSource.includes.map((filename) => '/I' + filename) 117 | ); 118 | } 119 | 120 | args = args 121 | .concat(config.get('cl:args')) 122 | .concat([ 123 | '/I' + join(config.get('tools:glew'), 'include'), 124 | '/FA', 125 | '/Fa' + obj + '.asm', 126 | '/c', 127 | '/Fo' + obj, 128 | cppSource.source, 129 | ]); 130 | 131 | return spawn('cl', args); 132 | }) 133 | ), 134 | ]).then(() => { 135 | outArgs = outArgs 136 | .concat(Object.keys(compilation.asm.sources)) 137 | .concat(Object.keys(compilation.cpp.sources)); 138 | 139 | return config.get('debug') 140 | ? spawn( 141 | 'link', 142 | compilation.linkArgs 143 | .concat(config.get('link:args')) 144 | .concat([ 145 | join( 146 | config.get('tools:glew'), 147 | 'lib', 148 | 'Release', 149 | 'Win32', 150 | 'glew32s.lib' 151 | ), 152 | ]) 153 | .concat(outArgs) 154 | ) 155 | : spawn( 156 | config.get('tools:crinkler'), 157 | compilation.crinklerArgs 158 | .concat(config.get('crinkler:args')) 159 | .concat(['/REPORT:' + join(buildDirectory, 'stats.html')]) 160 | .concat(outArgs) 161 | ); 162 | }); 163 | } 164 | -------------------------------------------------------------------------------- /scripts/shader-providers/simple.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'fs-extra'; 2 | import { Provider } from 'nconf'; 3 | import { join } from 'path'; 4 | 5 | import { 6 | IAnnotations, 7 | IShaderDefinition, 8 | IShaderProvider, 9 | } from '../definitions'; 10 | import { forEachMatch } from '../lib'; 11 | import { addConstant, addRegular, addUniform } from '../variables'; 12 | 13 | export class SimpleShaderProvider implements IShaderProvider { 14 | private config: Provider; 15 | 16 | constructor(config: Provider) { 17 | this.config = config; 18 | } 19 | 20 | getDefaultConfig() { 21 | return { 22 | filename: 'shader.frag', 23 | }; 24 | } 25 | 26 | checkConfig() { 27 | this.config.required(['demo:shader-provider:filename']); 28 | } 29 | 30 | async provide(definition: IShaderDefinition) { 31 | const demoDirectory: string = this.config.get('directory'); 32 | 33 | const shaderContents = await readFile( 34 | join(demoDirectory, this.config.get('demo:shader-provider:filename')), 35 | 'utf8' 36 | ); 37 | 38 | const versionMatch = shaderContents.match(/#version (.+)$/m); 39 | if (versionMatch) { 40 | definition.glslVersion = versionMatch[1]; 41 | } 42 | 43 | let prologCode = ''; 44 | 45 | type IPartAction = (code: string) => void; 46 | let partStartIndex = 0; 47 | let partAction: IPartAction = (code) => { 48 | prologCode = code; 49 | }; 50 | 51 | function takePart(partEndIndex?: number) { 52 | if (partAction) { 53 | const code = shaderContents 54 | .substring(partStartIndex, partEndIndex) 55 | .replace( 56 | /#ifdef\s+BUILD_ONLY([\s\S]*?)(?:#else[\s\S]*?)?#endif/g, 57 | '$1' 58 | ) 59 | .replace( 60 | /#ifndef\s+BUILD_ONLY[\s\S]*?(?:#else([\s\S]*?))?#endif/g, 61 | '$1' 62 | ) 63 | .replace(/void main\w+\(\)/g, 'void main()'); 64 | 65 | partAction(code); 66 | } 67 | } 68 | 69 | function ensurePassesHasIndex(index: number) { 70 | while (definition.passes.length <= index) { 71 | definition.passes.push({}); 72 | } 73 | } 74 | 75 | const partsRegExp = /^#pragma\s+(.+)\s+$/gm; 76 | forEachMatch(partsRegExp, shaderContents, (match) => { 77 | function innerTakePart(newPartAction: IPartAction) { 78 | takePart(match.index); 79 | partStartIndex = partsRegExp.lastIndex; 80 | partAction = newPartAction; 81 | } 82 | 83 | switch (match[1]) { 84 | case 'attributes': 85 | innerTakePart((code) => { 86 | definition.attributesCode = code; 87 | }); 88 | break; 89 | 90 | case 'common': 91 | innerTakePart((code) => { 92 | definition.commonCode = code; 93 | }); 94 | break; 95 | 96 | case 'outputs': 97 | innerTakePart((code) => { 98 | definition.outputsCode = code; 99 | }); 100 | break; 101 | 102 | case 'varyings': 103 | innerTakePart((code) => { 104 | definition.varyingsCode = code; 105 | }); 106 | break; 107 | } 108 | 109 | const vertexMatch = match[1].match(/^vertex (\d+)$/); 110 | if (vertexMatch) { 111 | const passIndex = parseInt(vertexMatch[1], 10); 112 | ensurePassesHasIndex(passIndex); 113 | innerTakePart((code) => { 114 | definition.passes[passIndex].vertexCode = code; 115 | }); 116 | } 117 | 118 | const fragmentMatch = match[1].match(/^fragment (\d+)$/); 119 | if (fragmentMatch) { 120 | const passIndex = parseInt(fragmentMatch[1], 10); 121 | ensurePassesHasIndex(passIndex); 122 | innerTakePart((code) => { 123 | definition.passes[passIndex].fragmentCode = code; 124 | }); 125 | } 126 | }); 127 | 128 | takePart(); 129 | 130 | const variableRegExp = /(?:(const|precision|uniform)\s+)?(\w+)\s+(\w+)\s*(=\s*([^;]+))?;(?:\s*\/\/!(.+))?$/gm; 131 | forEachMatch(variableRegExp, prologCode, (variableMatch) => { 132 | if (variableMatch[1] === 'precision') { 133 | return; 134 | } 135 | 136 | if (!variableMatch[5] !== (variableMatch[1] !== 'const')) { 137 | throw new Error( 138 | `Variable "${variableMatch[3]}" has a value and is not const.` 139 | ); 140 | } 141 | 142 | const annotations: IAnnotations = {}; 143 | 144 | if (variableMatch[6]) { 145 | const variableAnnotationRegExp = /(\w+)(?:\:(\w+))?/g; 146 | forEachMatch( 147 | variableAnnotationRegExp, 148 | variableMatch[6], 149 | (variableAnnotationMatch) => { 150 | annotations[variableAnnotationMatch[1]] = 151 | variableAnnotationMatch[2] || true; 152 | } 153 | ); 154 | } 155 | 156 | if (variableMatch[1] === 'const') { 157 | addConstant( 158 | definition.variables, 159 | variableMatch[2], 160 | variableMatch[3], 161 | variableMatch[5] 162 | ); 163 | } else if (variableMatch[1] === 'uniform') { 164 | addUniform(definition.variables, variableMatch[2], variableMatch[3]); 165 | } else { 166 | addRegular(definition.variables, variableMatch[2], variableMatch[3]); 167 | } 168 | }); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /scripts/shader-providers/synthclipse.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'fs-extra'; 2 | import { Provider } from 'nconf'; 3 | import { join } from 'path'; 4 | 5 | import { 6 | IConstVariable, 7 | IShaderDefinition, 8 | IShaderProvider, 9 | } from '../definitions'; 10 | import { forEachMatch } from '../lib'; 11 | import { addConstant, addRegular, addUniform } from '../variables'; 12 | 13 | export class SynthclipseShaderProvider implements IShaderProvider { 14 | private config: Provider; 15 | 16 | constructor(config: Provider) { 17 | this.config = config; 18 | } 19 | 20 | getDefaultConfig() { 21 | return { 22 | constantsPreset: 'Default', 23 | filename: 'shader.stoy', 24 | }; 25 | } 26 | 27 | checkConfig() { 28 | this.config.required(['demo:shader-provider:filename']); 29 | } 30 | 31 | async provide(definition: IShaderDefinition) { 32 | const demoDirectory: string = this.config.get('directory'); 33 | 34 | const shaderContents = await readFile( 35 | join(demoDirectory, this.config.get('demo:shader-provider:filename')), 36 | 'utf8' 37 | ); 38 | 39 | const versionMatch = shaderContents.match(/#version (.+)$/m); 40 | if (versionMatch) { 41 | definition.glslVersion = versionMatch[1]; 42 | } 43 | 44 | const presetFileMatch = shaderContents.match( 45 | /\/\/!\s+/ 46 | ); 47 | if (!presetFileMatch) { 48 | console.warn('Shader does not have any preset file.'); 49 | } else { 50 | const presetContents = await readFile( 51 | join(demoDirectory, presetFileMatch[1]), 52 | 'utf8' 53 | ); 54 | 55 | const presetRegExp = /\/\*!([\s\S]*? { 58 | if ( 59 | presetMatch[2] === 60 | this.config.get('demo:shader-provider:constantsPreset') 61 | ) { 62 | presetFound = true; 63 | 64 | const constantRegExp = /(\w+) = <.*?> (.*)/g; 65 | forEachMatch(constantRegExp, presetMatch[1], (constantMatch) => { 66 | const name = constantMatch[1]; 67 | 68 | const components = constantMatch[2].split(', '); 69 | switch (components.length) { 70 | case 1: 71 | addConstant(definition.variables, 'float', name, components[0]); 72 | break; 73 | 74 | case 2: 75 | addConstant( 76 | definition.variables, 77 | 'vec2', 78 | name, 79 | 'vec2(' + components.join(', ') + ')' 80 | ); 81 | break; 82 | 83 | case 3: 84 | addConstant( 85 | definition.variables, 86 | 'vec3', 87 | name, 88 | 'vec3(' + components.join(', ') + ')' 89 | ); 90 | break; 91 | 92 | case 4: 93 | addConstant( 94 | definition.variables, 95 | 'vec4', 96 | name, 97 | 'vec4(' + components.join(', ') + ')' 98 | ); 99 | break; 100 | } 101 | }); 102 | } 103 | }); 104 | 105 | if (!presetFound) { 106 | console.warn('Preset was not found.'); 107 | } 108 | } 109 | 110 | const startMatch = shaderContents.match( 111 | /^([\s\S]+)\/\/\s*?START([\s\S]+)$/ 112 | ); 113 | if (!startMatch) { 114 | throw new Error('Shader does not contain the magic line "// START".'); 115 | } 116 | 117 | const resolutionDot = this.config.get('forceResolution') ? '.' : ''; 118 | definition.passes.push({ 119 | fragmentCode: startMatch[2] 120 | .replace( 121 | /#ifdef\s+SYNTHCLIPSE_ONLY[\s\S]*?(?:#else([\s\S]*?))?#endif/g, 122 | '$1' 123 | ) 124 | .replace( 125 | /#ifndef\s+SYNTHCLIPSE_ONLY([\s\S]*?)(?:#else[\s\S]*?)?#endif/g, 126 | '$1' 127 | ) 128 | .replace(/synth_Resolution\.x/g, 'resolutionWidth' + resolutionDot) 129 | .replace(/synth_Resolution\.y/g, 'resolutionHeight' + resolutionDot), 130 | }); 131 | 132 | const variableRegExp = /(?:(const|precision|uniform)\s+)?(\w+)\s+(\w+)\s*(=\s*([^;]+))?;$/gm; 133 | forEachMatch(variableRegExp, startMatch[1], (variableMatch) => { 134 | if (variableMatch[1] === 'precision') { 135 | return; 136 | } 137 | 138 | if (!variableMatch[5] !== (variableMatch[1] !== 'const')) { 139 | throw new Error( 140 | `Variable "${variableMatch[3]}" has a value and is not const.` 141 | ); 142 | } 143 | 144 | if (variableMatch[1] === 'const') { 145 | addConstant( 146 | definition.variables, 147 | variableMatch[2], 148 | variableMatch[3], 149 | variableMatch[5] 150 | ); 151 | } else if (variableMatch[1] === 'uniform') { 152 | addUniform(definition.variables, variableMatch[2], variableMatch[3]); 153 | } else { 154 | addRegular(definition.variables, variableMatch[2], variableMatch[3]); 155 | } 156 | }); 157 | 158 | let synthResolutionIndex = -1; 159 | if ( 160 | definition.variables.some((variable, index) => { 161 | if (variable.name === 'synth_Resolution') { 162 | variable.kind = 'const'; 163 | (variable as IConstVariable).value = 164 | 'vec2(resolutionWidth, resolutionHeight)'; 165 | synthResolutionIndex = index; 166 | 167 | if (!this.config.get('forceResolution')) { 168 | addUniform(definition.variables, 'float', 'resolutionWidth'); 169 | addUniform(definition.variables, 'float', 'resolutionHeight'); 170 | } 171 | return true; 172 | } 173 | return false; 174 | }) 175 | ) { 176 | const variables = definition.variables.splice(synthResolutionIndex, 1); 177 | definition.variables.unshift(variables[0]); 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /scripts/shader-minifiers/shader-minifier.ts: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from 'fs-extra'; 2 | import { Provider } from 'nconf'; 3 | import { join } from 'path'; 4 | 5 | import { IShaderDefinition, IShaderMinifier } from '../definitions'; 6 | import { spawn } from '../lib'; 7 | 8 | export class ShaderMinifierShaderMinifier implements IShaderMinifier { 9 | private config: Provider; 10 | 11 | constructor(config: Provider) { 12 | this.config = config; 13 | } 14 | 15 | getDefaultConfig() { 16 | return {}; 17 | } 18 | 19 | checkConfig() { 20 | this.config.required(['tools:shader-minifier']); 21 | 22 | if (process.platform !== 'win32') { 23 | this.config.required(['tools:mono']); 24 | } 25 | } 26 | 27 | async minify(definition: IShaderDefinition) { 28 | const { variables } = definition; 29 | 30 | const buildDirectory: string = this.config.get('paths:build'); 31 | const input = join(buildDirectory, 'shader.glsl'); 32 | const output = join(buildDirectory, 'shader.min.glsl'); 33 | 34 | const shaderLines = ['// Uniform arrays', '']; 35 | 36 | Object.keys(definition.uniformArrays).forEach((type) => { 37 | shaderLines.push( 38 | `uniform ${type} ${definition.uniformArrays[type].name}[${definition.uniformArrays[type].variables.length}];` 39 | ); 40 | }); 41 | 42 | const nonUniformVariables = variables.filter( 43 | (variable) => variable.active && variable.kind !== 'uniform' 44 | ); 45 | 46 | shaderLines.push( 47 | '', 48 | '#pragma separator', 49 | '// Non-uniform global variables', 50 | '' 51 | ); 52 | 53 | nonUniformVariables.forEach((variable) => { 54 | shaderLines.push(variable.type + ' ' + variable.name + ';'); 55 | }); 56 | 57 | if (definition.attributesCode) { 58 | shaderLines.push( 59 | '', 60 | '#pragma separator', 61 | '// Attributes', 62 | '', 63 | definition.attributesCode 64 | ); 65 | } 66 | 67 | if (definition.varyingsCode) { 68 | shaderLines.push( 69 | '', 70 | '#pragma separator', 71 | '// Varyings', 72 | '', 73 | definition.varyingsCode 74 | ); 75 | } 76 | 77 | if (definition.outputsCode) { 78 | shaderLines.push( 79 | '', 80 | '#pragma separator', 81 | '// Outputs', 82 | '', 83 | definition.outputsCode 84 | ); 85 | } 86 | 87 | shaderLines.push('', '#pragma separator', '', definition.commonCode); 88 | 89 | definition.passes.forEach((passName, index) => { 90 | if (passName.vertexCode) { 91 | shaderLines.push( 92 | '', 93 | '#pragma separator', 94 | `// Pass ${index} vertex`, 95 | '' 96 | ); 97 | shaderLines.push(passName.vertexCode); 98 | } 99 | 100 | if (passName.fragmentCode) { 101 | shaderLines.push( 102 | '', 103 | '#pragma separator', 104 | `// Pass ${index} fragment`, 105 | '' 106 | ); 107 | shaderLines.push(passName.fragmentCode); 108 | } 109 | }); 110 | 111 | await writeFile(input, shaderLines.join('\n')); 112 | 113 | const shaderMinifierPath = this.config.get('tools:shader-minifier'); 114 | 115 | const args = [ 116 | '--field-names', 117 | 'rgba', 118 | '--format', 119 | 'none', 120 | '-o', 121 | output, 122 | '-v', 123 | '--', 124 | input, 125 | ]; 126 | 127 | if (process.platform === 'win32') { 128 | await spawn(shaderMinifierPath, args); 129 | } else { 130 | args.unshift(shaderMinifierPath); 131 | await spawn(this.config.get('tools:mono'), args); 132 | } 133 | 134 | const contents = (await readFile(output, 'utf8')).replace(/\r/g, ''); 135 | 136 | const parts = contents.split('#pragma separator'); 137 | 138 | function takePart() { 139 | const part = parts.shift(); 140 | if (typeof part === 'undefined') { 141 | throw new Error('Output is not well-formed.'); 142 | } 143 | 144 | let trimmedPart = part.trim(); 145 | 146 | // HACK https://github.com/laurentlb/Shader_Minifier/issues/19 147 | Object.keys(definition.uniformArrays).forEach((type) => { 148 | const uniformArray = definition.uniformArrays[type]; 149 | if (uniformArray.minifiedName) { 150 | const usageRegExp = new RegExp(`\\b${uniformArray.name}\\b`, 'g'); 151 | trimmedPart = trimmedPart.replace( 152 | usageRegExp, 153 | uniformArray.minifiedName 154 | ); 155 | } 156 | }); 157 | 158 | return trimmedPart; 159 | } 160 | 161 | const uniformsString = takePart(); 162 | const uniformsRegExp = /uniform \w+ (\w+)\[\d+\];/g; 163 | Object.keys(definition.uniformArrays).forEach((type) => { 164 | const uniformMatch = uniformsRegExp.exec(uniformsString); 165 | if (!uniformMatch) { 166 | throw new Error('Output is not well-formed.'); 167 | } 168 | 169 | definition.uniformArrays[type].minifiedName = uniformMatch[1]; 170 | }); 171 | 172 | const nonUniformGlobalsString = takePart(); 173 | const nonUniformGlobalsRegExp = /\w+ (\w+);/g; 174 | nonUniformVariables.forEach((variable) => { 175 | const nonUniformMatch = nonUniformGlobalsRegExp.exec( 176 | nonUniformGlobalsString 177 | ); 178 | if (!nonUniformMatch) { 179 | throw new Error('Output is not well-formed.'); 180 | } 181 | variable.minifiedName = nonUniformMatch[1]; 182 | }); 183 | 184 | if (definition.attributesCode) { 185 | definition.attributesCode = takePart(); 186 | } 187 | 188 | if (definition.varyingsCode) { 189 | definition.varyingsCode = takePart(); 190 | } 191 | 192 | if (definition.outputsCode) { 193 | definition.outputsCode = takePart(); 194 | } 195 | 196 | definition.commonCode = takePart(); 197 | 198 | definition.passes.forEach((passName) => { 199 | if (passName.vertexCode) { 200 | passName.vertexCode = takePart(); 201 | } 202 | 203 | if (passName.fragmentCode) { 204 | passName.fragmentCode = takePart(); 205 | } 206 | }); 207 | 208 | if (parts.length !== 0) { 209 | throw new Error('Output is not well-formed.'); 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /scripts/context.ts: -------------------------------------------------------------------------------- 1 | import { pathExistsSync, statSync } from 'fs-extra'; 2 | import { Provider } from 'nconf'; 3 | import * as yaml from 'nconf-yaml'; 4 | import { dirname, join } from 'path'; 5 | 6 | import { VierKlangAudioSynthesizer } from './audio-synthesizers/4klang'; 7 | import { AchtKlangAudioSynthesizer } from './audio-synthesizers/8klang'; 8 | import { OidosAudioSynthesizer } from './audio-synthesizers/oidos'; 9 | import { RealtimeAudioSynthesizer } from './audio-synthesizers/realtime'; 10 | import { 11 | IAudioSynthesizer, 12 | IContext, 13 | IContextOptions, 14 | IShaderMinifier, 15 | IShaderProvider, 16 | } from './definitions'; 17 | import { ShaderMinifierShaderMinifier } from './shader-minifiers/shader-minifier'; 18 | import { SimpleShaderProvider } from './shader-providers/simple'; 19 | import { SynthclipseShaderProvider } from './shader-providers/synthclipse'; 20 | 21 | export function provideContext(options: IContextOptions): IContext { 22 | const config = new Provider(); 23 | 24 | config.set('capture', options.capture); 25 | 26 | config 27 | .use('memory') 28 | .env() 29 | .argv({ 30 | debug: { 31 | alias: 'd', 32 | default: typeof options.debug !== 'undefined' ? options.debug : false, 33 | type: 'boolean', 34 | }, 35 | directory: { 36 | alias: 'dir', 37 | default: 'demo', 38 | type: 'string', 39 | }, 40 | minify: { 41 | alias: 'm', 42 | default: true, 43 | type: 'boolean', 44 | }, 45 | notify: { 46 | alias: 'n', 47 | default: false, 48 | type: 'boolean', 49 | }, 50 | server: { 51 | alias: 's', 52 | default: true, 53 | type: 'boolean', 54 | }, 55 | zip: { 56 | alias: 'z', 57 | default: false, 58 | describe: '', 59 | type: 'boolean', 60 | }, 61 | }); 62 | 63 | const demoDirectory = config.get('directory'); 64 | 65 | if (pathExistsSync(demoDirectory)) { 66 | const stats = statSync(demoDirectory); 67 | if (!stats.isDirectory()) { 68 | throw new Error('Demo directory is not a directory.'); 69 | } 70 | } else { 71 | throw new Error('Demo directory does not exist.'); 72 | } 73 | 74 | config 75 | .file('demo-local', { 76 | file: join(demoDirectory, 'config.local.yml'), 77 | format: yaml, 78 | }) 79 | .file('local', { 80 | file: 'config.local.yml', 81 | format: yaml, 82 | }) 83 | .file('demo', { 84 | file: join(demoDirectory, 'config.yml'), 85 | format: yaml, 86 | }); 87 | 88 | if ( 89 | config.get('demo:resolution:width') > 0 && 90 | config.get('demo:resolution:height') > 0 91 | ) { 92 | config.set('forceResolution', true); 93 | } 94 | 95 | let audioSynthesizer: IAudioSynthesizer | undefined; 96 | switch (config.get('demo:audio-synthesizer:tool') || 'realtime') { 97 | case '4klang': 98 | audioSynthesizer = new VierKlangAudioSynthesizer(config); 99 | break; 100 | 101 | case '8klang': 102 | audioSynthesizer = new AchtKlangAudioSynthesizer(config); 103 | break; 104 | 105 | case 'none': 106 | break; 107 | 108 | case 'oidos': 109 | audioSynthesizer = new OidosAudioSynthesizer(config); 110 | break; 111 | 112 | case 'realtime': 113 | audioSynthesizer = new RealtimeAudioSynthesizer(); 114 | break; 115 | 116 | default: 117 | throw new Error('Config key "demo:audio-synthesizer:tool" is not valid.'); 118 | } 119 | 120 | if (options.capture) { 121 | config.overrides({ 122 | capture: { 123 | fps: 60, 124 | height: 1080, 125 | width: 1920, 126 | }, 127 | }); 128 | 129 | config.set('forceResolution', true); 130 | } 131 | 132 | let shaderProvider: IShaderProvider; 133 | switch (config.get('demo:shader-provider:tool') || 'simple') { 134 | case 'simple': 135 | shaderProvider = new SimpleShaderProvider(config); 136 | break; 137 | 138 | case 'synthclipse': 139 | shaderProvider = new SynthclipseShaderProvider(config); 140 | break; 141 | 142 | default: 143 | throw new Error('Config key "demo:shader-provider:tool" is not valid.'); 144 | } 145 | 146 | let shaderMinifier: IShaderMinifier | undefined; 147 | if (config.get('minify')) { 148 | switch (config.get('demo:shader-minifier:tool') || 'shader-minifier') { 149 | case 'shader-minifier': 150 | shaderMinifier = new ShaderMinifierShaderMinifier(config); 151 | break; 152 | 153 | default: 154 | throw new Error('Config key "demo:shader-minifier:tool" is not valid.'); 155 | } 156 | } 157 | 158 | config.defaults({ 159 | cl: { 160 | args: config.get('debug') 161 | ? ['/EHsc'] 162 | : ['/O1', '/Oi', '/Oy', '/GR-', '/GS-', '/fp:fast', '/arch:IA32'], 163 | }, 164 | crinkler: { 165 | args: [ 166 | '/ENTRY:main', 167 | '/PRIORITY:NORMAL', 168 | '/COMPMODE:FAST', 169 | '/RANGE:opengl32', 170 | // '/TRUNCATEFLOATS:16', 171 | '/UNSAFEIMPORT', 172 | 'winmm.lib', 173 | 'gdi32.lib', 174 | 'opengl32.lib', 175 | 'kernel32.lib', 176 | 'user32.lib', 177 | ], 178 | }, 179 | demo: { 180 | 'audio-synthesizer': Object.assign( 181 | {}, 182 | audioSynthesizer && audioSynthesizer.getDefaultConfig() 183 | ), 184 | closeWhenFinished: false, 185 | gl: { 186 | constants: [], 187 | functions: [], 188 | }, 189 | hooks: 'hooks.cpp', 190 | loadingBlackScreen: false, 191 | // name 192 | resolution: { 193 | // height 194 | // scale 195 | // width 196 | }, 197 | 'shader-minifier': Object.assign( 198 | {}, 199 | shaderMinifier && shaderMinifier.getDefaultConfig() 200 | ), 201 | 'shader-provider': Object.assign({}, shaderProvider.getDefaultConfig()), 202 | }, 203 | link: { 204 | args: [ 205 | '/SUBSYSTEM:CONSOLE', 206 | '/MACHINE:X86', 207 | 'winmm.lib', 208 | 'gdi32.lib', 209 | 'opengl32.lib', 210 | 'kernel32.lib', 211 | 'user32.lib', 212 | ], 213 | }, 214 | paths: { 215 | build: 'build', 216 | get dist() { 217 | return dirname(config.get('paths:exe')); 218 | }, 219 | exe: join('dist', config.get('demo:name') + '.exe'), 220 | get frames() { 221 | return join(config.get('paths:build'), 'frames'); 222 | }, 223 | }, 224 | server: { 225 | port: 3000, 226 | }, 227 | tools: { 228 | // 4klang 229 | '7z': '7z', 230 | // 8klang 231 | crinkler: 'crinkler', 232 | ffmpeg: 'ffmpeg', 233 | // glew 234 | mono: 'mono', 235 | nasm: 'nasm', 236 | // oidos 237 | python2: 'python', 238 | }, 239 | }); 240 | 241 | config.required([ 242 | 'cl:args', 243 | 'demo:name', 244 | 'paths:build', 245 | 'paths:exe', 246 | 'tools:glew', 247 | ]); 248 | 249 | if (options.capture) { 250 | config.required(['paths:frames', 'tools:ffmpeg']); 251 | } 252 | 253 | if (config.get('debug')) { 254 | config.required(['link:args']); 255 | } else { 256 | config.required(['crinkler:args', 'tools:crinkler']); 257 | } 258 | 259 | if (config.get('zip')) { 260 | config.required(['tools:7z']); 261 | } 262 | 263 | if (audioSynthesizer) { 264 | audioSynthesizer.checkConfig(); 265 | } 266 | 267 | shaderProvider.checkConfig(); 268 | 269 | if (shaderMinifier) { 270 | shaderMinifier.checkConfig(); 271 | } 272 | 273 | return { 274 | audioSynthesizer, 275 | config, 276 | shaderMinifier, 277 | shaderProvider, 278 | }; 279 | } 280 | -------------------------------------------------------------------------------- /scripts/demo.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | import { 4 | ICompilationDefinition, 5 | IContext, 6 | IDemoDefinition, 7 | IShaderDefinition, 8 | Variable, 9 | } from './definitions'; 10 | import { addHooks } from './hooks'; 11 | import { addConstant } from './variables'; 12 | 13 | export async function provideDemo(context: IContext): Promise { 14 | const { config } = context; 15 | const buildDirectory: string = config.get('paths:build'); 16 | const demoDirectory: string = config.get('directory'); 17 | 18 | const variables: Variable[] = []; 19 | 20 | if (config.get('capture')) { 21 | addConstant( 22 | variables, 23 | 'float', 24 | 'resolutionWidth', 25 | config.get('capture:width') 26 | ); 27 | addConstant( 28 | variables, 29 | 'float', 30 | 'resolutionHeight', 31 | config.get('capture:height') 32 | ); 33 | } else { 34 | if ( 35 | config.get('demo:resolution:width') > 0 && 36 | config.get('demo:resolution:height') > 0 37 | ) { 38 | addConstant( 39 | variables, 40 | 'float', 41 | 'resolutionWidth', 42 | config.get('demo:resolution:width') 43 | ); 44 | addConstant( 45 | variables, 46 | 'float', 47 | 'resolutionHeight', 48 | config.get('demo:resolution:height') 49 | ); 50 | } 51 | } 52 | 53 | const shader: IShaderDefinition = { 54 | commonCode: '', 55 | passes: [], 56 | uniformArrays: {}, 57 | variables, 58 | }; 59 | 60 | await context.shaderProvider.provide(shader); 61 | 62 | if (shader.passes.length === 0) { 63 | throw new Error('Shader should define at least one pass.'); 64 | } 65 | 66 | // Replace constants by their value. 67 | // Deactivate unreferenced variables. 68 | variables.forEach((variable) => { 69 | if (variable.active) { 70 | const usageRegExp = new RegExp(`\\b${variable.name}\\b`, 'g'); 71 | 72 | if (variable.kind === 'const') { 73 | console.log( 74 | `Replacing references to constant "${variable.name}" by its value "${variable.value}".` 75 | ); 76 | 77 | if (shader.prologCode) { 78 | shader.prologCode = shader.prologCode.replace( 79 | usageRegExp, 80 | variable.value 81 | ); 82 | } 83 | 84 | shader.commonCode = shader.commonCode.replace( 85 | usageRegExp, 86 | variable.value 87 | ); 88 | 89 | shader.passes.forEach((pass) => { 90 | if (pass.vertexCode) { 91 | pass.vertexCode = pass.vertexCode.replace( 92 | usageRegExp, 93 | variable.value 94 | ); 95 | } 96 | 97 | if (pass.fragmentCode) { 98 | pass.fragmentCode = pass.fragmentCode.replace( 99 | usageRegExp, 100 | variable.value 101 | ); 102 | } 103 | }); 104 | 105 | variable.active = false; 106 | } else { 107 | const commonMatch = shader.commonCode.match(usageRegExp); 108 | let referenced = commonMatch ? commonMatch.length > 0 : false; 109 | 110 | if ( 111 | shader.passes.some((pass) => { 112 | if (pass.fragmentCode) { 113 | const fragmentMatch = pass.fragmentCode.match(usageRegExp); 114 | if (fragmentMatch && fragmentMatch.length > 0) { 115 | return true; 116 | } 117 | } 118 | if (pass.vertexCode) { 119 | const vertexMatch = pass.vertexCode.match(usageRegExp); 120 | if (vertexMatch && vertexMatch.length > 0) { 121 | return true; 122 | } 123 | } 124 | return false; 125 | }) 126 | ) { 127 | referenced = true; 128 | } 129 | 130 | if (!referenced) { 131 | console.log( 132 | `Global variable "${variable.name}" is not referenced and won't be used.` 133 | ); 134 | 135 | variable.active = false; 136 | } 137 | } 138 | } 139 | }); 140 | 141 | variables.forEach((variable) => { 142 | if (!variable.active) { 143 | return; 144 | } 145 | 146 | if (variable.kind === 'uniform') { 147 | if (!shader.uniformArrays[variable.type]) { 148 | shader.uniformArrays[variable.type] = { 149 | name: variable.type + 'Uniforms', 150 | variables: [], 151 | }; 152 | } 153 | 154 | const index = shader.uniformArrays[variable.type].variables.length; 155 | shader.uniformArrays[variable.type].variables.push(variable); 156 | 157 | const usageRegExp = new RegExp(`\\b${variable.name}\\b`, 'g'); 158 | const newWriting = 159 | shader.uniformArrays[variable.type].name + '[' + index + ']'; 160 | 161 | shader.commonCode = shader.commonCode.replace(usageRegExp, newWriting); 162 | 163 | shader.passes.forEach((pass) => { 164 | if (pass.fragmentCode) { 165 | pass.fragmentCode = pass.fragmentCode.replace( 166 | usageRegExp, 167 | newWriting 168 | ); 169 | } 170 | if (pass.vertexCode) { 171 | pass.vertexCode = pass.vertexCode.replace(usageRegExp, newWriting); 172 | } 173 | }); 174 | } 175 | }); 176 | 177 | if (context.shaderMinifier) { 178 | await context.shaderMinifier.minify(shader); 179 | } 180 | 181 | const globalsByTypes: { [type: string]: string[] } = {}; 182 | variables.forEach((variable) => { 183 | if (!variable.active) { 184 | return; 185 | } 186 | 187 | if (variable.kind !== 'uniform') { 188 | if (!globalsByTypes[variable.type]) { 189 | globalsByTypes[variable.type] = []; 190 | } 191 | 192 | let str = variable.minifiedName || variable.name; 193 | if (variable.kind === 'const') { 194 | str += ' = ' + variable.value; 195 | } 196 | globalsByTypes[variable.type].push(str); 197 | } 198 | }); 199 | 200 | if (shader.glslVersion) { 201 | shader.prologCode = `#version ${shader.glslVersion}\n`; 202 | } 203 | 204 | shader.commonCode = 205 | Object.keys(shader.uniformArrays) 206 | .map((type) => { 207 | const uniformArray = shader.uniformArrays[type]; 208 | return `uniform ${type} ${uniformArray.minifiedName || 209 | uniformArray.name}[${uniformArray.variables.length}];`; 210 | }) 211 | .concat( 212 | Object.keys(globalsByTypes).map((type) => { 213 | return type + ' ' + globalsByTypes[type].join(',') + ';'; 214 | }) 215 | ) 216 | .join('') + shader.commonCode; 217 | 218 | const compilation: ICompilationDefinition = { 219 | asm: { 220 | includePaths: [], 221 | nasmArgs: [], 222 | sources: {}, 223 | }, 224 | cpp: { 225 | clArgs: [], 226 | hooks: {}, 227 | sources: {}, 228 | }, 229 | crinklerArgs: [], 230 | linkArgs: [], 231 | }; 232 | 233 | compilation.cpp.sources[join(buildDirectory, 'main.obj')] = { 234 | dependencies: [ 235 | join(buildDirectory, 'demo-data.hpp'), 236 | join(demoDirectory, 'config.yml'), 237 | join(demoDirectory, 'config.local.yml'), 238 | ], 239 | source: join(buildDirectory, 'main.cpp'), 240 | }; 241 | 242 | if (config.get('debug')) { 243 | compilation.cpp.sources[join(buildDirectory, 'debug.obj')] = { 244 | source: join('engine', 'debug.cpp'), 245 | }; 246 | 247 | if (config.get('server')) { 248 | compilation.cpp.sources[join(buildDirectory, 'server.obj')] = { 249 | source: join('engine', 'server.cpp'), 250 | }; 251 | compilation.linkArgs.push('httpapi.lib'); 252 | } 253 | } 254 | 255 | if (config.get('capture')) { 256 | await addHooks(compilation.cpp.hooks, join('engine', 'capture-hooks.cpp')); 257 | } 258 | 259 | if (context.audioSynthesizer) { 260 | await context.audioSynthesizer.addToCompilation(compilation); 261 | } 262 | 263 | try { 264 | await addHooks( 265 | compilation.cpp.hooks, 266 | join(config.get('directory'), config.get('demo:hooks')) 267 | ); 268 | } catch (err) { 269 | if (err.code !== 'ENOENT') { 270 | throw err; 271 | } 272 | } 273 | 274 | return { 275 | compilation, 276 | shader, 277 | }; 278 | } 279 | -------------------------------------------------------------------------------- /engine/main-template.cpp: -------------------------------------------------------------------------------- 1 | #define WIN32_LEAN_AND_MEAN 2 | #define WIN32_EXTRA_LEAN 3 | #include 4 | 5 | #include "../engine/demo.hpp" 6 | 7 | #include "../engine/debug.hpp" 8 | #include "../engine/window.hpp" 9 | 10 | #ifdef SERVER 11 | #include "../engine/server.hpp" 12 | #endif 13 | 14 | #ifdef HAS_HOOK_DECLARATIONS 15 | REPLACE_HOOK_DECLARATIONS 16 | #endif 17 | 18 | #pragma code_seg(".main") 19 | void main() 20 | { 21 | #ifndef FORCE_RESOLUTION 22 | int resolutionWidth = GetSystemMetrics(SM_CXSCREEN); 23 | int resolutionHeight = GetSystemMetrics(SM_CYSCREEN); 24 | 25 | #ifdef SCALE_RESOLUTION 26 | resolutionWidth *= SCALE_RESOLUTION; 27 | resolutionHeight *= SCALE_RESOLUTION; 28 | #endif 29 | 30 | #ifdef uniformResolutionWidth 31 | uniformResolutionWidth = (float)resolutionWidth; 32 | #endif 33 | 34 | #ifdef uniformResolutionHeight 35 | uniformResolutionHeight = (float)resolutionHeight; 36 | #endif 37 | 38 | #endif 39 | 40 | auto hwnd = CreateWindowA("static", NULL, WS_POPUP | WS_VISIBLE, 0, 0, resolutionWidth, resolutionHeight, NULL, NULL, NULL, 0); 41 | auto hdc = GetDC(hwnd); 42 | SetPixelFormat(hdc, ChoosePixelFormat(hdc, &pfd), &pfd); 43 | wglMakeCurrent(hdc, wglCreateContext(hdc)); 44 | ShowCursor(FALSE); 45 | 46 | #ifdef DEBUG 47 | debugHwnd = hwnd; 48 | #endif 49 | 50 | #ifdef LOADING_BLACK_SCREEN 51 | wglSwapLayerBuffers(hdc, WGL_SWAP_MAIN_PLANE); 52 | #endif 53 | 54 | loadGLFunctions(); 55 | 56 | #ifdef DEBUG 57 | // Display Opengl info in console. 58 | std::cout << "OpenGL version: " << glGetString(GL_VERSION) << std::endl; 59 | // std::cout << "OpenGL extensions: " << glGetString(GL_EXTENSIONS) << std::endl; 60 | std::cout << std::endl; 61 | 62 | //TODO : here we set a callback function for GL to use when an error is encountered. Does not seem to work ! 63 | /* 64 | glEnable(GL_DEBUG_OUTPUT); 65 | glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); 66 | glDebugMessageCallback(showDebugMessageFromOpenGL, NULL); 67 | glDebugMessageControl( 68 | GL_DONT_CARE, 69 | GL_DONT_CARE, 70 | GL_DONT_CARE, 71 | 0, 72 | 0, 73 | true); 74 | */ 75 | #endif 76 | 77 | #ifdef SERVER 78 | StartServerOptions startServerOptions = {}; 79 | startServerOptions.port = SERVER_PORT; 80 | #endif 81 | 82 | #if PASS_COUNT == 1 83 | GLint program = glCreateProgram(); 84 | checkGLError(); 85 | 86 | #ifdef HAS_SHADER_PASS_0_VERTEX_CODE 87 | const char *vertexShaderSources[] = { 88 | #ifdef HAS_SHADER_PROLOG_CODE 89 | shaderPrologCode, 90 | #endif 91 | #ifdef HAS_SHADER_VERTEX_SPECIFIC_CODE 92 | shaderVertexSpecificCode, 93 | #endif 94 | #ifdef HAS_SHADER_COMMON_CODE 95 | shaderCommonCode, 96 | #endif 97 | shaderPassCodes[0], 98 | }; 99 | 100 | GLint vertexShader = glCreateShader(GL_VERTEX_SHADER); 101 | glShaderSource(vertexShader, sizeof(vertexShaderSources) / sizeof(vertexShaderSources[0]), vertexShaderSources, 0); 102 | glCompileShader(vertexShader); 103 | checkShaderCompilation(vertexShader); 104 | glAttachShader(program, vertexShader); 105 | #endif 106 | 107 | #ifdef HAS_SHADER_PASS_0_FRAGMENT_CODE 108 | const char *fragmentShaderSources[] = { 109 | #ifdef HAS_SHADER_PROLOG_CODE 110 | shaderPrologCode, 111 | #endif 112 | #ifdef HAS_SHADER_FRAGMENT_SPECIFIC_CODE 113 | shaderFragmentSpecificCode, 114 | #endif 115 | #ifdef HAS_SHADER_COMMON_CODE 116 | shaderCommonCode, 117 | #endif 118 | shaderPassCodes[1], 119 | }; 120 | 121 | GLint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); 122 | glShaderSource(fragmentShader, sizeof(fragmentShaderSources) / sizeof(fragmentShaderSources[0]), fragmentShaderSources, 0); 123 | glCompileShader(fragmentShader); 124 | checkShaderCompilation(fragmentShader); 125 | glAttachShader(program, fragmentShader); 126 | #endif 127 | 128 | glLinkProgram(program); 129 | checkGLError(); 130 | 131 | #ifdef DEBUG 132 | std::cout << "Uniform locations:" << std::endl; 133 | DEBUG_DISPLAY_UNIFORM_LOATIONS(program); 134 | std::cout << std::endl; 135 | #endif 136 | 137 | #ifdef SERVER 138 | startServerOptions.programs = &program; 139 | #endif 140 | 141 | glUseProgram(program); 142 | checkGLError(); 143 | 144 | #else 145 | GLint programs[PASS_COUNT]; 146 | 147 | for (auto i = 0; i < PASS_COUNT; ++i) 148 | { 149 | programs[i] = glCreateProgram(); 150 | checkGLError(); 151 | 152 | if (shaderPassCodes[i * 2]) 153 | { 154 | const char *vertexShaderSources[] = { 155 | #ifdef HAS_SHADER_PROLOG_CODE 156 | shaderPrologCode, 157 | #endif 158 | #ifdef HAS_SHADER_VERTEX_SPECIFIC_CODE 159 | shaderVertexSpecificCode, 160 | #endif 161 | #ifdef HAS_SHADER_COMMON_CODE 162 | shaderCommonCode, 163 | #endif 164 | shaderPassCodes[i * 2], 165 | }; 166 | 167 | GLint vertexShader = glCreateShader(GL_VERTEX_SHADER); 168 | checkGLError(); 169 | glShaderSource(vertexShader, sizeof(vertexShaderSources) / sizeof(vertexShaderSources[0]), vertexShaderSources, 0); 170 | checkGLError(); 171 | glCompileShader(vertexShader); 172 | checkGLError(); 173 | checkShaderCompilation(vertexShader); 174 | glAttachShader(programs[i], vertexShader); 175 | checkGLError(); 176 | 177 | #ifdef DEBUG 178 | debugVertexShaders[i] = vertexShader; 179 | #endif 180 | } 181 | 182 | if (shaderPassCodes[i * 2 + 1]) 183 | { 184 | const char *fragmentShaderSources[] = { 185 | #ifdef HAS_SHADER_PROLOG_CODE 186 | shaderPrologCode, 187 | #endif 188 | #ifdef HAS_SHADER_FRAGMENT_SPECIFIC_CODE 189 | shaderFragmentSpecificCode, 190 | #endif 191 | #ifdef HAS_SHADER_COMMON_CODE 192 | shaderCommonCode, 193 | #endif 194 | shaderPassCodes[i * 2 + 1], 195 | }; 196 | 197 | GLint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); 198 | checkGLError(); 199 | glShaderSource(fragmentShader, sizeof(fragmentShaderSources) / sizeof(fragmentShaderSources[0]), fragmentShaderSources, 0); 200 | checkGLError(); 201 | glCompileShader(fragmentShader); 202 | checkShaderCompilation(fragmentShader); 203 | glAttachShader(programs[i], fragmentShader); 204 | checkGLError(); 205 | 206 | #ifdef DEBUG 207 | debugFragmentShaders[i] = fragmentShader; 208 | #endif 209 | } 210 | 211 | glLinkProgram(programs[i]); 212 | checkGLError(); 213 | 214 | #ifdef DEBUG 215 | std::cout << "Uniform locations in pass " << i << ":" << std::endl; 216 | DEBUG_DISPLAY_UNIFORM_LOATIONS(programs[i]); 217 | std::cout << std::endl; 218 | #endif 219 | 220 | #ifdef SERVER 221 | startServerOptions.programs = programs; 222 | #endif 223 | } 224 | #endif 225 | 226 | #ifdef SERVER 227 | serverStart(startServerOptions); 228 | #endif 229 | 230 | #ifdef HAS_HOOK_INITIALIZE 231 | REPLACE_HOOK_INITIALIZE 232 | #endif 233 | 234 | #if !defined(CAPTURE) && defined(HAS_HOOK_AUDIO_START) 235 | REPLACE_HOOK_AUDIO_START 236 | #endif 237 | 238 | do 239 | { 240 | // Avoid 'not responding' system messages. 241 | PeekMessage(NULL, NULL, 0, 0, PM_REMOVE); 242 | 243 | #ifdef HAS_HOOK_TIME 244 | REPLACE_HOOK_TIME 245 | #elif defined(HAS_HOOK_CAPTURE_TIME) 246 | REPLACE_HOOK_CAPTURE_TIME 247 | #elif defined(HAS_HOOK_AUDIO_TIME) 248 | REPLACE_HOOK_AUDIO_TIME 249 | #endif 250 | 251 | #ifdef HAS_HOOK_RENDER 252 | REPLACE_HOOK_RENDER 253 | #else 254 | #ifdef uniformTime 255 | uniformTime = time; 256 | #endif 257 | 258 | glUniform1fv(0, FLOAT_UNIFORM_COUNT, floatUniforms); 259 | checkGLError(); 260 | 261 | glRects(-1, -1, 1, 1); 262 | checkGLError(); 263 | #endif 264 | 265 | #ifdef HAS_HOOK_CAPTURE_FRAME 266 | REPLACE_HOOK_CAPTURE_FRAME 267 | #endif 268 | 269 | #ifdef SERVER 270 | serverUpdate(); 271 | #endif 272 | 273 | wglSwapLayerBuffers(hdc, WGL_SWAP_MAIN_PLANE); 274 | } while ( 275 | #if defined(CLOSE_WHEN_FINISHED) 276 | #ifdef DURATION 277 | time <= DURATION 278 | #elif defined(HAS_HOOK_CAPTURE_IS_PLAYING) 279 | REPLACE_HOOK_CAPTURE_IS_PLAYING 280 | #elif defined(HAS_HOOK_AUDIO_IS_PLAYING) 281 | REPLACE_HOOK_AUDIO_IS_PLAYING 282 | #else 283 | #error 284 | #endif 285 | && 286 | #endif 287 | !GetAsyncKeyState(VK_ESCAPE)); 288 | 289 | #ifdef SERVER 290 | serverStop(); 291 | #endif 292 | 293 | ExitProcess(0); 294 | } 295 | -------------------------------------------------------------------------------- /scripts/generate-source-codes.ts: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from 'fs-extra'; 2 | import { join } from 'path'; 3 | 4 | import { IContext, IDemoDefinition } from './definitions'; 5 | import { replaceHooks } from './hooks'; 6 | import { forEachMatch } from './lib'; 7 | 8 | export async function writeDemoData(context: IContext, demo: IDemoDefinition) { 9 | const buildDirectory: string = context.config.get('paths:build'); 10 | 11 | function escape(str: string) { 12 | return str 13 | .replace(/\n/g, '\\n') 14 | .replace(/\r/g, '') 15 | .replace(/"/g, '\\"'); 16 | } 17 | 18 | const fileContents = ['#pragma once', '']; 19 | 20 | if (context.config.get('debug')) { 21 | fileContents.push('#define DEBUG', ''); 22 | 23 | if (context.config.get('server')) { 24 | fileContents.push( 25 | '#define SERVER', 26 | `#define SERVER_PORT ${context.config.get('server:port')}`, 27 | '' 28 | ); 29 | } 30 | } 31 | 32 | let debugDisplayUniformLocations = ''; 33 | 34 | Object.keys(demo.shader.uniformArrays).forEach((type) => { 35 | const uniformArray = demo.shader.uniformArrays[type]; 36 | 37 | const typeUpperCase = type.toUpperCase(); 38 | const nameMacro = `${typeUpperCase}_UNIFORM_NAME`; 39 | const countMacro = `${typeUpperCase}_UNIFORM_COUNT`; 40 | const arrayName = `${type}Uniforms`; 41 | const cppType = type.startsWith('sampler') ? 'int' : type; 42 | 43 | fileContents.push( 44 | `#define ${nameMacro} "${uniformArray.minifiedName || 45 | uniformArray.name}"`, 46 | `#define ${countMacro} ${uniformArray.variables.length}`, 47 | `static ${cppType} ${arrayName}[${countMacro}];` 48 | ); 49 | 50 | uniformArray.variables.forEach((variable, index) => { 51 | const name = variable.name 52 | .replace(/^\w|\b\w/g, (letter) => letter.toUpperCase()) 53 | .replace(/_+/g, ''); 54 | fileContents.push(`#define uniform${name} ${arrayName}[${index}]`); 55 | }); 56 | 57 | fileContents.push(''); 58 | 59 | debugDisplayUniformLocations += `std::cout << "${type}: " << glGetUniformLocation(PROGRAM, ${nameMacro}) << std::endl; \\\n`; 60 | }); 61 | 62 | fileContents.push( 63 | '#define DEBUG_DISPLAY_UNIFORM_LOATIONS(PROGRAM) \\', 64 | debugDisplayUniformLocations 65 | ); 66 | 67 | let prologCode = demo.shader.prologCode; 68 | let commonCode = demo.shader.commonCode; 69 | 70 | const stageVariableRegExp = /\w+ [\w,]+;/g; 71 | let vertexSpecificCode = ''; 72 | let fragmentSpecificCode = ''; 73 | 74 | if (demo.shader.attributesCode) { 75 | forEachMatch(stageVariableRegExp, demo.shader.attributesCode, (match) => { 76 | vertexSpecificCode += 'in ' + match[0]; 77 | }); 78 | } 79 | 80 | if (demo.shader.varyingsCode) { 81 | forEachMatch(stageVariableRegExp, demo.shader.varyingsCode, (match) => { 82 | vertexSpecificCode += 'out ' + match[0]; 83 | fragmentSpecificCode += 'in ' + match[0]; 84 | }); 85 | } 86 | 87 | if (demo.shader.outputsCode) { 88 | forEachMatch(stageVariableRegExp, demo.shader.outputsCode, (match) => { 89 | fragmentSpecificCode += 'out ' + match[0]; 90 | }); 91 | } 92 | 93 | if (prologCode && !vertexSpecificCode && !fragmentSpecificCode) { 94 | commonCode = prologCode + commonCode; 95 | prologCode = ''; 96 | } 97 | 98 | if (prologCode) { 99 | fileContents.push( 100 | '#define HAS_SHADER_PROLOG_CODE', 101 | `static const char *shaderPrologCode = "${escape(prologCode)}";`, 102 | '' 103 | ); 104 | } 105 | 106 | if (vertexSpecificCode) { 107 | fileContents.push( 108 | '#define HAS_SHADER_VERTEX_SPECIFIC_CODE', 109 | `static const char *shaderVertexSpecificCode = "${escape( 110 | vertexSpecificCode 111 | )}";`, 112 | '' 113 | ); 114 | } 115 | 116 | if (fragmentSpecificCode) { 117 | fileContents.push( 118 | '#define HAS_SHADER_FRAGMENT_SPECIFIC_CODE', 119 | `static const char *shaderFragmentSpecificCode = "${escape( 120 | fragmentSpecificCode 121 | )}";`, 122 | '' 123 | ); 124 | } 125 | 126 | if (commonCode) { 127 | fileContents.push( 128 | '#define HAS_SHADER_COMMON_CODE', 129 | `static const char *shaderCommonCode = "${escape(commonCode)}";`, 130 | '' 131 | ); 132 | } 133 | 134 | fileContents.push('#define PASS_COUNT ' + demo.shader.passes.length); 135 | 136 | fileContents.push('static const char *shaderPassCodes[] = {'); 137 | demo.shader.passes.forEach((pass, index) => { 138 | if (pass.vertexCode) { 139 | fileContents.push( 140 | `#define HAS_SHADER_PASS_${index}_VERTEX_CODE`, 141 | `"${escape(pass.vertexCode)}",` 142 | ); 143 | } else { 144 | fileContents.push('nullptr,'); 145 | } 146 | 147 | if (pass.fragmentCode) { 148 | fileContents.push( 149 | `#define HAS_SHADER_PASS_${index}_FRAGMENT_CODE`, 150 | `"${escape(pass.fragmentCode)}",` 151 | ); 152 | } else { 153 | fileContents.push('nullptr,'); 154 | } 155 | }); 156 | fileContents.push('};', ''); 157 | 158 | if (context.config.get('demo:audio-synthesizer:tool') === 'shader') { 159 | fileContents.unshift( 160 | '#include "audio-shader.cpp"', 161 | '#define AUDIO_TEXTURE' 162 | ); 163 | } 164 | 165 | if (context.config.get('capture')) { 166 | fileContents.push( 167 | '#define CAPTURE', 168 | '#define CAPTURE_FPS ' + context.config.get('capture:fps'), 169 | '#define FORCE_RESOLUTION', 170 | 'static const constexpr int resolutionWidth = ' + 171 | context.config.get('capture:width') + 172 | ';', 173 | 'static const constexpr int resolutionHeight = ' + 174 | context.config.get('capture:height') + 175 | ';' 176 | ); 177 | } else { 178 | fileContents.push('static void captureFrame() {}'); 179 | 180 | if ( 181 | context.config.get('demo:resolution:width') > 0 && 182 | context.config.get('demo:resolution:height') > 0 183 | ) { 184 | fileContents.push( 185 | '#define FORCE_RESOLUTION', 186 | 'static const constexpr int resolutionWidth = ' + 187 | context.config.get('demo:resolution:width') + 188 | ';', 189 | 'static const constexpr int resolutionHeight = ' + 190 | context.config.get('demo:resolution:height') + 191 | ';' 192 | ); 193 | } 194 | 195 | const scale = context.config.get('demo:resolution:scale'); 196 | if (scale > 0 && scale !== 1) { 197 | fileContents.push('#define SCALE_RESOLUTION ' + scale); 198 | } 199 | } 200 | 201 | fileContents.push(''); 202 | 203 | const duration = context.config.get('demo:duration'); 204 | if (duration) { 205 | fileContents.push(`#define DURATION ${duration}`, ''); 206 | } 207 | 208 | if ( 209 | duration || 210 | context.config.get('capture') || 211 | context.config.get('demo:closeWhenFinished') 212 | ) { 213 | fileContents.push('#define CLOSE_WHEN_FINISHED', ''); 214 | } 215 | 216 | if (context.config.get('demo:loadingBlackScreen')) { 217 | fileContents.push('#define LOADING_BLACK_SCREEN', ''); 218 | } 219 | 220 | Object.keys(demo.compilation.cpp.hooks).forEach((hookName) => { 221 | fileContents.push(`#define HAS_HOOK_${hookName.toUpperCase()}`); 222 | }); 223 | 224 | await writeFile( 225 | join(buildDirectory, 'demo-data.hpp'), 226 | fileContents.join('\n') 227 | ); 228 | } 229 | 230 | export async function writeDemoGl(context: IContext) { 231 | const fileContents = [ 232 | '#pragma once', 233 | '', 234 | '#include ', 235 | '', 236 | '#define GLAPIENTRY __stdcall', 237 | 'typedef char GLchar;', 238 | 'typedef ptrdiff_t GLintptr;', 239 | 'typedef ptrdiff_t GLsizeiptr;', 240 | 'typedef void (APIENTRY * GLDEBUGPROC)(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length,const GLchar * message,const void * userParam);', 241 | '', 242 | ]; 243 | 244 | const glConstantNames = ['GL_FRAGMENT_SHADER', 'GL_VERTEX_SHADER']; 245 | 246 | const glFunctionNames = [ 247 | 'glAttachShader', 248 | 'glCompileShader', 249 | 'glCreateProgram', 250 | 'glCreateShader', 251 | 'glLinkProgram', 252 | 'glShaderSource', 253 | 'glUniform1fv', 254 | 'glUseProgram', 255 | ]; 256 | 257 | function addGlConstantName(constantName: string) { 258 | if (glConstantNames.indexOf(constantName) === -1) { 259 | glConstantNames.push(constantName); 260 | } 261 | } 262 | 263 | function addGlFunctionName(functionName: string) { 264 | if (glFunctionNames.indexOf(functionName) === -1) { 265 | glFunctionNames.push(functionName); 266 | } 267 | } 268 | 269 | function addFromConfig(key: string, action: (name: string) => void) { 270 | const value = context.config.get(key); 271 | if (Array.isArray(value)) { 272 | value.forEach(action); 273 | } 274 | } 275 | 276 | addFromConfig('demo:gl:constants', addGlConstantName); 277 | addFromConfig('demo:gl:functions', addGlFunctionName); 278 | 279 | const glewContents = await readFile( 280 | join(context.config.get('tools:glew'), 'include', 'GL', 'glew.h'), 281 | 'utf8' 282 | ); 283 | 284 | glConstantNames.forEach((constantName: string) => { 285 | const match = glewContents.match( 286 | new RegExp(`^#define ${constantName} .+$`, 'gm') 287 | ); 288 | if (match) { 289 | fileContents.push(match[0]); 290 | } else { 291 | console.warn(`OpenGL constant ${constantName} does not seem to exist.`); 292 | } 293 | }); 294 | 295 | const glExtFunctionNames: string[] = []; 296 | 297 | glFunctionNames.forEach((functionName, index) => { 298 | const typedefName = 'PFN' + functionName.toUpperCase() + 'PROC'; 299 | const match = glewContents.match( 300 | new RegExp(`^typedef \\w+ \\(GLAPIENTRY \\* ${typedefName}\\).+$`, 'gm') 301 | ); 302 | if (match) { 303 | fileContents.push( 304 | match[0], 305 | `#define ${functionName} ((${typedefName})glExtFunctions[${index}])` 306 | ); 307 | glExtFunctionNames.push(`"${functionName}"`); 308 | } else { 309 | console.warn(`OpenGL function ${functionName} does not seem to exist.`); 310 | glExtFunctionNames.push(`0`); 311 | } 312 | }); 313 | 314 | fileContents.push( 315 | '#define GL_EXT_FUNCTION_COUNT ' + glExtFunctionNames.length, 316 | 'static const char *glExtFunctionNames[GL_EXT_FUNCTION_COUNT] = { ', 317 | glExtFunctionNames.join(',\n'), 318 | ' };', 319 | 'static void *glExtFunctions[GL_EXT_FUNCTION_COUNT];', 320 | '' 321 | ); 322 | 323 | const buildDirectory: string = context.config.get('paths:build'); 324 | 325 | await writeFile(join(buildDirectory, 'demo-gl.hpp'), fileContents.join('\n')); 326 | } 327 | 328 | export async function writeDemoMain(context: IContext, demo: IDemoDefinition) { 329 | const buildDirectory: string = context.config.get('paths:build'); 330 | 331 | let mainCode = await readFile(join('engine', 'main-template.cpp'), 'utf8'); 332 | 333 | mainCode = replaceHooks(demo.compilation.cpp.hooks, mainCode); 334 | 335 | await writeFile(join(buildDirectory, 'main.cpp'), mainCode); 336 | } 337 | -------------------------------------------------------------------------------- /engine/server.cpp: -------------------------------------------------------------------------------- 1 | // https://docs.microsoft.com/en-us/windows/win32/http/http-server-sample-application 2 | 3 | #define UNICODE 4 | #define _WIN32_WINNT 0x0600 5 | #define WIN32_LEAN_AND_MEAN 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "server.hpp" 13 | 14 | #include "debug.hpp" 15 | 16 | #define INITIALIZE_HTTP_RESPONSE(resp, status, reason) \ 17 | do \ 18 | { \ 19 | RtlZeroMemory((resp), sizeof(*(resp))); \ 20 | (resp)->StatusCode = (status); \ 21 | (resp)->pReason = (reason); \ 22 | (resp)->ReasonLength = (USHORT)strlen(reason); \ 23 | } while (FALSE) 24 | 25 | #define ADD_KNOWN_HEADER(Response, HeaderId, RawValue) \ 26 | do \ 27 | { \ 28 | (Response).Headers.KnownHeaders[(HeaderId)].pRawValue = \ 29 | (RawValue); \ 30 | (Response).Headers.KnownHeaders[(HeaderId)].RawValueLength = \ 31 | (USHORT)strlen(RawValue); \ 32 | } while (FALSE) 33 | 34 | #define ALLOC_MEM(cb) HeapAlloc(GetProcessHeap(), 0, (cb)) 35 | 36 | #define FREE_MEM(ptr) HeapFree(GetProcessHeap(), 0, (ptr)) 37 | 38 | static HANDLE hReqQueue = NULL; 39 | static HANDLE hCompletionPort = NULL; 40 | static StartServerOptions options; 41 | 42 | struct Buffer 43 | { 44 | char *data = nullptr; 45 | std::size_t length = 0; 46 | 47 | ~Buffer() 48 | { 49 | free(); 50 | } 51 | 52 | bool alloc(std::size_t _length) 53 | { 54 | data = new char[_length]; 55 | length = _length; 56 | return data != nullptr; 57 | } 58 | 59 | bool realloc(std::size_t length) 60 | { 61 | free(); 62 | return alloc(length); 63 | } 64 | 65 | void free() 66 | { 67 | delete[] data; 68 | } 69 | }; 70 | 71 | std::ostream &operator<<(std::ostream &os, const Buffer &buffer) 72 | { 73 | os << "[" << buffer.length << "]"; 74 | os.write(buffer.data, buffer.length); 75 | return os; 76 | } 77 | 78 | struct Context : OVERLAPPED 79 | { 80 | Buffer requestBuffer; 81 | HANDLE hFile; 82 | 83 | bool initialize() 84 | { 85 | return requestBuffer.alloc(sizeof(HTTP_REQUEST) + 2048); 86 | } 87 | 88 | const PHTTP_REQUEST getRequest() const 89 | { 90 | return reinterpret_cast(requestBuffer.data); 91 | } 92 | }; 93 | 94 | static Context context; 95 | 96 | static ULONG initializeAsyncReceive(HTTP_REQUEST_ID requestId = 0) 97 | { 98 | RtlZeroMemory(&context, sizeof(OVERLAPPED)); 99 | 100 | ULONG result = HttpReceiveHttpRequest( 101 | hReqQueue, // Req Queue 102 | requestId, // Req ID 103 | 0, // Flags 104 | context.getRequest(), // HTTP request buffer 105 | context.requestBuffer.length, // req buffer length 106 | nullptr, // bytes received 107 | &context // LPOVERLAPPED 108 | ); 109 | 110 | return result; 111 | } 112 | 113 | static std::unique_ptr readBody(const HTTP_REQUEST *pRequest) 114 | { 115 | ULONG result; 116 | 117 | std::unique_ptr totalBuffer{new Buffer{0}}; 118 | 119 | ULONG EntityBufferLength = 2048; 120 | PUCHAR pEntityBuffer = (PUCHAR)ALLOC_MEM(EntityBufferLength); 121 | 122 | if (pEntityBuffer == NULL) 123 | { 124 | std::cerr << "Insufficient resources." << std::endl; 125 | return totalBuffer; 126 | } 127 | 128 | ULONG BytesRead; 129 | 130 | if (pRequest->Flags & HTTP_REQUEST_FLAG_MORE_ENTITY_BODY_EXISTS) 131 | { 132 | for (;;) 133 | { 134 | BytesRead = 0; 135 | result = HttpReceiveRequestEntityBody( 136 | hReqQueue, 137 | pRequest->RequestId, 138 | 0, 139 | pEntityBuffer, 140 | EntityBufferLength, 141 | &BytesRead, 142 | NULL); 143 | 144 | switch (result) 145 | { 146 | case NO_ERROR: 147 | case ERROR_HANDLE_EOF: 148 | if (BytesRead != 0) 149 | { 150 | std::unique_ptr newTotalBuffer{new Buffer}; 151 | if (newTotalBuffer->alloc(totalBuffer->length + BytesRead)) 152 | { 153 | memcpy(newTotalBuffer->data, totalBuffer->data, totalBuffer->length); 154 | memcpy(newTotalBuffer->data + totalBuffer->length, pEntityBuffer, BytesRead); 155 | totalBuffer.swap(newTotalBuffer); 156 | } 157 | } 158 | if (result == ERROR_HANDLE_EOF) 159 | { 160 | return totalBuffer; 161 | } 162 | break; 163 | 164 | default: 165 | std::cerr << "HttpReceiveRequestEntityBody failed with " << result << "." << std::endl; 166 | return totalBuffer; 167 | } 168 | } 169 | } 170 | 171 | return totalBuffer; 172 | } 173 | 174 | static DWORD SendHttpResponse( 175 | const HTTP_REQUEST *pRequest, 176 | USHORT StatusCode, 177 | const char *pReason, 178 | const char *pEntityString) 179 | { 180 | HTTP_RESPONSE response; 181 | HTTP_DATA_CHUNK dataChunk; 182 | DWORD result; 183 | DWORD bytesSent; 184 | 185 | INITIALIZE_HTTP_RESPONSE(&response, StatusCode, pReason); 186 | ADD_KNOWN_HEADER(response, HttpHeaderContentType, "text/html"); 187 | 188 | if (pEntityString) 189 | { 190 | dataChunk.DataChunkType = HttpDataChunkFromMemory; 191 | dataChunk.FromMemory.pBuffer = const_cast(pEntityString); 192 | dataChunk.FromMemory.BufferLength = (ULONG)strlen(pEntityString); 193 | 194 | response.EntityChunkCount = 1; 195 | response.pEntityChunks = &dataChunk; 196 | } 197 | 198 | result = HttpSendHttpResponse( 199 | hReqQueue, 200 | pRequest->RequestId, 201 | 0, 202 | &response, 203 | NULL, 204 | &bytesSent, 205 | NULL, 206 | 0, 207 | NULL, 208 | NULL); 209 | 210 | if (result != NO_ERROR) 211 | { 212 | std::cerr << "HttpSendHttpResponse failed with " << result << "." << std::endl; 213 | } 214 | 215 | return result; 216 | } 217 | 218 | static void handleRequest(const HTTP_REQUEST *pRequest) 219 | { 220 | ULONG result; 221 | 222 | switch (pRequest->Verb) 223 | { 224 | case HttpVerbGET: 225 | result = SendHttpResponse(pRequest, 404, "Not Found", nullptr); 226 | break; 227 | 228 | case HttpVerbPOST: 229 | int passIndex; 230 | wchar_t shaderStage[16]; 231 | if (swscanf_s(pRequest->CookedUrl.pAbsPath, L"/passes/%d/%s", &passIndex, shaderStage, sizeof(shaderStage)) == 2) 232 | { 233 | if (passIndex < PASS_COUNT) 234 | { 235 | auto body = readBody(pRequest); 236 | GLint lengths[] = {(GLint)body->length}; 237 | 238 | if (!wcscmp(shaderStage, L"fragment")) 239 | { 240 | std::cout << "Setting new fragment shader for pass " << passIndex << ":" << std::endl; 241 | std::cout << *body << std::endl; 242 | 243 | GLint shader = debugFragmentShaders[passIndex]; 244 | checkGLError(); 245 | glShaderSource(shader, 1, &body->data, lengths); 246 | checkGLError(); 247 | glCompileShader(shader); 248 | checkShaderCompilation(shader); 249 | glLinkProgram(options.programs[passIndex]); 250 | checkGLError(); 251 | 252 | result = SendHttpResponse(pRequest, 200, "OK", nullptr); 253 | } 254 | else if (!wcscmp(shaderStage, L"vertex")) 255 | { 256 | std::cout << "Setting new vertex shader for pass " << passIndex << ":" << std::endl; 257 | std::cout << *body << std::endl; 258 | 259 | GLint shader = debugVertexShaders[passIndex]; 260 | checkGLError(); 261 | glShaderSource(shader, 1, &body->data, lengths); 262 | checkGLError(); 263 | glCompileShader(shader); 264 | checkShaderCompilation(shader); 265 | glLinkProgram(options.programs[passIndex]); 266 | checkGLError(); 267 | 268 | result = SendHttpResponse(pRequest, 200, "OK", nullptr); 269 | } 270 | else 271 | { 272 | result = SendHttpResponse(pRequest, 404, "Not Found", nullptr); 273 | } 274 | } 275 | else 276 | { 277 | result = SendHttpResponse(pRequest, 404, "Not Found", nullptr); 278 | } 279 | } 280 | else 281 | { 282 | result = SendHttpResponse(pRequest, 404, "Not Found", nullptr); 283 | } 284 | break; 285 | 286 | default: 287 | result = SendHttpResponse(pRequest, 404, "Not Found", nullptr); 288 | break; 289 | } 290 | 291 | if (result != NO_ERROR) 292 | { 293 | std::cerr << "Error handling request: 0x" << std::hex << result << "." << std::endl; 294 | } 295 | } 296 | 297 | void serverUpdate() 298 | { 299 | ULONG result; 300 | DWORD bytesRead; 301 | ULONG_PTR pKey; 302 | LPOVERLAPPED pOverlapped; 303 | 304 | if (GetQueuedCompletionStatus(hCompletionPort, &bytesRead, &pKey, &pOverlapped, 0)) 305 | { 306 | result = ERROR_SUCCESS; 307 | } 308 | else 309 | { 310 | result = GetLastError(); 311 | } 312 | 313 | switch (result) 314 | { 315 | case WAIT_TIMEOUT: 316 | // Fine. 317 | break; 318 | 319 | case ERROR_SUCCESS: 320 | { 321 | auto context = reinterpret_cast(pOverlapped); 322 | 323 | handleRequest(context->getRequest()); 324 | 325 | initializeAsyncReceive(); 326 | break; 327 | } 328 | 329 | case ERROR_MORE_DATA: 330 | { 331 | auto requestId = context.getRequest()->RequestId; 332 | 333 | if (!context.requestBuffer.realloc(bytesRead)) 334 | { 335 | result = ERROR_NOT_ENOUGH_MEMORY; 336 | break; 337 | } 338 | 339 | initializeAsyncReceive(requestId); 340 | break; 341 | } 342 | 343 | default: 344 | std::cerr << "GetQueuedCompletionStatus error 0x" << std::hex << result << "." << std::endl; 345 | break; 346 | } 347 | } 348 | 349 | static wchar_t urlBuffer[256]; 350 | 351 | void serverStart(const StartServerOptions &_options) 352 | { 353 | options = _options; 354 | 355 | ULONG result = HttpInitialize(HTTPAPI_VERSION_1, HTTP_INITIALIZE_SERVER, NULL); 356 | 357 | if (result != NO_ERROR) 358 | { 359 | std::cerr << "HttpInitialize failed with " << result << "." << std::endl; 360 | return; 361 | } 362 | 363 | result = HttpCreateHttpHandle(&hReqQueue, 0); 364 | 365 | if (result != NO_ERROR) 366 | { 367 | std::cerr << "HttpCreateHttpHandle failed with " << result << "." << std::endl; 368 | } 369 | 370 | swprintf_s(urlBuffer, sizeof(urlBuffer), L"http://localhost:%d/", options.port); 371 | 372 | result = HttpAddUrl(hReqQueue, urlBuffer, NULL); 373 | 374 | if (result != NO_ERROR) 375 | { 376 | std::cerr << "HttpAddUrl failed with " << result << "." << std::endl; 377 | return; 378 | } 379 | 380 | hCompletionPort = CreateIoCompletionPort(hReqQueue, nullptr, 0, 2); 381 | 382 | context.initialize(); 383 | 384 | initializeAsyncReceive(); 385 | 386 | std::cout << "Server listening on localhost:" << options.port << "." << std::endl; 387 | } 388 | 389 | void serverStop() 390 | { 391 | HttpRemoveUrl(hReqQueue, urlBuffer); 392 | 393 | if (hCompletionPort) 394 | { 395 | CloseHandle(hCompletionPort); 396 | } 397 | 398 | if (hReqQueue) 399 | { 400 | CloseHandle(hReqQueue); 401 | } 402 | 403 | HttpTerminate(HTTP_INITIALIZE_SERVER, NULL); 404 | } 405 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | DEPRECATED: please have a look at the upcoming [https://github.com/KoltesDigital/shiba](Shiba), which will have the same features and much more! 2 | 3 | # 4k Demo Oven 4 | 5 | ... with which you'll bake 4k demos for the [Cookie Demoparty](http://cookie.paris/) or other demoparties! 6 | 7 | This framework originates from [iq](http://iquilezles.org) with some parts from [wsmind](https://github.com/wsmind), [halcy](https://github.com/halcy/), and maybe others. 8 | 9 | Requirements: 10 | 11 | - Windows only. 12 | - [Crinkler](http://www.crinkler.net/) as linker. 13 | - [Node.js](https://nodejs.org/) for the toolchain. 14 | - [Synthclipse](http://synthclipse.sourceforge.net/) as IDE. 15 | - [Visual Studio](https://www.visualstudio.com/) for the C++ compiler. 16 | 17 | Recommended: 18 | 19 | - [4klang](http://4klang.untergrund.net/) or [Oidos](https://github.com/askeksa/Oidos) as synthesizer. 20 | - [7-Zip](https://www.7-zip.org/) to zip the demo. 21 | - [Ffmpeg](https://www.ffmpeg.org/) to capture the demo into a video. 22 | - [NASM](http://www.nasm.us) as ASM compiler, needed by the synthesizers. 23 | 24 | ## Setting up a new demo 25 | 26 | Fork the repository on GitHub. 27 | 28 | Clone your repository on your computer. I'll call XXX this directory's path. 29 | 30 | Open a _VS x86 tools prompt_ and execute: 31 | 32 | cd XXX 33 | npm install --production 34 | 35 | The demo settings are stored in _demo\config.yml_ for shared settings, and in _demo\config.local.yml_ for the settings specific to your computer. The latter file is ignored by git. 36 | 37 | By default, tools are searched on the PATH. You can explicit them in _demo\config.local.yml_, for instance: 38 | 39 | paths: 40 | 7z: C:\\Program Files\\7-Zip\\7z.exe 41 | crinkler: path\\to\\crinkler.exe 42 | oidos: path\\to\\oidos 43 | 44 | The demo template uses Oidos as synthesizer. If you don't want to use it, set **demo:audioTool** as `none` in _demo\config.yml_. 45 | 46 | In the _VS x86 tools prompt_, execute: 47 | 48 | gulp build 49 | 50 | After a few seconds, the build should be successful, and running _dist\template.exe_ should display spinning balls. Quit by pressing escape. 51 | 52 | If _7-Zip_ is installed and the path to _7z_ is available, the demo is zipped, together with the contents of _dist\static_. 53 | 54 | Open Synthclipse. You are prompted to choose a workspace. This is where Synthclipse will store metadata about your projects within the IDE. If you have no particular needs, keeping the default value is fine. 55 | 56 | Create a new Synthclipse project. Type in a title for your demo. **Uncheck** the default location, and choose _XXX\demo_. Click on _Finish_. 57 | 58 | You're good to go. Take a cookie and start developping your demo. 59 | 60 | Before releasing, check the demo's name in _config.yml_, update _static\file_id.diz_, and add some other files in _static_ if needed. 61 | 62 | ## Developing the demo 63 | 64 | In Synthclipse, edit _shader.stoy_. This is your demo. 65 | 66 | Click on the _Play_ button to compile the shader. 67 | 68 | In order to automatically build when a file changes, execute in a _VS x86 tools prompt_: 69 | 70 | gulp watch 71 | 72 | Whenever you save any fle in the project, a build is triggered in the background, and a notification displays the final size in bytes. Clicking on this notification will run the demo. 73 | 74 | In Synthclipse, uniforms' values can be controlled thanks to comment annotations. [Have a look at the available controls.](http://synthclipse.sourceforge.net/user_guide/fragx/uniform_controls.html) 75 | 76 | The framework embraces this feature and allows you to set values, which become constant values during the building process. For instance in the template project, `BPM` is adjustable through a uniform, but its value is hardcoded in the actual embedded shader. 77 | 78 | Save the controls' values into a preset. By default, the framework will compile the values from the preset named `Default`. 79 | 80 | The shader shall contain the line `// begin`. During the build process, the contents before the line is removed, and replaced by a generated block defining uniforms and consts as set in _config.yml_. 81 | 82 | ## Debugging & Minifying 83 | 84 | You can enable debugging by adding the parameter _debug_ to the build process : 85 | 86 | gulp build --debug 87 | 88 | It will add `#define DEBUG` in the engine code, which will display : openGL version, shader copile errors, uniform indices in console and/or message box. 89 | 90 | If you don't want your shader to be minified, for debugging purpose, you can add the parameter _nominify_ : 91 | 92 | gulp build --debug --no-minify 93 | 94 | ## Usage of multiple buffers 95 | 96 | If you wish to add multiple buffers feature, you can enable it by adding `demo:bufferCount: 3` in the config file to have 3 buffers. 97 | 98 | The engine will execute multiple renders to texture, which you will be able to sample from your shader. 99 | To do so, you will need to declare `uniform sampler2D buffer_n` as many time as there are buffers. As the uniform are not bind with the variable name but with their indices, your declaration will need to respect the order of the buffers (use _debug_ parameter to watch that uniforms indices are matching the code in _entry.cpp_). 100 | 101 | Only one shader is used for the multiple buffer passes, the `uniform int PASSINDEX` will tells which passes is being rendered. 102 | Only the last buffer pass will be displayed to the screen, the other ones will be rendered off-screen. (With 3 buffers, the displayed one will be code under the condition : `if (PASSINDEX == 2)`) 103 | 104 | Additional notes : 105 | 106 | - All buffers are the same size as the screen, but you can modify the code to suit you needs, as well as texture filtering, mipmaps... 107 | - Render passes use dual buffers to allow simultaneous read/writing to the same pass. 108 | - If you don't need multiple buffers but want to sample backbuffer (last frame) use : `demo:bufferCount: 1` 109 | - If you don't need multiple buffers and don't need to sample backbuffer, use : `demo:bufferCount: 0` 110 | 111 | ## Audio tools 112 | 113 | An audio tool can be set in the config file at `demo:audioTool`. `none` is a fallback tool which plays no music. The framework supports the following tools. 114 | 115 | ### [4klang](http://4klang.untergrund.net/) 116 | 117 | Modular synthesizer by Dominik 'Gopher' Ries and Paul 'pOWL' Kraus of Alcatraz. It comes in two flavors: `4klang` and `8klang`, the latter is more powerful but takes more space. 118 | 119 | [Getting started video](https://www.youtube.com/watch?v=wP__g_9FT4M) 120 | 121 | Within the VST, record and export the song into the `demo` directory. This will generate `4klang.h` and `4klang.inc` files. 122 | 123 | Give the path to _4klang_ or _8klang_ sources, in _config.local.yml_ as `paths:4klang` or `paths:8klang`. There shall be a file _4klang.asm_ inside. 124 | 125 | ### [Oidos](https://github.com/askeksa/Oidos) 126 | 127 | Additive synthesizer by Aske Simon 'Blueberry' Christensen. Follow the [Pouet thread](http://www.pouet.net/prod.php?which=69524) for precompiled releases. 128 | 129 | _Oidos_ converts the Renoise song into an assembly code. _Python 2_ is required for that. If `python` is not available in the PATH, give the path to it in _config.local.yml_ as `paths:python2`. 130 | 131 | Give the path to _Oidos_ sources in _config.local.yml_ as `paths:oidos`. There shall be directories _convert_ and _player_ inside. 132 | 133 | ### Shader 134 | 135 | Lets you make your own sound wave with a fragment shader. By default a 2048^2 pixels texture will be generated. At 44100, the whole texture can hold 190 seconds of sound. You can enable audio shader by setting the value of `demo:audioTool` to `shader` in the config file. See details of implementation in `shader.stoy`, `entry.cpp` and `audio-shader.cpp`. 136 | 137 | ## Capture 138 | 139 | By default, the recording is done at 60 FPS at 1920x1080, including the audio from _music.wav_. Customize in _config.yml_. 140 | 141 | Execute in a _VS x86 tools prompt_: 142 | 143 | gulp capture 144 | 145 | The demo is built with the special capture settings, then runs while saving each frame on disk, and finally these frames are merged into a video in the _dist_ directory. 146 | 147 | ## Tips 148 | 149 | Configure Synthclipse to compile the shader on save. 150 | 151 | Use the beat time to be synchronized with the music, cf. `beat` in the template demo. 152 | 153 | Have a look at _build\shader.glsl_ and _build\shader.min.glsl_ to understand how the shader is rewritten during the build process. 154 | 155 | Add your own uniforms computed on CPU side. 156 | 157 | Configure your antivirus to ignore XXX, because the demos may be recognized as viruses. 158 | 159 | ## Config reference 160 | 161 | - `capture`: used for the capture only. 162 | - `audioFilename`: the rendered music in _demo_ which plays with the captured demo. Default `music.wav`. Set to `null` to disable audio. 163 | - `fps`: default `60`. 164 | _ `height`: default `1080`. 165 | _ `width`: default `1920`. 166 | - `cl`: \* `args`: array of cli arguments. 167 | - `crinkler`: \* `args`: array of cli arguments. 168 | - `demo`: 169 | _ `audioFilename`: needed for some synthesizers. 170 | _ _Oidos_: default `music.xrns`. 171 | _ `audioTool`: `4klang`, `8klang`, `none`, `oidos`. Default `none`. 172 | _ `closeWhenFinished`: default `false`. 173 | _ `name`: used for the dist file names. 174 | _ `resolution`: used to force a resolution for dev purpose. 175 | _ `height` 176 | _ `scale` \* `width` 177 | - `paths`: by default, applications are searched in the PATH. 178 | _ `4klang`: path to source directory, if using `4klang`. 179 | _ `7z`: recommended to zip the build. 180 | _ `8klang`: path to source directory, if using `8klang`. 181 | _ `crinkler` 182 | _ `ffmpeg`: for the capture. 183 | _ `nasm` 184 | _ `oidos`: path to source directory, if using `oidos`. 185 | _ `python2`: if using `oidos`. 186 | - `shader`: 187 | _ `constantsPreset`: name of the preset used to transform uniforms to constants. Default `Default`. 188 | _ `filename`: default `shader.stoy`. 189 | _ `globals`: map type -> array of strings. 190 | _ `time`: used to provide the beat time. Set to `null` to disable. Automatically disabled if the beat const is not used. 191 | _ `beatConstant`: const name receiving the beat time. Default `beat`. 192 | _ `bpmUniform`: uniform name in Synthclipse stating the BPM. Default `BPM`. \* `uniforms`: array of strings. `time` is always prepended to the list.; 193 | 194 | ## Gulp task reference 195 | 196 | Tasks: 197 | 198 | - default: build and launch demo. 199 | - `build` 200 | - `capture`: compile in capture mode, then launch the demo, recording every frame. 201 | - `clean`: clear generated files. 202 | - `dev`: build and watch. 203 | - `encode`: transform recorded frames into a mov file. 204 | - `execute`: launch demo. 205 | - `watch`: compile every time a file is changed. 206 | 207 | Arguments: 208 | 209 | - `debug`, `d`: compile in debug mode, default depends on the task. 210 | - `directory`, `dir`: project path, defaults to `demo`. 211 | - `minify`, `m`: minify shader, defaults to `true`. 212 | - `notify`, `n`: show a notification when done, defaults to `false`. 213 | - `server`, `s`: launch a server for hot-reload, defaults to `true`. Only works in debug mode. 214 | - `zip`, `z`: zip the demo at the end, defaults to `false`. Requires [7-Zip](https://www.7-zip.org/download.html). 215 | --------------------------------------------------------------------------------