Exporting from Unity to WebGL

Unity 2,152 compiles a whole C# project to WebAssembly for its Web build target (formerly "WebGL"). The Unity 6.6 manual lists WebGL2 as the default web graphics API and WebGPU as an option you add in Player settings, with WebGL2 kept as a fallback; the Unity 6.3 LTS and 6.5 manuals still titled the WebGPU page "Experimental". A build produces a Build folder with [name].loader.js (the script your page includes), [name].framework.js (the JavaScript runtime), [name].wasm (the compiled game), [name].data (assets and scenes) and, for multithreaded builds, a .mem heap image. Unity's own compatibility page requires a WebGL 2-capable, 64-bit browser with WebAssembly, and lists Chrome 1 , Firefox 555 , Edge and Safari 10 (15 or later) on the desktop.

Serve the build compressed (Brotli or gzip) with matching Content-Encoding headers, and show progress while the .wasm and .data files download. Unity Personal is free under US$200,000 of revenue and funding in the last 12 months. No Unity Editor was available here, so nothing was built; the facts come from Unity's documentation.

The page a Unity Web build needs: a requirements check, a progress bar fed by the loader, then the game canvas (a stand-in scene here)HTMLLive
<!doctype html>
<style>
  body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
  .wrap { position: relative; max-width: 600px; }
  canvas { display: block; width: 100%; }
  #loading { position: absolute; left: 20%; right: 20%; top: 44%; font: 11px monospace; text-align: center; }
  #track { height: 10px; margin-top: 6px; background: #d8d2c6; border-radius: 5px; overflow: hidden; }
  #bar { height: 100%; width: 0; background: #1e5f8c; }
  ul { max-width: 600px; margin: 6px 0; padding-left: 22px; font: 11px monospace; }
  .yes { color: #2f6b2c; } .no { color: #a33b22; }
</style>
<div class="wrap"><canvas id="game" width="600" height="280"></canvas>
  <div id="loading"><span id="file">Build/booknest.loader.js</span><div id="track"><div id="bar"></div></div></div></div>
<ul id="checks"></ul>
<script>
// 1. Unity's compatibility list: WebGL 2, WebAssembly, a 64-bit browser; threads need cross-origin isolation.
const canvas = document.getElementById('game');
const gl = canvas.getContext('webgl2');
const checks = [['WebGL 2 context', !!gl], ['WebAssembly.instantiateStreaming', typeof WebAssembly?.instantiateStreaming === 'function'],
  ['SharedArrayBuffer (multithreaded builds)', typeof SharedArrayBuffer === 'function' && self.crossOriginIsolated === true],
  ['navigator.gpu (WebGPU build option)', 'gpu' in navigator]];
document.getElementById('checks').innerHTML = checks.map(([name, ok]) => `<li class="${ok ? 'yes' : 'no'}">${ok ? 'yes' : 'no '} ${name}</li>`).join('');
// 2. Stand-in for Build/booknest.loader.js: the real createUnityInstance downloads and starts the build.
function createUnityInstance(canvas, config, onProgress) {
  const files = [config.frameworkUrl, config.codeUrl, config.dataUrl];
  return new Promise((resolve) => {
    const start = performance.now();
    (function tick(now) {
      const progress = Math.min((now - start) / 1500, 1);
      onProgress(progress, files[Math.min(Math.floor(progress * 3), 2)]);
      if (progress < 1) requestAnimationFrame(tick); else resolve({ SendMessage() {}, Quit: async () => {} });
    })(start);
  });
}
const config = { dataUrl: 'Build/booknest.data', frameworkUrl: 'Build/booknest.framework.js', codeUrl: 'Build/booknest.wasm',
  companyName: 'BookNest', productName: 'Shelf Explorer', productVersion: '1.0' };
// 3. The game draws a slowly turning row of books once "loaded"; until then, the canvas shows a plain backdrop.
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
uniform float uTime; out vec3 vColor;
const vec3 colors[6] = vec3[](vec3(0.12, 0.37, 0.55), vec3(0.36, 0.25, 0.6), vec3(0.88, 0.6, 0.06),
                              vec3(0.25, 0.49, 0.23), vec3(0.71, 0.27, 0.18), vec3(0.16, 0.62, 0.56));
void main() {
  int book = gl_VertexID / 36, v = gl_VertexID % 36, face = v / 6, k = v % 6;
  vec2 q = vec2[](vec2(-1, -1), vec2(1, -1), vec2(1, 1), vec2(-1, -1), vec2(1, 1), vec2(-1, 1))[k];
  vec3 n = vec3[](vec3(0, 0, 1), vec3(0, 0, -1), vec3(1, 0, 0), vec3(-1, 0, 0), vec3(0, 1, 0), vec3(0, -1, 0))[face];
  vec3 u = vec3[](vec3(1, 0, 0), vec3(-1, 0, 0), vec3(0, 0, -1), vec3(0, 0, 1), vec3(1, 0, 0), vec3(1, 0, 0))[face];
  vec3 w = vec3[](vec3(0, 1, 0), vec3(0, 1, 0), vec3(0, 1, 0), vec3(0, 1, 0), vec3(0, 0, -1), vec3(0, 0, 1))[face];
  vec3 p = (n + q.x * u + q.y * w) * vec3(0.1, 0.36, 0.26);
  float t = 0.5 * sin(uTime + float(book) * 0.8), c = cos(t), s = sin(t);   // each book sways
  p = vec3(c * p.x + s * p.z, p.y, -s * p.x + c * p.z); vec3 nn = vec3(c * n.x + s * n.z, n.y, -s * n.x + c * n.z);
  vColor = colors[book] * (0.4 + 0.6 * max(dot(nn, normalize(vec3(0.3, 0.6, 1))), 0.0));
  p.x += (float(book) - 2.5) * 0.36;
  gl_Position = vec4(p.x * 0.75, p.y * 1.4 - p.z * 0.3, -p.z * 0.5, 1); }`], [gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float; in vec3 vColor; out vec4 c; void main() { c = vec4(vColor, 1); }`]]) {
  const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); gl.attachShader(program, s);
}
gl.linkProgram(program);
gl.bindVertexArray(gl.createVertexArray());
gl.clearColor(0.93, 0.91, 0.87, 1); gl.clear(gl.COLOR_BUFFER_BIT);
createUnityInstance(canvas, config, (progress, file) => {
  document.getElementById('bar').style.width = progress * 100 + '%';
  document.getElementById('file').textContent = `${file}  ${Math.round(progress * 100)}%`;
}).then(() => {
  document.getElementById('loading').remove();
  gl.useProgram(program); gl.enable(gl.DEPTH_TEST);
  (function frame(now) {
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    gl.uniform1f(gl.getUniformLocation(program, 'uTime'), now / 1500);
    gl.drawArrays(gl.TRIANGLES, 0, 6 * 36);
    requestAnimationFrame(frame);
  })(performance.now());
});
</script>