<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Realtime Ray-Traced Metallic Sphere over Street (Single HTML)</title>
<style>
  html,body { height:100%; margin:0; background:#111; color:#ddd; font-family:Inter,system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;}
  #app { display:flex; height:100vh; }
  canvas { display:block; width:100%; height:100%; background:#000; flex:1 1 auto;}
  #ui {
    width:320px; background:rgba(12,12,12,0.9); padding:12px; box-sizing:border-box;
    border-left:1px solid rgba(255,255,255,0.03); overflow:auto;
  }
  h1 { font-size:16px; margin:4px 0 10px; }
  label { display:block; font-size:12px; margin-top:8px; color:#bbb; }
  input[type=range] { width:100%; }
  .row { display:flex; gap:8px; align-items:center; }
  .small { font-size:12px; color:#9aa; }
  .footer { margin-top:12px; font-size:12px; color:#777; }
  button { margin-top:8px; padding:8px 10px; background:#1e1e1e; color:#ddd; border:1px solid #333; cursor:pointer; }
  a { color:#9cf; }
</style>
</head>
<body>
<div id="app">
  <canvas id="glcanvas"></canvas>
  <div id="ui">
    <h1>Realtime Ray-Traced Metallic Sphere — Street HDR</h1>
    <div class="small">Environment map: <a href="https://polyhaven.com/hdris/urban" target="_blank" rel="noreferrer">Poly Haven — Urban Street (CC0)</a>.</div>

    <label>Metallic (0..1) <span id="metalVal" class="small"></span></label>
    <input id="metal" type="range" min="0" max="1" step="0.01" value="1">

    <label>Roughness (0..1) <span id="roughVal" class="small"></span></label>
    <input id="rough" type="range" min="0" max="1" step="0.01" value="0.05">

    <label>Reflectivity (0..1) <span id="reflVal" class="small"></span></label>
    <input id="refl" type="range" min="0" max="1" step="0.01" value="1">

    <label>Base color (hue) <span id="hueVal" class="small"></span></label>
    <input id="hue" type="range" min="0" max="360" step="1" value="210">

    <label>Sphere height (m) <span id="hgtVal" class="small"></span></label>
    <input id="height" type="range" min="0.2" max="4" step="0.01" value="1.0">

    <label>Sphere radius (m) <span id="radVal" class="small"></span></label>
    <input id="radius" type="range" min="0.1" max="3" step="0.01" value="0.6">

    <label>Gloss samples (1..64) <span id="sampVal" class="small"></span></label>
    <input id="samples" type="range" min="1" max="64" step="1" value="8">

    <label>Exposure <span id="expVal" class="small"></span></label>
    <input id="exposure" type="range" min="0.2" max="4" step="0.01" value="1.0">

    <div style="margin-top:10px;">
      <button id="resetBtn">Reset camera</button>
      <button id="lowResBtn">Toggle low-res (perf)</button>
    </div>

    <div class="footer">
      Controls: drag to orbit, scroll to zoom. Shader: ray-sphere + env sampling; glossy = jittered reflection sampling. <br><br>
      Note: For fully physical multi-bounce path tracing you'd need many more samples — this is a realtime single-bounce approximation.
    </div>
  </div>
</div>

<script>
(() => {
  // ------------------------------
  // Configuration & helpers
  // ------------------------------
  const canvas = document.getElementById('glcanvas');
  const gl = canvas.getContext('webgl2', { antialias: false });
  if (!gl) { alert('WebGL2 is required. Use a modern browser.'); return; }

  // UI elements
  const ui = {
    metal: document.getElementById('metal'),
    rough: document.getElementById('rough'),
    refl: document.getElementById('refl'),
    hue: document.getElementById('hue'),
    height: document.getElementById('height'),
    radius: document.getElementById('radius'),
    samples: document.getElementById('samples'),
    exposure: document.getElementById('exposure'),
    resetBtn: document.getElementById('resetBtn'),
    lowResBtn: document.getElementById('lowResBtn'),
    metalVal: document.getElementById('metalVal'),
    roughVal: document.getElementById('roughVal'),
    reflVal: document.getElementById('reflVal'),
    hueVal: document.getElementById('hueVal'),
    hgtVal: document.getElementById('hgtVal'),
    radVal: document.getElementById('radVal'),
    sampVal: document.getElementById('sampVal'),
    expVal: document.getElementById('expVal'),
  };

  function updateLabels() {
    ui.metalVal.textContent = ui.metal.value;
    ui.roughVal.textContent = ui.rough.value;
    ui.reflVal.textContent = ui.refl.value;
    ui.hueVal.textContent = ui.hue.value;
    ui.hgtVal.textContent = ui.height.value;
    ui.radVal.textContent = ui.radius.value;
    ui.sampVal.textContent = ui.samples.value;
    ui.expVal.textContent = ui.exposure.value;
  }
  updateLabels();

  // Shader helpers
  function compileShader(type, src) {
    const s = gl.createShader(type);
    gl.shaderSource(s, src);
    gl.compileShader(s);
    if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
      console.error(gl.getShaderInfoLog(s));
      throw new Error('Shader compile failed');
    }
    return s;
  }
  function linkProgram(vs, fs) {
    const p = gl.createProgram();
    gl.attachShader(p, vs);
    gl.attachShader(p, fs);
    gl.linkProgram(p);
    if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {
      console.error(gl.getProgramInfoLog(p));
      throw new Error('Program link failed');
    }
    return p;
  }

  // ------------------------------
  // Fullscreen quad
  // ------------------------------
  const quadVS = `#version 300 es
  precision highp float;
  in vec2 aPos;
  out vec2 vUV;
  void main(){
    vUV = aPos * 0.5 + 0.5;
    gl_Position = vec4(aPos, 0.0, 1.0);
  }`;
  const quadFS = `#version 300 es
  precision highp float;
  precision highp sampler2D;

  in vec2 vUV;
  out vec4 outColor;

  uniform vec2 uResolution;
  uniform float uTime;
  uniform sampler2D uEnv; // equirectangular tonemapped JPG/PNG (sRGB)
  uniform vec3 uCamPos;
  uniform mat3 uCamMat; // camera basis
  uniform float uFov;

  // sphere props
  uniform vec3 uSpherePos;
  uniform float uSphereRadius;
  uniform vec3 uBaseColor;
  uniform float uMetallic;
  uniform float uRoughness;
  uniform float uReflectivity;
  uniform float uExposure;

  uniform int uSamples;
  uniform vec2 uRandomSeed;

  // small random generator per-pixel
  highp float rand(in vec2