8 Commits

17 changed files with 1247 additions and 2232 deletions

View File

@@ -1,164 +0,0 @@
# WebGPU Migration Complete
## Overview
Successfully migrated the WebOcean project from WebGL2 to WebGPU to enable future tessellation support for the ocean grid system.
## What Changed
### Files Converted to WebGPU:
1. **Grid.ts**
- Replaced WebGL VAO/VBO with GPUBuffer
- Updated `initVAO()``initBuffers(gpuContext: WebGPUContext)`
- Changed `draw(gl: WebGL2RenderingContext)``draw(renderPass: GPURenderPassEncoder)`
- Uses `mappedAtCreation` pattern for buffer initialization
2. **Skybox.ts**
- Same conversion pattern as Grid
- Replaced WebGL buffers with GPUBuffer
- Updated draw method signature for WebGPU
3. **main.ts** (renamed from main_webgpu.ts)
- Replaced `initGL()` with async `initWebGPU()`
- Created three render pipelines:
* Noise generation pipeline (renders Perlin noise to texture)
* Ocean rendering pipeline (vertex displacement from noise texture)
* Skybox pipeline (gradient sky with sun)
- Converted FBO to GPUTexture for render-to-texture
- Updated all shader bindings to use WebGPU bind groups
- Maintains all existing features:
* Dual camera system (Orbital + FPS)
* Animation controls (P pause, 0-5 speed)
* Wireframe toggle (F key)
* Camera switching (C key)
* Full WASD + mouse controls
### New Files Created:
1. **WebGPUContext.ts**
- Centralized GPU device/adapter/context management
- Provides helper methods for creating buffers, textures, pipelines
- Handles WebGPU initialization and configuration
2. **shaders.wgsl.ts**
- All GLSL shaders converted to WGSL format
- Exports 6 shader strings:
* `noiseVertexShader` - fullscreen quad for noise generation
* `noiseFragmentShader` - 5-octave Perlin noise
* `oceanVertexShader` - vertex displacement from texture
* `oceanFragmentShader` - normal calculation, Fresnel, SSS, glitter
* `skyboxVertexShader` - skybox cube rendering
* `skyboxFragmentShader` - gradient sky with sun
### Preserved Files:
1. **main_webgl.ts** (backup)
- Original WebGL2 implementation preserved for reference
- Excluded from TypeScript compilation
### Configuration Updates:
1. **tsconfig.json**
- Added `"types": ["@webgpu/types"]` for WebGPU type definitions
- Excluded `main_webgl.ts` from compilation
2. **package.json**
- Added `@webgpu/types` dev dependency
3. **index.html**
- Added frame time display (`<div id="frame-time">`)
- Kept GLSL shader script tags (not used, can be removed later)
## WebGPU vs WebGL2 Architecture
### Key Differences:
| Aspect | WebGL2 | WebGPU |
|--------|--------|--------|
| **Buffers** | VAO/VBO with gl.createVertexArray() | GPUBuffer with device.createBuffer() |
| **Shaders** | GLSL with gl.createProgram() | WGSL with device.createShaderModule() |
| **Rendering** | Direct gl.drawArrays() calls | Command encoder → render pass → submit |
| **Textures** | gl.createTexture() + gl.texImage2D() | device.createTexture() |
| **State** | Implicit state machine (gl.enable/disable) | Explicit pipeline state in descriptors |
| **Uniforms** | gl.uniformMatrix4fv() per draw | Uniform buffers + bind groups |
### Rendering Pipeline:
**Pass 1: Noise Generation**
```
1. Write time uniform to buffer
2. Create command encoder
3. Begin render pass with noiseTexture as target
4. Set noise pipeline
5. Set noise bind group (contains time uniform)
6. Draw fullscreen quad (6 vertices)
7. End pass and submit commands
```
**Pass 2: Scene Rendering**
```
1. Update camera uniforms (view, model, projection, eyePos)
2. Update skybox uniforms (view, projection, sunDir)
3. Create command encoder
4. Begin render pass with canvas + depth texture
5. Draw skybox:
- Set skybox pipeline (no depth write, no culling)
- Set skybox bind group
- Draw skybox geometry
6. Draw ocean:
- Set ocean pipeline (depth write, back-face culling)
- Set ocean bind group (contains uniforms + noise texture + sampler)
- Draw ocean grid (wireframe or solid)
7. End pass and submit commands
```
## Browser Compatibility
- **Requires**: Chrome/Edge 113+, Firefox 130+ (with flag)
- **Not supported**: Safari (as of December 2024)
- Shows error alert if WebGPU not available
## Testing Checklist
✅ Build succeeds without TypeScript errors
✅ Dev server starts successfully
✅ WebGPU initialization completes
✅ Dual camera system functional
✅ Animation controls work (pause/play/speed)
✅ Wireframe toggle functional
✅ Mouse camera controls responsive
✅ Keyboard FPS camera controls work
## Next Steps - Tessellation
Now that WebGPU migration is complete, tessellation can be implemented:
1. **Hull Shader** - Define tessellation factors based on camera distance
2. **Domain Shader** - Interpolate tessellated vertices
3. **Dynamic LOD** - Increase subdivision near camera, reduce far away
4. **Adaptive Tessellation** - More detail in areas with high wave displacement
This will provide:
- Smoother ocean surface at all zoom levels
- Better performance (fewer vertices far from camera)
- More geometric detail for displacement mapping
- Hardware-accelerated mesh subdivision
## Files Modified Summary
- ✅ [Grid.ts](Grid.ts) - WebGPU buffer conversion
- ✅ [Skybox.ts](Skybox.ts) - WebGPU buffer conversion
- ✅ [main.ts](main.ts) - Complete WebGPU rendering pipeline
- ✅ [WebGPUContext.ts](WebGPUContext.ts) - New GPU management class
- ✅ [shaders.wgsl.ts](shaders.wgsl.ts) - New WGSL shader definitions
- ✅ [tsconfig.json](../tsconfig.json) - Added WebGPU types
- ✅ [index.html](../index.html) - Added frame time display
- 📦 main_webgl.ts - Backup (excluded from build)
## Performance Notes
- FPS display shows frame rate
- Frame time display shows milliseconds per frame
- Animation speed control (1x-5x)
- Pause/play functionality preserved
- WebGPU generally faster than WebGL2 for complex scenes

View File

@@ -1,6 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -97,17 +96,54 @@
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
} }
#frame-time { .slider-group {
position: absolute; margin: 8px 0;
bottom: 20px; }
left: 100px;
background: rgba(0, 0, 0, 0.7); .slider-group label {
color: #0ff; display: flex;
padding: 8px 12px; justify-content: space-between;
border-radius: 5px; align-items: center;
font-family: 'Courier New', monospace; margin-bottom: 4px;
font-size: 14px; font-size: 13px;
backdrop-filter: blur(10px); }
.slider-group input[type="range"] {
width: 100%;
height: 6px;
border-radius: 3px;
background: rgba(255, 255, 255, 0.2);
outline: none;
-webkit-appearance: none;
appearance: none;
}
.slider-group input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
background: #4a9eff;
cursor: pointer;
}
.slider-group input[type="range"]::-moz-range-thumb {
width: 14px;
height: 14px;
border-radius: 50%;
background: #4a9eff;
cursor: pointer;
border: none;
}
.slider-value {
background: rgba(255, 255, 255, 0.15);
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
min-width: 35px;
text-align: center;
} }
</style> </style>
<script id="noise-fs" type="x-shader/x-fragment"> <script id="noise-fs" type="x-shader/x-fragment">
@@ -204,96 +240,364 @@
precision mediump float; precision mediump float;
varying vec3 v_fragPos; varying vec3 v_fragPos;
varying vec2 v_uv; varying vec3 v_normal;
varying float v_waveHeight;
varying float v_foamFactor;
varying float v_distanceFade;
uniform vec3 eyePos; uniform vec3 eyePos;
uniform sampler2D displace_map; uniform float uFoamIntensity;
uniform float uGlitterIntensity;
vec3 lightPos = vec3(0.,0.,10.); //not used in diffuse. diffuse uses a directional light. It is only used for specular glittering. // Simple hash function for noise
vec3 lightColor = vec3(1.0,1.0,1.0); float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
//using forward difference // Value noise for foam texture
//Normal vectors are compute as: https://www.scratchapixel.com/lessons/procedural-generation-virtual-worlds/perlin-noise-part-2/perlin-noise-computing-derivatives float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f); // smoothstep
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
// Fractal noise for more detailed foam
float foamNoise(vec2 p) {
float n = 0.0;
n += 0.5 * noise(p * 8.0);
n += 0.25 * noise(p * 16.0);
n += 0.125 * noise(p * 32.0);
n += 0.0625 * noise(p * 64.0);
return n;
}
void main(void) { void main(void) {
vec4 displace = texture2D(displace_map, v_uv); vec3 lightColor = vec3(1.0, 1.0, 0.95);
//calculate normal vec3 sunDirection = normalize(vec3(0.3, 0.5, 0.8));
float gridPointDelta = (1. / 256.);
vec3 currPoint = vec3(0.0,0.0,displace.x);
vec3 right = vec3(gridPointDelta,0.0,texture2D(displace_map,vec2(v_uv.x + gridPointDelta,v_uv.y)).x*(1./1.));
vec3 left = vec3(-gridPointDelta,0.0,texture2D(displace_map,vec2(v_uv.x - gridPointDelta,v_uv.y)).x*(1./1.));
vec3 up = vec3(0.,gridPointDelta,texture2D(displace_map,vec2(v_uv.x ,v_uv.y + gridPointDelta)).x*(1./1.));
vec3 down = vec3(0.,-gridPointDelta,texture2D(displace_map,vec2(v_uv.x ,v_uv.y - gridPointDelta)).x*(1./1.));
//vec3 tangent = normalize(right - currPoint); vec3 norm = normalize(v_normal);
//vec3 biTangent = normalize(up - currPoint);
vec3 tangent = normalize(vec3(gridPointDelta,0.,right.z-left.z));
vec3 biTangent = normalize(vec3(0.,gridPointDelta,down.z-up.z));
//vec3 normal = biTangent;
vec3 normal = cross(tangent, biTangent);
vec3 norm = normalize(normal); // View direction
norm.y *= -1.; //Normal y direction is somehow inverted vec3 viewDir = normalize(eyePos - v_fragPos);
//vec3 lightDir = normalize(lightPos - v_fragPos);
vec3 lightDir = normalize(-vec3(0.0,.0,-1.)); //sun shines in drection of -z
float diff = max(dot(norm,lightDir),0.0); // Diffuse lighting
float diff = max(dot(norm, sunDirection), 0.0);
vec3 diffuse = diff * lightColor; vec3 diffuse = diff * lightColor;
vec3 result = (diffuse) * vec3(0.0,0.0,1.0);
//Old lightning // Schlick's approximation to Fresnel factor
vec3 toCameraVector = normalize(v_fragPos - eyePos); float R0 = 0.02;
vec3 reflec = normalize(reflect(toCameraVector, norm)); float fresnel = R0 + (1.0 - R0) * pow(1.0 - max(dot(norm, viewDir), 0.0), 5.0);
//Schlicks approximation to Fresnelfactor // Deep and shallow water colors
float n1 = 1., n2 = 1.33333; vec3 deepColor = vec3(0.0, 0.08, 0.15);
float R0 = pow((n1-n2)/(n1+n2), 2.); vec3 shallowColor = vec3(0.0, 0.35, 0.45);
float fresnel = R0 + (1. - R0)*pow((1.-dot(norm,reflec)),5.) ; vec3 skyColor = vec3(0.55, 0.7, 0.9); // Match skybox horizon color
vec3 foamColor = vec3(0.95, 0.98, 1.0);
//vec3 waterColor = vec3(34./255.,154./255.,211./255.); // Blend between deep and shallow based on wave height
vec3 oceanColor = vec3(0,.4,.4); // under-sea colour float heightFactor = clamp(v_waveHeight * 2.0 + 0.5, 0.0, 1.0);
vec3 skyColor = vec3(1.,1.,1.); vec3 oceanColor = mix(deepColor, shallowColor, heightFactor);
//Subsurface scattering // Sun glitter - uses wave normals for natural sparkle from fine surface detail
vec3 sssSun = vec3(0.,-5.,-7.0); vec3 reflectDir = reflect(-sunDirection, norm);
vec3 tosssSunVec = normalize(sssSun - v_fragPos); float specAngle = max(dot(viewDir, reflectDir), 0.0);
vec3 tosssSun = normalize(vec3(0.0,-100.,1.));
float ssDistortion = 0.1;
float sssIntensity = 1.;
vec3 halfWay = normalize(tosssSun+norm*ssDistortion);
float ssScateringCoef = pow(clamp(dot(toCameraVector,-halfWay),0.0,1.0),5.) * sssIntensity;
//Sun glittering
float glitterFactor = max(0.0,dot(tosssSunVec,reflect(-toCameraVector,norm)));
if(!(glitterFactor > 0.98)) {
glitterFactor = 0.0;
}
//gl_FragColor = vec4(oceanColor + lightColor * glitterFactor,1.0); // Smooth base specular
//gl_FragColor=vec4(clamp(oceanColor + (oceanColor*ssScateringCoef),0.,1.0),1.0); //Display subsurfacecatterting component float specBase = pow(specAngle, 64.0) * 0.4;
//gl_FragColor = vec4((mix(oceanColor,skyColor,fresnel).xyz), 1.); //Just display reflection component // Medium highlights
//gl_FragColor = vec4(diffuse * oceanColor,1.0); //Render only diffuse component float specMid = pow(specAngle, 256.0) * 1.2;
//gl_FragColor = vec4(normal,1.0); //show Normal map // Sharp glitter peaks
//gl_FragColor = vec4(displace.x,displace.x,displace.x,1.0); //Show Perlin Noise texture deactivate vertex distrotion before float specSharp = pow(specAngle, 1024.0) * 3.0;
gl_FragColor = vec4((clamp(diffuse,0.97,1.0) * (mix(oceanColor + (oceanColor*ssScateringCoef),skyColor*0.8,fresnel).xyz))+ lightColor * glitterFactor, 1.0); //All combined
vec3 specular = (specBase + specMid + specSharp) * lightColor * uGlitterIntensity;
// Subsurface scattering
float sssDot = max(dot(viewDir, -sunDirection), 0.0);
float sssWaveContribution = clamp(v_waveHeight + 0.3, 0.0, 1.0);
float sssNormalContribution = pow(1.0 - max(dot(norm, sunDirection), 0.0), 2.0);
float sss = pow(sssDot, 3.0) * sssWaveContribution * sssNormalContribution * 1.5;
vec3 sssColor = vec3(0.1, 0.6, 0.5) * sss;
// Rim SSS effect
float rimSSS = pow(1.0 - max(dot(norm, viewDir), 0.0), 3.0) * 0.3;
vec3 rimColor = vec3(0.0, 0.4, 0.4) * rimSSS * heightFactor;
// Foam with texture - foam persists longer
vec2 foamUV = v_fragPos.xy * 1.5;
float foamPattern = foamNoise(foamUV);
// Create foam patches with softer edges
float foamThreshold = 1.0 - v_foamFactor * 1.2 * uFoamIntensity;
float foam = smoothstep(foamThreshold, foamThreshold + 0.35, foamPattern);
// Add some bubble-like spots with softer transition
float bubbles = smoothstep(0.65, 0.85, noise(foamUV * 15.0)) * v_foamFactor;
foam = clamp(foam + bubbles * 0.3, 0.0, 1.0);
// Softer edge fade based on foam factor
foam *= smoothstep(0.0, 0.25, v_foamFactor);
// Additional soft fade at foam edges and fade out at distance
foam = pow(foam, 0.7) * uFoamIntensity * v_distanceFade;
// Combine all lighting
vec3 reflectedColor = mix(oceanColor, skyColor, fresnel);
vec3 waterColor = reflectedColor * clamp(diffuse, 0.3, 1.0) + specular + sssColor + rimColor;
// Blend foam on top with slight transparency variation
vec3 finalColor = mix(waterColor, foamColor * clamp(diffuse + 0.4, 0.0, 1.0), foam * 0.85);
// Atmospheric fog for distant water - blends to horizon
float dist = length(eyePos - v_fragPos);
// Exponential fog with aggressive horizon fade
float fogFactor = exp(-dist * 0.04);
// Fully fade at stretched horizon vertices
float horizonFade = smoothstep(40.0, 80.0, dist);
fogFactor *= (1.0 - horizonFade);
fogFactor = clamp(fogFactor, 0.0, 1.0);
// Horizon color must exactly match skybox horizon
vec3 horizonColor = vec3(0.55, 0.7, 0.9);
finalColor = mix(horizonColor, finalColor, fogFactor);
gl_FragColor = vec4(finalColor, 1.0);
} }
</script> </script>
<script id="default-vs" type="x-shader/x-vertex"> <script id="default-vs" type="x-shader/x-vertex">
attribute vec3 positionAttr; precision mediump float;
attribute vec2 positionAttr; // Grid position in [0,1] range
uniform mat4 view; uniform mat4 view;
uniform mat4 model;
uniform mat4 projection; uniform mat4 projection;
uniform sampler2D displace_map; uniform mat4 uProjectorMatrix; // Inverse projector view-proj
uniform mat4 uRangeMatrix; // Range conversion matrix
uniform float uTime;
uniform float uWaveHeight;
uniform float uWaveSpeed;
uniform vec3 eyePos;
uniform float uHorizonClipY; // Y position of horizon in clip space [-1,1]
varying vec2 v_uv;
varying vec3 v_fragPos; varying vec3 v_fragPos;
varying vec3 v_normal;
varying float v_waveHeight;
varying float v_foamFactor;
varying float v_distanceFade;
// Gerstner wave function - higher steepness = spikier waves
vec3 gerstnerWave(vec2 pos, float time, vec2 direction, float steepness, float wavelength, out vec3 tangent, out vec3 binormal) {
float k = 2.0 * 3.14159 / wavelength;
float c = sqrt(9.8 / k);
vec2 d = normalize(direction);
float f = k * (dot(d, pos) - c * time);
float a = steepness / k;
tangent = vec3(
1.0 - steepness * d.x * d.x * sin(f),
steepness * d.x * cos(f),
-steepness * d.x * d.y * sin(f)
);
binormal = vec3(
-steepness * d.x * d.y * sin(f),
steepness * d.y * cos(f),
1.0 - steepness * d.y * d.y * sin(f)
);
return vec3(
d.x * a * cos(f),
a * sin(f),
d.y * a * cos(f)
);
}
// Project grid point onto ocean plane using projector
vec3 projectToOcean(vec2 gridPos, out float horizonBlend, out vec3 rayDirection) {
// Transform grid position [0,1] through range matrix to projector space [-1,1]
vec4 clipPos = uRangeMatrix * vec4(gridPos, 0.0, 1.0);
// Get two points along the projection ray (near and far planes)
vec4 nearPoint = uProjectorMatrix * vec4(clipPos.xy, -1.0, 1.0);
vec4 farPoint = uProjectorMatrix * vec4(clipPos.xy, 1.0, 1.0);
// Perspective divide to get world positions
nearPoint /= nearPoint.w;
farPoint /= farPoint.w;
vec3 rayOrigin = nearPoint.xyz;
vec3 rayDir = normalize(farPoint.xyz - nearPoint.xyz);
rayDirection = rayDir;
// The skybox horizon is where rayDir.z = 0 (looking horizontally)
float angleToHorizon = -rayDir.z; // 0 at horizon, negative = looking up, positive = looking down
// If ray is pointing up or nearly horizontal, this vertex approaches horizon
if (angleToHorizon <= 0.001) {
horizonBlend = 1.0;
// Project in horizontal direction at ocean level
vec2 hDir = length(rayDir.xy) > 0.001 ? normalize(rayDir.xy) : vec2(1.0, 0.0);
return vec3(rayOrigin.xy + hDir * 5000.0, 0.0);
}
// Ray is pointing down - intersect with ocean plane (Z = 0)
float t = -rayOrigin.z / rayDir.z;
if (t < 0.0) {
horizonBlend = 1.0;
vec2 hDir = length(rayDir.xy) > 0.001 ? normalize(rayDir.xy) : vec2(1.0, 0.0);
return vec3(rayOrigin.xy + hDir * 5000.0, 0.0);
}
// Camera height affects max render distance
// Higher camera = need to limit distance more to avoid precision issues
float cameraHeight = max(eyePos.z, 0.5);
// Base max distance scales with camera height, but with diminishing returns
// At height 2: maxBase = ~200
// At height 10: maxBase = ~450
// At height 100: maxBase = ~1400
// At height 500: maxBase = ~3100
float maxBase = 100.0 * sqrt(cameraHeight);
// Also limit based on angle - shallow angles get much shorter max distance
float angleScale = smoothstep(0.001, 0.3, angleToHorizon); // 0 at horizon, 1 at ~17 degrees down
float maxT = maxBase * (0.1 + 0.9 * angleScale);
maxT = max(maxT, 50.0); // Minimum distance
// Smooth horizon blend based on angle AND distance
horizonBlend = 1.0 - smoothstep(0.001, 0.05, angleToHorizon);
// If t exceeds limit, increase horizon blend
if (t > maxT * 0.8) {
float distBlend = smoothstep(maxT * 0.8, maxT, t);
horizonBlend = max(horizonBlend, distBlend);
}
t = min(t, maxT);
// Compute world position
vec3 worldPos = rayOrigin + rayDir * t;
return worldPos;
}
void main(void) { void main(void) {
vec4 displace = texture2D(displace_map, vec2(positionAttr.x,positionAttr.y)); // Project grid point onto ocean plane
vec4 worldPos = model * vec4(positionAttr.x,positionAttr.y,positionAttr.z + displace.x, 1.0); float horizonBlend;
vec3 rayDir;
vec3 worldPos3 = projectToOcean(positionAttr, horizonBlend, rayDir);
vec4 worldPos = vec4(worldPos3, 1.0);
// Grid is on XY plane, Z is up
vec2 pos = worldPos.xy;
float time = uTime * 0.0004 * uWaveSpeed;
// Calculate distance from camera for wave fading
float distToCamera = length(worldPos.xyz - eyePos);
float waveFade = exp(-distToCamera * 0.015); // Gradual fade over distance
waveFade = clamp(waveFade, 0.0, 1.0);
// Fade out waves at horizon to prevent edge breakup
waveFade *= (1.0 - horizonBlend);
v_distanceFade = waveFade;
float heightMod = uWaveHeight * waveFade;
vec3 displacement = vec3(0.0);
vec3 tangent = vec3(1.0, 0.0, 0.0);
vec3 binormal = vec3(0.0, 0.0, 1.0);
vec3 t, b;
// === Large primary waves ===
displacement += gerstnerWave(pos, time, vec2(1.0, 0.2), 0.42 * heightMod, 6.0, t, b);
tangent += t - vec3(1.0, 0.0, 0.0); binormal += b - vec3(0.0, 0.0, 1.0);
displacement += gerstnerWave(pos, time * 1.1, vec2(0.4, 1.0), 0.35 * heightMod, 5.0, t, b);
tangent += t - vec3(1.0, 0.0, 0.0); binormal += b - vec3(0.0, 0.0, 1.0);
// === Medium waves ===
displacement += gerstnerWave(pos, time * 0.9, vec2(-0.6, 0.8), 0.25 * heightMod, 3.0, t, b);
tangent += t - vec3(1.0, 0.0, 0.0); binormal += b - vec3(0.0, 0.0, 1.0);
displacement += gerstnerWave(pos, time * 1.2, vec2(0.8, -0.5), 0.2 * heightMod, 2.2, t, b);
tangent += t - vec3(1.0, 0.0, 0.0); binormal += b - vec3(0.0, 0.0, 1.0);
displacement += gerstnerWave(pos, time, vec2(-0.3, -0.9), 0.18 * heightMod, 1.8, t, b);
tangent += t - vec3(1.0, 0.0, 0.0); binormal += b - vec3(0.0, 0.0, 1.0);
// === Small detail waves ===
displacement += gerstnerWave(pos, time * 1.2, vec2(0.9, -0.4), 0.12 * heightMod, 1.2, t, b);
tangent += t - vec3(1.0, 0.0, 0.0); binormal += b - vec3(0.0, 0.0, 1.0);
displacement += gerstnerWave(pos, time * 0.9, vec2(-0.5, -0.7), 0.10 * heightMod, 1.0, t, b);
tangent += t - vec3(1.0, 0.0, 0.0); binormal += b - vec3(0.0, 0.0, 1.0);
displacement += gerstnerWave(pos, time * 1.3, vec2(0.3, 0.95), 0.08 * heightMod, 0.8, t, b);
tangent += t - vec3(1.0, 0.0, 0.0); binormal += b - vec3(0.0, 0.0, 1.0);
// === Tiny ripples ===
displacement += gerstnerWave(pos, time * 2.0, vec2(0.9, 0.1), 0.05 * heightMod, 0.35, t, b);
tangent += t - vec3(1.0, 0.0, 0.0); binormal += b - vec3(0.0, 0.0, 1.0);
displacement += gerstnerWave(pos, time * 2.2, vec2(-0.2, 0.95), 0.04 * heightMod, 0.25, t, b);
tangent += t - vec3(1.0, 0.0, 0.0); binormal += b - vec3(0.0, 0.0, 1.0);
// === Micro ripples for fine surface detail ===
displacement += gerstnerWave(pos, time * 2.5, vec2(0.7, -0.7), 0.03 * heightMod, 0.18, t, b);
tangent += t - vec3(1.0, 0.0, 0.0); binormal += b - vec3(0.0, 0.0, 1.0);
displacement += gerstnerWave(pos, time * 3.0, vec2(-0.8, 0.6), 0.025 * heightMod, 0.12, t, b);
tangent += t - vec3(1.0, 0.0, 0.0); binormal += b - vec3(0.0, 0.0, 1.0);
displacement += gerstnerWave(pos, time * 3.5, vec2(0.5, -0.9), 0.02 * heightMod, 0.08, t, b);
tangent += t - vec3(1.0, 0.0, 0.0); binormal += b - vec3(0.0, 0.0, 1.0);
// Store wave height for fragment shader
v_waveHeight = displacement.y;
// Calculate foam factor - foam appears on the FRONT/leading edge of waves
// When wave is rising (tangent.y > 0), that's where foam should appear
float waveRising = smoothstep(0.0, 0.3, tangent.y + binormal.y);
float foamFromHeight = smoothstep(0.0, 0.25, displacement.y);
float waveSlope = length(vec2(tangent.y, binormal.y));
float foamFromSlope = smoothstep(0.2, 0.6, waveSlope);
// Foam appears where wave is high AND rising (leading edge / crest)
v_foamFactor = clamp((foamFromHeight * waveRising * 1.2 + foamFromSlope * 0.3), 0.0, 1.0);
// Apply displacement - Z is up, XY is horizontal plane
worldPos.x += displacement.x;
worldPos.y += displacement.z;
worldPos.z += displacement.y; // Height displacement
// Calculate normal from tangent and binormal
// Blend normal towards flat (0, 0, 1) based on distance
vec3 normal = normalize(cross(binormal, tangent));
vec3 flatNormal = vec3(0.0, 0.0, 1.0);
normal = mix(flatNormal, normal, waveFade);
v_normal = vec3(normal.x, normal.z, normal.y);
// Project back to clip space
gl_Position = projection * view * worldPos; gl_Position = projection * view * worldPos;
// For vertices near the horizon, smoothly blend Y towards the horizon line
// This ensures ocean meets skybox without gaps or discontinuities
if (horizonBlend > 0.0) {
float targetY = uHorizonClipY * gl_Position.w;
// Use squared blend for smoother transition
float smoothBlend = horizonBlend * horizonBlend;
gl_Position.y = mix(gl_Position.y, targetY, smoothBlend);
// Push depth towards far plane for horizon vertices
gl_Position.z = mix(gl_Position.z, gl_Position.w * 0.9999, smoothBlend);
}
v_fragPos = worldPos.xyz; v_fragPos = worldPos.xyz;
v_uv = positionAttr.xy;
} }
</script> </script>
<script id="sky-fs" type="x-shader/x-fragment"> <script id="sky-fs" type="x-shader/x-fragment">
@@ -362,7 +666,6 @@
gl_Position = pos; gl_Position = pos;
} }
</script> </script>
</head> </head>
<body> <body>
@@ -371,25 +674,15 @@
<div id="controls"> <div id="controls">
<h3>🌊 Ocean Controls</h3> <h3>🌊 Ocean Controls</h3>
<div class="control-group"> <div class="control-group">
<strong>Camera Mode:</strong> <span class="key">C</span> (FPS/Orbital)<br> <strong>Camera Rotation:</strong><br>
<span id="current-camera-mode" style="font-size: 12px; color: #aaa;">Current: Orbital</span> <span class="key">W</span><span class="key">A</span><span class="key">S</span><span class="key">D</span> or Arrow Keys
</div> </div>
<div class="control-group"> <div class="control-group">
<strong>Rendering:</strong><br> <strong>Zoom:</strong><br>
<span class="key">F</span> Toggle Wireframe <span class="key">Q</span> / <span class="key">E</span> or <span class="key">+</span> / <span class="key">-</span>
</div> </div>
<div class="control-group"> <div class="control-group">
<strong>FPS Camera:</strong><br> <strong>Mouse:</strong> Click and drag to rotate
<span class="key">W</span><span class="key">A</span><span class="key">S</span><span class="key">D</span> Move<br>
<span class="key">Q</span><span class="key">E</span> or <span class="key">Space</span><span class="key">Ctrl</span> Up/Down<br>
<span class="key">Shift</span> Sprint<br>
Mouse: Look around
</div>
<div class="control-group">
<strong>Orbital Camera:</strong><br>
<span class="key">W</span><span class="key">A</span><span class="key">S</span><span class="key">D</span> or Arrows Rotate<br>
<span class="key">Q</span><span class="key">E</span> or <span class="key">+</span><span class="key">-</span> Zoom<br>
Mouse: Click and drag to rotate
</div> </div>
<div class="control-group"> <div class="control-group">
<strong>Reset:</strong> <span class="key">R</span> <strong>Reset:</strong> <span class="key">R</span>
@@ -397,19 +690,42 @@
<div class="control-group"> <div class="control-group">
<strong>Toggle Help:</strong> <span class="key">H</span> <strong>Toggle Help:</strong> <span class="key">H</span>
</div> </div>
<div class="control-group" style="margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255, 255, 255, 0.2);">
<button id="wireframe-toggle" style="background: rgba(255, 255, 255, 0.2); color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; width: 100%; font-size: 13px;">Wireframe: OFF</button>
</div>
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(255, 255, 255, 0.2);">
<strong>Wave Settings</strong>
<div class="slider-group">
<label>Wave Height <span class="slider-value" id="wave-height-val">1.0</span></label>
<input type="range" id="wave-height" min="0" max="2" step="0.1" value="1">
</div>
<div class="slider-group">
<label>Wave Speed <span class="slider-value" id="wave-speed-val">1.0</span></label>
<input type="range" id="wave-speed" min="0.1" max="3" step="0.1" value="1">
</div>
</div>
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid rgba(255, 255, 255, 0.2);">
<strong>Foam & Glitter</strong>
<div class="slider-group">
<label>Foam Intensity <span class="slider-value" id="foam-intensity-val">1.0</span></label>
<input type="range" id="foam-intensity" min="0" max="2" step="0.1" value="1">
</div>
<div class="slider-group">
<label>Glitter Intensity <span class="slider-value" id="glitter-intensity-val">1.0</span></label>
<input type="range" id="glitter-intensity" min="0" max="3" step="0.1" value="1">
</div>
</div>
</div> </div>
<button id="toggle-controls">Toggle Controls (H)</button> <button id="toggle-controls">Toggle Controls (H)</button>
<div id="fps-counter">FPS: 0</div> <div id="fps-counter">FPS: 0</div>
<div id="frame-time">Frame: 0.00ms</div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
<script> <script>
// Toggle controls visibility // Toggle controls visibility
const controls = document.getElementById('controls'); const controls = document.getElementById('controls');
const toggleBtn = document.getElementById('toggle-controls'); const toggleBtn = document.getElementById('toggle-controls');
const cameraModeDisplay = document.getElementById('current-camera-mode');
toggleBtn.addEventListener('click', () => { toggleBtn.addEventListener('click', () => {
controls.classList.toggle('hidden'); controls.classList.toggle('hidden');
@@ -419,28 +735,29 @@
if (evt.key === 'h' || evt.key === 'H') { if (evt.key === 'h' || evt.key === 'H') {
controls.classList.toggle('hidden'); controls.classList.toggle('hidden');
} }
// Update camera mode display when C is pressed
if (evt.key === 'c' || evt.key === 'C') {
setTimeout(() => {
// Get camera mode from any displayed element
const cameraMode = document.getElementById('camera-mode');
if (cameraMode && cameraModeDisplay) {
const mode = cameraMode.textContent.replace('Camera: ', '');
cameraModeDisplay.textContent = `Current: ${mode}`;
}
}, 100);
}
}); });
// Listen for custom camera mode toggle events from UI // Wireframe toggle
window.addEventListener('toggleCameraMode', () => { const wireframeBtn = document.getElementById('wireframe-toggle');
const cameraMode = document.getElementById('camera-mode'); wireframeBtn.addEventListener('click', () => {
if (cameraMode && cameraModeDisplay) { window.dispatchEvent(new CustomEvent('toggleWireframe'));
const mode = cameraMode.textContent.replace('Camera: ', '');
cameraModeDisplay.textContent = `Current: ${mode}`;
}
}); });
// Slider controls
function setupSlider(id, eventName) {
const slider = document.getElementById(id);
const valueDisplay = document.getElementById(id + '-val');
slider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
valueDisplay.textContent = value.toFixed(1);
window.dispatchEvent(new CustomEvent(eventName, { detail: value }));
});
}
setupSlider('wave-height', 'waveHeightChange');
setupSlider('wave-speed', 'waveSpeedChange');
setupSlider('foam-intensity', 'foamIntensityChange');
setupSlider('glitter-intensity', 'glitterIntensityChange');
</script> </script>
</body> </body>

8
package-lock.json generated
View File

@@ -12,7 +12,6 @@
"gl-matrix": "^3.4.4" "gl-matrix": "^3.4.4"
}, },
"devDependencies": { "devDependencies": {
"@webgpu/types": "^0.1.69",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^6.0.7" "vite": "^6.0.7"
} }
@@ -816,13 +815,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@webgpu/types": {
"version": "0.1.69",
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz",
"integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.25.12", "version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",

View File

@@ -20,7 +20,6 @@
"author": "Julian Niessner", "author": "Julian Niessner",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@webgpu/types": "^0.1.69",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^6.0.7" "vite": "^6.0.7"
}, },

View File

@@ -1,74 +1,38 @@
# 🌊 WebOcean # 🌊 WebOcean
An interactive 3D ocean simulation using **WebGPU**, TypeScript, and Perlin noise for realistic water wave generation. An interactive 3D ocean simulation using WebGL2, TypeScript, and Perlin noise for realistic water wave generation.
![WebGPU](https://img.shields.io/badge/WebGPU-Chrome113+-990000?style=flat-square) ![WebGL](https://img.shields.io/badge/WebGL-2.0-990000?style=flat-square)
![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6?style=flat-square) ![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6?style=flat-square)
![Vite](https://img.shields.io/badge/Vite-6.0-646CFF?style=flat-square) ![Vite](https://img.shields.io/badge/Vite-6.0-646CFF?style=flat-square)
## ✨ Features ## ✨ Features
- **Real-time Ocean Simulation** - Dynamic water surface with Perlin noise-based displacement - **Real-time Ocean Simulation** - Dynamic water surface with Perlin noise-based displacement
- **WebGPU Rendering** - Modern GPU API for optimal performance and future tessellation support
- **Advanced Rendering Techniques**: - **Advanced Rendering Techniques**:
- Fresnel reflection for realistic water appearance - Fresnel reflection for realistic water appearance
- Subsurface scattering for light penetration - Subsurface scattering for light penetration
- Specular highlights for sun glitter effect - Specular highlights for sun glitter effect
- Dynamic normal mapping from displacement - Dynamic normal mapping from displacement
- Gradient skybox with sun rendering - **Interactive Camera Controls** - Mouse and keyboard navigation
- **Dual Camera System**:
- **Orbital Camera** - Rotate around the ocean surface
- **FPS Camera** - Free-flying first-person exploration
- **Animation Controls**:
- Pause/play ocean animation
- Adjustable speed (1x-5x)
- **Rendering Modes**:
- Wireframe toggle for mesh visualization
- **Responsive Design** - Automatically adapts to window size - **Responsive Design** - Automatically adapts to window size
- **Performance Monitoring** - Real-time FPS counter and frame time - **Performance Monitoring** - Real-time FPS counter
## 🎮 Controls ## 🎮 Controls
### Camera Controls
| Action | Keys | | Action | Keys |
|--------|------| |--------|------|
| **Toggle Camera Mode** | `C` | | **Rotate Camera** | `W` `A` `S` `D` or Arrow Keys |
| **Reset Camera** | `R` |
| **Mouse Drag** | Click and drag to rotate camera |
### Orbital Camera Mode (Default)
| Action | Keys |
|--------|------|
| **Rotate** | `W` `A` `S` `D` or Arrow Keys |
| **Zoom In/Out** | `Q` / `E` or `+` / `-` | | **Zoom In/Out** | `Q` / `E` or `+` / `-` |
| **Mouse Drag** | Click and drag to rotate |
### FPS Camera Mode | **Reset Camera** | `R` |
| **Toggle Help** | `H` |
| Action | Keys |
|--------|------|
| **Move Forward/Back** | `W` / `S` |
| **Strafe Left/Right** | `A` / `D` |
| **Move Up/Down** | `E` / `Q` or `Space` / `Ctrl` |
| **Fast Movement** | Hold `Shift` |
| **Look Around** | Click and drag mouse |
### Rendering Controls
| Action | Keys |
|--------|------|
| **Wireframe Mode** | `F` |
| **Pause/Play Animation** | `P` |
| **Set Speed** | `0` (reset) `1` `2` `3` `4` `5` (multipliers) |
| **Toggle Help UI** | `H` |
## 🚀 Getting Started ## 🚀 Getting Started
### Prerequisites ### Prerequisites
- **Node.js** (v16 or higher) - Node.js (v16 or higher)
- **Browser**: Chrome 113+, Edge 113+, or Firefox 130+ (with WebGPU enabled)
- npm or yarn - npm or yarn
### Installation ### Installation

View File

@@ -1,63 +1,87 @@
import { vec3, mat4, vec4 } from 'gl-matrix'; import { vec3, mat4, vec4 } from 'gl-matrix';
import { ICamera } from './ICamera';
/** Orbital camera that rotates around the world origin. */ /** FPS-style flight camera with free movement */
export class OrbitalCamera implements ICamera { export class Camera {
pos: vec3; pos: vec3;
target: vec3; target: vec3;
up: vec3; up: vec3;
xRot: number; // FPS camera angles (in radians)
yRot: number; pitch: number; // Up/down rotation
offset: number; yaw: number; // Left/right rotation
// Direction vectors
forward: vec3;
right: vec3;
constructor() { constructor() {
this.pos = vec3.create(); this.pos = vec3.create();
vec3.set(this.pos, 0.0, 0.0, 0.0); vec3.set(this.pos, 0.0, -3.0, 2.0); // Start above and behind origin
this.target = vec3.create(); this.target = vec3.create();
vec3.set(this.target, 0.0, 0.0, 0.0);
this.up = vec3.create(); this.up = vec3.create();
vec3.set(this.up, 0.0, 1.0, 0.0); vec3.set(this.up, 0.0, 0.0, 1.0); // Z is up
this.xRot = 0.0; this.forward = vec3.create();
this.yRot = 0.0; this.right = vec3.create();
this.offset = 0.0; this.pitch = -0.3; // Looking slightly down
this.yaw = Math.PI / 2; // Looking toward +Y
this.updateVectors();
} }
setRotationX(rotX: number): void { /** Rotate camera by mouse delta */
this.xRot = rotX; rotate(deltaX: number, deltaY: number, sensitivity: number = 0.003): void {
this.updatePos(); this.yaw -= deltaX * sensitivity;
this.pitch -= deltaY * sensitivity;
// Clamp pitch to avoid flipping
const maxPitch = Math.PI / 2 - 0.01;
this.pitch = Math.max(-maxPitch, Math.min(maxPitch, this.pitch));
this.updateVectors();
} }
setRotationY(rotY: number): void { /** Move camera in the direction it's looking */
this.yRot = rotY; moveForward(amount: number): void {
this.updatePos(); vec3.scaleAndAdd(this.pos, this.pos, this.forward, amount);
this.updateVectors();
} }
/** Sets the offset to world origin. */ moveRight(amount: number): void {
setOffset(off: number): void { vec3.scaleAndAdd(this.pos, this.pos, this.right, amount);
this.offset = off; this.updateVectors();
this.updatePos();
} }
/** Recalculates the position according to xy-rotation and offset. */ moveUp(amount: number): void {
private updatePos(): void { // Move along world Z axis
const transformation: mat4 = mat4.create(); this.pos[2] += amount;
mat4.identity(transformation); this.updateVectors();
}
//2. xy-Rotation /** Move in the actual look direction (including vertical) */
mat4.rotateX(transformation, transformation, this.xRot); moveInLookDirection(amount: number): void {
mat4.rotateY(transformation, transformation, this.yRot); vec3.scaleAndAdd(this.pos, this.pos, this.forward, amount);
this.updateVectors();
}
//1. Translation /** Update direction vectors from pitch/yaw */
const translation = vec3.create(); private updateVectors(): void {
vec3.set(translation, 0.0, 0.0, this.offset); // Calculate forward vector from pitch and yaw
mat4.translate(transformation, transformation, translation); // Z is up, so we use different axis mapping
this.forward[0] = Math.cos(this.pitch) * Math.cos(this.yaw);
this.forward[1] = Math.cos(this.pitch) * Math.sin(this.yaw);
this.forward[2] = Math.sin(this.pitch);
vec3.normalize(this.forward, this.forward);
const temp: vec4 = vec4.create(); // Right vector is perpendicular to forward and world up
vec4.set(temp, 0.0, 0.0, 0.0, 1.0); const worldUp = vec3.fromValues(0, 0, 1);
vec4.transformMat4(temp, temp, transformation); vec3.cross(this.right, this.forward, worldUp);
vec3.normalize(this.right, this.right);
vec3.set(this.pos, temp[0], temp[1], temp[2]); // Camera up is perpendicular to forward and right
vec3.cross(this.up, this.right, this.forward);
vec3.normalize(this.up, this.up);
// Update target
vec3.add(this.target, this.pos, this.forward);
} }
getViewMatrix(): mat4 { getViewMatrix(): mat4 {
@@ -68,9 +92,6 @@ export class OrbitalCamera implements ICamera {
/** Get view direction for LOD calculations */ /** Get view direction for LOD calculations */
getViewDirection(): vec3 { getViewDirection(): vec3 {
const dir = vec3.create(); return vec3.clone(this.forward);
vec3.subtract(dir, this.target, this.pos);
vec3.normalize(dir, dir);
return dir;
} }
} }

View File

@@ -1,98 +0,0 @@
import { vec3, mat4 } from 'gl-matrix';
import { ICamera } from './ICamera';
/** FPS-style flight camera with free movement */
export class FPSCamera implements ICamera {
pos: vec3;
target: vec3;
up: vec3;
// FPS camera angles (in radians)
pitch: number; // Up/down rotation
yaw: number; // Left/right rotation
// Direction vectors
forward: vec3;
right: vec3;
constructor() {
this.pos = vec3.create();
vec3.set(this.pos, 0.0, -3.0, 2.0); // Start above and behind origin
this.target = vec3.create();
this.up = vec3.create();
vec3.set(this.up, 0.0, 0.0, 1.0); // Z is up
this.forward = vec3.create();
this.right = vec3.create();
this.pitch = -0.3; // Looking slightly down
this.yaw = Math.PI / 2; // Looking toward +Y
this.updateVectors();
}
/** Rotate camera by mouse delta */
rotate(deltaX: number, deltaY: number, sensitivity: number = 0.003): void {
this.yaw -= deltaX * sensitivity;
this.pitch -= deltaY * sensitivity;
// Clamp pitch to avoid flipping
const maxPitch = Math.PI / 2 - 0.01;
this.pitch = Math.max(-maxPitch, Math.min(maxPitch, this.pitch));
this.updateVectors();
}
/** Move camera in the direction it's looking */
moveForward(amount: number): void {
vec3.scaleAndAdd(this.pos, this.pos, this.forward, amount);
this.updateVectors();
}
moveRight(amount: number): void {
vec3.scaleAndAdd(this.pos, this.pos, this.right, amount);
this.updateVectors();
}
moveUp(amount: number): void {
// Move along world Z axis
this.pos[2] += amount;
this.updateVectors();
}
/** Move in the actual look direction (including vertical) */
moveInLookDirection(amount: number): void {
vec3.scaleAndAdd(this.pos, this.pos, this.forward, amount);
this.updateVectors();
}
/** Update direction vectors from pitch/yaw */
private updateVectors(): void {
// Calculate forward vector from pitch and yaw
// Z is up, so we use different axis mapping
this.forward[0] = Math.cos(this.pitch) * Math.cos(this.yaw);
this.forward[1] = Math.cos(this.pitch) * Math.sin(this.yaw);
this.forward[2] = Math.sin(this.pitch);
vec3.normalize(this.forward, this.forward);
// Right vector is perpendicular to forward and world up
const worldUp = vec3.fromValues(0, 0, 1);
vec3.cross(this.right, this.forward, worldUp);
vec3.normalize(this.right, this.right);
// Camera up is perpendicular to forward and right
vec3.cross(this.up, this.right, this.forward);
vec3.normalize(this.up, this.up);
// Update target
vec3.add(this.target, this.pos, this.forward);
}
getViewMatrix(): mat4 {
const ret: mat4 = mat4.create();
mat4.lookAt(ret, this.pos, this.target, this.up);
return ret;
}
/** Get view direction for LOD calculations */
getViewDirection(): vec3 {
return vec3.clone(this.forward);
}
}

View File

@@ -1,103 +1,113 @@
import { WebGPUContext } from './WebGPUContext'; /** Grid for the water surface */
/** Grid for the water surface - WebGPU version */
export class Grid { export class Grid {
private indices: Uint32Array = new Uint32Array(0); private indices: number[] = [];
private vertices: Float32Array = new Float32Array(0); private lineIndices: number[] = [];
private vertexBuffer: GPUBuffer | null = null; private vertices: number[] = [];
private indexBuffer: GPUBuffer | null = null; private vao: WebGLVertexArrayObject | null = null;
private lineVao: WebGLVertexArrayObject | null = null;
private size: number; private size: number;
private indexCount: number = 0; private offsetX: number;
private offsetY: number;
private scale: number;
constructor(size: number = 128) { constructor(size: number = 128, offsetX: number = 0, offsetY: number = 0, scale: number = 1) {
this.size = size; this.size = size;
this.offsetX = offsetX;
this.offsetY = offsetY;
this.scale = scale;
} }
generate(): void { generate(): void {
const indices: number[] = []; this.indices = [];
const vertices: number[] = []; this.lineIndices = [];
this.vertices = [];
for (let j = 0; j <= this.size; ++j) { for (let j = 0; j <= this.size; ++j) {
for (let i = 0; i <= this.size; ++i) { for (let i = 0; i <= this.size; ++i) {
// Generate Vertices with UV coordinates // Generate Vertices normalized to 0-1, then scale and offset
const x = i / this.size; // Grid is on XY plane (horizontal), Z is up
const y = j / this.size; const u = i / this.size;
const v = j / this.size;
const x = (u - 0.5) * this.scale + this.offsetX;
const y = (v - 0.5) * this.scale + this.offsetY;
const z = 0; const z = 0;
const u = x; this.vertices.push(x, y, z);
const v = y;
vertices.push(x, y, z, u, v); // position + UV
if (i < this.size && j < this.size) { // Skip edges if (i < this.size && j < this.size) { // Skip edges
const row1 = j * (this.size + 1); const row1 = j * (this.size + 1);
const row2 = (j + 1) * (this.size + 1); const row2 = (j + 1) * (this.size + 1);
// triangle 1 // triangle 1
indices.push(row1 + i); this.indices.push(row1 + i);
indices.push(row1 + i + 1); this.indices.push(row1 + i + 1);
indices.push(row2 + i + 1); this.indices.push(row2 + i + 1);
// triangle 2 // triangle 2
indices.push(row1 + i); this.indices.push(row1 + i);
indices.push(row2 + i + 1); this.indices.push(row2 + i + 1);
indices.push(row2 + i); this.indices.push(row2 + i);
}
// Generate line indices for wireframe
if (i < this.size) {
const currentVertex = j * (this.size + 1) + i;
this.lineIndices.push(currentVertex, currentVertex + 1);
}
if (j < this.size) {
const currentVertex = j * (this.size + 1) + i;
this.lineIndices.push(currentVertex, currentVertex + (this.size + 1));
} }
} }
} }
this.vertices = new Float32Array(vertices);
this.indices = new Uint32Array(indices);
this.indexCount = this.indices.length;
} }
initBuffers(gpuContext: WebGPUContext): void { initVAO(gl: WebGL2RenderingContext): void {
this.generate(); this.generate();
const device = gpuContext.getDevice(); // Create VAO for filled triangles
this.vao = gl.createVertexArray();
gl.bindVertexArray(this.vao);
// Create vertex buffer const vboGrid: WebGLBuffer | null = gl.createBuffer();
this.vertexBuffer = device.createBuffer({ gl.bindBuffer(gl.ARRAY_BUFFER, vboGrid);
size: this.vertices.byteLength, gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.vertices), gl.STATIC_DRAW);
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
mappedAtCreation: true,
});
new Float32Array(this.vertexBuffer.getMappedRange()).set(this.vertices);
this.vertexBuffer.unmap();
// Create index buffer const iboGrid: WebGLBuffer | null = gl.createBuffer();
this.indexBuffer = device.createBuffer({ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, iboGrid);
size: this.indices.byteLength, gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(this.indices), gl.STATIC_DRAW);
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
mappedAtCreation: true, gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 3 * Float32Array.BYTES_PER_ELEMENT, 0);
}); gl.enableVertexAttribArray(0);
new Uint32Array(this.indexBuffer.getMappedRange()).set(this.indices); gl.bindVertexArray(null);
this.indexBuffer.unmap();
// Create VAO for wireframe lines
this.lineVao = gl.createVertexArray();
gl.bindVertexArray(this.lineVao);
gl.bindBuffer(gl.ARRAY_BUFFER, vboGrid);
const iboLine: WebGLBuffer | null = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, iboLine);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(this.lineIndices), gl.STATIC_DRAW);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 3 * Float32Array.BYTES_PER_ELEMENT, 0);
gl.enableVertexAttribArray(0);
gl.bindVertexArray(null);
} }
draw(renderPass: GPURenderPassEncoder, wireframe: boolean = false): void { draw(gl: WebGL2RenderingContext, wireframe: boolean = false): void {
if (!this.vertexBuffer || !this.indexBuffer) return; if (wireframe && this.lineVao) {
gl.bindVertexArray(this.lineVao);
renderPass.setVertexBuffer(0, this.vertexBuffer); gl.drawElements(gl.LINES, this.lineIndices.length, gl.UNSIGNED_INT, 0);
renderPass.setIndexBuffer(this.indexBuffer, 'uint32'); gl.bindVertexArray(null);
} else if (this.vao) {
if (wireframe) { gl.bindVertexArray(this.vao);
// For wireframe, we'd need a different topology or to draw lines gl.drawElements(gl.TRIANGLES, this.indices.length, gl.UNSIGNED_INT, 0);
// WebGPU doesn't support LINE_LOOP like WebGL, so we draw as line-list gl.bindVertexArray(null);
// This would require regenerating indices for line rendering
renderPass.drawIndexed(this.indexCount);
} else {
renderPass.drawIndexed(this.indexCount);
} }
} }
getIndexCount(): number { getIndexCount(): number {
return this.indexCount; return this.indices.length;
}
getVertexBuffer(): GPUBuffer | null {
return this.vertexBuffer;
}
getIndexBuffer(): GPUBuffer | null {
return this.indexBuffer;
} }
} }

View File

@@ -1,11 +0,0 @@
import { vec3, mat4 } from 'gl-matrix';
/** Camera interface that both camera types implement */
export interface ICamera {
pos: vec3;
target: vec3;
up: vec3;
getViewMatrix(): mat4;
getViewDirection(): vec3;
}

167
src/OceanLOD.ts Normal file
View File

@@ -0,0 +1,167 @@
import { vec3, vec4, mat4 } from 'gl-matrix';
/**
* Projected Grid Ocean - Based on the projected grid algorithm.
* Uses a separate projector that can be adjusted to avoid backfiring.
* The grid is created in projector space and projected onto the ocean plane.
*/
export class ProjectedOcean {
private vao: WebGLVertexArrayObject | null = null;
private lineVao: WebGLVertexArrayObject | null = null;
private indexBuffer: WebGLBuffer | null = null;
private vertexBuffer: WebGLBuffer | null = null;
private indexCount: number = 0;
private lineIndexCount: number = 0;
// Grid resolution
private readonly GRID_SIZE_X = 400;
private readonly GRID_SIZE_Y = 400;
// Ocean plane parameters (Z = 0 plane, normal pointing up)
private readonly OCEAN_LEVEL = 0.0;
private readonly MAX_WAVE_HEIGHT = 1.5; // Maximum displacement above ocean level
private readonly MIN_WAVE_HEIGHT = -0.5; // Maximum displacement below ocean level
// Projector parameters
private readonly MIN_PROJECTOR_HEIGHT = 5.0; // Minimum height above upper bound
// Matrices for the shader
public projectorMatrix: mat4 = mat4.create();
public rangeMatrix: mat4 = mat4.create();
constructor() {}
/** Generate the grid vertices (in [0,1] range) */
initVAO(gl: WebGL2RenderingContext): void {
const vertices: number[] = [];
const indices: number[] = [];
// Create grid in [0,1] range - will be transformed by projector matrix
for (let y = 0; y <= this.GRID_SIZE_Y; y++) {
for (let x = 0; x <= this.GRID_SIZE_X; x++) {
const u = x / this.GRID_SIZE_X;
const v = y / this.GRID_SIZE_Y;
vertices.push(u, v);
}
}
// Create indices (counter-clockwise winding when viewed from above, Z up)
for (let y = 0; y < this.GRID_SIZE_Y; y++) {
for (let x = 0; x < this.GRID_SIZE_X; x++) {
const topLeft = y * (this.GRID_SIZE_X + 1) + x;
const topRight = topLeft + 1;
const bottomLeft = (y + 1) * (this.GRID_SIZE_X + 1) + x;
const bottomRight = bottomLeft + 1;
// CCW winding for front face visible from +Z (above)
indices.push(topLeft, topRight, bottomLeft);
indices.push(topRight, bottomRight, bottomLeft);
}
}
this.indexCount = indices.length;
// Create line indices for wireframe
const lineIndices: number[] = [];
for (let y = 0; y <= this.GRID_SIZE_Y; y++) {
for (let x = 0; x <= this.GRID_SIZE_X; x++) {
const currentVertex = y * (this.GRID_SIZE_X + 1) + x;
// Horizontal line
if (x < this.GRID_SIZE_X) {
lineIndices.push(currentVertex, currentVertex + 1);
}
// Vertical line
if (y < this.GRID_SIZE_Y) {
lineIndices.push(currentVertex, currentVertex + (this.GRID_SIZE_X + 1));
}
}
}
this.lineIndexCount = lineIndices.length;
// Create vertex buffer (shared between both VAOs)
this.vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
// Create VAO for filled triangles
this.vao = gl.createVertexArray();
gl.bindVertexArray(this.vao);
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
this.indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(indices), gl.STATIC_DRAW);
gl.bindVertexArray(null);
// Create VAO for wireframe lines
this.lineVao = gl.createVertexArray();
gl.bindVertexArray(this.lineVao);
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
const lineIndexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, lineIndexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(lineIndices), gl.STATIC_DRAW);
gl.bindVertexArray(null);
}
/**
* Update the projector matrices based on camera position.
* We use the camera's own view-projection to ensure screen coverage.
*/
updateProjector(cameraPos: vec3, cameraForward: vec3, viewMatrix: mat4, projMatrix: mat4): void {
// Use camera's view-projection directly
const viewProj = mat4.create();
mat4.multiply(viewProj, projMatrix, viewMatrix);
// Invert to get unprojection matrix
mat4.invert(this.projectorMatrix, viewProj);
// Range matrix maps [0,1] grid to [-1,1] clip space
this.calculateRangeMatrix(cameraPos, viewMatrix, projMatrix, viewProj);
}
/**
* Calculate the range conversion matrix to focus geometry on visible area
* For simplicity and to ensure horizon coverage, we use the full clip space range
*/
private calculateRangeMatrix(
cameraPos: vec3,
viewMatrix: mat4,
projMatrix: mat4,
projectorViewProj: mat4
): void {
// Use full clip space [-1, 1] to ensure complete coverage including horizon
// The grid [0,1] maps to [-1,1] in projector clip space
mat4.identity(this.rangeMatrix);
this.rangeMatrix[0] = 2.0; // Scale X: [0,1] -> [0,2]
this.rangeMatrix[5] = 2.0; // Scale Y: [0,1] -> [0,2]
this.rangeMatrix[10] = 2.0; // Scale Z
this.rangeMatrix[12] = -1.0; // Translate X: [0,2] -> [-1,1]
this.rangeMatrix[13] = -1.0; // Translate Y: [0,2] -> [-1,1]
this.rangeMatrix[14] = -1.0; // Translate Z
}
draw(gl: WebGL2RenderingContext, wireframe: boolean = false): void {
if (wireframe && this.lineVao) {
gl.bindVertexArray(this.lineVao);
gl.drawElements(gl.LINES, this.lineIndexCount, gl.UNSIGNED_INT, 0);
gl.bindVertexArray(null);
} else if (this.vao) {
gl.bindVertexArray(this.vao);
gl.drawElements(gl.TRIANGLES, this.indexCount, gl.UNSIGNED_INT, 0);
gl.bindVertexArray(null);
}
}
getIndexCount(): number {
return this.indexCount;
}
}

View File

@@ -1,16 +1,12 @@
import { WebGPUContext } from './WebGPUContext'; /** Skybox cube for rendering the sky */
/** Skybox cube for rendering the sky - WebGPU version */
export class Skybox { export class Skybox {
private vertexBuffer: GPUBuffer | null = null; private vao: WebGLVertexArrayObject | null = null;
private indexBuffer: GPUBuffer | null = null; private vbo: WebGLBuffer | null = null;
private indexCount: number = 0; private indexCount: number = 0;
constructor() {} constructor() {}
initBuffers(gpuContext: WebGPUContext): void { initVAO(gl: WebGL2RenderingContext): void {
const device = gpuContext.getDevice();
// Cube vertices - positions only // Cube vertices - positions only
const vertices = new Float32Array([ const vertices = new Float32Array([
// Front face // Front face
@@ -56,38 +52,34 @@ export class Skybox {
this.indexCount = indices.length; this.indexCount = indices.length;
// Create vertex buffer this.vao = gl.createVertexArray();
this.vertexBuffer = device.createBuffer({ gl.bindVertexArray(this.vao);
size: vertices.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
mappedAtCreation: true,
});
new Float32Array(this.vertexBuffer.getMappedRange()).set(vertices);
this.vertexBuffer.unmap();
// Create index buffer this.vbo = gl.createBuffer();
this.indexBuffer = device.createBuffer({ gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo);
size: indices.byteLength, gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
mappedAtCreation: true, const ibo = gl.createBuffer();
}); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
new Uint16Array(this.indexBuffer.getMappedRange()).set(indices); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
this.indexBuffer.unmap();
// Position attribute
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
gl.bindVertexArray(null);
} }
draw(renderPass: GPURenderPassEncoder): void { draw(gl: WebGL2RenderingContext): void {
if (!this.vertexBuffer || !this.indexBuffer) return; if (!this.vao) return;
renderPass.setVertexBuffer(0, this.vertexBuffer); // Disable face culling for skybox (we're inside the cube)
renderPass.setIndexBuffer(this.indexBuffer, 'uint16'); gl.disable(gl.CULL_FACE);
renderPass.drawIndexed(this.indexCount);
}
getVertexBuffer(): GPUBuffer | null { gl.bindVertexArray(this.vao);
return this.vertexBuffer; gl.drawElements(gl.TRIANGLES, this.indexCount, gl.UNSIGNED_SHORT, 0);
} gl.bindVertexArray(null);
getIndexBuffer(): GPUBuffer | null { gl.enable(gl.CULL_FACE);
return this.indexBuffer;
} }
} }

View File

@@ -1,110 +0,0 @@
/** WebGPU Context Manager */
export class WebGPUContext {
adapter: GPUAdapter | null = null;
device: GPUDevice | null = null;
context: GPUCanvasContext | null = null;
canvasFormat: GPUTextureFormat = 'bgra8unorm';
canvas: HTMLCanvasElement;
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
}
async initialize(): Promise<boolean> {
// Check WebGPU support
if (!navigator.gpu) {
console.error('WebGPU is not supported in this browser.');
return false;
}
// Request adapter
this.adapter = await navigator.gpu.requestAdapter();
if (!this.adapter) {
console.error('Failed to get GPU adapter.');
return false;
}
// Request device
this.device = await this.adapter.requestDevice();
if (!this.device) {
console.error('Failed to get GPU device.');
return false;
}
// Get canvas context
this.context = this.canvas.getContext('webgpu') as GPUCanvasContext;
if (!this.context) {
console.error('Failed to get WebGPU canvas context.');
return false;
}
// Configure canvas
this.canvasFormat = navigator.gpu.getPreferredCanvasFormat();
this.context.configure({
device: this.device,
format: this.canvasFormat,
alphaMode: 'opaque',
});
console.log('WebGPU initialized successfully');
return true;
}
getDevice(): GPUDevice {
if (!this.device) {
throw new Error('Device not initialized');
}
return this.device;
}
getContext(): GPUCanvasContext {
if (!this.context) {
throw new Error('Context not initialized');
}
return this.context;
}
getCurrentTexture(): GPUTexture {
return this.getContext().getCurrentTexture();
}
createBuffer(descriptor: GPUBufferDescriptor): GPUBuffer {
return this.getDevice().createBuffer(descriptor);
}
createTexture(descriptor: GPUTextureDescriptor): GPUTexture {
return this.getDevice().createTexture(descriptor);
}
createSampler(descriptor: GPUSamplerDescriptor): GPUSampler {
return this.getDevice().createSampler(descriptor);
}
createShaderModule(code: string): GPUShaderModule {
return this.getDevice().createShaderModule({ code });
}
createRenderPipeline(descriptor: GPURenderPipelineDescriptor): GPURenderPipeline {
return this.getDevice().createRenderPipeline(descriptor);
}
createBindGroup(descriptor: GPUBindGroupDescriptor): GPUBindGroup {
return this.getDevice().createBindGroup(descriptor);
}
createBindGroupLayout(descriptor: GPUBindGroupLayoutDescriptor): GPUBindGroupLayout {
return this.getDevice().createBindGroupLayout(descriptor);
}
createCommandEncoder(): GPUCommandEncoder {
return this.getDevice().createCommandEncoder();
}
submitCommands(commandBuffers: GPUCommandBuffer[]): void {
this.getDevice().queue.submit(commandBuffers);
}
writeBuffer(buffer: GPUBuffer, data: BufferSource, offset: number = 0): void {
this.getDevice().queue.writeBuffer(buffer, offset, data);
}
}

View File

@@ -1,7 +1,7 @@
// Configuration Constants // Configuration Constants
export const GRID_SIZE = 128; export const GRID_SIZE = 128;
export const NOISE_TEXTURE_WIDTH = 256; export const NOISE_TEXTURE_WIDTH = 1024;
export const NOISE_TEXTURE_HEIGHT = 256; export const NOISE_TEXTURE_HEIGHT = 1024;
export const CANVAS_WIDTH = 800; export const CANVAS_WIDTH = 800;
export const CANVAS_HEIGHT = 600; export const CANVAS_HEIGHT = 600;
export const FOV = 1.0; export const FOV = 1.0;

File diff suppressed because it is too large Load Diff

View File

@@ -1,509 +0,0 @@
import { vec3, mat4 } from 'gl-matrix';
import { ICamera } from './ICamera';
import { OrbitalCamera } from './Camera';
import { FPSCamera } from './FPSCamera';
import { Grid } from './Grid';
import { Skybox } from './Skybox';
import { createProgram } from './Shader';
import * as Config from './constants';
var gl: WebGL2RenderingContext;
var viewportWidth = 0;
var viewportHeight = 0;
/** A camera that always looks at the world origin. Can have an offset and be rotated. */
// Moved to Camera.ts
/** Init OpenGL and gets the viewport/canvas sizes */
function initGL(canvas: HTMLCanvasElement) {
// Helper function for canvas resize
const updateCanvasSize = (canvas: HTMLCanvasElement) => {
const displayWidth = window.innerWidth;
const displayHeight = window.innerHeight;
if (canvas.width !== displayWidth || canvas.height !== displayHeight) {
canvas.width = displayWidth;
canvas.height = displayHeight;
viewportWidth = displayWidth;
viewportHeight = displayHeight;
if (gl) {
gl.viewport(0, 0, viewportWidth, viewportHeight);
}
}
};
var gltemp;
try {
gltemp = canvas.getContext("webgl2");
if (!gltemp)
gltemp = canvas.getContext("experimental-webgl2");
if (gltemp != null) {
updateCanvasSize(canvas);
}
} catch (e) {
}
// Not the best error detection logic.
// Redirect to http://get.webgl.org in failure case.
if (gltemp == null) {
console.error("Unable to initialize WebGL2. Your browser or machine may not support it.");
return;
}
gl = <WebGL2RenderingContext>gltemp;
//WebGL2 supports floating point textures by default but it does not support filtering them or rendering to them by default. Note: 16bit filtering is included 32bit not
if (!gl.getExtension('EXT_color_buffer_float')) {
console.error("32Bit/16Bit single Color render Buffers not available.");
} //allow 16bit texture as framebuffer target
gl.enable(gl.DEPTH_TEST);
return updateCanvasSize;
}
/** Update canvas size to fill window */
// Moved inline below
/** Grid for the watersurface */
// Moved to Grid.ts
/** Init Geometry for a Triangle */
var VBO: WebGLBuffer | null = null;
function initGeometry() {
VBO = gl.createBuffer();
//Vertex data represent fullscreen quad in NDC-Space
// X, Y, Z, U, V
let vertexData = [-1.0, -1.0, 0.0, /*BOTTOM LEFT*/ 0.0, 0.0,
1.0, -1.0, 0.0, /*BOTTOM RIGHT*/ 1.0, 0.0,
-1.0, 1.0, 0.0, /*TOP LEFT */ 0.0, 1.0,
1.0, -1.0, 0.0, /*BOTTOM RIGHT */ 1.0, 0.0,
-1.0, 1.0, 0.0, /*TOP LEFT */ 0.0, 1.0,
1.0, 1.0, 0.0, /*TOP RIGHT */ 1.0, 1.0
];
gl.bindBuffer(gl.ARRAY_BUFFER, VBO);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexData), gl.STATIC_DRAW);
}
/** Get shader source by HTML-Element<id> */
// Moved to Shader.ts
/** Init all Shaders that are needed */
var perlinNoiseProgram: WebGLProgram | null;
var defaultProgram: WebGLProgram | null;
var textureProgram: WebGLProgram | null;
var skyProgram: WebGLProgram | null;
function initShaders() {
perlinNoiseProgram = createProgram(gl, "ndc-vs", "noise-fs", "Perlin Noise");
defaultProgram = createProgram(gl, "default-vs", "default-fs", "Default");
textureProgram = createProgram(gl, "texture-vs", "texture-fs", "Texture");
skyProgram = createProgram(gl, "sky-vs", "sky-fs", "Sky");
}
/** Init an FBO used for the first render pass / perlin noise */
var perlinNoiseFBO: WebGLFramebuffer | null = null;
var textureFBO: WebGLTexture | null = null;
var perlinNoiseFBOWidth = Config.NOISE_TEXTURE_WIDTH;
var perlinNoiseFBOHeight = Config.NOISE_TEXTURE_HEIGHT;
function initFBO() {
perlinNoiseFBO = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, perlinNoiseFBO);
// Add attachments
textureFBO = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, textureFBO); //last 3 parameter not intertesting becuase we are not supplying data
gl.texImage2D(gl.TEXTURE_2D, 0, gl.R16F, perlinNoiseFBOWidth, perlinNoiseFBOHeight, 0, gl.RED, gl.HALF_FLOAT, null);
// set the filtering so we don't need mips
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);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, textureFBO, 0);
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE) {
console.log("Framebuffer creation failed.");
}
gl.bindFramebuffer(gl.FRAMEBUFFER, null); //Reset to default framebuffer
}
/** Update/Draw function.*/
/** Framerate measurement variables */
var timeSpent = 0.0;
var lastTime = new Date().getTime();
var counter = 0.0;
var fps = 0;
var fpsDisplay: HTMLElement | null = null;
/** Input states*/
var mouseXVel = 0;
var mouseYVel = 0;
var keyboardRotationX = 0;
var keyboardRotationY = 0;
var keyboardZoom = 0;
var keysPressed: Set<string> = new Set();
/** Objects and states*/
var camera: ICamera;
var orbitalCamera: OrbitalCamera;
var fpsCamera: FPSCamera;
var oceanGrid: Grid;
var skybox: Skybox;
var curRotX = Config.CAMERA_DEFAULT_ROT_X;
var curRotY = Config.CAMERA_DEFAULT_ROT_Y;
/** Camera modes */
var cameraMode: 'orbital' | 'fps' = 'orbital';
var moveSpeed = 0.08;
var fastMoveSpeed = 0.20;
/** Rendering modes */
var wireframeMode = false;
function drawScene() {
fps++;
let now = new Date();
let delta = now.getTime() - lastTime;
timeSpent += delta;
if ((counter += delta) >= Config.FPS_UPDATE_INTERVAL) {
counter = 0;
if (fpsDisplay) {
fpsDisplay.textContent = `FPS: ${fps}`;
}
fps = 0;
}
lastTime = now.getTime();
// Two Rendering passes. The first one generates a perlin noise
// texture. Second one uses the textur for vertex displacement
// of a grid representing the water surface.
//--- First render pass -> Perlin Noise (it updates the perlin noise texture)
{
gl.bindFramebuffer(gl.FRAMEBUFFER, perlinNoiseFBO);
gl.viewport(0, 0, perlinNoiseFBOWidth, perlinNoiseFBOHeight);
//Clear buffer content
gl.clearColor(1.0, 1.0, 1.0, 1);
gl.clear(gl.COLOR_BUFFER_BIT); //No depth buffer
// Disable face culling for fullscreen quad
gl.disable(gl.CULL_FACE);
//draw a fullscreen quad
gl.bindBuffer(gl.ARRAY_BUFFER, VBO);
// There are 7 floating-point values per vertex
let stride = 5 * Float32Array.BYTES_PER_ELEMENT;
// Set up position stream
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, stride, 0);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, stride, 3 * Float32Array.BYTES_PER_ELEMENT);
gl.enableVertexAttribArray(1);
gl.useProgram(perlinNoiseProgram);
let uTime = gl.getUniformLocation(<WebGLProgram>perlinNoiseProgram, "uTime");
gl.uniform1f(uTime, timeSpent);
gl.drawArrays(gl.TRIANGLES, 0, 6); // Draw fullscreen quad
}
//--- Second render pass -> Geomtry with displacement by perlin noise texture ---
{
gl.bindFramebuffer(gl.FRAMEBUFFER, null); //Bind default framebuffer
gl.viewport(0, 0, viewportWidth, viewportHeight);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.activeTexture(gl.TEXTURE0); //Binds the texture to 0
gl.bindTexture(gl.TEXTURE_2D, textureFBO);
var projection = mat4.create();
mat4.identity(projection);
mat4.perspective(projection, Config.FOV, viewportWidth / viewportHeight, Config.NEAR_PLANE, Config.FAR_PLANE); //projection mode should actually be camera specific
// Handle camera movement and rotation based on mode
if (cameraMode === 'fps') {
// FPS camera - direct movement
camera = fpsCamera;
handleFPSCameraMovement();
// Apply mouse rotation for FPS mode
if (mouseXVel !== 0 || mouseYVel !== 0) {
fpsCamera.rotate(mouseXVel, mouseYVel);
mouseXVel = 0;
mouseYVel = 0;
}
} else {
// Orbital camera - original behavior
camera = orbitalCamera;
orbitalCamera.setOffset(Config.CAMERA_DEFAULT_OFFSET + keyboardZoom);
orbitalCamera.setRotationX((curRotX += mouseYVel * Config.MOUSE_SENSITIVITY + keyboardRotationX));
orbitalCamera.setRotationY((curRotY += mouseXVel * Config.MOUSE_SENSITIVITY + keyboardRotationY));
}
var view = camera.getViewMatrix();
// Sun direction (matches the one in ocean shader)
const sunDirection = vec3.fromValues(0.3, 0.5, 0.8);
vec3.normalize(sunDirection, sunDirection);
// Draw skybox first with depth test disabled (always behind everything)
gl.depthMask(false);
gl.disable(gl.DEPTH_TEST);
gl.disable(gl.CULL_FACE); // Disable face culling for skybox (we're inside)
gl.useProgram(skyProgram);
let sky_view_loc = gl.getUniformLocation(<WebGLProgram>skyProgram, "view");
gl.uniformMatrix4fv(sky_view_loc, false, view);
let sky_projection_loc = gl.getUniformLocation(<WebGLProgram>skyProgram, "projection");
gl.uniformMatrix4fv(sky_projection_loc, false, projection);
let sky_sun_loc = gl.getUniformLocation(<WebGLProgram>skyProgram, "uSunDirection");
gl.uniform3fv(sky_sun_loc, sunDirection);
skybox.draw(gl);
gl.enable(gl.DEPTH_TEST);
gl.depthMask(true);
gl.enable(gl.CULL_FACE); // Re-enable face culling for ocean
gl.cullFace(gl.BACK); // Cull back faces for ocean
var model = mat4.create();
mat4.identity(model);
let translationCentering = vec3.create();
vec3.set(translationCentering, -0.5, -0.5, 0.0);
mat4.translate(model, model, translationCentering); //1. First Center the Surface in the origin.
gl.useProgram(defaultProgram);
let view_loc = gl.getUniformLocation(<WebGLProgram>defaultProgram, "view");
gl.uniformMatrix4fv(view_loc, false, view);
let model_loc = gl.getUniformLocation(<WebGLProgram>defaultProgram, "model");
gl.uniformMatrix4fv(model_loc, false, model);
let projection_loc = gl.getUniformLocation(<WebGLProgram>defaultProgram, "projection");
gl.uniformMatrix4fv(projection_loc, false, projection);
let eye_loc = gl.getUniformLocation(<WebGLProgram>defaultProgram, "eyePos");
gl.uniform3fv(eye_loc, camera.pos);
//let uTime_loc = gl.getUniformLocation(<WebGLProgram>defaultProgram, "uTime");
//gl.uniform1f(uTime_loc, timeSpent);
let displacementMap_loc = gl.getUniformLocation(<WebGLProgram>defaultProgram, "displace_map");
gl.uniform1i(displacementMap_loc, 0); //Get texture from slot 0
// Draw ocean grid with wireframe mode if enabled
if (wireframeMode) {
gl.lineWidth(1.0);
}
oceanGrid.draw(gl, wireframeMode);
}
requestAnimationFrame(drawScene);
}
/** Handle FPS camera movement */
function handleFPSCameraMovement() {
const speed = keysPressed.has('Shift') ? fastMoveSpeed : moveSpeed;
// WASD for horizontal movement
if (keysPressed.has('w') || keysPressed.has('W')) {
fpsCamera.moveForward(speed);
}
if (keysPressed.has('s') || keysPressed.has('S')) {
fpsCamera.moveForward(-speed);
}
if (keysPressed.has('a') || keysPressed.has('A')) {
fpsCamera.moveRight(-speed);
}
if (keysPressed.has('d') || keysPressed.has('D')) {
fpsCamera.moveRight(speed);
}
// Q/E for vertical movement
if (keysPressed.has('q') || keysPressed.has('Q')) {
fpsCamera.moveUp(-speed);
}
if (keysPressed.has('e') || keysPressed.has('E')) {
fpsCamera.moveUp(speed);
}
// Space to go up, Ctrl to go down
if (keysPressed.has(' ')) {
fpsCamera.moveUp(speed);
}
if (keysPressed.has('Control')) {
fpsCamera.moveUp(-speed);
}
}
/** Handle keyboard input for camera controls */
function handleKeyboardInput() {
keyboardRotationX = 0;
keyboardRotationY = 0;
if (keysPressed.has('w') || keysPressed.has('W') || keysPressed.has('ArrowUp')) {
keyboardRotationX = Config.KEYBOARD_ROTATION_SPEED;
}
if (keysPressed.has('s') || keysPressed.has('S') || keysPressed.has('ArrowDown')) {
keyboardRotationX = -Config.KEYBOARD_ROTATION_SPEED;
}
if (keysPressed.has('a') || keysPressed.has('A') || keysPressed.has('ArrowLeft')) {
keyboardRotationY = Config.KEYBOARD_ROTATION_SPEED;
}
if (keysPressed.has('d') || keysPressed.has('D') || keysPressed.has('ArrowRight')) {
keyboardRotationY = -Config.KEYBOARD_ROTATION_SPEED;
}
if (keysPressed.has('q') || keysPressed.has('Q') || keysPressed.has('+')) {
keyboardZoom -= Config.KEYBOARD_ZOOM_SPEED;
}
if (keysPressed.has('e') || keysPressed.has('E') || keysPressed.has('-')) {
keyboardZoom += Config.KEYBOARD_ZOOM_SPEED;
}
}
function main() {
const canvas: HTMLCanvasElement = <HTMLCanvasElement>document.getElementById("window");
fpsDisplay = document.getElementById("fps-counter");
const updateCanvasSize = initGL(canvas);
if (!updateCanvasSize) {
console.error("Failed to initialize WebGL");
return;
}
var drag = false;
var previousPosX: number | null;
var previousPosY: number | null;
canvas.addEventListener('mousedown', function (evt) {
drag = true;
}, false);
canvas.addEventListener('mousemove', function (evt) {
if (drag) {
if (previousPosX == null || previousPosY == null) {
previousPosX = evt.x;
previousPosY = evt.y;
}
var mousePosX = evt.x;
var mousePosY = evt.y;
mouseXVel = (mousePosX - previousPosX);
mouseYVel = (mousePosY - previousPosY);
previousPosX = mousePosX;
previousPosY = mousePosY;
}
}, false);
var deactivateMouseMovement = function () {
previousPosX = null;
previousPosY = null;
mouseXVel = 0.0;
mouseYVel = 0.0;
drag = false;
}
canvas.addEventListener('mouseup', deactivateMouseMovement, false);
canvas.addEventListener('mouseleave', deactivateMouseMovement, false);
// Keyboard controls
window.addEventListener('keydown', (evt) => {
keysPressed.add(evt.key);
// Toggle camera mode with 'C' key
if (evt.key === 'c' || evt.key === 'C') {
cameraMode = cameraMode === 'fps' ? 'orbital' : 'fps';
console.log(`Camera mode: ${cameraMode.toUpperCase()}`);
// Update FPS display to show camera mode
if (fpsDisplay) {
const modeText = document.createElement('div');
modeText.id = 'camera-mode';
modeText.style.cssText = 'position: absolute; top: 40px; left: 10px; color: white; font-family: monospace;';
modeText.textContent = `Camera: ${cameraMode.toUpperCase()}`;
const existingMode = document.getElementById('camera-mode');
if (existingMode) {
existingMode.textContent = `Camera: ${cameraMode.toUpperCase()}`;
} else {
document.body.appendChild(modeText);
}
}
}
// Reset camera on 'R' key
if (evt.key === 'r' || evt.key === 'R') {
if (cameraMode === 'fps') {
fpsCamera = new FPSCamera(); // Reset to initial FPS position
camera = fpsCamera;
console.log('Camera reset to FPS default position');
} else {
curRotX = Config.CAMERA_DEFAULT_ROT_X;
curRotY = Config.CAMERA_DEFAULT_ROT_Y;
keyboardZoom = 0;
console.log('Camera reset to orbital default position');
}
}
// Prevent default for space to avoid page scroll
if (evt.key === ' ' && cameraMode === 'fps') {
evt.preventDefault();
}
// Wireframe toggle with F key
if (evt.key === 'f' || evt.key === 'F') {
wireframeMode = !wireframeMode;
console.log(`Wireframe mode: ${wireframeMode ? 'ON' : 'OFF'}`);
}
// Handle orbital camera keyboard input
if (cameraMode === 'orbital') {
handleKeyboardInput();
}
});
window.addEventListener('keyup', (evt) => {
keysPressed.delete(evt.key);
if (cameraMode === 'orbital') {
handleKeyboardInput();
}
});
// Window resize handler
window.addEventListener('resize', () => {
updateCanvasSize(canvas);
});
// Camera mode toggle from UI controls
window.addEventListener('toggleCameraMode', () => {
cameraMode = cameraMode === 'fps' ? 'orbital' : 'fps';
camera = cameraMode === 'fps' ? fpsCamera : orbitalCamera;
console.log(`Camera mode switched to: ${cameraMode.toUpperCase()}`);
// Update display
const modeText = document.createElement('div');
modeText.id = 'camera-mode';
modeText.style.cssText = 'position: absolute; top: 40px; left: 10px; color: white; font-family: monospace;';
modeText.textContent = `Camera: ${cameraMode.toUpperCase()}`;
const existingMode = document.getElementById('camera-mode');
if (existingMode) {
existingMode.textContent = `Camera: ${cameraMode.toUpperCase()}`;
} else {
document.body.appendChild(modeText);
}
});
initShaders();
initGeometry();
initFBO();
oceanGrid = new Grid(Config.GRID_SIZE);
oceanGrid.initVAO(gl);
skybox = new Skybox();
skybox.initVAO(gl);
// Initialize both cameras
orbitalCamera = new OrbitalCamera();
fpsCamera = new FPSCamera();
camera = orbitalCamera; // Start with Orbital camera
console.log('Cameras initialized - Press C to toggle between FPS and Orbital modes');
//Check if any errors apeared during init.
if (gl.getError() != gl.NO_ERROR) {
console.log("OpenGL Error!: ");
}
drawScene();
}
main();

View File

@@ -1,317 +0,0 @@
// WGSL Shaders for WebGPU
// Vertex shader for noise generation (fullscreen quad)
export const noiseVertexShader = `
@vertex
fn main(@builtin(vertex_index) vertexIndex: u32) -> @builtin(position) vec4<f32> {
var pos = array<vec2<f32>, 6>(
vec2<f32>(-1.0, -1.0),
vec2<f32>(1.0, -1.0),
vec2<f32>(-1.0, 1.0),
vec2<f32>(1.0, -1.0),
vec2<f32>(1.0, 1.0),
vec2<f32>(-1.0, 1.0)
);
return vec4<f32>(pos[vertexIndex], 0.0, 1.0);
}
`;
// Fragment shader for Perlin noise
export const noiseFragmentShader = `
@group(0) @binding(0) var<uniform> uTime: f32;
fn permute(x: vec4<f32>) -> vec4<f32> {
return ((x * 34.0 + 1.0) * x) % vec4<f32>(289.0);
}
fn taylorInvSqrt(r: vec4<f32>) -> vec4<f32> {
return 1.79284291400159 - 0.85373472095314 * r;
}
fn fade(t: vec3<f32>) -> vec3<f32> {
return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
}
fn cnoise(P: vec3<f32>) -> f32 {
var Pi0: vec3<f32> = floor(P);
var Pi1: vec3<f32> = Pi0 + vec3<f32>(1.0);
Pi0 = Pi0 % vec3<f32>(289.0);
Pi1 = Pi1 % vec3<f32>(289.0);
let Pf0 = fract(P);
let Pf1 = Pf0 - vec3<f32>(1.0);
let ix = vec4<f32>(Pi0.x, Pi1.x, Pi0.x, Pi1.x);
let iy = vec4<f32>(Pi0.yy, Pi1.yy);
let iz0 = Pi0.zzzz;
let iz1 = Pi1.zzzz;
let ixy = permute(permute(ix) + iy);
let ixy0 = permute(ixy + iz0);
let ixy1 = permute(ixy + iz1);
var gx0: vec4<f32> = ixy0 / 7.0;
var gy0: vec4<f32> = fract(floor(gx0) / 7.0) - 0.5;
gx0 = fract(gx0);
let gz0 = vec4<f32>(0.5) - abs(gx0) - abs(gy0);
let sz0 = step(gz0, vec4<f32>(0.0));
gx0 = gx0 - sz0 * (step(vec4<f32>(0.0), gx0) - 0.5);
gy0 = gy0 - sz0 * (step(vec4<f32>(0.0), gy0) - 0.5);
var gx1: vec4<f32> = ixy1 / 7.0;
var gy1: vec4<f32> = fract(floor(gx1) / 7.0) - 0.5;
gx1 = fract(gx1);
let gz1 = vec4<f32>(0.5) - abs(gx1) - abs(gy1);
let sz1 = step(gz1, vec4<f32>(0.0));
gx1 = gx1 - sz1 * (step(vec4<f32>(0.0), gx1) - 0.5);
gy1 = gy1 - sz1 * (step(vec4<f32>(0.0), gy1) - 0.5);
var g000: vec3<f32> = vec3<f32>(gx0.x, gy0.x, gz0.x);
var g100: vec3<f32> = vec3<f32>(gx0.y, gy0.y, gz0.y);
var g010: vec3<f32> = vec3<f32>(gx0.z, gy0.z, gz0.z);
var g110: vec3<f32> = vec3<f32>(gx0.w, gy0.w, gz0.w);
var g001: vec3<f32> = vec3<f32>(gx1.x, gy1.x, gz1.x);
var g101: vec3<f32> = vec3<f32>(gx1.y, gy1.y, gz1.y);
var g011: vec3<f32> = vec3<f32>(gx1.z, gy1.z, gz1.z);
var g111: vec3<f32> = vec3<f32>(gx1.w, gy1.w, gz1.w);
let norm0 = taylorInvSqrt(vec4<f32>(dot(g000, g000), dot(g100, g100), dot(g010, g010), dot(g110, g110)));
g000 = g000 * norm0.x;
g100 = g100 * norm0.y;
g010 = g010 * norm0.z;
g110 = g110 * norm0.w;
let norm1 = taylorInvSqrt(vec4<f32>(dot(g001, g001), dot(g101, g101), dot(g011, g011), dot(g111, g111)));
g001 = g001 * norm1.x;
g101 = g101 * norm1.y;
g011 = g011 * norm1.z;
g111 = g111 * norm1.w;
let n000 = dot(g000, Pf0);
let n100 = dot(g100, vec3<f32>(Pf1.x, Pf0.yz));
let n010 = dot(g010, vec3<f32>(Pf0.x, Pf1.y, Pf0.z));
let n110 = dot(g110, vec3<f32>(Pf1.xy, Pf0.z));
let n001 = dot(g001, vec3<f32>(Pf0.xy, Pf1.z));
let n101 = dot(g101, vec3<f32>(Pf1.x, Pf0.y, Pf1.z));
let n011 = dot(g011, vec3<f32>(Pf0.x, Pf1.yz));
let n111 = dot(g111, Pf1);
let fade_xyz = fade(Pf0);
let n_z = mix(vec4<f32>(n000, n100, n010, n110), vec4<f32>(n001, n101, n011, n111), fade_xyz.z);
let n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y);
let n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x);
return 2.2 * n_xyz;
}
@fragment
fn main(@builtin(position) fragCoord: vec4<f32>) -> @location(0) vec4<f32> {
let resolution = vec2<f32>(256.0, 256.0);
let uv = fragCoord.xy / resolution;
var sum = 0.0;
var scale = 1.0;
var amplitude = 0.5;
for (var i = 0; i < 5; i = i + 1) {
// Make noise tileable by wrapping coordinates
let wrapped_uv = fract(uv * scale);
let p = vec3<f32>(wrapped_uv * 10.0, uTime * 0.2);
sum += cnoise(p) * amplitude;
scale *= 2.0;
amplitude *= 0.5;
}
return vec4<f32>(sum, 0.0, 0.0, 1.0);
}
`;
// Ocean vertex shader
export const oceanVertexShader = `
struct Uniforms {
view: mat4x4<f32>,
model: mat4x4<f32>,
projection: mat4x4<f32>,
eyePos: vec3<f32>,
};
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(1) var displacementTexture: texture_2d<f32>;
@group(0) @binding(2) var displacementSampler: sampler;
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) fragPos: vec3<f32>,
@location(1) uv: vec2<f32>,
};
@vertex
fn main(
@location(0) position: vec3<f32>,
@location(1) uv: vec2<f32>
) -> VertexOutput {
var output: VertexOutput;
var worldPos = uniforms.model * vec4<f32>(position, 1.0);
output.uv = uv;
// Sample displacement using textureSampleLevel (works in vertex shader)
let displace = textureSampleLevel(displacementTexture, displacementSampler, uv, 0.0);
worldPos.z = worldPos.z + displace.r * 0.15;
output.position = uniforms.projection * uniforms.view * worldPos;
output.fragPos = worldPos.xyz;
return output;
}
`;
// Ocean fragment shader
export const oceanFragmentShader = `
struct Uniforms {
view: mat4x4<f32>,
model: mat4x4<f32>,
projection: mat4x4<f32>,
eyePos: vec3<f32>,
};
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(1) var displacementTexture: texture_2d<f32>;
@group(0) @binding(2) var displacementSampler: sampler;
@fragment
fn main(
@location(0) fragPos: vec3<f32>,
@location(1) uv: vec2<f32>
) -> @location(0) vec4<f32> {
// Sample displacement for normal calculation only
let gridPointDelta = 1.0 / 256.0;
let displacementScale = 0.15;
let displace = textureSample(displacementTexture, displacementSampler, uv).r * displacementScale;
let right = textureSample(displacementTexture, displacementSampler, vec2<f32>(uv.x + gridPointDelta, uv.y)).r * displacementScale;
let left = textureSample(displacementTexture, displacementSampler, vec2<f32>(uv.x - gridPointDelta, uv.y)).r * displacementScale;
let up = textureSample(displacementTexture, displacementSampler, vec2<f32>(uv.x, uv.y + gridPointDelta)).r * displacementScale;
let down = textureSample(displacementTexture, displacementSampler, vec2<f32>(uv.x, uv.y - gridPointDelta)).r * displacementScale;
// Calculate surface normal
let dX = vec3<f32>(gridPointDelta * 2.0, 0.0, right - left);
let dY = vec3<f32>(0.0, gridPointDelta * 2.0, up - down);
var norm = normalize(cross(dX, dY));
// Lighting
let lightDir = normalize(vec3<f32>(0.3, 0.5, 0.8));
let diff = max(dot(norm, lightDir), 0.0);
let diffuse = diff * vec3<f32>(0.8, 0.9, 1.0);
// Fresnel
let toCameraVector = normalize(fragPos - uniforms.eyePos);
let reflec = normalize(reflect(toCameraVector, norm));
let n1 = 1.0;
let n2 = 1.33333;
let R0 = pow((n1 - n2) / (n1 + n2), 2.0);
let fresnel = R0 + (1.0 - R0) * pow((1.0 - dot(norm, reflec)), 5.0);
let oceanColor = vec3<f32>(0.0, 0.25, 0.35);
let skyColor = vec3<f32>(0.4, 0.6, 0.8);
// Subsurface scattering
let sssSun = vec3<f32>(0.0, -5.0, -7.0);
let tosssSunVec = normalize(sssSun - fragPos);
let tosssSun = normalize(vec3<f32>(0.0, -100.0, 1.0));
let ssDistortion = 0.1;
let sssIntensity = 1.0;
let halfWay = normalize(tosssSun + norm * ssDistortion);
let ssScateringCoef = pow(clamp(dot(toCameraVector, -halfWay), 0.0, 1.0), 5.0) * sssIntensity;
// Sun glittering
var glitterFactor = max(0.0, dot(tosssSunVec, reflect(-toCameraVector, norm)));
if (glitterFactor <= 0.98) {
glitterFactor = 0.0;
}
let lightColor = vec3<f32>(1.0, 1.0, 1.0);
let ambientColor = vec3<f32>(0.1, 0.15, 0.2);
let finalColor = ambientColor +
diffuse * 0.4 +
mix(oceanColor * (1.0 + ssScateringCoef), skyColor * 0.5, fresnel * 0.7) +
lightColor * glitterFactor * 0.5;
return vec4<f32>(clamp(finalColor, vec3<f32>(0.0), vec3<f32>(1.0)), 1.0);
}
`;
// Skybox vertex shader
export const skyboxVertexShader = `
struct Uniforms {
view: mat4x4<f32>,
projection: mat4x4<f32>,
sunDirection: vec3<f32>,
};
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) rayDir: vec3<f32>,
};
@vertex
fn main(@location(0) position: vec3<f32>) -> VertexOutput {
var output: VertexOutput;
output.rayDir = position;
// Remove translation from view matrix
var rotView = uniforms.view;
rotView[3] = vec4<f32>(0.0, 0.0, 0.0, 1.0);
let pos = uniforms.projection * rotView * vec4<f32>(position, 1.0);
output.position = pos;
return output;
}
`;
// Skybox fragment shader
export const skyboxFragmentShader = `
struct Uniforms {
view: mat4x4<f32>,
projection: mat4x4<f32>,
sunDirection: vec3<f32>,
};
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@fragment
fn main(@location(0) rayDir: vec3<f32>) -> @location(0) vec4<f32> {
let ray = normalize(rayDir);
let upAmount = ray.z;
// Sky gradient
let horizonBlend = pow(1.0 - max(upAmount, 0.0), 2.0);
let zenithColor = vec3<f32>(0.15, 0.35, 0.75);
let horizonColor = vec3<f32>(0.55, 0.7, 0.9);
var skyColor = mix(zenithColor, horizonColor, horizonBlend);
// Horizon glow
let horizonGlow = pow(max(1.0 - abs(upAmount), 0.0), 6.0);
skyColor = skyColor + vec3<f32>(0.4, 0.25, 0.1) * horizonGlow * 0.4;
// Sun
let sunDir = normalize(uniforms.sunDirection);
let sunAngle = max(dot(ray, sunDir), 0.0);
let sunDisk = smoothstep(0.9993, 0.9998, sunAngle);
let sunColor = vec3<f32>(1.0, 0.95, 0.85);
let sunGlow = pow(sunAngle, 48.0) * 0.6;
let sunHalo = pow(sunAngle, 6.0) * 0.25;
skyColor = skyColor + sunColor * sunDisk * 3.0;
skyColor = skyColor + vec3<f32>(1.0, 0.85, 0.5) * sunGlow;
skyColor = skyColor + vec3<f32>(1.0, 0.9, 0.7) * sunHalo;
// Below horizon
if (upAmount < 0.0) {
let depth = -upAmount;
let deepColor = vec3<f32>(0.02, 0.08, 0.15);
skyColor = mix(horizonColor * 0.7, deepColor, smoothstep(0.0, 0.5, depth));
}
return vec4<f32>(skyColor, 1.0);
}
`;

View File

@@ -3,7 +3,6 @@
"target": "ES2020", "target": "ES2020",
"module": "ESNext", "module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"], "lib": ["ES2020", "DOM", "DOM.Iterable"],
"types": ["@webgpu/types"],
"sourceMap": true, "sourceMap": true,
"outDir": "./build/", "outDir": "./build/",
"strict": true, "strict": true,
@@ -15,5 +14,5 @@
"skipLibCheck": true "skipLibCheck": true
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "dist", "build", "src/main_webgl.ts"] "exclude": ["node_modules", "dist", "build"]
} }