"use strict"; (() => { const makeSphere = (meridians = 3, bandsPerHemisphere = 1) => { const assert = (cond, msg) => { if (!cond) throw (msg ? msg : "failed assertion"); }; // The number of meridians dividing the sphere's surface. // Two meridians would make an XZ plane. assert(meridians >= 3); assert(Number.isInteger(meridians)); // The number of latitude bands comprising each hemisphere. assert(bandsPerHemisphere >= 1); assert(Number.isInteger(bandsPerHemisphere)); const parallels = bandsPerHemisphere * 2 + 1; // The seam needs two of each vertex so we can map the texture correctly. const vertices = new Float32Array((meridians + 1) * parallels * 5); const vertexStride = 5 * vertices.constructor.BYTES_PER_ELEMENT; const positionOffset = 0; const textureCoordOffset = 3 * vertices.constructor.BYTES_PER_ELEMENT; let verticesIdx = 0; /* In order to cut down on distortion at the poles, the parallels are not at equal intervals of latitude. Instead, the parallels describe the intersections of the (circum)sphere with a set of concentric, evenly spaced, vertically oriented cylinders. This makes the parallels denser towards the poles, in proportion to how badly that density is needed at any given latitude. */ for (let p = 0; p < parallels; p++) { const π = Math.PI; const sin = Math.sin; const lat = π * sin(π * (p - bandsPerHemisphere) / 2 / bandsPerHemisphere) / 2; const y = Math.sin(lat); for (let m = 0; m < meridians + 1; m++) { const long = m * 2 * Math.PI / meridians; const x = Math.cos(lat) * Math.sin(long); const z = Math.cos(lat) * Math.cos(long); const u = m / meridians; const v = lat / π + 0.5; vertices[verticesIdx++] = x; vertices[verticesIdx++] = y; vertices[verticesIdx++] = z; vertices[verticesIdx++] = u; vertices[verticesIdx++] = v; } } const indices = new Uint16Array(meridians * (parallels - 1) * 6); let indicesIdx = 0; for (let p = 0; p < parallels - 1; p++) { for (let m = 0; m < meridians; m++) { const lowerLeft = p * (meridians + 1) + m; const lowerRight = p * (meridians + 1) + (m + 1); const upperLeft = (p + 1) * (meridians + 1) + m; const upperRight = (p + 1) * (meridians + 1) + (m + 1); const currIndices = [ // Lower triangle lowerLeft, lowerRight, upperRight, // Upper triangle lowerLeft, upperRight, upperLeft, ]; for (const index of currIndices) { indices[indicesIdx++] = index; } } } return { vertices, indices, vertexStride, positionOffset, textureCoordOffset, }; } const initShaderProgram = (gl) => { const vShaderSrc = ` attribute vec3 position; attribute vec2 textureCoord; uniform mat4 viewMatrix; uniform mat4 projectionMatrix; varying highp vec2 vTextureCoord; void main() { gl_Position = projectionMatrix * viewMatrix * vec4(position, 1); vTextureCoord = textureCoord; } `; const fShaderSrc = ` varying highp vec2 vTextureCoord; uniform sampler2D sampler; void main() { gl_FragColor = texture2D(sampler, vTextureCoord); } `; const compileShader = (src, type) => { let shader = gl.createShader(type); gl.shaderSource(shader, src); gl.compileShader(shader); let success = gl.getShaderParameter(shader, gl.COMPILE_STATUS); if (!success) { throw gl.getShaderInfoLog(shader); } return shader; } let vShader = compileShader(vShaderSrc, gl.VERTEX_SHADER); let fShader = compileShader(fShaderSrc, gl.FRAGMENT_SHADER); let program = gl.createProgram(); gl.attachShader(program, vShader); gl.attachShader(program, fShader); gl.linkProgram(program); let success = gl.getProgramParameter(program, gl.LINK_STATUS); if (!success) { throw gl.getProgramInfoLog(program); } return program; }; const loadTexture = (gl, image, textureUnit = 0) => { const textureUnitName = `TEXTURE${textureUnit}` const texture = gl.createTexture(); const samplerLocation = gl.getUniformLocation(program, "sampler"); image.addEventListener("load", () => { gl.activeTexture(gl[textureUnitName]); gl.bindTexture(gl.TEXTURE_2D, texture); gl.uniform1i(samplerLocation, textureUnit); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); 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); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); }); return texture; }; let viewer = document.getElementById("viewer"); let gl = viewer.getContext("webgl2"); let program = initShaderProgram(gl); let viewMatrixLocation = gl.getUniformLocation(program, "viewMatrix"); let projectionMatrixLocation = gl.getUniformLocation(program, "projectionMatrix"); // Set up mouse-panning state machine. let yaw = 0; let pitch = 0; const PanState = Object.freeze({ idle: (event) => { if (event.type === "mousedown") { return PanState.panning; } return PanState.idle; }, panning: (event) => { switch (event.type) { case "mouseup": return PanState.idle; case "mouseover": if ((event.buttons & 1) == 0) { return PanState.idle; } break; case "mousemove": yaw += event.movementX * 0.005; pitch += event.movementY * 0.005; if (pitch < -Math.PI / 2) { pitch = -Math.PI / 2; } else if (pitch > Math.PI / 2) { pitch = Math.PI / 2; } break } return PanState.panning; }, }); const runPanState = (mouseEvent) => { panState = panState(mouseEvent); } var panState = PanState.idle; viewer.addEventListener("mousedown", runPanState); viewer.addEventListener("mouseup", runPanState); viewer.addEventListener("mouseover", runPanState); viewer.addEventListener("mousemove", runPanState); const minZoom = 1; let zoom = 3; viewer.addEventListener("wheel", (evt) => { zoom = Math.max(zoom * (1 - evt.deltaY * 0.005), minZoom); evt.preventDefault(); }); gl.useProgram(program); const sphere = makeSphere(24, 24); let vertexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); gl.bufferData(gl.ARRAY_BUFFER, sphere.vertices, gl.STATIC_DRAW); const positionLocation = gl.getAttribLocation(program, "position"); const textureCoordLocation = gl.getAttribLocation(program, "textureCoord"); let indexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, sphere.indices, gl.STATIC_DRAW); gl.enable(gl.CULL_FACE); // Load texture let image = document.getElementById("image"); loadTexture(gl, image); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); const render = () => { gl.useProgram(program); // Create the transformation matrix. const viewMatrix = mat4.create(); mat4.rotateX(viewMatrix, viewMatrix, pitch); mat4.rotateY(viewMatrix, viewMatrix, yaw); gl.uniformMatrix4fv(viewMatrixLocation, false, viewMatrix); const aspect = gl.drawingBufferWidth / gl.drawingBufferHeight; const projectionMatrix = mat4.ortho(mat4.create(), -aspect / zoom, aspect / zoom, -1 / zoom, 1 / zoom, -1, 1); gl.uniformMatrix4fv(projectionMatrixLocation, false, projectionMatrix); gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); gl.enableVertexAttribArray(positionLocation); gl.vertexAttribPointer(positionLocation, 3, gl.FLOAT, false, sphere.vertexStride, sphere.positionOffset); gl.enableVertexAttribArray(textureCoordLocation); gl.vertexAttribPointer(textureCoordLocation, 2, gl.FLOAT, false, sphere.vertexStride, sphere.textureCoordOffset); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); gl.drawElements(gl.TRIANGLES, sphere.indices.length, gl.UNSIGNED_SHORT, 0); requestAnimationFrame(render); }; requestAnimationFrame(render); })();