glm-5.2 Code

Model-specific source generated from the shared prompt shown on the gallery page. The prompt itself is intentionally not duplicated here.

glm-5.2 ocean UI screenshot
src/main.js src/styles.css package.json
JavaScript

src/main.js

1import './styles.css';2import * as THREE from 'three';3 4const MODEL_NAME = 'glm-5-2';5 6const app = document.querySelector('#app');7 8const state = {9  paused: false,10  waveHeight: 0.6,11  speed: 1.0,12  choppiness: 1.0,13  windAngle: 0,14  wireframe: false,15  theta: 0.0,16  phi: 0.32,17  radius: 26,18  autoOrbit: true,19};20 21function buildUI() {22  const panel = document.createElement('div');23  panel.className = 'panel';24  panel.innerHTML = `25    <div class="panel-header">26      <div class="identity">27        <span class="title">Ocean UI</span>28        <span class="meta" id="meta">model run: ${MODEL_NAME} · 0 fps</span>29      </div>30      <button class="collapse-btn" id="collapse" aria-label="Toggle controls">–</button>31    </div>32    <div class="panel-body">33      <div class="btn-group">34        <button class="btn primary" id="pause">Pause</button>35        <button class="btn" id="reset">Reset view</button>36      </div>37      <div class="row">38        <label>Wave height <span class="val" id="vWave">0.60</span></label>39        <input type="range" id="waveHeight" min="0" max="1.6" step="0.01" value="0.6" />40      </div>41      <div class="row">42        <label>Speed <span class="val" id="vSpeed">1.00</span></label>43        <input type="range" id="speed" min="0" max="2.5" step="0.01" value="1" />44      </div>45      <div class="row">46        <label>Choppiness <span class="val" id="vChop">1.00</span></label>47        <input type="range" id="choppiness" min="0.4" max="2.6" step="0.01" value="1" />48      </div>49      <div class="row">50        <label>Wind direction <span class="val" id="vWind">0°</span></label>51        <input type="range" id="windAngle" min="0" max="360" step="1" value="0" />52      </div>53      <div class="row">54        <label>Render mode</label>55        <div class="btn-group">56          <button class="btn active" id="modeSolid">Solid</button>57          <button class="btn" id="modeWire">Wireframe</button>58        </div>59      </div>60      <div class="hint">Drag to orbit · scroll/pinch to zoom · <kbd>Space</kbd> pause</div>61    </div>62  `;63  app.appendChild(panel);64 65  const $ = (id) => document.getElementById(id);66 67  const pauseBtn = $('pause');68  pauseBtn.addEventListener('click', () => {69    state.paused = !state.paused;70    pauseBtn.textContent = state.paused ? 'Resume' : 'Pause';71    pauseBtn.classList.toggle('paused', state.paused);72  });73 74  $('reset').addEventListener('click', () => {75    state.theta = 0;76    state.phi = 0.32;77    state.radius = 26;78    state.autoOrbit = true;79  });80 81  const linkSlider = (id, key, fmt, onChange) => {82    const el = $(id);83    const val = $('v' + id.charAt(0).toUpperCase() + id.slice(1));84    el.addEventListener('input', () => {85      state[key] = parseFloat(el.value);86      if (val) val.textContent = fmt(state[key]);87      if (onChange) onChange(state[key]);88      state.autoOrbit = false;89    });90  };91  linkSlider('waveHeight', 'waveHeight', (v) => v.toFixed(2), (v) => { oceanUniforms.uWaveHeight.value = v; });92  linkSlider('speed', 'speed', (v) => v.toFixed(2), (v) => { oceanUniforms.uSpeed.value = v; });93  linkSlider('choppiness', 'choppiness', (v) => v.toFixed(2), (v) => { oceanUniforms.uChoppiness.value = v; });94  const windEl = $('windAngle');95  const windVal = $('vWind');96  windEl.addEventListener('input', () => {97    state.windAngle = parseFloat(windEl.value);98    windVal.textContent = Math.round(state.windAngle) + '°';99    oceanUniforms.uWindAngle.value = state.windAngle * Math.PI / 180;100    state.autoOrbit = false;101  });102 103  const modeSolid = $('modeSolid');104  const modeWire = $('modeWire');105  modeSolid.addEventListener('click', () => {106    state.wireframe = false;107    oceanMat.wireframe = false;108    modeSolid.classList.add('active');109    modeWire.classList.remove('active');110  });111  modeWire.addEventListener('click', () => {112    state.wireframe = true;113    oceanMat.wireframe = true;114    modeWire.classList.add('active');115    modeSolid.classList.remove('active');116  });117 118  const collapse = $('collapse');119  collapse.addEventListener('click', () => {120    panel.classList.toggle('collapsed');121    collapse.textContent = panel.classList.contains('collapsed') ? '+' : '–';122  });123 124  window.addEventListener('keydown', (e) => {125    if (e.code === 'Space') {126      e.preventDefault();127      pauseBtn.click();128    }129  });130 131  return $('meta');132}133 134const scene = new THREE.Scene();135const horizonColor = new THREE.Color(0xbfe3f0);136scene.fog = new THREE.Fog(horizonColor, 45, 130);137 138const camera = new THREE.PerspectiveCamera(58, window.innerWidth / window.innerHeight, 0.1, 600);139camera.position.set(0, 8, 26);140camera.lookAt(0, 1.5, 0);141 142const renderer = new THREE.WebGLRenderer({ antialias: true });143renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));144renderer.setSize(window.innerWidth, window.innerHeight);145renderer.outputColorSpace = THREE.SRGBColorSpace;146app.appendChild(renderer.domElement);147 148const sunDir = new THREE.Vector3(0.55, 0.45, -0.35).normalize();149 150const skyUniforms = {151  top: { value: new THREE.Color(0x2a6fb0) },152  bottom: { value: new THREE.Color(0xddeef6) },153  sunDir: { value: sunDir.clone() },154  sunColor: { value: new THREE.Color(0xfff4d6) },155};156const sky = new THREE.Mesh(157  new THREE.SphereGeometry(300, 32, 16),158  new THREE.ShaderMaterial({159    side: THREE.BackSide,160    depthWrite: false,161    uniforms: skyUniforms,162    vertexShader: `163      varying vec3 vWorldPos;164      void main() {165        vec4 w = modelMatrix * vec4(position, 1.0);166        vWorldPos = w.xyz;167        gl_Position = projectionMatrix * viewMatrix * w;168      }169    `,170    fragmentShader: `171      varying vec3 vWorldPos;172      uniform vec3 top;173      uniform vec3 bottom;174      uniform vec3 sunDir;175      uniform vec3 sunColor;176      void main() {177        vec3 dir = normalize(vWorldPos);178        float h = clamp(dir.y * 0.5 + 0.5, 0.0, 1.0);179        vec3 col = mix(bottom, top, smoothstep(0.0, 1.0, h));180        float sd = max(dot(dir, normalize(sunDir)), 0.0);181        col += sunColor * pow(sd, 90.0) * 1.2;182        col += sunColor * pow(sd, 8.0) * 0.12;183        gl_FragColor = vec4(col, 1.0);184      }185    `,186  })187);188scene.add(sky);189 190const oceanUniforms = {191  uTime: { value: 0 },192  uWaveHeight: { value: state.waveHeight },193  uSpeed: { value: state.speed },194  uChoppiness: { value: state.choppiness },195  uWindAngle: { value: state.windAngle },196  uMaxAmp: { value: 2.53 },197  uSunDirection: { value: sunDir.clone() },198  uCameraPos: { value: new THREE.Vector3() },199  uDeepColor: { value: new THREE.Color(0x0a3d62) },200  uShallowColor: { value: new THREE.Color(0x2f93c4) },201  uSkyColor: { value: new THREE.Color(0xbfe3f0) },202  uHorizonColor: { value: horizonColor.clone() },203};204 205const oceanMat = new THREE.ShaderMaterial({206  uniforms: oceanUniforms,207  vertexShader: `208    uniform float uTime;209    uniform float uWaveHeight;210    uniform float uSpeed;211    uniform float uChoppiness;212    uniform float uWindAngle;213    uniform float uMaxAmp;214    varying vec3 vWorldPos;215    varying vec3 vNormal;216    varying float vElevation;217 218    vec2 rotateDir(vec2 d, float ang) {219      float ca = cos(ang), sa = sin(ang);220      return vec2(d.x * ca - d.y * sa, d.x * sa + d.y * ca);221    }222 223    void addWave(inout float h, inout vec2 g, vec2 p, float t, vec2 baseDir, float wavelength, float amplitude, float speedMul) {224      vec2 d = rotateDir(normalize(baseDir), uWindAngle);225      float k = 6.28318530718 / wavelength * uChoppiness;226      float phase = k * dot(d, p) + t * speedMul;227      h += amplitude * sin(phase);228      g += amplitude * k * d * cos(phase);229    }230 231    void main() {232      vec3 pos = position;233      float t = uTime * uSpeed;234      vec2 p = pos.xz;235      float h = 0.0;236      vec2 g = vec2(0.0);237      addWave(h, g, p, t, vec2(1.0, 0.6), 22.0, 1.0, 1.0);238      addWave(h, g, p, t, vec2(1.0, -0.5), 14.0, 0.65, 1.15);239      addWave(h, g, p, t, vec2(0.5, 1.0), 8.5, 0.4, 1.3);240      addWave(h, g, p, t, vec2(-0.6, 0.8), 5.0, 0.25, 1.5);241      addWave(h, g, p, t, vec2(0.9, 0.2), 3.0, 0.15, 1.8);242      addWave(h, g, p, t, vec2(-0.3, -0.9), 1.7, 0.08, 2.2);243 244      float disp = h * uWaveHeight;245      pos.y += disp;246 247      vec3 nLocal = vec3(-g.x * uWaveHeight, 1.0, -g.y * uWaveHeight);248      nLocal.y = max(nLocal.y, 0.25);249      nLocal = normalize(nLocal);250 251      vec4 worldPos = modelMatrix * vec4(pos, 1.0);252      vWorldPos = worldPos.xyz;253      vNormal = normalize(mat3(modelMatrix) * nLocal);254      vElevation = h / uMaxAmp;255      gl_Position = projectionMatrix * viewMatrix * worldPos;256    }257  `,258  fragmentShader: `259    uniform vec3 uSunDirection;260    uniform vec3 uCameraPos;261    uniform vec3 uDeepColor;262    uniform vec3 uShallowColor;263    uniform vec3 uSkyColor;264    uniform vec3 uHorizonColor;265    varying vec3 vWorldPos;266    varying vec3 vNormal;267    varying float vElevation;268 269    void main() {270      vec3 N = normalize(vNormal);271      vec3 V = normalize(uCameraPos - vWorldPos);272      vec3 L = normalize(uSunDirection);273 274      float depthMix = clamp(vElevation * 0.5 + 0.5, 0.0, 1.0);275      vec3 waterColor = mix(uDeepColor, uShallowColor, depthMix);276 277      float fresnel = pow(1.0 - max(dot(N, V), 0.0), 3.0);278      fresnel = clamp(fresnel, 0.0, 1.0);279 280      float diff = max(dot(N, L), 0.0);281      vec3 color = waterColor * (0.35 + 0.65 * diff);282 283      vec3 skyRefl = uSkyColor;284      color = mix(color, skyRefl, fresnel * 0.85);285 286      vec3 H = normalize(L + V);287      float spec = pow(max(dot(N, H), 0.0), 90.0);288      color += vec3(1.0, 0.96, 0.82) * spec * 1.6;289 290      float foam = smoothstep(0.72, 1.0, vElevation);291      color = mix(color, vec3(0.96, 0.98, 1.0), foam * 0.55);292 293      float dist = length(uCameraPos - vWorldPos);294      float fogF = smoothstep(45.0, 130.0, dist);295      color = mix(color, uHorizonColor, fogF);296 297      gl_FragColor = vec4(color, 1.0);298    }299  `,300});301 302const oceanGeo = new THREE.PlaneGeometry(260, 260, 240, 240);303oceanGeo.rotateX(-Math.PI / 2);304const ocean = new THREE.Mesh(oceanGeo, oceanMat);305scene.add(ocean);306 307const metaEl = buildUI();308 309const clock = new THREE.Clock();310let elapsed = 0;311let fpsEMA = 60;312let lastFpsUpdate = 0;313 314function updateCamera() {315  if (!state.paused && state.autoOrbit) {316    state.theta += 0.0014;317  }318  const r = state.radius;319  const phi = Math.min(Math.max(state.phi, 0.04), 1.35);320  camera.position.set(321    r * Math.cos(phi) * Math.sin(state.theta),322    r * Math.sin(phi) + 1.5,323    r * Math.cos(phi) * Math.cos(state.theta)324  );325  camera.lookAt(0, 1.5, 0);326  oceanUniforms.uCameraPos.value.copy(camera.position);327}328 329function animate() {330  requestAnimationFrame(animate);331  const dt = clock.getDelta();332  if (!state.paused) {333    elapsed += dt;334    oceanUniforms.uTime.value = elapsed;335  }336  updateCamera();337  renderer.render(scene, camera);338 339  const now = performance.now();340  if (dt > 0) fpsEMA = fpsEMA * 0.9 + (1 / dt) * 0.1;341  if (now - lastFpsUpdate > 250) {342    lastFpsUpdate = now;343    if (metaEl) {344      metaEl.textContent = `model run: ${MODEL_NAME} · ${Math.round(fpsEMA)} fps`;345    }346  }347}348animate();349 350window.addEventListener('resize', () => {351  camera.aspect = window.innerWidth / window.innerHeight;352  camera.updateProjectionMatrix();353  renderer.setSize(window.innerWidth, window.innerHeight);354});355 356const dom = renderer.domElement;357let dragging = false;358let lastX = 0;359let lastY = 0;360let pinchDist = 0;361 362dom.addEventListener('pointerdown', (e) => {363  dragging = true;364  lastX = e.clientX;365  lastY = e.clientY;366  state.autoOrbit = false;367  dom.setPointerCapture(e.pointerId);368});369dom.addEventListener('pointermove', (e) => {370  if (!dragging) return;371  const dx = e.clientX - lastX;372  const dy = e.clientY - lastY;373  lastX = e.clientX;374  lastY = e.clientY;375  state.theta -= dx * 0.005;376  state.phi += dy * 0.005;377  state.phi = Math.min(Math.max(state.phi, 0.04), 1.35);378});379dom.addEventListener('pointerup', (e) => {380  dragging = false;381  try { dom.releasePointerCapture(e.pointerId); } catch (_) {}382});383dom.addEventListener('pointercancel', () => { dragging = false; });384 385dom.addEventListener('wheel', (e) => {386  e.preventDefault();387  state.radius = Math.min(Math.max(state.radius + e.deltaY * 0.02, 12), 60);388  state.autoOrbit = false;389}, { passive: false });390 391dom.addEventListener('touchmove', (e) => {392  if (e.touches.length === 2) {393    e.preventDefault();394    const dx = e.touches[0].clientX - e.touches[1].clientX;395    const dy = e.touches[0].clientY - e.touches[1].clientY;396    const dist = Math.hypot(dx, dy);397    if (pinchDist > 0) {398      state.radius = Math.min(Math.max(state.radius - (dist - pinchDist) * 0.05, 12), 60);399    }400    pinchDist = dist;401    state.autoOrbit = false;402  }403}, { passive: false });404dom.addEventListener('touchend', () => { pinchDist = 0; });405 
CSS

src/styles.css

1* {2  box-sizing: border-box;3}4 5html,6body,7#app {8  width: 100%;9  height: 100%;10  margin: 0;11}12 13body {14  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;15  overflow: hidden;16  background: #0a3d62;17  color: #e8f4fb;18  -webkit-font-smoothing: antialiased;19}20 21#app {22  position: relative;23  overflow: hidden;24}25 26#app canvas {27  position: absolute;28  inset: 0;29  display: block;30  touch-action: none;31}32 33.panel {34  position: absolute;35  top: 16px;36  left: 16px;37  width: 280px;38  max-width: calc(100vw - 32px);39  padding: 14px 16px 16px;40  border-radius: 14px;41  background: rgba(10, 28, 44, 0.55);42  border: 1px solid rgba(160, 210, 240, 0.18);43  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);44  backdrop-filter: blur(14px) saturate(120%);45  -webkit-backdrop-filter: blur(14px) saturate(120%);46  z-index: 10;47  user-select: none;48  display: flex;49  flex-direction: column;50  gap: 12px;51}52 53.panel.collapsed .panel-body {54  display: none;55}56 57.panel-header {58  display: flex;59  align-items: center;60  justify-content: space-between;61  gap: 8px;62}63 64.identity {65  display: flex;66  flex-direction: column;67  line-height: 1.25;68}69 70.identity .title {71  font-size: 14px;72  font-weight: 600;73  letter-spacing: 0.2px;74}75 76.identity .meta {77  font-size: 11px;78  color: #9fc7e0;79  font-variant-numeric: tabular-nums;80}81 82.collapse-btn {83  border: 1px solid rgba(160, 210, 240, 0.25);84  background: rgba(160, 210, 240, 0.08);85  color: #e8f4fb;86  width: 28px;87  height: 28px;88  border-radius: 8px;89  cursor: pointer;90  font-size: 15px;91  line-height: 1;92  display: flex;93  align-items: center;94  justify-content: center;95  transition: background 0.15s ease;96}97 98.collapse-btn:hover {99  background: rgba(160, 210, 240, 0.18);100}101 102.panel-body {103  display: flex;104  flex-direction: column;105  gap: 12px;106}107 108.row {109  display: flex;110  flex-direction: column;111  gap: 6px;112}113 114.row label {115  font-size: 11px;116  text-transform: uppercase;117  letter-spacing: 0.6px;118  color: #9fc7e0;119  display: flex;120  justify-content: space-between;121}122 123.row label .val {124  color: #e8f4fb;125  font-variant-numeric: tabular-nums;126  text-transform: none;127  letter-spacing: 0;128}129 130input[type="range"] {131  -webkit-appearance: none;132  appearance: none;133  width: 100%;134  height: 4px;135  border-radius: 4px;136  background: rgba(160, 210, 240, 0.2);137  outline: none;138  cursor: pointer;139}140 141input[type="range"]::-webkit-slider-thumb {142  -webkit-appearance: none;143  appearance: none;144  width: 16px;145  height: 16px;146  border-radius: 50%;147  background: #6fc1ee;148  border: 2px solid #0a3d62;149  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);150}151 152input[type="range"]::-moz-range-thumb {153  width: 16px;154  height: 16px;155  border-radius: 50%;156  background: #6fc1ee;157  border: 2px solid #0a3d62;158  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);159}160 161.btn-group {162  display: flex;163  gap: 8px;164}165 166.btn {167  flex: 1;168  padding: 8px 10px;169  border-radius: 9px;170  border: 1px solid rgba(160, 210, 240, 0.22);171  background: rgba(160, 210, 240, 0.08);172  color: #e8f4fb;173  font-size: 12px;174  font-weight: 500;175  cursor: pointer;176  transition: background 0.15s ease, border-color 0.15s ease;177}178 179.btn:hover {180  background: rgba(160, 210, 240, 0.16);181}182 183.btn.active {184  background: #2a8fbd;185  border-color: #6fc1ee;186  color: #fff;187}188 189.btn.primary {190  background: #2a8fbd;191  border-color: #6fc1ee;192  color: #fff;193}194 195.btn.primary.paused {196  background: #c66a3d;197  border-color: #e8a070;198}199 200.hint {201  font-size: 11px;202  color: #8fb4cf;203  line-height: 1.4;204}205 206.hint kbd {207  background: rgba(160, 210, 240, 0.15);208  border: 1px solid rgba(160, 210, 240, 0.25);209  border-radius: 4px;210  padding: 0 4px;211  font-family: inherit;212  font-size: 10px;213}214 215@media (max-width: 600px) {216  .panel {217    top: auto;218    bottom: 12px;219    left: 12px;220    right: 12px;221    width: auto;222    max-width: none;223    padding: 12px 14px 14px;224  }225}226 
Package

package.json

1{2  "name": "ocean-ui-model-run",3  "version": "0.1.0",4  "private": true,5  "type": "module",6  "scripts": {7    "dev": "vite --host 127.0.0.1",8    "build": "vite build",9    "preview": "vite preview --host 127.0.0.1"10  },11  "dependencies": {12    "three": "^0.171.0",13    "vite": "^6.0.7"14  },15  "devDependencies": {}16}17