const canvas = document.getElementById('gl'); const gl = canvas.getContext('webgl'); if (!gl) { alert('此瀏覽器不支援 WebGL'); } function resize() { const dpr = Math.min(window.devicePixelRatio || 1, 2); const w = Math.floor(window.innerWidth * dpr); const h = Math.floor((window.innerHeight - document.querySelector('.ui').offsetHeight) * dpr); canvas.width = w; canvas.height = Math.max(h, 2); canvas.style.width = '100%'; canvas.style.height = '100%'; gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); } window.addEventListener('resize', resize); const vertSrc = ` attribute vec2 aPos; attribute vec2 aUV; varying vec2 vUV; void main(){ vUV = aUV; gl_Position = vec4(aPos, 0.0, 1.0); }`; // fragment shader(點質量透鏡公式 β = θ - θ_E² θ / |θ|²) const fragSrc = ` precision highp float; varying vec2 vUV; uniform sampler2D uTex; uniform vec2 uPosition; uniform float uThetaE2; uniform float uRatio; uniform float uShadowScale; void main(){ float eps = 1e-6; vec2 d = vUV - uPosition; vec2 d_n = d / vec2(uRatio, 1.0); float r2 = dot(d_n, d_n) + eps; vec2 beta_n = d_n - (uThetaE2 * d_n / r2); vec2 sampleUV = beta_n * vec2(uRatio, 1.0) + uPosition; vec4 res = texture2D(uTex, clamp(sampleUV, 0.0, 1.0)); if (length(d_n) < uShadowScale * sqrt(uThetaE2)) { res.rgb = vec3(0.0); } gl_FragColor = res; }`; function compile(type, src){ const sh = gl.createShader(type); gl.shaderSource(sh, src); gl.compileShader(sh); if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) { throw new Error(gl.getShaderInfoLog(sh) || 'shader compile error'); } return sh; } const vs = compile(gl.VERTEX_SHADER, vertSrc); const fs = compile(gl.FRAGMENT_SHADER, fragSrc); const prog = gl.createProgram(); gl.attachShader(prog, vs); gl.attachShader(prog, fs); gl.linkProgram(prog); if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) { throw new Error(gl.getProgramInfoLog(prog) || 'program link error'); } gl.useProgram(prog); const quad = new Float32Array([ -1, -1, 0, 0, 1, -1, 1, 0, -1, 1, 0, 1, 1, 1, 1, 1, ]); const ibo = new Uint16Array([0,1,2,2,1,3]); const vbo = gl.createBuffer(); const ebo = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vbo); gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, ibo, gl.STATIC_DRAW); const aPos = gl.getAttribLocation(prog, 'aPos'); const aUV = gl.getAttribLocation(prog, 'aUV'); gl.enableVertexAttribArray(aPos); gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 16, 0); gl.enableVertexAttribArray(aUV); gl.vertexAttribPointer(aUV, 2, gl.FLOAT, false, 16, 8); const uTex = gl.getUniformLocation(prog, 'uTex'); const uPosition = gl.getUniformLocation(prog, 'uPosition'); const uThetaE2 = gl.getUniformLocation(prog, 'uThetaE2'); const uRatio = gl.getUniformLocation(prog, 'uRatio'); const uShadowScale = gl.getUniformLocation(prog, 'uShadowScale'); gl.uniform1i(uTex, 0); let state = { position: { x: 0.5, y: 0.5 }, thetaE2: parseFloat(document.getElementById('thetaE').value), shadowScale: parseFloat(document.getElementById('shadowScale').value) }; function updateUniforms(){ const ratio = gl.drawingBufferWidth / gl.drawingBufferHeight; gl.uniform2f(uPosition, state.position.x, state.position.y); gl.uniform1f(uThetaE2, state.thetaE2); gl.uniform1f(uRatio, ratio); gl.uniform1f(uShadowScale, state.shadowScale); } let texture = gl.createTexture(); function useTextureFromImage(img){ gl.bindTexture(gl.TEXTURE_2D, texture); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); } function genStarfield(w=1024, h=512){ const cvs = document.createElement('canvas'); cvs.width = w; cvs.height = h; const ctx = cvs.getContext('2d'); ctx.fillStyle = '#000'; ctx.fillRect(0,0,w,h); for(let i=0;i<5000;i++){ const x=Math.random()*w, y=Math.random()*h; const s=Math.random()*1.6+0.2; const a=Math.random()*0.9+0.1; ctx.fillStyle = `rgba(255,255,255,${a})`; ctx.fillRect(x, y, s, s); } return cvs; } useTextureFromImage(genStarfield()); const imgInput = document.getElementById('img'); imgInput.addEventListener('change', e => { const file = e.target.files[0]; if (!file) return; const img = new Image(); img.onload = () => useTextureFromImage(img); img.src = URL.createObjectURL(file); }); let dragging = false; let animate = true; const chkAnimate = document.getElementById('animate'); chkAnimate.addEventListener('change', () => animate = chkAnimate.checked); canvas.addEventListener('mousedown', (e)=>{ dragging = true; move(e); }); window.addEventListener('mouseup', ()=> dragging = false); window.addEventListener('mousemove', (e)=>{ if (dragging) move(e); }); function move(e){ const rect = canvas.getBoundingClientRect(); const x = (e.clientX - rect.left) / rect.width; const y = (e.clientY - rect.top) / rect.height; state.position.x = x; state.position.y = 1.0 - y; } const thetaEEl = document.getElementById('thetaE'); const shadowEl = document.getElementById('shadowScale'); thetaEEl.addEventListener('input', ()=> state.thetaE2 = parseFloat(thetaEEl.value)); shadowEl.addEventListener('input', ()=> state.shadowScale = parseFloat(shadowEl.value)); document.getElementById('reset').addEventListener('click', ()=>{ state = { position: {x:0.5, y:0.5}, thetaE2: 0.01, shadowScale: 2.598 }; thetaEEl.value = state.thetaE2; shadowEl.value = state.shadowScale; chkAnimate.checked = false; animate = false; }); resize(); let t0 = performance.now(); function render(now){ if (animate){ const t = (now - t0) * 0.001; const r = 0.25; const cx = 0.5, cy = 0.55; state.position.x = cx + r * Math.cos(t*0.3); state.position.y = cy + r * Math.sin(t*0.3); state.thetaE2 = 0.01 + 0.005*Math.sin(t*0.7); } updateUniforms(); gl.clearColor(0,0,0,1); gl.clear(gl.COLOR_BUFFER_BIT); gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0); requestAnimationFrame(render); } requestAnimationFrame(render);