diff --git a/06-boat-ocean.html b/06-boat-ocean.html new file mode 100644 index 0000000..422f0bd --- /dev/null +++ b/06-boat-ocean.html @@ -0,0 +1,43 @@ + + + + + 06-boat-ocean + + + + + + + +
+ + +
+ + + diff --git a/06-boat-ocean.js b/06-boat-ocean.js new file mode 100644 index 0000000..8512bed --- /dev/null +++ b/06-boat-ocean.js @@ -0,0 +1,463 @@ +import * as twgl from 'https://unpkg.com/twgl.js@4.19.2/dist/4.x/twgl-full.module.js'; +import listenToInputs, { update as inputUpdate } from './lib/input.js'; +import { degToRad } from './lib/utils.js'; +import { matrix4 } from './lib/matrix.js'; + +const vertexShaderSource = ` +attribute vec4 a_position; +attribute vec2 a_texcoord; +attribute vec4 a_normal; + +uniform mat4 u_matrix; +uniform mat4 u_worldMatrix; +uniform mat4 u_normalMatrix; +uniform vec3 u_worldViewerPosition; + +uniform mat4 u_reflectionMatrix; +uniform mat4 u_lightProjectionMatrix; + +varying vec2 v_texcoord; +varying vec3 v_worldSurface; +varying vec3 v_surfaceToViewer; + +varying mat3 v_normalMatrix; + +varying vec4 v_reflectionTexcoord; +varying float v_depth; +varying vec4 v_lightProjection; + +void main() { + gl_Position = u_matrix * a_position; + v_texcoord = vec2(a_texcoord.x, 1.0 - a_texcoord.y); + + vec3 normal = normalize((u_normalMatrix * a_normal).xyz); + vec3 normalMatrixI = normal.y >= 1.0 ? vec3(1, 0, 0) : normalize(cross(vec3(0, 1, 0), normal)); + vec3 normalMatrixJ = normalize(cross(normal, normalMatrixI)); + + v_normalMatrix = mat3( + normalMatrixI, + normalMatrixJ, + normal + ); + + vec4 worldPosition = u_worldMatrix * a_position; + v_worldSurface = worldPosition.xyz; + v_surfaceToViewer = u_worldViewerPosition - v_worldSurface; + + v_reflectionTexcoord = u_reflectionMatrix * worldPosition; + v_depth = gl_Position.z / 2.0 + 0.5; + v_lightProjection = u_lightProjectionMatrix * worldPosition; +} +`; + +const fragmentShaderSource = ` +precision highp float; + +uniform vec3 u_lightDirection; +uniform vec3 u_ambient; + +uniform vec3 u_diffuse; +uniform vec3 u_specular; +uniform float u_specularExponent; +uniform vec3 u_emissive; + +uniform sampler2D u_normalMap; +uniform sampler2D u_diffuseMap; + +uniform sampler2D u_lightProjectionMap; + +varying vec2 v_texcoord; +varying vec3 v_surfaceToViewer; + +varying mat3 v_normalMatrix; + +varying vec4 v_lightProjection; + +void main() { + vec3 diffuse = u_diffuse + texture2D(u_diffuseMap, v_texcoord).rgb; + vec3 ambient = u_ambient * diffuse; + vec3 normal = texture2D(u_normalMap, v_texcoord).xyz * 2.0 - 1.0; + normal = normalize(v_normalMatrix * normal); + vec3 surfaceToLightDirection = normalize(-u_lightDirection); + float diffuseBrightness = clamp(dot(surfaceToLightDirection, normal), 0.0, 1.0); + + vec3 surfaceToViewerDirection = normalize(v_surfaceToViewer); + vec3 halfVector = normalize(surfaceToLightDirection + surfaceToViewerDirection); + float specularBrightness = clamp(pow(dot(halfVector, normal), u_specularExponent), 0.0, 1.0); + + vec2 lightProjectionCoord = v_lightProjection.xy / v_lightProjection.w * 0.5 + 0.5; + float lightToSurfaceDepth = v_lightProjection.z / v_lightProjection.w * 0.5 + 0.5; + float lightProjectedDepth = texture2D(u_lightProjectionMap, lightProjectionCoord).r; + + float occulusion = lightToSurfaceDepth > 0.01 + lightProjectedDepth ? 0.5 : 0.0; + + diffuseBrightness *= 1.0 - occulusion; + specularBrightness *= 1.0 - occulusion * 2.0; + + gl_FragColor = vec4( + clamp( + diffuse * diffuseBrightness + + u_specular * specularBrightness + + u_emissive, + ambient, vec3(1, 1, 1) + ), + 1 + ); +} + + +`; + +const depthFragmentShaderSource = ` +precision highp float; + +varying float v_depth; + +void main() { + gl_FragColor = vec4(v_depth, v_depth, v_depth, 1); +} +`; + +const oceanFragmentShaderSource = ` +precision highp float; + +uniform vec3 u_lightDirection; +uniform vec3 u_ambient; + +uniform vec3 u_diffuse; +uniform vec3 u_specular; +uniform float u_specularExponent; +uniform vec3 u_emissive; + +uniform sampler2D u_normalMap; +uniform sampler2D u_diffuseMap; + +uniform sampler2D u_lightProjectionMap; +uniform float u_time; + +varying vec2 v_texcoord; +varying vec3 v_worldSurface; +varying vec3 v_surfaceToViewer; + +varying mat3 v_normalMatrix; + +varying vec4 v_reflectionTexcoord; +varying vec4 v_lightProjection; + +void main() { + vec2 reflectionTexcoord = (v_reflectionTexcoord.xy / v_reflectionTexcoord.w) * 0.5 + 0.5; + vec3 normal = texture2D(u_normalMap, v_texcoord * 256.0).xyz * 2.0 - 1.0; + + reflectionTexcoord += normal.xy * 0.1; + vec3 diffuse = u_diffuse + texture2D(u_diffuseMap, reflectionTexcoord).rgb; + vec3 ambient = u_ambient * diffuse; + + normal = normalize(v_normalMatrix * normal); + vec3 surfaceToLightDirection = normalize(-u_lightDirection); + float diffuseBrightness = clamp(dot(surfaceToLightDirection, normal) + 0.5, 0.0, 1.0); + + vec3 surfaceToViewerDirection = normalize(v_surfaceToViewer); + vec3 halfVector = normalize(surfaceToLightDirection + surfaceToViewerDirection); + float specularBrightness = clamp(pow(dot(halfVector, normal), u_specularExponent), 0.0, 1.0); + + vec2 lightProjectionCoord = v_lightProjection.xy / v_lightProjection.w * 0.5 + 0.5; + lightProjectionCoord += normal.xy * 0.01; + float lightToSurfaceDepth = v_lightProjection.z / v_lightProjection.w * 0.5 + 0.5; + float lightProjectedDepth = texture2D(u_lightProjectionMap, lightProjectionCoord).r; + + float occulusion = lightToSurfaceDepth > 0.01 + lightProjectedDepth ? 0.5 : 0.0; + + diffuseBrightness *= 1.0 - occulusion; + specularBrightness *= 1.0 - occulusion * 2.0; + + gl_FragColor = vec4( + clamp( + diffuse * diffuseBrightness + + u_specular * specularBrightness + + u_emissive, + ambient, vec3(1, 1, 1) + ), + 1 + ); +} +`; + +async function setup() { + const canvas = document.getElementById('canvas'); + const gl = canvas.getContext('webgl'); + + const oesVaoExt = gl.getExtension('OES_vertex_array_object'); + if (oesVaoExt) { + gl.createVertexArray = (...args) => oesVaoExt.createVertexArrayOES(...args); + gl.deleteVertexArray = (...args) => oesVaoExt.deleteVertexArrayOES(...args); + gl.isVertexArray = (...args) => oesVaoExt.isVertexArrayOES(...args); + gl.bindVertexArray = (...args) => oesVaoExt.bindVertexArrayOES(...args); + } else { + throw new Error('Your browser does not support WebGL ext: OES_vertex_array_object') + } + + const webglDepthTexExt = gl.getExtension('WEBGL_depth_texture'); + if (!webglDepthTexExt) { + throw new Error('Your browser does not support WebGL ext: WEBGL_depth_texture') + } + + twgl.setAttributePrefix('a_'); + + const programInfo = twgl.createProgramInfo(gl, [vertexShaderSource, fragmentShaderSource]); + const depthProgramInfo = twgl.createProgramInfo(gl, [vertexShaderSource, depthFragmentShaderSource]); + const oceanProgramInfo = twgl.createProgramInfo(gl, [vertexShaderSource, oceanFragmentShaderSource]); + + const textures = twgl.createTextures(gl, { + scale: { + src: 'https://i.imgur.com/IuTNc8Ah.jpg', + min: gl.LINEAR_MIPMAP_LINEAR, mag: gl.LINEAR, crossOrigin: true, + }, + scaleNormal: { + src: 'https://i.imgur.com/kWO2b7jh.jpg', + min: gl.LINEAR_MIPMAP_LINEAR, mag: gl.LINEAR, crossOrigin: true, + }, + oceanNormal: { + src: 'https://i.imgur.com/eCBtjB8h.jpg', + min: gl.LINEAR_MIPMAP_LINEAR, mag: gl.LINEAR, crossOrigin: true, + }, + nil: { src: [0, 0, 0, 255] }, + nilNormal: { src: [127, 127, 255, 255] }, + }); + + const framebuffers = {}; + framebuffers.reflection = twgl.createFramebufferInfo(gl, null, 2048, 2048); + textures.reflection = framebuffers.reflection.attachments[0]; + + framebuffers.lightProjection = twgl.createFramebufferInfo(gl, [{ + attachmentPoint: gl.DEPTH_ATTACHMENT, + format: gl.DEPTH_COMPONENT, + }], 2048, 2048); + textures.lightProjection = framebuffers.lightProjection.attachments[0]; + + const objects = {}; + + { // ball + const attribs = twgl.primitives.createSphereVertices(1, 32, 32); + const bufferInfo = twgl.createBufferInfoFromArrays(gl, attribs); + const vao = twgl.createVAOFromBufferInfo(gl, programInfo, bufferInfo); + + objects.ball = { + attribs, + bufferInfo, + vao, + }; + } + + { // plane + const attribs = twgl.primitives.createPlaneVertices() + const bufferInfo = twgl.createBufferInfoFromArrays(gl, attribs); + const vao = twgl.createVAOFromBufferInfo(gl, programInfo, bufferInfo); + + objects.plane = { + attribs, + bufferInfo, + vao, + }; + } + + gl.enable(gl.CULL_FACE); + gl.enable(gl.DEPTH_TEST); + + return { + gl, + programInfo, depthProgramInfo, oceanProgramInfo, + textures, framebuffers, objects, + state: { + fieldOfView: degToRad(45), + cameraRotationXY: [degToRad(-45), 0], + cameraDistance: 15, + cameraViewing: [0, 0, 0], + cameraViewingVelocity: [0, 0, 0], + lightRotationXY: [0, 0], + resolutionRatio: 1, + }, + time: 0, + }; +} + +function render(app) { + const { + gl, + framebuffers, textures, + programInfo, depthProgramInfo, oceanProgramInfo, + state, + } = app; + + const lightProjectionViewMatrix = matrix4.multiply( + matrix4.translate(1, -1, 0), + matrix4.projection(20, 20, 10), + [ // shearing + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, Math.tan(state.lightRotationXY[0]), 1, 0, + 0, 0, 0, 1, + ], + matrix4.inverse( + matrix4.multiply( + matrix4.yRotate(state.lightRotationXY[1]), + matrix4.xRotate(degToRad(90)), + ) + ), + ); + + const reflectionCameraMatrix = matrix4.multiply( + matrix4.translate(...state.cameraViewing), + matrix4.yRotate(state.cameraRotationXY[1]), + matrix4.xRotate(-state.cameraRotationXY[0]), + matrix4.translate(0, 0, state.cameraDistance), + ); + + const reflectionMatrix = matrix4.multiply( + matrix4.perspective(state.fieldOfView, gl.canvas.width / gl.canvas.height, 0.1, 2000), + matrix4.inverse(reflectionCameraMatrix), + ); + + const cameraMatrix = matrix4.multiply( + matrix4.translate(...state.cameraViewing), + matrix4.yRotate(state.cameraRotationXY[1]), + matrix4.xRotate(state.cameraRotationXY[0]), + matrix4.translate(0, 0, state.cameraDistance), + ); + + const viewMatrix = matrix4.multiply( + matrix4.perspective(state.fieldOfView, gl.canvas.width / gl.canvas.height, 0.1, 2000), + matrix4.inverse(cameraMatrix), + ); + + const lightDirection = matrix4.transformVector( + matrix4.multiply( + matrix4.yRotate(state.lightRotationXY[1]), + matrix4.xRotate(state.lightRotationXY[0]), + ), + [0, -1, 0, 1], + ).slice(0, 3); + + const globalUniforms = { + u_worldViewerPosition: cameraMatrix.slice(12, 15), + u_lightDirection: lightDirection, + u_ambient: [0.4, 0.4, 0.4], + u_lightProjectionMatrix: lightProjectionViewMatrix, + u_lightProjectionMap: textures.lightProjection, + } + + { // lightProjection + gl.useProgram(depthProgramInfo.program); + + twgl.bindFramebufferInfo(gl, framebuffers.lightProjection); + gl.clear(gl.DEPTH_BUFFER_BIT); + + renderBall(app, lightProjectionViewMatrix, depthProgramInfo); + renderOcean(app, lightProjectionViewMatrix, reflectionMatrix, depthProgramInfo); + } + + gl.useProgram(programInfo.program); + twgl.setUniforms(programInfo, globalUniforms); + + { // reflection + twgl.bindFramebufferInfo(gl, framebuffers.reflection); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + + renderBall(app, reflectionMatrix, programInfo); + } + + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + + twgl.resizeCanvasToDisplaySize(gl.canvas, state.resolutionRatio); + gl.viewport(0, 0, canvas.width, canvas.height); + + renderBall(app, viewMatrix, programInfo); + + gl.useProgram(oceanProgramInfo.program); + twgl.setUniforms(oceanProgramInfo, globalUniforms); + renderOcean(app, viewMatrix, reflectionMatrix, oceanProgramInfo); +} + +function renderBall(app, viewMatrix, programInfo) { + const { gl, textures, objects } = app; + + gl.bindVertexArray(objects.ball.vao); + + const worldMatrix = matrix4.multiply( + matrix4.translate(0, 1, 0), + matrix4.scale(1, 1, 1), + ); + + twgl.setUniforms(programInfo, { + u_matrix: matrix4.multiply(viewMatrix, worldMatrix), + u_worldMatrix: worldMatrix, + u_normalMatrix: matrix4.transpose(matrix4.inverse(worldMatrix)), + u_normalMap: textures.scaleNormal, + u_diffuse: [0, 0, 0], + u_diffuseMap: textures.scale, + u_specular: [1, 1, 1], + u_specularExponent: 40, + u_emissive: [0.15, 0.15, 0.15], + }); + + twgl.drawBufferInfo(gl, objects.ball.bufferInfo); +} + +function renderOcean(app, viewMatrix, reflectionMatrix, programInfo) { + const { gl, textures, objects, time } = app; + + gl.bindVertexArray(objects.plane.vao); + + const worldMatrix = matrix4.scale(4000, 1, 4000); + + twgl.setUniforms(programInfo, { + u_matrix: matrix4.multiply(viewMatrix, worldMatrix), + u_worldMatrix: worldMatrix, + u_normalMatrix: matrix4.transpose(matrix4.inverse(worldMatrix)), + u_diffuse: [45/255, 141/255, 169/255], + u_diffuseMap: textures.reflection, + u_normalMap: textures.oceanNormal, + u_specular: [1, 1, 1], + u_specularExponent: 200, + u_emissive: [0, 0, 0], + + u_reflectionMatrix: reflectionMatrix, + u_time: time / 1000, + }); + + twgl.drawBufferInfo(gl, objects.plane.bufferInfo); +} + +function startLoop(app, now = 0) { + const timeDiff = now - app.time; + app.time = now; + + inputUpdate(app.input, app.state); + app.state.lightRotationXY[0] = Math.sin(now * 0.0001) * 0.25 * Math.PI; + app.state.lightRotationXY[1] += timeDiff * 0.0001; + + render(app, timeDiff); + requestAnimationFrame(now => startLoop(app, now)); +} + +async function main() { + const app = await setup(); + window.app = app; + window.gl = app.gl; + + app.input = listenToInputs(app.gl.canvas, app.state); + + const controlsForm = document.getElementById('controls'); + controlsForm.addEventListener('input', () => { + const formData = new FormData(controlsForm); + app.state.resolutionRatio = parseFloat(formData.get('resolution-ratio')); + }); + + if (window.devicePixelRatio > 1) { + const retinaOption = document.getElementById('resolution-ratio-retina'); + retinaOption.value = window.devicePixelRatio; + retinaOption.disabled = false; + } + + startLoop(app); +} +main();