KNI spike: engine core + Friflo render via WebGL in the browser
spikes/KniWeb (outside LittleSim.sln): a kni-blazor-gl template project (KNI 4.2.9001, net8.0) referencing MrGameEng.Core directly. A mini-host in the GameHost mold drives EngineContext/GameClock/Scene phases over KNI's Game; the scene moves 300 Friflo entities in the update phase and draws them with SpriteBatch (WebGL). Verified in a real browser: sprites render and animate, browser console is clean. Decision (docs/web-client.md): path A — KNI — is the primary route for the web client; the core runs in Blazor WASM unchanged thanks to the Core/Host split. Known follow-ups: per-platform compilation of the graphics libraries against nkast.* packages, shader compatibility for Renderer2D, HTTP-served content instead of the filesystem. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
8a7e2cce52
commit
ce8f8ba2ed
@@ -0,0 +1,97 @@
|
||||
|
||||
html, body
|
||||
{
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1:focus
|
||||
{
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a, .btn-link
|
||||
{
|
||||
color: #0077cc;
|
||||
}
|
||||
|
||||
.btn-primary
|
||||
{
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.content
|
||||
{
|
||||
padding-top: 1.1rem;
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox])
|
||||
{
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid
|
||||
{
|
||||
outline: 1px solid red;
|
||||
}
|
||||
|
||||
.validation-message
|
||||
{
|
||||
color: red;
|
||||
}
|
||||
|
||||
#blazor-error-ui
|
||||
{
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss
|
||||
{
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.blazor-error-boundary
|
||||
{
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
padding: 1rem 1rem 1rem 3.7rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blazor-error-boundary::after
|
||||
{
|
||||
content: "An error has occurred."
|
||||
}
|
||||
|
||||
#theCanvas
|
||||
{
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
|
||||
/* Disable text highlighting and magnifying glass on iPhone/webkit */
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
|
||||
#canvas
|
||||
{
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,114 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>KniWebSpike</title>
|
||||
<base href="./" />
|
||||
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="css/app.css" rel="stylesheet" />
|
||||
<link href="KniWebSpike.styles.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="app">
|
||||
<div id="loading" style="display: table-cell; margin: auto; width:100vw; height:100vh; vertical-align: middle; background: #ffcc10;">
|
||||
<div style="display: block; margin: auto; width: 9em; color: white;font-family: 'Segoe UI', sans-serif;">
|
||||
<div style="text-align: center; font-size: 0.85em;">Made with<br/><a href="https://github.com/kniEngine/kni"><img src="kni.png" border="0" alt="Kni"></a></div>
|
||||
<div style="text-align: center; font-size: 1.8em;">loading <marquee style="width:0.9em; vertical-align: bottom;">. . . </marquee></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">x</a>
|
||||
</div>
|
||||
|
||||
<script src="_framework/blazor.webassembly.js" autostart="false"></script>
|
||||
<script type="module">
|
||||
import { BrotliDecode } from './js/decode.min.js';
|
||||
window.BrotliDecode = BrotliDecode;
|
||||
// Set this to enable Brotli (.br) decompression on static webServers
|
||||
// that don't support content compression and http://.
|
||||
var enableBrotliDecompression = false;
|
||||
Blazor.start({
|
||||
loadBootResource: function (type, name, defaultUri, integrity)
|
||||
{
|
||||
if (enableBrotliDecompression === true && type !== 'dotnetjs' && location.hostname !== 'localhost')
|
||||
{
|
||||
return (async function()
|
||||
{
|
||||
const response = await fetch(defaultUri + '.br', { cache: 'no-cache' });
|
||||
if (!response.ok)
|
||||
throw new Error(response.statusText);
|
||||
const originalResponseBuffer = await response.arrayBuffer();
|
||||
const originalResponseArray = new Int8Array(originalResponseBuffer);
|
||||
const contentType = (type === 'dotnetwasm')
|
||||
? 'application/wasm'
|
||||
: 'application/octet-stream';
|
||||
const decompressedResponseArray = BrotliDecode(originalResponseArray);
|
||||
return new Response(decompressedResponseArray,
|
||||
{ headers: { 'content-type': contentType }
|
||||
});
|
||||
})();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script src="_content/nkast.Wasm.JSInterop/js/JSObject.8.0.11.js"></script>
|
||||
<script src="_content/nkast.Wasm.Dom/js/Window.8.0.11.js"></script>
|
||||
<script src="_content/nkast.Wasm.Dom/js/Document.8.0.11.js"></script>
|
||||
<script src="_content/nkast.Wasm.Dom/js/Navigator.8.0.11.js"></script>
|
||||
<script src="_content/nkast.Wasm.Dom/js/Gamepad.8.0.11.js"></script>
|
||||
<script src="_content/nkast.Wasm.Dom/js/Media.8.0.11.js"></script>
|
||||
<script src="_content/nkast.Wasm.XHR/js/XHR.8.0.11.js"></script>
|
||||
<script src="_content/nkast.Wasm.Canvas/js/Canvas.8.0.11.js"></script>
|
||||
<script src="_content/nkast.Wasm.Canvas/js/CanvasGLContext.8.0.11.js"></script>
|
||||
<script src="_content/nkast.Wasm.Audio/js/Audio.8.0.11.js"></script>
|
||||
<script src="_content/nkast.Wasm.XR/js/XR.8.0.11.js"></script>
|
||||
|
||||
<script>
|
||||
function tickJS()
|
||||
{
|
||||
window.theInstance.invokeMethod('TickDotNet');
|
||||
window.requestAnimationFrame(tickJS);
|
||||
}
|
||||
|
||||
window.initRenderJS = (instance) =>
|
||||
{
|
||||
window.theInstance = instance;
|
||||
|
||||
// set initial canvas size
|
||||
var canvas = document.getElementById('theCanvas');
|
||||
var holder = document.getElementById('canvasHolder');
|
||||
canvas.width = holder.clientWidth;
|
||||
canvas.height = holder.clientHeight;
|
||||
// disable context menu on right click
|
||||
canvas.addEventListener("contextmenu", e => e.preventDefault());
|
||||
|
||||
// begin game loop
|
||||
window.requestAnimationFrame(tickJS);
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", function(event)
|
||||
{
|
||||
// Prevent Arrows Keys and Spacebar scrolling the outer page
|
||||
// when running inside an iframe. e.g: itch.io embedding.
|
||||
if ([32, 37, 38, 39, 40].indexOf(event.keyCode) > -1)
|
||||
event.preventDefault();
|
||||
});
|
||||
window.addEventListener("wheel", function(event)
|
||||
{
|
||||
// Prevent Mousewheel scrolling the outer page
|
||||
// when running inside an iframe. e.g: itch.io embedding.
|
||||
event.preventDefault();
|
||||
}, { passive: false });
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,77 @@
|
||||
// micProcessor.js
|
||||
class MicProcessor extends AudioWorkletProcessor
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
|
||||
// global variables for testing
|
||||
var sampleRate = globalThis.sampleRate;
|
||||
var currentFrame = globalThis.currentFrame;
|
||||
var currentTime = globalThis.currentTime;
|
||||
var currentRenderQuantum = globalThis.currentRenderQuantum;
|
||||
|
||||
this.SampleRate = sampleRate;
|
||||
this.TargetSamples = Math.floor(this.SampleRate * 0.1); // 100ms
|
||||
this.Buffer = new Float32Array(this.TargetSamples);
|
||||
this.BufferIndex = 0;
|
||||
|
||||
this.port.onmessage = (event) =>
|
||||
{
|
||||
var data = event.data;
|
||||
|
||||
if (typeof data === 'number')
|
||||
{
|
||||
//this.port.postMessage(data); // echo back test
|
||||
}
|
||||
if (data instanceof Uint8Array)
|
||||
{
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
process(inputs, outputs, parameters)
|
||||
{
|
||||
var inChannel0 = inputs[0][0];
|
||||
if (!inChannel0) return true;
|
||||
|
||||
let srcIndex = 0;
|
||||
var srcLen = inChannel0.length;
|
||||
|
||||
while (srcIndex < srcLen)
|
||||
{
|
||||
var remaining = this.TargetSamples - this.BufferIndex;
|
||||
var copyCount = Math.min(remaining, srcLen - srcIndex);
|
||||
|
||||
this.Buffer.set(
|
||||
inChannel0.subarray(srcIndex, srcIndex + copyCount),
|
||||
this.BufferIndex);
|
||||
|
||||
this.BufferIndex += copyCount;
|
||||
srcIndex += copyCount;
|
||||
|
||||
if (this.BufferIndex >= this.TargetSamples)
|
||||
{
|
||||
this.SendBuffer();
|
||||
this.BufferIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
SendBuffer()
|
||||
{
|
||||
// convert to 16-6bit PCM
|
||||
var int16 = new Int16Array(this.TargetSamples);
|
||||
for (var i = 0; i < this.TargetSamples; i++)
|
||||
{
|
||||
int16[i] = this.Buffer[i] * 32767;
|
||||
}
|
||||
|
||||
var byteArray = new Uint8Array(int16.buffer);
|
||||
this.port.postMessage(byteArray, [byteArray.buffer]);
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('mic-processor', MicProcessor);
|
||||
@@ -0,0 +1,87 @@
|
||||
// streamProcessor.js
|
||||
class StreamProcessor extends AudioWorkletProcessor
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
super();
|
||||
this.queue = [];
|
||||
|
||||
this.port.onmessage = (event) =>
|
||||
{
|
||||
var data = event.data;
|
||||
|
||||
if (typeof data === 'number')
|
||||
{
|
||||
if (data === 2)
|
||||
{
|
||||
this.queue = [];
|
||||
}
|
||||
}
|
||||
if (data instanceof Uint8Array)
|
||||
{
|
||||
const buffer = new Int16Array(data.buffer, data.byteOffset, data.length / 2);
|
||||
buffer.offset = 0;
|
||||
this.queue.push(buffer);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
process(inputs, outputs, parameters)
|
||||
{
|
||||
const output = outputs[0];
|
||||
|
||||
const channelCount = output.length;
|
||||
const sampleCount = output[0].length;
|
||||
|
||||
let written = 0;
|
||||
|
||||
while (written < sampleCount && this.queue.length > 0)
|
||||
{
|
||||
const buffer = this.queue[0];
|
||||
const offset = buffer.offset;
|
||||
|
||||
const available = buffer.length - offset;
|
||||
const needed = sampleCount - written;
|
||||
const copyCount = Math.min(available, needed);
|
||||
|
||||
for (let i = 0; i < copyCount; i++)
|
||||
{
|
||||
for (let c = 0; c < channelCount; c++)
|
||||
{
|
||||
const channel = output[c];
|
||||
let value = (buffer[offset+i] / 32767);
|
||||
channel[written+i] = value;
|
||||
}
|
||||
}
|
||||
|
||||
written += copyCount;
|
||||
buffer.offset += copyCount;
|
||||
|
||||
if (buffer.offset >= buffer.length)
|
||||
{
|
||||
this.queue.shift();
|
||||
this.port.postMessage(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Fill remaining samples with silence
|
||||
if (written < sampleCount)
|
||||
{
|
||||
for (let c = 0; c < channelCount; c++)
|
||||
{
|
||||
const channel = output[c];
|
||||
|
||||
for (let i = written; i < sampleCount; i++)
|
||||
{
|
||||
let value = 0;
|
||||
channel[i] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor("stream-processor", StreamProcessor);
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 423 B |
Reference in New Issue
Block a user