File size: 10,029 Bytes
337fbee
993eabd
337fbee
 
 
 
 
 
 
993eabd
337fbee
 
 
 
 
 
 
 
993eabd
337fbee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
993eabd
337fbee
 
 
 
 
 
 
 
 
 
 
 
 
 
993eabd
 
337fbee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
"use strict";

function main() {
  // Get a WebGL2 context
  const canvas = document.querySelector("#canvas");
  const gl = canvas.getContext("webgl2");
  if (!gl) {
    return;
  }

  //────────────────────────────────────────────────────────────
  // Vertex shader: simply pass the vertex positions along.
  const vs = `#version 300 es
    in vec4 a_position;
    void main() {
      gl_Position = a_position;
    }
  `;

  //────────────────────────────────────────────────────────────
  // Fragment shader: a scientifically–inspired 3D cymatic display.
  // This shader ray–marches a vibrating β€œplate” (whose height is defined
  // by the sum of two sinusoidal (mode) functions) and then shades it with
  // diffuse and specular lighting. The palette() function is used to inject
  // a pleasing color variation based on the local vibration amplitude.
  const fs = `#version 300 es
  precision highp float;
  
  uniform vec2 iResolution;
  uniform vec2 iMouse;
  uniform float iTime;
  out vec4 outColor;
  
  // A color palette function (from Shadertoy) to add some β€œpop”
  vec3 palette( float t ) {
      vec3 a = vec3(0.5, 0.5, 0.5);
      vec3 b = vec3(0.5, 0.5, 0.5);
      vec3 c = vec3(1.0, 1.0, 1.0);
      vec3 d = vec3(0.263, 0.416, 0.557);
      return a + b * cos( 6.28318 * (c * t + d) );
  }
  
  // The vibrating plate – defined on the xz–plane (with x,z in [-1,1])
  // and with vertical displacement given by y = plate(x,z,t).
  // Two modes are added (a β€œfundamental” and a second–harmonic mode) to mimic
  // realistic cymatic (Chladni) patterns on a clamped plate.
  float plate(vec2 pos, float t) {
      // Map pos from [-1,1] to [0,1] (for clamped–edge conditions)
      vec2 uv = (pos + 1.0) * 0.5;
      float mode1 = sin(3.14159 * uv.x) * sin(3.14159 * uv.y) * cos(3.14159 * t);
      float mode2 = sin(2.0 * 3.14159 * uv.x) * sin(2.0 * 3.14159 * uv.y) * cos(2.0 * 3.14159 * t);
      return 0.2 * (mode1 + mode2);
  }
  
  // Compute the normal of the heightfield (the vibrating plate) using finite differences.
  vec3 calcNormal(vec2 pos, float t) {
      float eps = 0.001;
      float h = plate(pos, t);
      float hx = plate(pos + vec2(eps, 0.0), t) - h;
      float hz = plate(pos + vec2(0.0, eps), t) - h;
      return normalize(vec3(-hx, 1.0, -hz));
  }
  
  // Given a 3D point p, return its vertical distance to the plate surface.
  // (If p is exactly on the surface then p.y = plate(p.xz,t) and the result is zero.)
  float mapHeight(vec3 p, float t) {
      // Outside the domain x,z ∈ [-1,1] we assume a flat floor at y=0.
      if (abs(p.x) > 1.0 || abs(p.z) > 1.0) {
          return p.y;
      }
      return p.y - plate(vec2(p.x, p.z), t);
  }
  
  // A simple raycast function that marches a ray from the camera and
  // returns the distance along the ray at which the plate is hit.
  float raycast(vec3 ro, vec3 rd, float t) {
      float tMin = 0.0;
      float tMax = 20.0;
      float tCurrent = tMin;
      float stepSize = 0.02;
      bool hit = false;
      for (int i = 0; i < 500; i++) {
          vec3 pos = ro + rd * tCurrent;
          float d = mapHeight(pos, t);
          if (d < 0.001) { 
              hit = true;
              break;
          }
          tCurrent += stepSize;
          if (tCurrent > tMax) break;
      }
      if (!hit) return -1.0;
      // Refine the hit point with a short binary search.
      float tA = tCurrent - stepSize;
      float tB = tCurrent;
      for (int i = 0; i < 10; i++) {
          float tMid = (tA + tB) * 0.5;
          float dMid = mapHeight(ro + rd * tMid, t);
          if (dMid > 0.0) {
              tA = tMid;
          } else {
              tB = tMid;
          }
      }
      return (tA + tB) * 0.5;
  }
  
  void main() {
      // Compute normalized screen coordinates (centered on 0)
      vec2 uv = (gl_FragCoord.xy - 0.5 * iResolution.xy) / iResolution.y;
  
      // Use the mouse to control the camera’s azimuth and pitch.
      // Horizontal movement rotates 0–2Ο€; vertical movement adjusts pitch.
      float angle = iMouse.x / iResolution.x * 6.28318; // full rotation
      float pitch = mix(0.4, 1.2, iMouse.y / iResolution.y);
      float radius = 4.0;
      vec3 ro = vec3(
          radius * cos(pitch) * cos(angle),
          radius * sin(pitch),
          radius * cos(pitch) * sin(angle)
      );
      vec3 target = vec3(0.0, 0.0, 0.0);
  
      // Construct a simple camera coordinate system.
      vec3 forward = normalize(target - ro);
      vec3 right = normalize(cross(forward, vec3(0.0, 1.0, 0.0)));
      vec3 up = cross(right, forward);
  
      // Compute the ray direction using a basic perspective projection.
      vec3 rd = normalize(forward + uv.x * right + uv.y * up);
  
      // March the ray to see if and where it hits the vibrating plate.
      float tHit = raycast(ro, rd, iTime);
      vec3 color;
      if (tHit > 0.0) {
          vec3 pos = ro + rd * tHit;
          // Get the local normal from the heightfield
          vec3 normal = calcNormal(vec2(pos.x, pos.z), iTime);
  
          // Standard lighting: diffuse + specular
          vec3 lightDir = normalize(vec3(0.5, 1.0, 0.8));
          float diff = max(dot(normal, lightDir), 0.0);
          vec3 viewDir = normalize(ro - pos);
          vec3 halfDir = normalize(lightDir + viewDir);
          float spec = pow(max(dot(normal, halfDir), 0.0), 32.0);
  
          // Base color comes from the palette – modulated by the local vibration amplitude.
          float h = plate(vec2(pos.x, pos.z), iTime);
          vec3 baseColor = palette(h * 5.0);
  
          color = baseColor * diff + vec3(0.1) * spec + vec3(0.1);
      } else {
          // If no hit, use a subtle background gradient.
          color = mix(vec3(0.0, 0.0, 0.1), vec3(0.0), uv.y + 0.5);
      }
  
      outColor = vec4(color, 1.0);
  }
  `;
  
  //────────────────────────────────────────────────────────────
  // Create and compile the shader program using webgl-utils.
  const program = webglUtils.createProgramFromSources(gl, [vs, fs]);
  
  // Look up attribute and uniform locations.
  const positionAttributeLocation = gl.getAttribLocation(program, "a_position");
  const resolutionLocation = gl.getUniformLocation(program, "iResolution");
  const mouseLocation = gl.getUniformLocation(program, "iMouse");
  const timeLocation = gl.getUniformLocation(program, "iTime");
  
  // Create a vertex array object (VAO) and bind it.
  const vao = gl.createVertexArray();
  gl.bindVertexArray(vao);
  
  // Create a buffer and put a full–screen quad in it.
  const positionBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  gl.bufferData(
    gl.ARRAY_BUFFER,
    new Float32Array([
      -1, -1,
       1, -1,
      -1,  1,
      -1,  1,
       1, -1,
       1,  1,
    ]),
    gl.STATIC_DRAW
  );
  
  // Enable the position attribute.
  gl.enableVertexAttribArray(positionAttributeLocation);
  gl.vertexAttribPointer(
    positionAttributeLocation,
    2,           // 2 components per vertex
    gl.FLOAT,    // data type is float
    false,
    0,
    0
  );
  
  //────────────────────────────────────────────────────────────
  // Setup mouse / touch interactions.
  const playpauseElem = document.querySelector(".playpause");
  const inputElem = document.querySelector(".divcanvas");
  inputElem.addEventListener("mouseover", requestFrame);
  inputElem.addEventListener("mouseout", cancelFrame);
  
  let mouseX = 0;
  let mouseY = 0;
  function setMousePosition(e) {
    const rect = inputElem.getBoundingClientRect();
    mouseX = e.clientX - rect.left;
    mouseY = rect.height - (e.clientY - rect.top) - 1;
  }
  
  inputElem.addEventListener("mousemove", setMousePosition);
  inputElem.addEventListener("touchstart", (e) => {
    e.preventDefault();
    playpauseElem.classList.add("playpausehide");
    requestFrame();
  }, { passive: false });
  inputElem.addEventListener("touchmove", (e) => {
    e.preventDefault();
    setMousePosition(e.touches[0]);
  }, { passive: false });
  inputElem.addEventListener("touchend", (e) => {
    e.preventDefault();
    playpauseElem.classList.remove("playpausehide");
    cancelFrame();
  }, { passive: false });
  
  //────────────────────────────────────────────────────────────
  // Animation loop variables and functions.
  let requestId;
  function requestFrame() {
    if (!requestId) {
      requestId = requestAnimationFrame(render);
    }
  }
  function cancelFrame() {
    if (requestId) {
      cancelAnimationFrame(requestId);
      requestId = undefined;
    }
  }
  
  let then = 0;
  let time = 0;
  function render(now) {
    requestId = undefined;
    now *= 0.001; // convert milliseconds to seconds
    const elapsedTime = Math.min(now - then, 0.1);
    time += elapsedTime;
    then = now;
  
    // Resize canvas if needed.
    webglUtils.resizeCanvasToDisplaySize(gl.canvas);
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
  
    // Use our program and bind our VAO.
    gl.useProgram(program);
    gl.bindVertexArray(vao);
  
    // Set the uniforms.
    gl.uniform2f(resolutionLocation, gl.canvas.width, gl.canvas.height);
    gl.uniform2f(mouseLocation, mouseX, mouseY);
    gl.uniform1f(timeLocation, time);
  
    // Draw the full–screen quad.
    gl.drawArrays(gl.TRIANGLES, 0, 6);
  
    requestFrame();
  }
  
  requestFrame();
  requestAnimationFrame(cancelFrame);
}

main();