summaryrefslogtreecommitdiff
path: root/themes
diff options
context:
space:
mode:
Diffstat (limited to 'themes')
-rw-r--r--themes/dist/output.js922
-rwxr-xr-xthemes/dist/output.wasmbin106091 -> 126843 bytes
-rw-r--r--themes/meson.build16
-rw-r--r--themes/src/_shaders/sky.frag78
-rw-r--r--themes/src/_shaders/sky.vert10
-rw-r--r--themes/src/_shaders/water.frag63
-rw-r--r--themes/src/_shaders/water.vert63
-rw-r--r--themes/src/shaders/sky_frag.cpp81
-rw-r--r--themes/src/shaders/sky_frag.h4
-rw-r--r--themes/src/shaders/sky_vert.cpp13
-rw-r--r--themes/src/shaders/sky_vert.h4
-rw-r--r--themes/src/shaders/water_frag.cpp66
-rw-r--r--themes/src/shaders/water_frag.h4
-rw-r--r--themes/src/shaders/water_vert.cpp66
-rw-r--r--themes/src/shaders/water_vert.h4
-rw-r--r--themes/src/summer/sky_model.cpp64
-rw-r--r--themes/src/summer/sky_model.h43
-rw-r--r--themes/src/summer/summer_theme.cpp30
-rw-r--r--themes/src/summer/summer_theme.h4
-rw-r--r--themes/src/summer/water_model.cpp127
-rw-r--r--themes/src/summer/water_model.h70
-rw-r--r--themes/src/tools/shader.cjs (renamed from themes/src/tools/shader.js)0
22 files changed, 1244 insertions, 488 deletions
diff --git a/themes/dist/output.js b/themes/dist/output.js
index b19a1a3..190fe21 100644
--- a/themes/dist/output.js
+++ b/themes/dist/output.js
@@ -19,17 +19,13 @@ var Module = typeof Module != 'undefined' ? Module : {};
// Attempt to auto-detect the environment
var ENVIRONMENT_IS_WEB = typeof window == 'object';
-var ENVIRONMENT_IS_WORKER = typeof importScripts == 'function';
+var ENVIRONMENT_IS_WORKER = typeof WorkerGlobalScope != 'undefined';
// N.b. Electron.js environment is simultaneously a NODE-environment, but
// also a web environment.
var ENVIRONMENT_IS_NODE = typeof process == 'object' && typeof process.versions == 'object' && typeof process.versions.node == 'string' && process.type != 'renderer';
var ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER;
if (ENVIRONMENT_IS_NODE) {
- // `require()` is no-op in an ESM module, use `createRequire()` to construct
- // the require()` function. This is only necessary for multi-environment
- // builds, `-sENVIRONMENT=node` emits a static import declaration instead.
- // TODO: Swap all `require()`'s with `import()`'s?
}
@@ -82,23 +78,19 @@ if (ENVIRONMENT_IS_NODE) {
// include: node_shell_read.js
readBinary = (filename) => {
- // We need to re-wrap `file://` strings to URLs. Normalizing isn't
- // necessary in that case, the path should already be absolute.
- filename = isFileURI(filename) ? new URL(filename) : nodePath.normalize(filename);
+ // We need to re-wrap `file://` strings to URLs.
+ filename = isFileURI(filename) ? new URL(filename) : filename;
var ret = fs.readFileSync(filename);
- assert(ret.buffer);
+ assert(Buffer.isBuffer(ret));
return ret;
};
-readAsync = (filename, binary = true) => {
+readAsync = async (filename, binary = true) => {
// See the comment in the `readBinary` function.
- filename = isFileURI(filename) ? new URL(filename) : nodePath.normalize(filename);
- return new Promise((resolve, reject) => {
- fs.readFile(filename, binary ? undefined : 'utf8', (err, data) => {
- if (err) reject(err);
- else resolve(binary ? data.buffer : data);
- });
- });
+ filename = isFileURI(filename) ? new URL(filename) : filename;
+ var ret = fs.readFileSync(filename, binary ? undefined : 'utf8');
+ assert(binary ? Buffer.isBuffer(ret) : typeof ret == 'string');
+ return ret;
};
// end include: node_shell_read.js
if (!Module['thisProgram'] && process.argv.length > 1) {
@@ -119,7 +111,7 @@ readAsync = (filename, binary = true) => {
} else
if (ENVIRONMENT_IS_SHELL) {
- if ((typeof process == 'object' && typeof require === 'function') || typeof window == 'object' || typeof importScripts == 'function') throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)');
+ if ((typeof process == 'object' && typeof require === 'function') || typeof window == 'object' || typeof WorkerGlobalScope != 'undefined') throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)');
} else
@@ -141,10 +133,10 @@ if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) {
if (scriptDirectory.startsWith('blob:')) {
scriptDirectory = '';
} else {
- scriptDirectory = scriptDirectory.substr(0, scriptDirectory.replace(/[?#].*/, '').lastIndexOf('/')+1);
+ scriptDirectory = scriptDirectory.slice(0, scriptDirectory.replace(/[?#].*/, '').lastIndexOf('/')+1);
}
- if (!(typeof window == 'object' || typeof importScripts == 'function')) throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)');
+ if (!(typeof window == 'object' || typeof WorkerGlobalScope != 'undefined')) throw new Error('not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)');
{
// include: web_or_worker_shell_read.js
@@ -158,7 +150,7 @@ if (ENVIRONMENT_IS_WORKER) {
};
}
- readAsync = (url) => {
+ readAsync = async (url) => {
// Fetch has some additional restrictions over XHR, like it can't be used on a file:// url.
// See https://github.com/github/fetch/pull/92#issuecomment-140665932
// Cordova or Electron apps are typically loaded from a file:// url.
@@ -179,13 +171,11 @@ if (ENVIRONMENT_IS_WORKER) {
xhr.send(null);
});
}
- return fetch(url, { credentials: 'same-origin' })
- .then((response) => {
- if (response.ok) {
- return response.arrayBuffer();
- }
- return Promise.reject(new Error(response.status + ' : ' + response.url));
- })
+ var response = await fetch(url, { credentials: 'same-origin' });
+ if (response.ok) {
+ return response.arrayBuffer();
+ }
+ throw new Error(response.status + ' : ' + response.url);
};
// end include: web_or_worker_shell_read.js
}
@@ -307,32 +297,24 @@ var HEAP,
HEAPU32,
/** @type {!Float32Array} */
HEAPF32,
+/* BigInt64Array type is not correctly defined in closure
+/** not-@type {!BigInt64Array} */
+ HEAP64,
+/* BigUint64Array type is not correctly defined in closure
+/** not-t@type {!BigUint64Array} */
+ HEAPU64,
/** @type {!Float64Array} */
HEAPF64;
-// include: runtime_shared.js
-function updateMemoryViews() {
- var b = wasmMemory.buffer;
- Module['HEAP8'] = HEAP8 = new Int8Array(b);
- Module['HEAP16'] = HEAP16 = new Int16Array(b);
- Module['HEAPU8'] = HEAPU8 = new Uint8Array(b);
- Module['HEAPU16'] = HEAPU16 = new Uint16Array(b);
- Module['HEAP32'] = HEAP32 = new Int32Array(b);
- Module['HEAPU32'] = HEAPU32 = new Uint32Array(b);
- Module['HEAPF32'] = HEAPF32 = new Float32Array(b);
- Module['HEAPF64'] = HEAPF64 = new Float64Array(b);
-}
-
-// end include: runtime_shared.js
-assert(!Module['STACK_SIZE'], 'STACK_SIZE can no longer be set at runtime. Use -sSTACK_SIZE at link time')
-
-assert(typeof Int32Array != 'undefined' && typeof Float64Array !== 'undefined' && Int32Array.prototype.subarray != undefined && Int32Array.prototype.set != undefined,
- 'JS engine does not provide full typed array support');
+var runtimeInitialized = false;
-// If memory is defined in wasm, the user can't provide it, or set INITIAL_MEMORY
-assert(!Module['wasmMemory'], 'Use of `wasmMemory` detected. Use -sIMPORTED_MEMORY to define wasmMemory externally');
-assert(!Module['INITIAL_MEMORY'], 'Detected runtime INITIAL_MEMORY setting. Use -sIMPORTED_MEMORY to define wasmMemory dynamically');
+/**
+ * Indicates whether filename is delivered via file protocol (as opposed to http/https)
+ * @noinline
+ */
+var isFileURI = (filename) => filename.startsWith('file://');
+// include: runtime_shared.js
// include: runtime_stack_check.js
// Initializes the stack cookie. Called at the startup of main and at the startup of each thread in pthreads mode.
function writeStackCookie() {
@@ -371,84 +353,194 @@ function checkStackCookie() {
}
}
// end include: runtime_stack_check.js
-var __ATPRERUN__ = []; // functions called before the runtime is initialized
-var __ATINIT__ = []; // functions called during startup
-var __ATMAIN__ = []; // functions called when main() is to be run
-var __ATEXIT__ = []; // functions called during shutdown
-var __ATPOSTRUN__ = []; // functions called after the main() is called
+// include: runtime_exceptions.js
+// end include: runtime_exceptions.js
+// include: runtime_debug.js
+// Endianness check
+(() => {
+ var h16 = new Int16Array(1);
+ var h8 = new Int8Array(h16.buffer);
+ h16[0] = 0x6373;
+ if (h8[0] !== 0x73 || h8[1] !== 0x63) throw 'Runtime error: expected the system to be little-endian! (Run with -sSUPPORT_BIG_ENDIAN to bypass)';
+})();
-var runtimeInitialized = false;
+if (Module['ENVIRONMENT']) {
+ throw new Error('Module.ENVIRONMENT has been deprecated. To force the environment, use the ENVIRONMENT compile-time option (for example, -sENVIRONMENT=web or -sENVIRONMENT=node)');
+}
-function preRun() {
- var preRuns = Module['preRun'];
- if (preRuns) {
- if (typeof preRuns == 'function') preRuns = [preRuns];
- preRuns.forEach(addOnPreRun);
+function legacyModuleProp(prop, newName, incoming=true) {
+ if (!Object.getOwnPropertyDescriptor(Module, prop)) {
+ Object.defineProperty(Module, prop, {
+ configurable: true,
+ get() {
+ let extra = incoming ? ' (the initial value can be provided on Module, but after startup the value is only looked for on a local variable of that name)' : '';
+ abort(`\`Module.${prop}\` has been replaced by \`${newName}\`` + extra);
+
+ }
+ });
}
- callRuntimeCallbacks(__ATPRERUN__);
}
-function initRuntime() {
- assert(!runtimeInitialized);
- runtimeInitialized = true;
+function ignoredModuleProp(prop) {
+ if (Object.getOwnPropertyDescriptor(Module, prop)) {
+ abort(`\`Module.${prop}\` was supplied but \`${prop}\` not included in INCOMING_MODULE_JS_API`);
+ }
+}
- checkStackCookie();
+// forcing the filesystem exports a few things by default
+function isExportedByForceFilesystem(name) {
+ return name === 'FS_createPath' ||
+ name === 'FS_createDataFile' ||
+ name === 'FS_createPreloadedFile' ||
+ name === 'FS_unlink' ||
+ name === 'addRunDependency' ||
+ // The old FS has some functionality that WasmFS lacks.
+ name === 'FS_createLazyFile' ||
+ name === 'FS_createDevice' ||
+ name === 'removeRunDependency';
+}
-
- callRuntimeCallbacks(__ATINIT__);
+/**
+ * Intercept access to a global symbol. This enables us to give informative
+ * warnings/errors when folks attempt to use symbols they did not include in
+ * their build, or no symbols that no longer exist.
+ */
+function hookGlobalSymbolAccess(sym, func) {
+ if (typeof globalThis != 'undefined' && !Object.getOwnPropertyDescriptor(globalThis, sym)) {
+ Object.defineProperty(globalThis, sym, {
+ configurable: true,
+ get() {
+ func();
+ return undefined;
+ }
+ });
+ }
}
-function preMain() {
- checkStackCookie();
-
- callRuntimeCallbacks(__ATMAIN__);
+function missingGlobal(sym, msg) {
+ hookGlobalSymbolAccess(sym, () => {
+ warnOnce(`\`${sym}\` is not longer defined by emscripten. ${msg}`);
+ });
}
-function postRun() {
- checkStackCookie();
+missingGlobal('buffer', 'Please use HEAP8.buffer or wasmMemory.buffer');
+missingGlobal('asm', 'Please use wasmExports instead');
- var postRuns = Module['postRun'];
- if (postRuns) {
- if (typeof postRuns == 'function') postRuns = [postRuns];
- postRuns.forEach(addOnPostRun);
- }
+function missingLibrarySymbol(sym) {
+ hookGlobalSymbolAccess(sym, () => {
+ // Can't `abort()` here because it would break code that does runtime
+ // checks. e.g. `if (typeof SDL === 'undefined')`.
+ var msg = `\`${sym}\` is a library symbol and not included by default; add it to your library.js __deps or to DEFAULT_LIBRARY_FUNCS_TO_INCLUDE on the command line`;
+ // DEFAULT_LIBRARY_FUNCS_TO_INCLUDE requires the name as it appears in
+ // library.js, which means $name for a JS name with no prefix, or name
+ // for a JS name like _name.
+ var librarySymbol = sym;
+ if (!librarySymbol.startsWith('_')) {
+ librarySymbol = '$' + sym;
+ }
+ msg += ` (e.g. -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE='${librarySymbol}')`;
+ if (isExportedByForceFilesystem(sym)) {
+ msg += '. Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you';
+ }
+ warnOnce(msg);
+ });
- callRuntimeCallbacks(__ATPOSTRUN__);
+ // Any symbol that is not included from the JS library is also (by definition)
+ // not exported on the Module object.
+ unexportedRuntimeSymbol(sym);
}
-function addOnPreRun(cb) {
- __ATPRERUN__.unshift(cb);
+function unexportedRuntimeSymbol(sym) {
+ if (!Object.getOwnPropertyDescriptor(Module, sym)) {
+ Object.defineProperty(Module, sym, {
+ configurable: true,
+ get() {
+ var msg = `'${sym}' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the Emscripten FAQ)`;
+ if (isExportedByForceFilesystem(sym)) {
+ msg += '. Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you';
+ }
+ abort(msg);
+ }
+ });
+ }
}
-function addOnInit(cb) {
- __ATINIT__.unshift(cb);
+// Used by XXXXX_DEBUG settings to output debug messages.
+function dbg(...args) {
+ // TODO(sbc): Make this configurable somehow. Its not always convenient for
+ // logging to show up as warnings.
+ console.warn(...args);
}
+// end include: runtime_debug.js
+// include: memoryprofiler.js
+// end include: memoryprofiler.js
+
-function addOnPreMain(cb) {
- __ATMAIN__.unshift(cb);
+function updateMemoryViews() {
+ var b = wasmMemory.buffer;
+ Module['HEAP8'] = HEAP8 = new Int8Array(b);
+ Module['HEAP16'] = HEAP16 = new Int16Array(b);
+ Module['HEAPU8'] = HEAPU8 = new Uint8Array(b);
+ Module['HEAPU16'] = HEAPU16 = new Uint16Array(b);
+ Module['HEAP32'] = HEAP32 = new Int32Array(b);
+ Module['HEAPU32'] = HEAPU32 = new Uint32Array(b);
+ Module['HEAPF32'] = HEAPF32 = new Float32Array(b);
+ Module['HEAPF64'] = HEAPF64 = new Float64Array(b);
+ Module['HEAP64'] = HEAP64 = new BigInt64Array(b);
+ Module['HEAPU64'] = HEAPU64 = new BigUint64Array(b);
}
-function addOnExit(cb) {
+// end include: runtime_shared.js
+assert(!Module['STACK_SIZE'], 'STACK_SIZE can no longer be set at runtime. Use -sSTACK_SIZE at link time')
+
+assert(typeof Int32Array != 'undefined' && typeof Float64Array !== 'undefined' && Int32Array.prototype.subarray != undefined && Int32Array.prototype.set != undefined,
+ 'JS engine does not provide full typed array support');
+
+// If memory is defined in wasm, the user can't provide it, or set INITIAL_MEMORY
+assert(!Module['wasmMemory'], 'Use of `wasmMemory` detected. Use -sIMPORTED_MEMORY to define wasmMemory externally');
+assert(!Module['INITIAL_MEMORY'], 'Detected runtime INITIAL_MEMORY setting. Use -sIMPORTED_MEMORY to define wasmMemory dynamically');
+
+function preRun() {
+ if (Module['preRun']) {
+ if (typeof Module['preRun'] == 'function') Module['preRun'] = [Module['preRun']];
+ while (Module['preRun'].length) {
+ addOnPreRun(Module['preRun'].shift());
+ }
+ }
+ callRuntimeCallbacks(onPreRuns);
}
-function addOnPostRun(cb) {
- __ATPOSTRUN__.unshift(cb);
+function initRuntime() {
+ assert(!runtimeInitialized);
+ runtimeInitialized = true;
+
+ checkStackCookie();
+
+
+
+ wasmExports['__wasm_call_ctors']();
+
+
}
-// include: runtime_math.js
-// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul
+function preMain() {
+ checkStackCookie();
+
+}
-// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/fround
+function postRun() {
+ checkStackCookie();
-// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32
+ if (Module['postRun']) {
+ if (typeof Module['postRun'] == 'function') Module['postRun'] = [Module['postRun']];
+ while (Module['postRun'].length) {
+ addOnPostRun(Module['postRun'].shift());
+ }
+ }
-// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc
+ callRuntimeCallbacks(onPostRuns);
+}
-assert(Math.imul, 'This browser does not support Math.imul(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill');
-assert(Math.fround, 'This browser does not support Math.fround(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill');
-assert(Math.clz32, 'This browser does not support Math.clz32(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill');
-assert(Math.trunc, 'This browser does not support Math.trunc(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill');
-// end include: runtime_math.js
// A counter of dependencies for calling run(). If we need to
// do asynchronous work before running, increment this and
// decrement it. Incrementing must happen in a place like
@@ -457,9 +549,9 @@ assert(Math.trunc, 'This browser does not support Math.trunc(), build with LEGAC
// it happens right before run - run will be postponed until
// the dependencies are met.
var runDependencies = 0;
-var runDependencyWatcher = null;
var dependenciesFulfilled = null; // overridden to take different actions when all run dependencies are fulfilled
var runDependencyTracking = {};
+var runDependencyWatcher = null;
function getUniqueRunDependency(id) {
var orig = id;
@@ -560,8 +652,6 @@ function abort(what) {
throw e;
}
-// include: memoryprofiler.js
-// end include: memoryprofiler.js
// show errors on likely calls to FS when it was not included
var FS = {
error() {
@@ -581,22 +671,6 @@ var FS = {
Module['FS_createDataFile'] = FS.createDataFile;
Module['FS_createPreloadedFile'] = FS.createPreloadedFile;
-// include: URIUtils.js
-// Prefix of data URIs emitted by SINGLE_FILE and related options.
-var dataURIPrefix = 'data:application/octet-stream;base64,';
-
-/**
- * Indicates whether filename is a base64 data URI.
- * @noinline
- */
-var isDataURI = (filename) => filename.startsWith(dataURIPrefix);
-
-/**
- * Indicates whether filename is delivered via file protocol (as opposed to http/https)
- * @noinline
- */
-var isFileURI = (filename) => filename.startsWith('file://');
-// end include: URIUtils.js
function createExportWrapper(name, nargs) {
return (...args) => {
assert(runtimeInitialized, `native function \`${name}\` called before runtime initialization`);
@@ -608,18 +682,11 @@ function createExportWrapper(name, nargs) {
};
}
-// include: runtime_exceptions.js
-// end include: runtime_exceptions.js
+var wasmBinaryFile;
function findWasmBinary() {
- var f = 'output.wasm';
- if (!isDataURI(f)) {
- return locateFile(f);
- }
- return f;
+ return locateFile('output.wasm');
}
-var wasmBinaryFile;
-
function getBinarySync(file) {
if (file == wasmBinaryFile && wasmBinary) {
return new Uint8Array(wasmBinary);
@@ -630,26 +697,28 @@ function getBinarySync(file) {
throw 'both async and sync fetching of the wasm failed';
}
-function getBinaryPromise(binaryFile) {
+async function getWasmBinary(binaryFile) {
// If we don't have the binary yet, load it asynchronously using readAsync.
- if (!wasmBinary
- ) {
+ if (!wasmBinary) {
// Fetch the binary using readAsync
- return readAsync(binaryFile).then(
- (response) => new Uint8Array(/** @type{!ArrayBuffer} */(response)),
- // Fall back to getBinarySync if readAsync fails
- () => getBinarySync(binaryFile)
- );
+ try {
+ var response = await readAsync(binaryFile);
+ return new Uint8Array(response);
+ } catch {
+ // Fall back to getBinarySync below;
+ }
}
// Otherwise, getBinarySync should be able to get it synchronously
- return Promise.resolve().then(() => getBinarySync(binaryFile));
+ return getBinarySync(binaryFile);
}
-function instantiateArrayBuffer(binaryFile, imports, receiver) {
- return getBinaryPromise(binaryFile).then((binary) => {
- return WebAssembly.instantiate(binary, imports);
- }).then(receiver, (reason) => {
+async function instantiateArrayBuffer(binaryFile, imports) {
+ try {
+ var binary = await getWasmBinary(binaryFile);
+ var instance = await WebAssembly.instantiate(binary, imports);
+ return instance;
+ } catch (reason) {
err(`failed to asynchronously prepare wasm: ${reason}`);
// Warn on some common problems.
@@ -657,43 +726,34 @@ function instantiateArrayBuffer(binaryFile, imports, receiver) {
err(`warning: Loading from a file URI (${wasmBinaryFile}) is not supported in most browsers. See https://emscripten.org/docs/getting_started/FAQ.html#how-do-i-run-a-local-webserver-for-testing-why-does-my-program-stall-in-downloading-or-preparing`);
}
abort(reason);
- });
+ }
}
-function instantiateAsync(binary, binaryFile, imports, callback) {
- if (!binary &&
- typeof WebAssembly.instantiateStreaming == 'function' &&
- !isDataURI(binaryFile) &&
+async function instantiateAsync(binary, binaryFile, imports) {
+ if (!binary && typeof WebAssembly.instantiateStreaming == 'function'
// Don't use streaming for file:// delivered objects in a webview, fetch them synchronously.
- !isFileURI(binaryFile) &&
+ && !isFileURI(binaryFile)
// Avoid instantiateStreaming() on Node.js environment for now, as while
// Node.js v18.1.0 implements it, it does not have a full fetch()
// implementation yet.
//
// Reference:
// https://github.com/emscripten-core/emscripten/pull/16917
- !ENVIRONMENT_IS_NODE &&
- typeof fetch == 'function') {
- return fetch(binaryFile, { credentials: 'same-origin' }).then((response) => {
- // Suppress closure warning here since the upstream definition for
- // instantiateStreaming only allows Promise<Repsponse> rather than
- // an actual Response.
- // TODO(https://github.com/google/closure-compiler/pull/3913): Remove if/when upstream closure is fixed.
- /** @suppress {checkTypes} */
- var result = WebAssembly.instantiateStreaming(response, imports);
-
- return result.then(
- callback,
- function(reason) {
- // We expect the most common failure cause to be a bad MIME type for the binary,
- // in which case falling back to ArrayBuffer instantiation should work.
- err(`wasm streaming compile failed: ${reason}`);
- err('falling back to ArrayBuffer instantiation');
- return instantiateArrayBuffer(binaryFile, imports, callback);
- });
- });
+ && !ENVIRONMENT_IS_NODE
+ ) {
+ try {
+ var response = fetch(binaryFile, { credentials: 'same-origin' });
+ var instantiationResult = await WebAssembly.instantiateStreaming(response, imports);
+ return instantiationResult;
+ } catch (reason) {
+ // We expect the most common failure cause to be a bad MIME type for the binary,
+ // in which case falling back to ArrayBuffer instantiation should work.
+ err(`wasm streaming compile failed: ${reason}`);
+ err('falling back to ArrayBuffer instantiation');
+ // fall back of instantiateArrayBuffer below
+ };
}
- return instantiateArrayBuffer(binaryFile, imports, callback);
+ return instantiateArrayBuffer(binaryFile, imports);
}
function getWasmImports() {
@@ -706,8 +766,7 @@ function getWasmImports() {
// Create the wasm instance.
// Receives the wasm imports, returns the exports.
-function createWasm() {
- var info = getWasmImports();
+async function createWasm() {
// Load the wasm module and create an instance of using native support in the JS engine.
// handle a generated wasm instance, receiving its exports and
// performing other necessary setup
@@ -726,8 +785,6 @@ function createWasm() {
assert(wasmTable, 'table not found in wasm exports');
- addOnInit(wasmExports['__wasm_call_ctors']);
-
removeRunDependency('wasm-instantiate');
return wasmExports;
}
@@ -746,9 +803,11 @@ function createWasm() {
trueModule = null;
// TODO: Due to Closure regression https://github.com/google/closure-compiler/issues/3193, the above line no longer optimizes out down to the following line.
// When the regression is fixed, can restore the above PTHREADS-enabled path.
- receiveInstance(result['instance']);
+ return receiveInstance(result['instance']);
}
+ var info = getWasmImports();
+
// User shell pages can write their own Module.instantiateWasm = function(imports, successCallback) callback
// to manually instantiate the Wasm module themselves. This allows pages to
// run the instantiation parallel to any other async startup actions they are
@@ -756,156 +815,50 @@ function createWasm() {
// Also pthreads and wasm workers initialize the wasm instance through this
// path.
if (Module['instantiateWasm']) {
- try {
- return Module['instantiateWasm'](info, receiveInstance);
- } catch(e) {
- err(`Module.instantiateWasm callback failed with error: ${e}`);
- return false;
- }
- }
-
- wasmBinaryFile ??= findWasmBinary();
-
- instantiateAsync(wasmBinary, wasmBinaryFile, info, receiveInstantiationResult);
- return {}; // no exports yet; we'll fill them in later
-}
-
-// Globals used by JS i64 conversions (see makeSetValue)
-var tempDouble;
-var tempI64;
-
-// include: runtime_debug.js
-// Endianness check
-(() => {
- var h16 = new Int16Array(1);
- var h8 = new Int8Array(h16.buffer);
- h16[0] = 0x6373;
- if (h8[0] !== 0x73 || h8[1] !== 0x63) throw 'Runtime error: expected the system to be little-endian! (Run with -sSUPPORT_BIG_ENDIAN to bypass)';
-})();
-
-if (Module['ENVIRONMENT']) {
- throw new Error('Module.ENVIRONMENT has been deprecated. To force the environment, use the ENVIRONMENT compile-time option (for example, -sENVIRONMENT=web or -sENVIRONMENT=node)');
-}
-
-function legacyModuleProp(prop, newName, incoming=true) {
- if (!Object.getOwnPropertyDescriptor(Module, prop)) {
- Object.defineProperty(Module, prop, {
- configurable: true,
- get() {
- let extra = incoming ? ' (the initial value can be provided on Module, but after startup the value is only looked for on a local variable of that name)' : '';
- abort(`\`Module.${prop}\` has been replaced by \`${newName}\`` + extra);
-
- }
- });
- }
-}
-
-function ignoredModuleProp(prop) {
- if (Object.getOwnPropertyDescriptor(Module, prop)) {
- abort(`\`Module.${prop}\` was supplied but \`${prop}\` not included in INCOMING_MODULE_JS_API`);
- }
-}
-
-// forcing the filesystem exports a few things by default
-function isExportedByForceFilesystem(name) {
- return name === 'FS_createPath' ||
- name === 'FS_createDataFile' ||
- name === 'FS_createPreloadedFile' ||
- name === 'FS_unlink' ||
- name === 'addRunDependency' ||
- // The old FS has some functionality that WasmFS lacks.
- name === 'FS_createLazyFile' ||
- name === 'FS_createDevice' ||
- name === 'removeRunDependency';
-}
-
-/**
- * Intercept access to a global symbol. This enables us to give informative
- * warnings/errors when folks attempt to use symbols they did not include in
- * their build, or no symbols that no longer exist.
- */
-function hookGlobalSymbolAccess(sym, func) {
- if (typeof globalThis != 'undefined' && !Object.getOwnPropertyDescriptor(globalThis, sym)) {
- Object.defineProperty(globalThis, sym, {
- configurable: true,
- get() {
- func();
- return undefined;
+ return new Promise((resolve, reject) => {
+ try {
+ Module['instantiateWasm'](info, (mod, inst) => {
+ receiveInstance(mod, inst);
+ resolve(mod.exports);
+ });
+ } catch(e) {
+ err(`Module.instantiateWasm callback failed with error: ${e}`);
+ reject(e);
}
});
}
-}
-
-function missingGlobal(sym, msg) {
- hookGlobalSymbolAccess(sym, () => {
- warnOnce(`\`${sym}\` is not longer defined by emscripten. ${msg}`);
- });
-}
-
-missingGlobal('buffer', 'Please use HEAP8.buffer or wasmMemory.buffer');
-missingGlobal('asm', 'Please use wasmExports instead');
-
-function missingLibrarySymbol(sym) {
- hookGlobalSymbolAccess(sym, () => {
- // Can't `abort()` here because it would break code that does runtime
- // checks. e.g. `if (typeof SDL === 'undefined')`.
- var msg = `\`${sym}\` is a library symbol and not included by default; add it to your library.js __deps or to DEFAULT_LIBRARY_FUNCS_TO_INCLUDE on the command line`;
- // DEFAULT_LIBRARY_FUNCS_TO_INCLUDE requires the name as it appears in
- // library.js, which means $name for a JS name with no prefix, or name
- // for a JS name like _name.
- var librarySymbol = sym;
- if (!librarySymbol.startsWith('_')) {
- librarySymbol = '$' + sym;
- }
- msg += ` (e.g. -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE='${librarySymbol}')`;
- if (isExportedByForceFilesystem(sym)) {
- msg += '. Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you';
- }
- warnOnce(msg);
- });
- // Any symbol that is not included from the JS library is also (by definition)
- // not exported on the Module object.
- unexportedRuntimeSymbol(sym);
-}
+ wasmBinaryFile ??= findWasmBinary();
-function unexportedRuntimeSymbol(sym) {
- if (!Object.getOwnPropertyDescriptor(Module, sym)) {
- Object.defineProperty(Module, sym, {
- configurable: true,
- get() {
- var msg = `'${sym}' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the Emscripten FAQ)`;
- if (isExportedByForceFilesystem(sym)) {
- msg += '. Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you';
- }
- abort(msg);
- }
- });
- }
+ var result = await instantiateAsync(wasmBinary, wasmBinaryFile, info);
+ var exports = receiveInstantiationResult(result);
+ return exports;
}
-// Used by XXXXX_DEBUG settings to output debug messages.
-function dbg(...args) {
- // TODO(sbc): Make this configurable somehow. Its not always convenient for
- // logging to show up as warnings.
- console.warn(...args);
-}
-// end include: runtime_debug.js
// === Body ===
// end include: preamble.js
- /** @constructor */
- function ExitStatus(status) {
- this.name = 'ExitStatus';
- this.message = `Program terminated with exit(${status})`;
- this.status = status;
+ class ExitStatus {
+ name = 'ExitStatus';
+ constructor(status) {
+ this.message = `Program terminated with exit(${status})`;
+ this.status = status;
+ }
}
var callRuntimeCallbacks = (callbacks) => {
- // Pass the module as the first argument.
- callbacks.forEach((f) => f(Module));
+ while (callbacks.length > 0) {
+ // Pass the module as the first argument.
+ callbacks.shift()(Module);
+ }
};
+ var onPostRuns = [];
+ var addOnPostRun = (cb) => onPostRuns.unshift(cb);
+
+ var onPreRuns = [];
+ var addOnPreRun = (cb) => onPreRuns.unshift(cb);
+
/**
@@ -919,7 +872,7 @@ function dbg(...args) {
case 'i8': return HEAP8[ptr];
case 'i16': return HEAP16[((ptr)>>1)];
case 'i32': return HEAP32[((ptr)>>2)];
- case 'i64': abort('to do getValue(i64) use WASM_BIGINT');
+ case 'i64': return HEAP64[((ptr)>>3)];
case 'float': return HEAPF32[((ptr)>>2)];
case 'double': return HEAPF64[((ptr)>>3)];
case '*': return HEAPU32[((ptr)>>2)];
@@ -949,7 +902,7 @@ function dbg(...args) {
case 'i8': HEAP8[ptr] = value; break;
case 'i16': HEAP16[((ptr)>>1)] = value; break;
case 'i32': HEAP32[((ptr)>>2)] = value; break;
- case 'i64': abort('to do setValue(i64) use WASM_BIGINT');
+ case 'i64': HEAP64[((ptr)>>3)] = BigInt(value); break;
case 'float': HEAPF32[((ptr)>>2)] = value; break;
case 'double': HEAPF64[((ptr)>>3)] = value; break;
case '*': HEAPU32[((ptr)>>2)] = value; break;
@@ -970,99 +923,8 @@ function dbg(...args) {
}
};
- var UTF8Decoder = typeof TextDecoder != 'undefined' ? new TextDecoder() : undefined;
-
- /**
- * Given a pointer 'idx' to a null-terminated UTF8-encoded string in the given
- * array that contains uint8 values, returns a copy of that string as a
- * Javascript String object.
- * heapOrArray is either a regular array, or a JavaScript typed array view.
- * @param {number=} idx
- * @param {number=} maxBytesToRead
- * @return {string}
- */
- var UTF8ArrayToString = (heapOrArray, idx = 0, maxBytesToRead = NaN) => {
- var endIdx = idx + maxBytesToRead;
- var endPtr = idx;
- // TextDecoder needs to know the byte length in advance, it doesn't stop on
- // null terminator by itself. Also, use the length info to avoid running tiny
- // strings through TextDecoder, since .subarray() allocates garbage.
- // (As a tiny code save trick, compare endPtr against endIdx using a negation,
- // so that undefined/NaN means Infinity)
- while (heapOrArray[endPtr] && !(endPtr >= endIdx)) ++endPtr;
-
- if (endPtr - idx > 16 && heapOrArray.buffer && UTF8Decoder) {
- return UTF8Decoder.decode(heapOrArray.subarray(idx, endPtr));
- }
- var str = '';
- // If building with TextDecoder, we have already computed the string length
- // above, so test loop end condition against that
- while (idx < endPtr) {
- // For UTF8 byte structure, see:
- // http://en.wikipedia.org/wiki/UTF-8#Description
- // https://www.ietf.org/rfc/rfc2279.txt
- // https://tools.ietf.org/html/rfc3629
- var u0 = heapOrArray[idx++];
- if (!(u0 & 0x80)) { str += String.fromCharCode(u0); continue; }
- var u1 = heapOrArray[idx++] & 63;
- if ((u0 & 0xE0) == 0xC0) { str += String.fromCharCode(((u0 & 31) << 6) | u1); continue; }
- var u2 = heapOrArray[idx++] & 63;
- if ((u0 & 0xF0) == 0xE0) {
- u0 = ((u0 & 15) << 12) | (u1 << 6) | u2;
- } else {
- if ((u0 & 0xF8) != 0xF0) warnOnce('Invalid UTF-8 leading byte ' + ptrToString(u0) + ' encountered when deserializing a UTF-8 string in wasm memory to a JS string!');
- u0 = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | (heapOrArray[idx++] & 63);
- }
-
- if (u0 < 0x10000) {
- str += String.fromCharCode(u0);
- } else {
- var ch = u0 - 0x10000;
- str += String.fromCharCode(0xD800 | (ch >> 10), 0xDC00 | (ch & 0x3FF));
- }
- }
- return str;
- };
-
- /**
- * Given a pointer 'ptr' to a null-terminated UTF8-encoded string in the
- * emscripten HEAP, returns a copy of that string as a Javascript String object.
- *
- * @param {number} ptr
- * @param {number=} maxBytesToRead - An optional length that specifies the
- * maximum number of bytes to read. You can omit this parameter to scan the
- * string until the first 0 byte. If maxBytesToRead is passed, and the string
- * at [ptr, ptr+maxBytesToReadr[ contains a null byte in the middle, then the
- * string will cut short at that byte index (i.e. maxBytesToRead will not
- * produce a string of exact length [ptr, ptr+maxBytesToRead[) N.B. mixing
- * frequent uses of UTF8ToString() with and without maxBytesToRead may throw
- * JS JIT optimizations off, so it is worth to consider consistently using one
- * @return {string}
- */
- var UTF8ToString = (ptr, maxBytesToRead) => {
- assert(typeof ptr == 'number', `UTF8ToString expects a number (got ${typeof ptr})`);
- return ptr ? UTF8ArrayToString(HEAPU8, ptr, maxBytesToRead) : '';
- };
- var ___assert_fail = (condition, filename, line, func) => {
- abort(`Assertion failed: ${UTF8ToString(condition)}, at: ` + [filename ? UTF8ToString(filename) : 'unknown filename', line, func ? UTF8ToString(func) : 'unknown function']);
- };
-
- var __abort_js = () => {
+ var __abort_js = () =>
abort('native code called abort()');
- };
-
- function __emscripten_fetch_free(id) {
- if (Fetch.xhrs.has(id)) {
- var xhr = Fetch.xhrs.get(id);
- Fetch.xhrs.free(id);
- // check if fetch is still in progress and should be aborted
- if (xhr.readyState > 0 && xhr.readyState < 4) {
- xhr.abort();
- }
- }
- }
-
- var __emscripten_memcpy_js = (dest, src, num) => HEAPU8.copyWithin(dest, src, src + num);
var isLeapYear = (year) => year%4 === 0 && (year%100 !== 0 || year%400 === 0);
@@ -1077,13 +939,12 @@ function dbg(...args) {
return yday;
};
- var convertI32PairToI53Checked = (lo, hi) => {
- assert(lo == (lo >>> 0) || lo == (lo|0)); // lo should either be a i32 or a u32
- assert(hi === (hi|0)); // hi should be a i32
- return ((hi + 0x200000) >>> 0 < 0x400001 - !!lo) ? (lo >>> 0) + hi * 4294967296 : NaN;
- };
- function __localtime_js(time_low, time_high,tmPtr) {
- var time = convertI32PairToI53Checked(time_low, time_high);
+ var INT53_MAX = 9007199254740992;
+
+ var INT53_MIN = -9007199254740992;
+ var bigintToI53Checked = (num) => (num < INT53_MIN || num > INT53_MAX) ? NaN : Number(num);
+ function __localtime_js(time, tmPtr) {
+ time = bigintToI53Checked(time);
var date = new Date(time*1000);
@@ -1235,7 +1096,23 @@ function dbg(...args) {
var _emscripten_date_now = () => Date.now();
+ function _emscripten_fetch_free(id) {
+ if (Fetch.xhrs.has(id)) {
+ var xhr = Fetch.xhrs.get(id);
+ Fetch.xhrs.free(id);
+ // check if fetch is still in progress and should be aborted
+ if (xhr.readyState > 0 && xhr.readyState < 4) {
+ xhr.abort();
+ }
+ }
+ }
+
+ var onExits = [];
+ var addOnExit = (cb) => onExits.unshift(cb);
var JSEvents = {
+ memcpy(target, src, size) {
+ HEAP8.set(HEAP8.subarray(src, src + size), target);
+ },
removeAllEventListeners() {
while (JSEvents.eventHandlers.length) {
JSEvents._removeHandler(JSEvents.eventHandlers.length - 1);
@@ -1356,6 +1233,79 @@ function dbg(...args) {
},
};
+ var UTF8Decoder = typeof TextDecoder != 'undefined' ? new TextDecoder() : undefined;
+
+ /**
+ * Given a pointer 'idx' to a null-terminated UTF8-encoded string in the given
+ * array that contains uint8 values, returns a copy of that string as a
+ * Javascript String object.
+ * heapOrArray is either a regular array, or a JavaScript typed array view.
+ * @param {number=} idx
+ * @param {number=} maxBytesToRead
+ * @return {string}
+ */
+ var UTF8ArrayToString = (heapOrArray, idx = 0, maxBytesToRead = NaN) => {
+ var endIdx = idx + maxBytesToRead;
+ var endPtr = idx;
+ // TextDecoder needs to know the byte length in advance, it doesn't stop on
+ // null terminator by itself. Also, use the length info to avoid running tiny
+ // strings through TextDecoder, since .subarray() allocates garbage.
+ // (As a tiny code save trick, compare endPtr against endIdx using a negation,
+ // so that undefined/NaN means Infinity)
+ while (heapOrArray[endPtr] && !(endPtr >= endIdx)) ++endPtr;
+
+ if (endPtr - idx > 16 && heapOrArray.buffer && UTF8Decoder) {
+ return UTF8Decoder.decode(heapOrArray.subarray(idx, endPtr));
+ }
+ var str = '';
+ // If building with TextDecoder, we have already computed the string length
+ // above, so test loop end condition against that
+ while (idx < endPtr) {
+ // For UTF8 byte structure, see:
+ // http://en.wikipedia.org/wiki/UTF-8#Description
+ // https://www.ietf.org/rfc/rfc2279.txt
+ // https://tools.ietf.org/html/rfc3629
+ var u0 = heapOrArray[idx++];
+ if (!(u0 & 0x80)) { str += String.fromCharCode(u0); continue; }
+ var u1 = heapOrArray[idx++] & 63;
+ if ((u0 & 0xE0) == 0xC0) { str += String.fromCharCode(((u0 & 31) << 6) | u1); continue; }
+ var u2 = heapOrArray[idx++] & 63;
+ if ((u0 & 0xF0) == 0xE0) {
+ u0 = ((u0 & 15) << 12) | (u1 << 6) | u2;
+ } else {
+ if ((u0 & 0xF8) != 0xF0) warnOnce('Invalid UTF-8 leading byte ' + ptrToString(u0) + ' encountered when deserializing a UTF-8 string in wasm memory to a JS string!');
+ u0 = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | (heapOrArray[idx++] & 63);
+ }
+
+ if (u0 < 0x10000) {
+ str += String.fromCharCode(u0);
+ } else {
+ var ch = u0 - 0x10000;
+ str += String.fromCharCode(0xD800 | (ch >> 10), 0xDC00 | (ch & 0x3FF));
+ }
+ }
+ return str;
+ };
+
+ /**
+ * Given a pointer 'ptr' to a null-terminated UTF8-encoded string in the
+ * emscripten HEAP, returns a copy of that string as a Javascript String object.
+ *
+ * @param {number} ptr
+ * @param {number=} maxBytesToRead - An optional length that specifies the
+ * maximum number of bytes to read. You can omit this parameter to scan the
+ * string until the first 0 byte. If maxBytesToRead is passed, and the string
+ * at [ptr, ptr+maxBytesToReadr[ contains a null byte in the middle, then the
+ * string will cut short at that byte index (i.e. maxBytesToRead will not
+ * produce a string of exact length [ptr, ptr+maxBytesToRead[) N.B. mixing
+ * frequent uses of UTF8ToString() with and without maxBytesToRead may throw
+ * JS JIT optimizations off, so it is worth to consider consistently using one
+ * @return {string}
+ */
+ var UTF8ToString = (ptr, maxBytesToRead) => {
+ assert(typeof ptr == 'number', `UTF8ToString expects a number (got ${typeof ptr})`);
+ return ptr ? UTF8ArrayToString(HEAPU8, ptr, maxBytesToRead) : '';
+ };
var maybeCStringToJsString = (cString) => {
// "cString > 2" checks if the input is a number, and isn't of the special
// values we accept here, EMSCRIPTEN_EVENT_TARGET_* (which map to 0, 1, 2).
@@ -1368,7 +1318,7 @@ function dbg(...args) {
var specialHTMLTargets = [0, typeof document != 'undefined' ? document : 0, typeof window != 'undefined' ? window : 0];
var findEventTarget = (target) => {
target = maybeCStringToJsString(target);
- var domElement = specialHTMLTargets[target] || (typeof document != 'undefined' ? document.querySelector(target) : undefined);
+ var domElement = specialHTMLTargets[target] || (typeof document != 'undefined' ? document.querySelector(target) : null);
return domElement;
};
@@ -1395,8 +1345,10 @@ function dbg(...args) {
var func = wasmTableMirror[funcPtr];
if (!func) {
if (funcPtr >= wasmTableMirror.length) wasmTableMirror.length = funcPtr + 1;
+ /** @suppress {checkTypes} */
wasmTableMirror[funcPtr] = func = wasmTable.get(funcPtr);
}
+ /** @suppress {checkTypes} */
assert(wasmTable.get(funcPtr) == func, 'JavaScript-side Wasm function table mirror is out of date!');
return func;
};
@@ -1604,12 +1556,8 @@ function dbg(...args) {
class HandleAllocator {
- constructor() {
- // TODO(https://github.com/emscripten-core/emscripten/issues/21414):
- // Use inline field declarations.
- this.allocated = [undefined];
- this.freelist = [];
- }
+ allocated = [undefined];
+ freelist = [];
get(id) {
assert(this.allocated[id] !== undefined, `invalid handle: ${id}`);
return this.allocated[id];
@@ -1856,6 +1804,7 @@ function dbg(...args) {
quit_(code, new ExitStatus(code));
};
+
/** @suppress {duplicate } */
/** @param {boolean|number=} implicit */
var exitJS = (status, implicit) => {
@@ -2183,22 +2132,18 @@ function dbg(...args) {
return !!(ctx.mdibvbi = ctx.getExtension('WEBGL_multi_draw_instanced_base_vertex_base_instance'));
};
- var webgl_enable_EXT_polygon_offset_clamp = (ctx) => {
- return !!(ctx.extPolygonOffsetClamp = ctx.getExtension('EXT_polygon_offset_clamp'));
- };
+ var webgl_enable_EXT_polygon_offset_clamp = (ctx) =>
+ !!(ctx.extPolygonOffsetClamp = ctx.getExtension('EXT_polygon_offset_clamp'));
- var webgl_enable_EXT_clip_control = (ctx) => {
- return !!(ctx.extClipControl = ctx.getExtension('EXT_clip_control'));
- };
+ var webgl_enable_EXT_clip_control = (ctx) =>
+ !!(ctx.extClipControl = ctx.getExtension('EXT_clip_control'));
- var webgl_enable_WEBGL_polygon_mode = (ctx) => {
- return !!(ctx.webglPolygonMode = ctx.getExtension('WEBGL_polygon_mode'));
- };
+ var webgl_enable_WEBGL_polygon_mode = (ctx) =>
+ !!(ctx.webglPolygonMode = ctx.getExtension('WEBGL_polygon_mode'));
- var webgl_enable_WEBGL_multi_draw = (ctx) => {
+ var webgl_enable_WEBGL_multi_draw = (ctx) =>
// Closure is expected to be allowed to minify the '.multiDrawWebgl' property, so not accessing it quoted.
- return !!(ctx.multiDrawWebgl = ctx.getExtension('WEBGL_multi_draw'));
- };
+ !!(ctx.multiDrawWebgl = ctx.getExtension('WEBGL_multi_draw'));
var getEmscriptenSupportedExtensions = (ctx) => {
// Restrict the list of advertised extensions to those that we actually
@@ -2297,6 +2242,11 @@ function dbg(...args) {
for (var i = table.length; i < ret; i++) {
table[i] = null;
}
+ // Skip over any non-null elements that might have been created by
+ // glBindBuffer.
+ while (table[ret]) {
+ ret = GL.counter++;
+ }
return ret;
},
genObject:(n, buffers, createFunction, objectTable
@@ -2530,7 +2480,7 @@ function dbg(...args) {
// Active Emscripten GL layer context object.
GL.currentContext = GL.contexts[contextHandle];
// Active WebGL context object.
- Module.ctx = GLctx = GL.currentContext?.GLctx;
+ Module['ctx'] = GLctx = GL.currentContext?.GLctx;
return !(contextHandle && !GLctx);
},
getContext:(contextHandle) => {
@@ -2547,7 +2497,7 @@ function dbg(...args) {
}
// Make sure the canvas object no longer refers to the context object so
// there are no GC surprises.
- if (GL.contexts[contextHandle] && GL.contexts[contextHandle].GLctx.canvas) {
+ if (GL.contexts[contextHandle]?.GLctx.canvas) {
GL.contexts[contextHandle].GLctx.canvas.GLctxObject = undefined;
}
GL.contexts[contextHandle] = null;
@@ -2633,6 +2583,11 @@ function dbg(...args) {
renderViaOffscreenBackBuffer: HEAP8[attributes + 32]
};
+ // TODO: Make these into hard errors at some point in the future
+ if (contextAttributes.majorVersion !== 1 && contextAttributes.majorVersion !== 2) {
+ err(`Invalid WebGL version requested: ${contextAttributes.majorVersion}`);
+ }
+
var canvas = findCanvasEventTarget(target);
if (!canvas) {
@@ -2664,8 +2619,8 @@ function dbg(...args) {
abort('fd_close called without SYSCALLS_REQUIRE_FILESYSTEM');
};
- function _fd_seek(fd,offset_low, offset_high,whence,newOffset) {
- var offset = convertI32PairToI53Checked(offset_low, offset_high);
+ function _fd_seek(fd, offset, whence, newOffset) {
+ offset = bigintToI53Checked(offset);
return 70;
@@ -2714,6 +2669,14 @@ function dbg(...args) {
};
var _glBindBuffer = (target, buffer) => {
+ // Calling glBindBuffer with an unknown buffer will implicitly create a
+ // new one. Here we bypass `GL.counter` and directly using the ID passed
+ // in.
+ if (buffer && !GL.buffers[buffer]) {
+ var b = GLctx.createBuffer();
+ b.name = buffer;
+ GL.buffers[buffer] = b;
+ }
if (target == 0x8892 /*GL_ARRAY_BUFFER*/) {
GLctx.currentArrayBufferBinding = buffer;
} else if (target == 0x8893 /*GL_ELEMENT_ARRAY_BUFFER*/) {
@@ -2866,6 +2829,8 @@ function dbg(...args) {
GLctx.depthMask(!!flag);
};
+ var _glDisable = (x0) => GLctx.disable(x0);
+
var _glDrawArrays = (mode, first, count) => {
// bind any client-side buffers
GL.preDrawHandleClientVertexAttribBindings(first + count);
@@ -2946,9 +2911,8 @@ function dbg(...args) {
};
- var _glGetAttribLocation = (program, name) => {
- return GLctx.getAttribLocation(GL.programs[program], UTF8ToString(name));
- };
+ var _glGetAttribLocation = (program, name) =>
+ GLctx.getAttribLocation(GL.programs[program], UTF8ToString(name));
var _glGetProgramInfoLog = (program, maxLength, length, infoLog) => {
var log = GLctx.getProgramInfoLog(GL.programs[program]);
@@ -3179,6 +3143,11 @@ function dbg(...args) {
};
+ var _glUniform3f = (location, v0, v1, v2) => {
+ GLctx.uniform3f(webglGetUniformLocation(location), v0, v1, v2);
+ };
+
+
var miniTempWebGLFloatBuffers = [];
var _glUniformMatrix4fv = (location, count, transpose, value) => {
@@ -3268,20 +3237,16 @@ function checkIncomingModuleAPI() {
}
var wasmImports = {
/** @export */
- __assert_fail: ___assert_fail,
- /** @export */
_abort_js: __abort_js,
/** @export */
- _emscripten_fetch_free: __emscripten_fetch_free,
- /** @export */
- _emscripten_memcpy_js: __emscripten_memcpy_js,
- /** @export */
_localtime_js: __localtime_js,
/** @export */
_tzset_js: __tzset_js,
/** @export */
emscripten_date_now: _emscripten_date_now,
/** @export */
+ emscripten_fetch_free: _emscripten_fetch_free,
+ /** @export */
emscripten_get_element_css_size: _emscripten_get_element_css_size,
/** @export */
emscripten_is_main_browser_thread: _emscripten_is_main_browser_thread,
@@ -3344,6 +3309,8 @@ var wasmImports = {
/** @export */
glDepthMask: _glDepthMask,
/** @export */
+ glDisable: _glDisable,
+ /** @export */
glDrawArrays: _glDrawArrays,
/** @export */
glDrawArraysInstanced: _glDrawArraysInstanced,
@@ -3376,6 +3343,8 @@ var wasmImports = {
/** @export */
glUniform1f: _glUniform1f,
/** @export */
+ glUniform3f: _glUniform3f,
+ /** @export */
glUniformMatrix4fv: _glUniformMatrix4fv,
/** @export */
glUseProgram: _glUseProgram,
@@ -3384,15 +3353,14 @@ var wasmImports = {
/** @export */
glVertexAttribPointer: _glVertexAttribPointer
};
-var wasmExports = createWasm();
+var wasmExports;
+createWasm();
var ___wasm_call_ctors = createExportWrapper('__wasm_call_ctors', 0);
var _main = Module['_main'] = createExportWrapper('main', 2);
var _free = createExportWrapper('free', 1);
var _malloc = createExportWrapper('malloc', 1);
var _fflush = createExportWrapper('fflush', 1);
var _strerror = createExportWrapper('strerror', 1);
-var __emscripten_tempret_set = createExportWrapper('_emscripten_tempret_set', 1);
-var __emscripten_tempret_get = createExportWrapper('_emscripten_tempret_get', 0);
var _emscripten_stack_init = () => (_emscripten_stack_init = wasmExports['emscripten_stack_init'])();
var _emscripten_stack_get_free = () => (_emscripten_stack_get_free = wasmExports['emscripten_stack_get_free'])();
var _emscripten_stack_get_base = () => (_emscripten_stack_get_base = wasmExports['emscripten_stack_get_base'])();
@@ -3400,7 +3368,6 @@ var _emscripten_stack_get_end = () => (_emscripten_stack_get_end = wasmExports['
var __emscripten_stack_restore = (a0) => (__emscripten_stack_restore = wasmExports['_emscripten_stack_restore'])(a0);
var __emscripten_stack_alloc = (a0) => (__emscripten_stack_alloc = wasmExports['_emscripten_stack_alloc'])(a0);
var _emscripten_stack_get_current = () => (_emscripten_stack_get_current = wasmExports['emscripten_stack_get_current'])();
-var dynCall_jiji = Module['dynCall_jiji'] = createExportWrapper('dynCall_jiji', 5);
// include: postamble.js
@@ -3412,6 +3379,7 @@ var missingLibrarySymbols = [
'writeI53ToU64Clamped',
'writeI53ToU64Signaling',
'convertI32PairToI53',
+ 'convertI32PairToI53Checked',
'convertU32PairToI53',
'stackAlloc',
'getTempRet0',
@@ -3424,14 +3392,11 @@ var missingLibrarySymbols = [
'inetNtop6',
'readSockaddr',
'writeSockaddr',
- 'initRandomFill',
- 'randomFill',
'emscriptenLog',
'readEmAsmArgs',
'getExecutableName',
'listenOnce',
'autoResumeAudioContext',
- 'dynCallLegacy',
'getDynCaller',
'dynCall',
'runtimeKeepalivePush',
@@ -3440,6 +3405,9 @@ var missingLibrarySymbols = [
'asyncLoad',
'mmapAlloc',
'getNativeTypeSize',
+ 'addOnInit',
+ 'addOnPostCtor',
+ 'addOnPreMain',
'STACK_SIZE',
'STACK_ALIGN',
'POINTER_SIZE',
@@ -3516,11 +3484,12 @@ var missingLibrarySymbols = [
'checkWasiClock',
'wasiRightsToMuslOFlags',
'wasiOFlagsToMuslOFlags',
- 'createDyncallWrapper',
+ 'initRandomFill',
+ 'randomFill',
'safeSetTimeout',
'setImmediateWrapped',
+ 'safeRequestAnimationFrame',
'clearImmediateWrapped',
- 'polyfillSetImmediate',
'registerPostMainLoop',
'getPromise',
'makePromise',
@@ -3529,7 +3498,6 @@ var missingLibrarySymbols = [
'ExceptionInfo',
'findMatchingCatch',
'Browser_asyncPrepareDataCounter',
- 'safeRequestAnimationFrame',
'arraySum',
'addDays',
'getSocketFromFD',
@@ -3570,11 +3538,6 @@ missingLibrarySymbols.forEach(missingLibrarySymbol)
var unexportedSymbols = [
'run',
- 'addOnPreRun',
- 'addOnInit',
- 'addOnPreMain',
- 'addOnExit',
- 'addOnPostRun',
'addRunDependency',
'removeRunDependency',
'out',
@@ -3588,7 +3551,9 @@ var unexportedSymbols = [
'writeI53ToI64',
'readI53FromI64',
'readI53FromU64',
- 'convertI32PairToI53Checked',
+ 'INT53_MAX',
+ 'INT53_MIN',
+ 'bigintToI53Checked',
'stackSave',
'stackRestore',
'ptrToString',
@@ -3613,6 +3578,9 @@ var unexportedSymbols = [
'HandleAllocator',
'wasmTable',
'noExitRuntime',
+ 'addOnPreRun',
+ 'addOnExit',
+ 'addOnPostRun',
'freeTableIndexes',
'functionsInTableMap',
'setValue',
@@ -3640,6 +3608,9 @@ var unexportedSymbols = [
'UNWIND_CACHE',
'ExitStatus',
'flush_NO_FILESYSTEM',
+ 'emSetImmediate',
+ 'emClearImmediate_deps',
+ 'emClearImmediate',
'registerPreMainLoop',
'promiseMap',
'uncaughtExceptionCount',
@@ -3704,17 +3675,10 @@ unexportedSymbols.forEach(unexportedRuntimeSymbol);
var calledRun;
-var calledPrerun;
-
-dependenciesFulfilled = function runCaller() {
- // If run has never been called, and we should call run (INVOKE_RUN is true, and Module.noInitialRun is not false)
- if (!calledRun) run();
- if (!calledRun) dependenciesFulfilled = runCaller; // try this again later, after new deps are fulfilled
-};
function callMain() {
assert(runDependencies == 0, 'cannot call main when async dependencies remain! (listen on Module["onRuntimeInitialized"])');
- assert(calledPrerun, 'cannot call main without calling preRun first');
+ assert(typeof onPreRuns === 'undefined' || onPreRuns.length == 0, 'cannot call main when preRun functions remain to be called');
var entryFunction = _main;
@@ -3728,8 +3692,7 @@ function callMain() {
// if we're not running an evented main loop, it's time to exit
exitJS(ret, /* implicit = */ true);
return ret;
- }
- catch (e) {
+ } catch (e) {
return handleException(e);
}
}
@@ -3746,27 +3709,26 @@ function stackCheckInit() {
function run() {
if (runDependencies > 0) {
+ dependenciesFulfilled = run;
return;
}
- stackCheckInit();
+ stackCheckInit();
- if (!calledPrerun) {
- calledPrerun = 1;
- preRun();
+ preRun();
- // a preRun added a dependency, run will be called later
- if (runDependencies > 0) {
- return;
- }
+ // a preRun added a dependency, run will be called later
+ if (runDependencies > 0) {
+ dependenciesFulfilled = run;
+ return;
}
function doRun() {
// run may have just been called through dependencies being fulfilled just in this very frame,
// or while the async setStatus time below was happening
- if (calledRun) return;
- calledRun = 1;
- Module['calledRun'] = 1;
+ assert(!calledRun);
+ calledRun = true;
+ Module['calledRun'] = true;
if (ABORT) return;
@@ -3776,7 +3738,8 @@ function run() {
Module['onRuntimeInitialized']?.();
- if (shouldRunNow) callMain();
+ var noInitialRun = Module['noInitialRun'];legacyModuleProp('noInitialRun', 'noInitialRun');
+ if (!noInitialRun) callMain();
postRun();
}
@@ -3830,11 +3793,6 @@ if (Module['preInit']) {
}
}
-// shouldRunNow refers to calling main(), not run().
-var shouldRunNow = true;
-
-if (Module['noInitialRun']) shouldRunNow = false;
-
run();
// end include: postamble.js
diff --git a/themes/dist/output.wasm b/themes/dist/output.wasm
index 1f413e5..27157fe 100755
--- a/themes/dist/output.wasm
+++ b/themes/dist/output.wasm
Binary files differ
diff --git a/themes/meson.build b/themes/meson.build
index f99bf5c..c4af1c6 100644
--- a/themes/meson.build
+++ b/themes/meson.build
@@ -42,6 +42,10 @@ sources = files(
'src/shaders/snowflake_vert.cpp',
'src/shaders/grass_frag.cpp',
'src/shaders/grass_vert.cpp',
+ 'src/shaders/water_frag.cpp',
+ 'src/shaders/water_vert.cpp',
+ 'src/shaders/sky_frag.cpp',
+ 'src/shaders/sky_vert.cpp',
# Autumn theme
'src/autumn/autumn_theme.cpp',
@@ -58,7 +62,9 @@ sources = files(
'src/spring/grass_renderer.cpp',
# Summer theme
- 'src/summer/summer_theme.cpp'
+ 'src/summer/summer_theme.cpp',
+ 'src/summer/water_model.cpp',
+ 'src/summer/sky_model.cpp'
)
# Include directories
@@ -75,7 +81,7 @@ inc = include_directories(
# Custom target to build shaders from GLSL files
node = find_program('node')
-shader_script = meson.project_source_root() + '/src/tools/shader.js'
+shader_script = meson.project_source_root() + '/src/tools/shader.cjs'
# List all GLSL shader files as inputs
shader_inputs = files(
@@ -88,7 +94,11 @@ shader_inputs = files(
'src/_shaders/snowflake.frag',
'src/_shaders/snowflake.vert',
'src/_shaders/grass.frag',
- 'src/_shaders/grass.vert'
+ 'src/_shaders/grass.vert',
+ 'src/_shaders/water.frag',
+ 'src/_shaders/water.vert',
+ 'src/_shaders/sky.frag',
+ 'src/_shaders/sky.vert'
)
# Custom target that runs whenever shader files change
diff --git a/themes/src/_shaders/sky.frag b/themes/src/_shaders/sky.frag
new file mode 100644
index 0000000..a2eb18b
--- /dev/null
+++ b/themes/src/_shaders/sky.frag
@@ -0,0 +1,78 @@
+precision highp float;
+
+varying vec2 vUv;
+
+uniform float time; // seconds, for slow cloud drift
+uniform float horizon; // screen-space y of the water horizon (~0.5)
+
+float hash(vec2 p) {
+ p = fract(p * vec2(123.34, 456.21));
+ p += dot(p, p + 45.32);
+ return fract(p.x * p.y);
+}
+
+float valueNoise(vec2 p) {
+ vec2 i = floor(p);
+ vec2 f = fract(p);
+ float a = hash(i);
+ float b = hash(i + vec2(1.0, 0.0));
+ float c = hash(i + vec2(0.0, 1.0));
+ float d = hash(i + vec2(1.0, 1.0));
+ vec2 u = f * f * (3.0 - 2.0 * f);
+ return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
+}
+
+float fbm(vec2 p) {
+ float v = 0.0;
+ float amp = 0.5;
+ for (int i = 0; i < 4; i++) {
+ v += amp * valueNoise(p);
+ p *= 2.0;
+ amp *= 0.5;
+ }
+ return v;
+}
+
+const vec3 SKY_HORIZON = vec3(0.80, 0.88, 0.98); // matches water horizon fade
+const vec3 SKY_TOP = vec3(0.22, 0.48, 0.86); // deeper zenith blue
+const vec3 CLOUD_COLOR = vec3(1.00, 0.98, 0.95); // white, slightly warm
+
+const float DEPTH_BIAS = 0.05; // caps frequency at the horizon (larger = gentler shrink)
+const float CLOUD_FREQ = 2.7; // overall cloud scale (larger = more, smaller clouds)
+const float X_STRETCH = 0.35; // < 1 stretches clouds horizontally -> wispy
+const float CLOUD_THRESH = 0.55; // coverage cutoff; higher = sparser sky
+const float CLOUD_SOFT = 0.22; // edge softness of the cloud mask
+const float CLOUD_OPACITY = 0.85; // max cloud whiteness
+const float DRIFT_SPEED = 0.03; // slow lateral drift (noise units / sec)
+const float H_FADE = 0.12; // band over which clouds fade in above horizon
+
+void main() {
+ // Height above the water horizon: 0 at the waterline, (1 - horizon) at the top.
+ float above = max(vUv.y - horizon, 0.0);
+
+ // Vertical gradient.
+ float g = clamp(above / (1.0 - horizon), 0.0, 1.0);
+ vec3 col = mix(SKY_HORIZON, SKY_TOP, pow(g, 0.75));
+ float haze = 1.0 - smoothstep(0.0, 0.35, g);
+ col += vec3(0.06, 0.04, 0.00) * haze;
+
+ // Flat cloud-ceiling perspective: distance along the view ~ 1/above, so
+ // features shrink AND compress vertically toward the horizon.
+ float depth = 1.0 / (above + DEPTH_BIAS);
+ vec2 q;
+ q.x = (vUv.x - 0.5) * depth;
+ q.y = depth;
+
+ vec2 p = vec2(q.x * X_STRETCH, q.y) * CLOUD_FREQ;
+ p.x += time * DRIFT_SPEED;
+
+ float n = fbm(p);
+ float cloud = smoothstep(CLOUD_THRESH, CLOUD_THRESH + CLOUD_SOFT, n);
+
+ // Fade clouds into the haze right at the horizon.
+ cloud *= smoothstep(0.0, H_FADE, above);
+
+ col = mix(col, CLOUD_COLOR, cloud * CLOUD_OPACITY);
+
+ gl_FragColor = vec4(col, 1.0);
+}
diff --git a/themes/src/_shaders/sky.vert b/themes/src/_shaders/sky.vert
new file mode 100644
index 0000000..249603c
--- /dev/null
+++ b/themes/src/_shaders/sky.vert
@@ -0,0 +1,10 @@
+precision highp float;
+
+attribute vec2 position; // fullscreen quad, clip-space corners in [-1, 1]
+
+varying vec2 vUv;
+
+void main() {
+ vUv = position * 0.5 + 0.5; // uv.y = 0 bottom, 1 top
+ gl_Position = vec4(position, 0.0, 1.0);
+}
diff --git a/themes/src/_shaders/water.frag b/themes/src/_shaders/water.frag
new file mode 100644
index 0000000..0ceffeb
--- /dev/null
+++ b/themes/src/_shaders/water.frag
@@ -0,0 +1,63 @@
+precision highp float;
+
+varying vec3 vWorldPos;
+varying vec3 vNormal;
+
+uniform vec3 cameraPos;
+uniform vec3 sunDir; // normalized direction from the water toward the sun
+uniform vec3 sunColor;
+uniform float time;
+
+float hash(vec2 p) {
+ p = fract(p * vec2(123.34, 456.21));
+ p += dot(p, p + 45.32);
+ return fract(p.x * p.y);
+}
+
+float valueNoise(vec2 p) {
+ vec2 i = floor(p);
+ vec2 f = fract(p);
+ float a = hash(i);
+ float b = hash(i + vec2(1.0, 0.0));
+ float c = hash(i + vec2(0.0, 1.0));
+ float d = hash(i + vec2(1.0, 1.0));
+ vec2 u = f * f * (3.0 - 2.0 * f);
+ return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
+}
+
+void main() {
+ vec3 N = normalize(vNormal);
+ vec3 V = normalize(cameraPos - vWorldPos);
+
+ // Fresnel (Schlick).
+ float f0 = 0.02;
+ float fres = f0 + (1.0 - f0) * pow(1.0 - max(dot(N, V), 0.0), 5.0);
+
+ // Refraction: depth-tinted water body, wobbled by the surface normal.
+ float depth01 = clamp((vWorldPos.z + 350.0) / 400.0, 0.0, 1.0);
+ float wob = (N.x + N.z) * 0.5;
+ vec3 shallow = vec3(0.10, 0.55, 0.60);
+ vec3 deep = vec3(0.02, 0.14, 0.28);
+ vec3 refractCol = mix(shallow, deep, clamp(depth01 + wob * 0.15, 0.0, 1.0));
+
+ // Reflection: sky gradient + reflected sun glow + broken-up specular glitter.
+ vec3 R = reflect(-V, N);
+ vec3 skyHorizon = vec3(0.80, 0.88, 0.98);
+ vec3 skyTop = vec3(0.30, 0.55, 0.95);
+ float skyT = clamp(R.y * 2.0, 0.0, 1.0);
+ vec3 reflectCol = mix(skyHorizon, skyTop, skyT);
+
+ float sunAmt = max(dot(R, sunDir), 0.0);
+ reflectCol += sunColor * pow(sunAmt, 8.0) * 0.6;
+ float sparkle = valueNoise(vWorldPos.xz * 0.5 + vec2(time * 1.3, time * 0.7));
+ float spec = pow(sunAmt, 200.0) * smoothstep(0.55, 0.9, sparkle);
+ reflectCol += sunColor * spec * 4.0;
+
+ vec3 color = mix(refractCol, reflectCol, fres);
+
+ // Horizon haze: hide the far straight edge of the plane by fading to sky.
+ float horizonFade = clamp((vWorldPos.z + 350.0) / 90.0, 0.0, 1.0);
+ color = mix(skyHorizon, color, horizonFade);
+
+ gl_FragColor = vec4(color, 1.0); // opaque -> occludes the sun below the waterline
+}
diff --git a/themes/src/_shaders/water.vert b/themes/src/_shaders/water.vert
new file mode 100644
index 0000000..7121ab1
--- /dev/null
+++ b/themes/src/_shaders/water.vert
@@ -0,0 +1,63 @@
+precision highp float;
+
+attribute vec2 position; // local (x, z) on the plane
+
+uniform mat4 projection;
+uniform mat4 view;
+uniform mat4 model;
+uniform float time;
+uniform float amplitude;
+
+varying vec3 vWorldPos;
+varying vec3 vNormal;
+
+float hash(vec2 p) {
+ p = fract(p * vec2(123.34, 456.21));
+ p += dot(p, p + 45.32);
+ return fract(p.x * p.y);
+}
+
+float valueNoise(vec2 p) {
+ vec2 i = floor(p);
+ vec2 f = fract(p);
+ float a = hash(i);
+ float b = hash(i + vec2(1.0, 0.0));
+ float c = hash(i + vec2(0.0, 1.0));
+ float d = hash(i + vec2(1.0, 1.0));
+ vec2 u = f * f * (3.0 - 2.0 * f);
+ return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
+}
+
+// Irregular height: non-harmonic directional sines plus two octaves of
+// scrolling value noise so it never reads as a single clean wave.
+float waveHeight(vec2 p, float t) {
+ float h = 0.0;
+ h += 0.30 * sin(dot(p, vec2(0.90, 0.30)) * 0.35 + t * 1.1);
+ h += 0.20 * sin(dot(p, vec2(-0.40, 1.00)) * 0.55 - t * 1.7);
+ h += 0.12 * sin(dot(p, vec2(0.70, -0.80)) * 0.90 + t * 2.3);
+ h += 0.08 * sin(dot(p, vec2(1.00, 0.60)) * 1.60 - t * 3.1);
+ h += 0.18 * (valueNoise(p * 0.25 + vec2(t * 0.15, t * 0.10)) - 0.5);
+ h += 0.09 * (valueNoise(p * 0.70 - vec2(t * 0.20, 0.0)) - 0.5);
+ return h;
+}
+
+void main() {
+ vec2 p = position;
+ float t = time;
+
+ float h = waveHeight(p, t) * amplitude;
+
+ // Analytic normal via central differences of the same height function.
+ float e = 2.0;
+ float hL = waveHeight(p - vec2(e, 0.0), t) * amplitude;
+ float hR = waveHeight(p + vec2(e, 0.0), t) * amplitude;
+ float hD = waveHeight(p - vec2(0.0, e), t) * amplitude;
+ float hU = waveHeight(p + vec2(0.0, e), t) * amplitude;
+ vec3 N = normalize(vec3(hL - hR, 2.0 * e, hD - hU));
+
+ vec4 worldPos = model * vec4(p.x, h, p.y, 1.0);
+ vWorldPos = worldPos.xyz;
+ vNormal = N; // model is translation-only, so no normal matrix needed
+
+ gl_Position = projection * view * worldPos;
+}
diff --git a/themes/src/shaders/sky_frag.cpp b/themes/src/shaders/sky_frag.cpp
new file mode 100644
index 0000000..400f07c
--- /dev/null
+++ b/themes/src/shaders/sky_frag.cpp
@@ -0,0 +1,81 @@
+#include "sky_frag.h"
+
+const char* shader_sky_frag = "precision highp float; \n"
+" \n"
+"varying vec2 vUv; \n"
+" \n"
+"uniform float time; // seconds, for slow cloud drift \n"
+"uniform float horizon; // screen-space y of the water horizon (~0.5) \n"
+" \n"
+"float hash(vec2 p) { \n"
+" p = fract(p * vec2(123.34, 456.21)); \n"
+" p += dot(p, p + 45.32); \n"
+" return fract(p.x * p.y); \n"
+"} \n"
+" \n"
+"float valueNoise(vec2 p) { \n"
+" vec2 i = floor(p); \n"
+" vec2 f = fract(p); \n"
+" float a = hash(i); \n"
+" float b = hash(i + vec2(1.0, 0.0)); \n"
+" float c = hash(i + vec2(0.0, 1.0)); \n"
+" float d = hash(i + vec2(1.0, 1.0)); \n"
+" vec2 u = f * f * (3.0 - 2.0 * f); \n"
+" return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); \n"
+"} \n"
+" \n"
+"float fbm(vec2 p) { \n"
+" float v = 0.0; \n"
+" float amp = 0.5; \n"
+" for (int i = 0; i < 4; i++) { \n"
+" v += amp * valueNoise(p); \n"
+" p *= 2.0; \n"
+" amp *= 0.5; \n"
+" } \n"
+" return v; \n"
+"} \n"
+" \n"
+"const vec3 SKY_HORIZON = vec3(0.80, 0.88, 0.98); // matches water horizon fade \n"
+"const vec3 SKY_TOP = vec3(0.22, 0.48, 0.86); // deeper zenith blue \n"
+"const vec3 CLOUD_COLOR = vec3(1.00, 0.98, 0.95); // white, slightly warm \n"
+" \n"
+"const float DEPTH_BIAS = 0.05; // caps frequency at the horizon (larger = gentler shrink) \n"
+"const float CLOUD_FREQ = 2.7; // overall cloud scale (larger = more, smaller clouds) \n"
+"const float X_STRETCH = 0.35; // < 1 stretches clouds horizontally -> wispy \n"
+"const float CLOUD_THRESH = 0.55; // coverage cutoff; higher = sparser sky \n"
+"const float CLOUD_SOFT = 0.22; // edge softness of the cloud mask \n"
+"const float CLOUD_OPACITY = 0.85; // max cloud whiteness \n"
+"const float DRIFT_SPEED = 0.03; // slow lateral drift (noise units / sec) \n"
+"const float H_FADE = 0.12; // band over which clouds fade in above horizon \n"
+" \n"
+"void main() { \n"
+" // Height above the water horizon: 0 at the waterline, (1 - horizon) at the top. \n"
+" float above = max(vUv.y - horizon, 0.0); \n"
+" \n"
+" // Vertical gradient. \n"
+" float g = clamp(above / (1.0 - horizon), 0.0, 1.0); \n"
+" vec3 col = mix(SKY_HORIZON, SKY_TOP, pow(g, 0.75)); \n"
+" float haze = 1.0 - smoothstep(0.0, 0.35, g); \n"
+" col += vec3(0.06, 0.04, 0.00) * haze; \n"
+" \n"
+" // Flat cloud-ceiling perspective: distance along the view ~ 1/above, so \n"
+" // features shrink AND compress vertically toward the horizon. \n"
+" float depth = 1.0 / (above + DEPTH_BIAS); \n"
+" vec2 q; \n"
+" q.x = (vUv.x - 0.5) * depth; \n"
+" q.y = depth; \n"
+" \n"
+" vec2 p = vec2(q.x * X_STRETCH, q.y) * CLOUD_FREQ; \n"
+" p.x += time * DRIFT_SPEED; \n"
+" \n"
+" float n = fbm(p); \n"
+" float cloud = smoothstep(CLOUD_THRESH, CLOUD_THRESH + CLOUD_SOFT, n); \n"
+" \n"
+" // Fade clouds into the haze right at the horizon. \n"
+" cloud *= smoothstep(0.0, H_FADE, above); \n"
+" \n"
+" col = mix(col, CLOUD_COLOR, cloud * CLOUD_OPACITY); \n"
+" \n"
+" gl_FragColor = vec4(col, 1.0); \n"
+"} \n"
+" \n";
diff --git a/themes/src/shaders/sky_frag.h b/themes/src/shaders/sky_frag.h
new file mode 100644
index 0000000..886b6eb
--- /dev/null
+++ b/themes/src/shaders/sky_frag.h
@@ -0,0 +1,4 @@
+#ifndef SHADER_SKY_FRAG
+#define SHADER_SKY_FRAG
+extern const char* shader_sky_frag;
+#endif
diff --git a/themes/src/shaders/sky_vert.cpp b/themes/src/shaders/sky_vert.cpp
new file mode 100644
index 0000000..b43aa73
--- /dev/null
+++ b/themes/src/shaders/sky_vert.cpp
@@ -0,0 +1,13 @@
+#include "sky_vert.h"
+
+const char* shader_sky_vert = "precision highp float; \n"
+" \n"
+"attribute vec2 position; // fullscreen quad, clip-space corners in [-1, 1] \n"
+" \n"
+"varying vec2 vUv; \n"
+" \n"
+"void main() { \n"
+" vUv = position * 0.5 + 0.5; // uv.y = 0 bottom, 1 top \n"
+" gl_Position = vec4(position, 0.0, 1.0); \n"
+"} \n"
+" \n";
diff --git a/themes/src/shaders/sky_vert.h b/themes/src/shaders/sky_vert.h
new file mode 100644
index 0000000..114fe63
--- /dev/null
+++ b/themes/src/shaders/sky_vert.h
@@ -0,0 +1,4 @@
+#ifndef SHADER_SKY_VERT
+#define SHADER_SKY_VERT
+extern const char* shader_sky_vert;
+#endif
diff --git a/themes/src/shaders/water_frag.cpp b/themes/src/shaders/water_frag.cpp
new file mode 100644
index 0000000..91c50f9
--- /dev/null
+++ b/themes/src/shaders/water_frag.cpp
@@ -0,0 +1,66 @@
+#include "water_frag.h"
+
+const char* shader_water_frag = "precision highp float; \n"
+" \n"
+"varying vec3 vWorldPos; \n"
+"varying vec3 vNormal; \n"
+" \n"
+"uniform vec3 cameraPos; \n"
+"uniform vec3 sunDir; // normalized direction from the water toward the sun \n"
+"uniform vec3 sunColor; \n"
+"uniform float time; \n"
+" \n"
+"float hash(vec2 p) { \n"
+" p = fract(p * vec2(123.34, 456.21)); \n"
+" p += dot(p, p + 45.32); \n"
+" return fract(p.x * p.y); \n"
+"} \n"
+" \n"
+"float valueNoise(vec2 p) { \n"
+" vec2 i = floor(p); \n"
+" vec2 f = fract(p); \n"
+" float a = hash(i); \n"
+" float b = hash(i + vec2(1.0, 0.0)); \n"
+" float c = hash(i + vec2(0.0, 1.0)); \n"
+" float d = hash(i + vec2(1.0, 1.0)); \n"
+" vec2 u = f * f * (3.0 - 2.0 * f); \n"
+" return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); \n"
+"} \n"
+" \n"
+"void main() { \n"
+" vec3 N = normalize(vNormal); \n"
+" vec3 V = normalize(cameraPos - vWorldPos); \n"
+" \n"
+" // Fresnel (Schlick). \n"
+" float f0 = 0.02; \n"
+" float fres = f0 + (1.0 - f0) * pow(1.0 - max(dot(N, V), 0.0), 5.0); \n"
+" \n"
+" // Refraction: depth-tinted water body, wobbled by the surface normal. \n"
+" float depth01 = clamp((vWorldPos.z + 350.0) / 400.0, 0.0, 1.0); \n"
+" float wob = (N.x + N.z) * 0.5; \n"
+" vec3 shallow = vec3(0.10, 0.55, 0.60); \n"
+" vec3 deep = vec3(0.02, 0.14, 0.28); \n"
+" vec3 refractCol = mix(shallow, deep, clamp(depth01 + wob * 0.15, 0.0, 1.0)); \n"
+" \n"
+" // Reflection: sky gradient + reflected sun glow + broken-up specular glitter. \n"
+" vec3 R = reflect(-V, N); \n"
+" vec3 skyHorizon = vec3(0.80, 0.88, 0.98); \n"
+" vec3 skyTop = vec3(0.30, 0.55, 0.95); \n"
+" float skyT = clamp(R.y * 2.0, 0.0, 1.0); \n"
+" vec3 reflectCol = mix(skyHorizon, skyTop, skyT); \n"
+" \n"
+" float sunAmt = max(dot(R, sunDir), 0.0); \n"
+" reflectCol += sunColor * pow(sunAmt, 8.0) * 0.6; \n"
+" float sparkle = valueNoise(vWorldPos.xz * 0.5 + vec2(time * 1.3, time * 0.7)); \n"
+" float spec = pow(sunAmt, 200.0) * smoothstep(0.55, 0.9, sparkle); \n"
+" reflectCol += sunColor * spec * 4.0; \n"
+" \n"
+" vec3 color = mix(refractCol, reflectCol, fres); \n"
+" \n"
+" // Horizon haze: hide the far straight edge of the plane by fading to sky. \n"
+" float horizonFade = clamp((vWorldPos.z + 350.0) / 90.0, 0.0, 1.0); \n"
+" color = mix(skyHorizon, color, horizonFade); \n"
+" \n"
+" gl_FragColor = vec4(color, 1.0); // opaque -> occludes the sun below the waterline \n"
+"} \n"
+" \n";
diff --git a/themes/src/shaders/water_frag.h b/themes/src/shaders/water_frag.h
new file mode 100644
index 0000000..104cb04
--- /dev/null
+++ b/themes/src/shaders/water_frag.h
@@ -0,0 +1,4 @@
+#ifndef SHADER_WATER_FRAG
+#define SHADER_WATER_FRAG
+extern const char* shader_water_frag;
+#endif
diff --git a/themes/src/shaders/water_vert.cpp b/themes/src/shaders/water_vert.cpp
new file mode 100644
index 0000000..44dabd8
--- /dev/null
+++ b/themes/src/shaders/water_vert.cpp
@@ -0,0 +1,66 @@
+#include "water_vert.h"
+
+const char* shader_water_vert = "precision highp float; \n"
+" \n"
+"attribute vec2 position; // local (x, z) on the plane \n"
+" \n"
+"uniform mat4 projection; \n"
+"uniform mat4 view; \n"
+"uniform mat4 model; \n"
+"uniform float time; \n"
+"uniform float amplitude; \n"
+" \n"
+"varying vec3 vWorldPos; \n"
+"varying vec3 vNormal; \n"
+" \n"
+"float hash(vec2 p) { \n"
+" p = fract(p * vec2(123.34, 456.21)); \n"
+" p += dot(p, p + 45.32); \n"
+" return fract(p.x * p.y); \n"
+"} \n"
+" \n"
+"float valueNoise(vec2 p) { \n"
+" vec2 i = floor(p); \n"
+" vec2 f = fract(p); \n"
+" float a = hash(i); \n"
+" float b = hash(i + vec2(1.0, 0.0)); \n"
+" float c = hash(i + vec2(0.0, 1.0)); \n"
+" float d = hash(i + vec2(1.0, 1.0)); \n"
+" vec2 u = f * f * (3.0 - 2.0 * f); \n"
+" return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); \n"
+"} \n"
+" \n"
+"// Irregular height: non-harmonic directional sines plus two octaves of \n"
+"// scrolling value noise so it never reads as a single clean wave. \n"
+"float waveHeight(vec2 p, float t) { \n"
+" float h = 0.0; \n"
+" h += 0.30 * sin(dot(p, vec2(0.90, 0.30)) * 0.35 + t * 1.1); \n"
+" h += 0.20 * sin(dot(p, vec2(-0.40, 1.00)) * 0.55 - t * 1.7); \n"
+" h += 0.12 * sin(dot(p, vec2(0.70, -0.80)) * 0.90 + t * 2.3); \n"
+" h += 0.08 * sin(dot(p, vec2(1.00, 0.60)) * 1.60 - t * 3.1); \n"
+" h += 0.18 * (valueNoise(p * 0.25 + vec2(t * 0.15, t * 0.10)) - 0.5); \n"
+" h += 0.09 * (valueNoise(p * 0.70 - vec2(t * 0.20, 0.0)) - 0.5); \n"
+" return h; \n"
+"} \n"
+" \n"
+"void main() { \n"
+" vec2 p = position; \n"
+" float t = time; \n"
+" \n"
+" float h = waveHeight(p, t) * amplitude; \n"
+" \n"
+" // Analytic normal via central differences of the same height function. \n"
+" float e = 2.0; \n"
+" float hL = waveHeight(p - vec2(e, 0.0), t) * amplitude; \n"
+" float hR = waveHeight(p + vec2(e, 0.0), t) * amplitude; \n"
+" float hD = waveHeight(p - vec2(0.0, e), t) * amplitude; \n"
+" float hU = waveHeight(p + vec2(0.0, e), t) * amplitude; \n"
+" vec3 N = normalize(vec3(hL - hR, 2.0 * e, hD - hU)); \n"
+" \n"
+" vec4 worldPos = model * vec4(p.x, h, p.y, 1.0); \n"
+" vWorldPos = worldPos.xyz; \n"
+" vNormal = N; // model is translation-only, so no normal matrix needed \n"
+" \n"
+" gl_Position = projection * view * worldPos; \n"
+"} \n"
+" \n";
diff --git a/themes/src/shaders/water_vert.h b/themes/src/shaders/water_vert.h
new file mode 100644
index 0000000..638b22b
--- /dev/null
+++ b/themes/src/shaders/water_vert.h
@@ -0,0 +1,4 @@
+#ifndef SHADER_WATER_VERT
+#define SHADER_WATER_VERT
+extern const char* shader_water_vert;
+#endif
diff --git a/themes/src/summer/sky_model.cpp b/themes/src/summer/sky_model.cpp
new file mode 100644
index 0000000..d700f34
--- /dev/null
+++ b/themes/src/summer/sky_model.cpp
@@ -0,0 +1,64 @@
+#include "sky_model.h"
+#include "../webgl_context.h"
+#include "../mathlib.h"
+#include "../shaders/sky_vert.h"
+#include "../shaders/sky_frag.h"
+
+SkyModel::SkyModel() {}
+
+SkyModel::~SkyModel() { unload(); }
+
+void SkyModel::load(WebglContext* inContext) {
+ context = inContext;
+ shader = loadShader(shader_sky_vert, shader_sky_frag);
+ useShader(shader);
+
+ attributes.position = getShaderAttribute(shader, "position");
+ uniforms.time = getShaderUniform(shader, "time");
+ uniforms.horizon = getShaderUniform(shader, "horizon");
+
+ // Full-screen quad in clip space (two triangles).
+ Vector2 verts[6] = {
+ Vector2(-1.f, -1.f), Vector2(1.f, -1.f), Vector2(-1.f, 1.f),
+ Vector2(-1.f, 1.f), Vector2(1.f, -1.f), Vector2( 1.f, 1.f)
+ };
+
+ glGenVertexArrays(1, &vao);
+ glBindVertexArray(vao);
+
+ glGenBuffers(1, &vbo);
+ glBindBuffer(GL_ARRAY_BUFFER, vbo);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
+
+ glEnableVertexAttribArray(attributes.position);
+ glVertexAttribPointer(attributes.position, 2, GL_FLOAT, GL_FALSE, sizeof(Vector2), (GLvoid*)0);
+
+ glBindVertexArray(0);
+}
+
+void SkyModel::update(f32 dtSeconds) {
+ elapsedTime += dtSeconds;
+}
+
+void SkyModel::render() {
+ useShader(shader);
+
+ // Background: paint every pixel first, without touching the depth buffer so
+ // it can neither reject nor be rejected by the sun/water drawn afterward.
+ glDepthMask(GL_FALSE);
+ glDisable(GL_DEPTH_TEST);
+ glDisable(GL_BLEND);
+
+ setShaderFloat(uniforms.time, elapsedTime);
+ setShaderFloat(uniforms.horizon, horizon);
+
+ glBindVertexArray(vao);
+ glDrawArrays(GL_TRIANGLES, 0, 6);
+ glBindVertexArray(0);
+}
+
+void SkyModel::unload() {
+ if (vao) { glDeleteVertexArrays(1, &vao); vao = 0; }
+ if (vbo) { glDeleteBuffers(1, &vbo); vbo = 0; }
+ if (shader) { glDeleteProgram(shader); shader = 0; }
+}
diff --git a/themes/src/summer/sky_model.h b/themes/src/summer/sky_model.h
new file mode 100644
index 0000000..d957f05
--- /dev/null
+++ b/themes/src/summer/sky_model.h
@@ -0,0 +1,43 @@
+#ifndef SKY_MODEL_H
+#define SKY_MODEL_H
+
+#include "../shader.h"
+#include "../types.h"
+
+struct WebglContext;
+
+/// A full-screen sky background. Renders a mid-afternoon gradient (from the
+/// water horizon up to the top of the screen) and sparse, wispy procedural
+/// clouds that shrink toward the horizon. Standalone: owns its own shader and
+/// a two-triangle full-screen quad, drawn before the sun and water.
+class SkyModel {
+public:
+ SkyModel();
+ ~SkyModel();
+
+ void load(WebglContext* context);
+ void update(f32 dtSeconds);
+ void render();
+ void unload();
+
+ /// Screen-space y (0..1) of the water horizon, where the gradient starts.
+ f32 horizon = 0.5f;
+
+private:
+ WebglContext* context = nullptr;
+ Shader shader = 0;
+ u32 vao = 0;
+ u32 vbo = 0;
+ f32 elapsedTime = 0.f;
+
+ struct {
+ i32 position = -1;
+ } attributes;
+
+ struct {
+ i32 time = -1;
+ i32 horizon = -1;
+ } uniforms;
+};
+
+#endif // SKY_MODEL_H
diff --git a/themes/src/summer/summer_theme.cpp b/themes/src/summer/summer_theme.cpp
index 6d2cfec..3b237e2 100644
--- a/themes/src/summer/summer_theme.cpp
+++ b/themes/src/summer/summer_theme.cpp
@@ -4,7 +4,6 @@
#include "../mathlib.h"
#include "../shaders/sun_frag.h"
#include "../shaders/sun_vert.h"
-#include <vector>
SummerTheme::SummerTheme(WebglContext* context)
{
@@ -21,20 +20,45 @@ void SummerTheme::load(WebglContext* context) {
renderer.load(context, shader_sun_vert, shader_sun_frag);
renderer.clearColor = Vector4(0, 181, 286, 255.f).toNormalizedColor();
sun.sectors = 180;
- sun.radius = renderer.context->width / 4.f;
+ sun.radius = renderer.context->width / 6.f;
sun.load(&renderer);
+
+ sky.load(context);
+ water.load(context);
+ // Align the fake reflection with the sun disk: pointing up and into the
+ // scene (x = 0 keeps the glitter column centered under the sun).
+ water.setSun(Vector3(0.f, 0.4f, -1.f), Vector4(249, 215, 28, 255).toNormalizedColor());
}
void SummerTheme::update(f32 dtSeconds) {
sun.update(dtSeconds);
+ sky.update(dtSeconds);
+ water.update(dtSeconds);
}
void SummerTheme::render() {
renderer.render();
+
+ // Gradient sky + clouds behind everything. It uses its own shader and
+ // disables depth writes, so restore the sun shader + projection and
+ // re-enable depth writes before drawing the sun.
+ sky.render();
+ useShader(renderer.shader);
+ setShaderMat4(renderer.uniforms.projection, renderer.projection);
+ glDepthMask(GL_TRUE);
+ // sky.render() disabled blending; the sun's soft glowing edge needs it back,
+ // otherwise its semi-transparent rim writes opaque as a hard white border.
+ glEnable(GL_BLEND);
+
sun.render(&renderer);
+ // Drawn last: the opaque water paints over the lower half of the sun,
+ // hiding the part below the waterline while reflecting the part above.
+ water.render();
}
void SummerTheme::unload() {
+ water.unload();
+ sky.unload();
sun.unload();
}
@@ -61,7 +85,7 @@ void Sun::load(Renderer2d* renderer) {
}
mesh.load(&vertices.data[0], vertices.numElements, &indices.data[0], indices.numElements, renderer);
- mesh.model = Mat4x4().translateByVec2(Vector2(renderer->context->width / 2.f, renderer->context->height / 2.f));
+ mesh.model = Mat4x4().translateByVec2(Vector2(renderer->context->width / 2.f, renderer->context->height * (3.f/ 4.f)));
timeUniform = getShaderUniform(renderer->shader, "time");
diff --git a/themes/src/summer/summer_theme.h b/themes/src/summer/summer_theme.h
index eb404fd..e048a7a 100644
--- a/themes/src/summer/summer_theme.h
+++ b/themes/src/summer/summer_theme.h
@@ -2,6 +2,8 @@
#include "../types.h"
#include "../renderer_2d.h"
#include "../theme.h"
+#include "sky_model.h"
+#include "water_model.h"
#include <vector>
struct Sun {
@@ -22,6 +24,8 @@ public:
SummerTheme(WebglContext*);
~SummerTheme();
Sun sun;
+ SkyModel sky;
+ WaterModel water{ Vector3(0.f, 0.f, -150.f), Vector3(600.f, 0.6f, 400.f) };
void load(WebglContext*);
void update(f32 dtSeconds);
void render();
diff --git a/themes/src/summer/water_model.cpp b/themes/src/summer/water_model.cpp
new file mode 100644
index 0000000..3ad02bb
--- /dev/null
+++ b/themes/src/summer/water_model.cpp
@@ -0,0 +1,127 @@
+#include "water_model.h"
+#include "../webgl_context.h"
+#include "../list.h"
+#include "../shaders/water_vert.h"
+#include "../shaders/water_frag.h"
+
+WaterModel::WaterModel(Vector3 inPosition, Vector3 inSize)
+ : position(inPosition), size(inSize) {}
+
+WaterModel::~WaterModel() { unload(); }
+
+void WaterModel::setSun(Vector3 direction, Vector4 color) {
+ sunDir = direction.normalize();
+ sunColor = color.toVector3();
+}
+
+void WaterModel::load(WebglContext* inContext) {
+ context = inContext;
+ shader = loadShader(shader_water_vert, shader_water_frag);
+ useShader(shader);
+
+ attributes.position = getShaderAttribute(shader, "position");
+ uniforms.projection = getShaderUniform(shader, "projection");
+ uniforms.view = getShaderUniform(shader, "view");
+ uniforms.model = getShaderUniform(shader, "model");
+ uniforms.time = getShaderUniform(shader, "time");
+ uniforms.amplitude = getShaderUniform(shader, "amplitude");
+ uniforms.cameraPos = getShaderUniform(shader, "cameraPos");
+ uniforms.sunDir = getShaderUniform(shader, "sunDir");
+ uniforms.sunColor = getShaderUniform(shader, "sunColor");
+
+ // Camera: forward almost horizontal with a slight downward pitch so the
+ // horizon sits near the vertical center and the plane fills the lower half.
+ cameraPos = Vector3(0.f, 5.f, 20.f);
+ projection = Mat4x4().getPerspectiveProjection(
+ 0.1f, 1000.f, 0.9f,
+ static_cast<f32>(context->width) / static_cast<f32>(context->height));
+ view = Mat4x4().getLookAt(cameraPos, Vector3(0.f, 4.f, -10.f), Vector3(0.f, 1.f, 0.f));
+ model = Mat4x4().translate(position);
+
+ // Sensible defaults; the theme overrides these with the actual sun via setSun().
+ sunDir = Vector3(0.f, 0.4f, -1.f).normalize();
+ sunColor = Vector3(1.0f, 0.95f, 0.75f);
+
+ // Build the grid in local XZ, centered on the origin, extents from size.
+ matte::List<Vector2> verts;
+ matte::List<u32> idx;
+ verts.allocate((RES + 1) * (RES + 1));
+ idx.allocate(RES * RES * 6);
+
+ for (i32 z = 0; z <= RES; z++) {
+ for (i32 x = 0; x <= RES; x++) {
+ f32 fx = ((static_cast<f32>(x) / RES) - 0.5f) * size.x;
+ f32 fz = ((static_cast<f32>(z) / RES) - 0.5f) * size.z;
+ verts.add(Vector2(fx, fz));
+ }
+ }
+
+ const i32 stride = RES + 1;
+ for (i32 z = 0; z < RES; z++) {
+ for (i32 x = 0; x < RES; x++) {
+ u32 tl = z * stride + x;
+ u32 tr = tl + 1;
+ u32 bl = (z + 1) * stride + x;
+ u32 br = bl + 1;
+ idx.add(tl); idx.add(bl); idx.add(tr);
+ idx.add(tr); idx.add(bl); idx.add(br);
+ }
+ }
+ numIndices = static_cast<i32>(idx.numElements);
+
+ glGenVertexArrays(1, &vao);
+ glBindVertexArray(vao);
+
+ glGenBuffers(1, &vbo);
+ glBindBuffer(GL_ARRAY_BUFFER, vbo);
+ glBufferData(GL_ARRAY_BUFFER, verts.numElements * sizeof(Vector2), &verts.data[0], GL_STATIC_DRAW);
+
+ glGenBuffers(1, &ebo);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, idx.numElements * sizeof(u32), &idx.data[0], GL_STATIC_DRAW);
+
+ glEnableVertexAttribArray(attributes.position);
+ glVertexAttribPointer(attributes.position, 2, GL_FLOAT, GL_FALSE, sizeof(Vector2), (GLvoid*)0);
+
+ glBindVertexArray(0);
+ verts.deallocate();
+ idx.deallocate();
+}
+
+void WaterModel::update(f32 dtSeconds) {
+ elapsedTime += dtSeconds;
+}
+
+void WaterModel::render() {
+ useShader(shader);
+
+ // Opaque, depth-tested. Wipe the depth buffer first: the 2D sun was drawn at
+ // z=0 (nearest) with depth writes on, so without this the LEQUAL test would
+ // reject every perspective water fragment. Clearing depth (not color) lets the
+ // opaque water paint over the lower half of the sun while still self-occluding.
+ glDisable(GL_BLEND);
+ glEnable(GL_DEPTH_TEST);
+ glDepthFunc(GL_LEQUAL);
+ glDepthMask(GL_TRUE);
+ glClear(GL_DEPTH_BUFFER_BIT);
+
+ setShaderMat4(uniforms.projection, projection);
+ setShaderMat4(uniforms.view, view);
+ setShaderMat4(uniforms.model, model);
+ setShaderFloat(uniforms.time, elapsedTime);
+ setShaderFloat(uniforms.amplitude, size.y);
+ glUniform3f(uniforms.cameraPos, cameraPos.x, cameraPos.y, cameraPos.z);
+ glUniform3f(uniforms.sunDir, sunDir.x, sunDir.y, sunDir.z);
+ glUniform3f(uniforms.sunColor, sunColor.x, sunColor.y, sunColor.z);
+
+ glBindVertexArray(vao);
+ glDrawElements(GL_TRIANGLES, numIndices, GL_UNSIGNED_INT, 0);
+ glBindVertexArray(0);
+}
+
+void WaterModel::unload() {
+ if (vao) { glDeleteVertexArrays(1, &vao); vao = 0; }
+ if (vbo) { glDeleteBuffers(1, &vbo); vbo = 0; }
+ if (ebo) { glDeleteBuffers(1, &ebo); ebo = 0; }
+ if (shader) { glDeleteProgram(shader); shader = 0; }
+}
diff --git a/themes/src/summer/water_model.h b/themes/src/summer/water_model.h
new file mode 100644
index 0000000..a99f91d
--- /dev/null
+++ b/themes/src/summer/water_model.h
@@ -0,0 +1,70 @@
+#ifndef WATER_MODEL_H
+#define WATER_MODEL_H
+
+#include "../mathlib.h"
+#include "../shader.h"
+#include "../types.h"
+
+struct WebglContext;
+
+/// A water mesh is a 3D plane in the simulation world.
+///
+/// The mesh reflects the sun above it and refracts whatever is
+/// below its surface. It owns its own shader, geometry buffers,
+/// and perspective camera, so it is fully standalone.
+///
+/// The mesh can be rendered and updated.
+class WaterModel {
+public:
+ WaterModel(Vector3 position, Vector3 size);
+ ~WaterModel();
+
+ /// Compile the water shader, build the grid mesh, and set up the camera.
+ void load(WebglContext* context);
+ void update(f32 dtSeconds);
+ void render();
+ void unload();
+
+ /// Direction pointing toward the sun (normalized internally) and its color,
+ /// used to fake the surface reflection.
+ void setSun(Vector3 direction, Vector4 color);
+
+private:
+ Vector3 position;
+ Vector3 size;
+
+ WebglContext* context = nullptr;
+ Shader shader = 0;
+ u32 vao = 0;
+ u32 vbo = 0;
+ u32 ebo = 0;
+ i32 numIndices = 0;
+ f32 elapsedTime = 0.f;
+
+ Mat4x4 projection;
+ Mat4x4 view;
+ Mat4x4 model;
+
+ Vector3 cameraPos;
+ Vector3 sunDir;
+ Vector3 sunColor;
+
+ static const i32 RES = 200;
+
+ struct {
+ i32 position = -1;
+ } attributes;
+
+ struct {
+ i32 projection = -1;
+ i32 view = -1;
+ i32 model = -1;
+ i32 time = -1;
+ i32 amplitude = -1;
+ i32 cameraPos = -1;
+ i32 sunDir = -1;
+ i32 sunColor = -1;
+ } uniforms;
+};
+
+#endif // WATER_MODEL_H
diff --git a/themes/src/tools/shader.js b/themes/src/tools/shader.cjs
index 10889db..10889db 100644
--- a/themes/src/tools/shader.js
+++ b/themes/src/tools/shader.cjs