Upload index.html
Browse files- index.html +98 -39
index.html
CHANGED
|
@@ -51,7 +51,7 @@ const scl = (a,s)=>[a[0]*s,a[1]*s,a[2]*s];
|
|
| 51 |
const cross = (a,b)=>[a[1]*b[2]-a[2]*b[1], a[2]*b[0]-a[0]*b[2], a[0]*b[1]-a[1]*b[0]];
|
| 52 |
const dot = (a,b)=>a[0]*b[0]+a[1]*b[1]+a[2]*b[2];
|
| 53 |
const norm = v=>{const l=Math.hypot(v[0],v[1],v[2])||1;return [v[0]/l,v[1]/l,v[2]/l];};
|
| 54 |
-
|
| 55 |
|
| 56 |
function perspective(fov, aspect, near, far){
|
| 57 |
const f = 1/Math.tan(fov/2);
|
|
@@ -103,7 +103,7 @@ function waveH(x, z, t){
|
|
| 103 |
is there at all. The profile below is mirrored in the shader, so the boat sits on the same
|
| 104 |
ground it is drawn on. */
|
| 105 |
const ICELL = 48, MAX_ISLANDS = 48, MAXR = 70; // a 170 m grid plus the reach of the largest island
|
| 106 |
-
const LAND = { beach: 0.
|
| 107 |
let SEED = 1337;
|
| 108 |
|
| 109 |
function ihash(n){
|
|
@@ -137,12 +137,13 @@ function islandAt(cx, cz){
|
|
| 137 |
const h = ihash((Math.imul(cx, 73856093) + Math.imul(cz, 19349153) + SEED) >>> 0);
|
| 138 |
const low = h & 255;
|
| 139 |
let type = 0;
|
| 140 |
-
if(low <
|
| 141 |
-
else if(low <
|
| 142 |
else if(low >= 96) return null; // most of the world is open sea
|
| 143 |
const s = ihash(h) & 0xFFFFFF;
|
| 144 |
const rb = s & 63, pk = (s >> 6) & 15, sh = (s >> 10) & 15, rg = (s >> 14) & 15;
|
| 145 |
-
|
|
|
|
| 146 |
return {
|
| 147 |
// x, z and r are stored as float32 values because that is what the shader receives. Otherwise
|
| 148 |
// the two sides can land on different lattice cells and disagree about the island's shape.
|
|
@@ -280,14 +281,18 @@ float landH(vec2 p){
|
|
| 280 |
const LOA = 4.06, BEAM = 1.15; // hull footprint, in world units
|
| 281 |
const SIT = 0.06; // how high the boat sits on the waterline
|
| 282 |
const DRAFT = 0.625; // keel depth below the origin - the boat must stay wet
|
| 283 |
-
// Heave:
|
| 284 |
-
//
|
| 285 |
-
//
|
| 286 |
-
|
| 287 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
// On land there is nothing to ring. Both springs go critically damped, and the tilt comes from the
|
| 289 |
// ground under the hull instead of from the waterplane.
|
| 290 |
-
const GROUND = {heave:{period:0.
|
| 291 |
|
| 292 |
const STATIONS = [];
|
| 293 |
(function(){
|
|
@@ -297,14 +302,34 @@ const STATIONS = [];
|
|
| 297 |
|
| 298 |
// what is actually under a point: the ground if it is above sea level, otherwise the sea.
|
| 299 |
// Same rule the mesh draws with, which is what keeps the boat on the surface it is sitting on.
|
| 300 |
-
|
|
|
|
| 301 |
|
| 302 |
// average height under the footprint. u runs along the keel, v across the beam.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
function hullH(x, z, heading, t){
|
| 304 |
const ax = Math.sin(heading), az = Math.cos(heading);
|
| 305 |
-
let sum = 0;
|
| 306 |
-
for(const s of STATIONS)
|
| 307 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
}
|
| 309 |
// highest ground under the footprint. A rigid flat-bottomed hull rests on the high point of a
|
| 310 |
// slope rather than on the average of it, so grounding has to use the max, not the mean.
|
|
@@ -314,14 +339,35 @@ function hullLand(x, z, heading){
|
|
| 314 |
for(const s of STATIONS) m = Math.max(m, landH(x + ax*s[0] + az*s[1], z + az*s[0] - ax*s[1]));
|
| 315 |
return m;
|
| 316 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
|
| 318 |
// slope of that averaged water: the boat tips by the difference across its own length/beam
|
| 319 |
function hullSlope(x, z, heading, t){
|
| 320 |
const ax = Math.sin(heading), az = Math.cos(heading);
|
| 321 |
-
const fore =
|
| 322 |
-
const aft =
|
| 323 |
-
const star =
|
| 324 |
-
const port =
|
| 325 |
return [(fore - aft) / LOA, (star - port) / BEAM];
|
| 326 |
}
|
| 327 |
|
|
@@ -374,7 +420,7 @@ uniform mat4 uProj, uView;
|
|
| 374 |
uniform vec2 uOrigin;
|
| 375 |
uniform float uTime, uSpeed, uPopAge;
|
| 376 |
uniform vec3 uBoat, uFwd, uPopPos;
|
| 377 |
-
out vec3 vWorld; out vec3 vNormal; out float vFoam; out float vLand, vDepth;
|
| 378 |
${WAVES_GLSL}
|
| 379 |
${NOISE_GLSL}
|
| 380 |
${ISLANDS_GLSL}
|
|
@@ -407,19 +453,24 @@ void main(){
|
|
| 407 |
// The sea and the ground are not the same kind of surface. The wave only exists where there is
|
| 408 |
// water: once the ground is above sea level it is solid and does not move with the swell. Using
|
| 409 |
// max(land, water) here made low land near the shore ride the waves like a liquid.
|
|
|
|
|
|
|
|
|
|
| 410 |
float land = landH(w);
|
| 411 |
float water = waveH(w, uTime) - dip + ripple;
|
| 412 |
-
|
|
|
|
| 413 |
vec3 wp = vec3(w.x, h, w.y);
|
| 414 |
vWorld = wp;
|
| 415 |
vLand = land;
|
| 416 |
vDepth = water - land; // depth of water over the seabed
|
|
|
|
| 417 |
vNormal = waveN(w, uTime); // land normals come from the facets in the fragment stage
|
| 418 |
-
vFoam = clamp((wake + hullFoam + popFoam) * (
|
| 419 |
gl_Position = uProj * uView * vec4(wp, 1.0);
|
| 420 |
}`, `#version 300 es
|
| 421 |
precision highp float;
|
| 422 |
-
in vec3 vWorld; in vec3 vNormal; in float vFoam; in float vLand, vDepth;
|
| 423 |
uniform vec3 uCam;
|
| 424 |
uniform float uTime;
|
| 425 |
uniform vec3 uSand, uGrass, uRock;
|
|
@@ -433,7 +484,7 @@ void main(){
|
|
| 433 |
// or water decision is made per fragment from interpolated values, so a triangle that straddles
|
| 434 |
// the shoreline is split diagonally between the two materials, and on a tall island that split
|
| 435 |
// runs most of the way up the cliff. Deciding from the facet keeps a cliff one material.
|
| 436 |
-
bool isLand =
|
| 437 |
if(isLand) n = fn;
|
| 438 |
if(n.y < 0.0) n = -n;
|
| 439 |
vec3 L = normalize(vec3(0.5, 0.75, -0.45));
|
|
@@ -652,7 +703,7 @@ for(let i=0;i<MAX_ISLANDS;i++) islLoc.push(gl.getUniformLocation(waterP.p, "uIsl
|
|
| 652 |
const puffBuf = gl.createBuffer();
|
| 653 |
const skyBuf = gl.createBuffer();
|
| 654 |
const maxIndex = (N+1)*(N+1) - 1;
|
| 655 |
-
if (maxIndex > 65535) console.
|
| 656 |
|
| 657 |
// attribute state is applied per draw instead of cached in a VAO
|
| 658 |
function setAttribs(pr, layout, stride){
|
|
@@ -789,7 +840,7 @@ function buildBoat(rudder, t, heat){
|
|
| 789 |
// the rub rail, which is what the plain stripe quad used to be standing in for.
|
| 790 |
const wAt = z => z < 0 ? 0.42 + 0.04*Math.sin(clamp((z+1.55)/1.55,0,1)*1.5708)
|
| 791 |
: 0.46*Math.pow(1 - clamp(z/1.45,0,1), 1.35);
|
| 792 |
-
const dAt = z => 0.
|
| 793 |
const bAt = z => -0.50 + 0.34*Math.pow(clamp((z+1.55)/3.2,0,1), 2.2);
|
| 794 |
const ring = z => {
|
| 795 |
const w = wAt(z), d = dAt(z), b = bAt(z), sy = d - 0.07, by = b + 0.16*(d-b);
|
|
@@ -820,8 +871,8 @@ function buildBoat(rudder, t, heat){
|
|
| 820 |
// the pipes are what hold the boiler up: they come out of its underside, dip below the
|
| 821 |
// waterline just aft of it - that dip is the U bend the water sits in - and the mouth
|
| 822 |
// stays submerged at the back.
|
| 823 |
-
poly([[side*0.12, by-0.01, bz-0.02], [side*0.15, 0.
|
| 824 |
-
[side*0.
|
| 825 |
}
|
| 826 |
|
| 827 |
// the candle stands on the deck under the boiler and slides forward towards it, so at full
|
|
@@ -1059,7 +1110,7 @@ function reset(){
|
|
| 1059 |
|
| 1060 |
function spawnPuff(model, origin, fwd, side, heat){
|
| 1061 |
if(puffs.length > 160) return;
|
| 1062 |
-
const lp = [side*0.
|
| 1063 |
const wp = [
|
| 1064 |
origin[0] + model[0]*lp[0] + model[4]*lp[1] + model[8]*lp[2],
|
| 1065 |
origin[1] + model[1]*lp[0] + model[5]*lp[1] + model[9]*lp[2],
|
|
@@ -1097,10 +1148,16 @@ function frame(nowMs){
|
|
| 1097 |
const maxSpeed = 6.0, idleThrust = 0.35; // the candle is always heating, so it always pulls a little
|
| 1098 |
|
| 1099 |
/* --- what is under the hull --- */
|
| 1100 |
-
const
|
| 1101 |
-
const surf =
|
| 1102 |
-
|
| 1103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1104 |
|
| 1105 |
// A putt-putt pushes against water. Out of the water it only spins, so the engine is scaled by how
|
| 1106 |
// much of the hull is still wet. What moves a grounded boat is gravity along the hull - and since
|
|
@@ -1121,7 +1178,7 @@ function frame(nowMs){
|
|
| 1121 |
let nz = boat.z + Math.cos(boat.heading) * boat.speed * dt;
|
| 1122 |
// It may sit on the beach, but it cannot climb. The motion into the land is removed and the rest
|
| 1123 |
// slides along the contour, which is what stops it sticking fast in a cove.
|
| 1124 |
-
if(
|
| 1125 |
const g = norm([landGrad(nx, nz)[0], 0, landGrad(nx, nz)[1]]);
|
| 1126 |
const mv = [nx - boat.x, 0, nz - boat.z];
|
| 1127 |
const into = dot(mv, g);
|
|
@@ -1134,14 +1191,16 @@ function frame(nowMs){
|
|
| 1134 |
|
| 1135 |
/* --- float: the hull feels the average surface over its footprint, not one point --- */
|
| 1136 |
// The boat cannot sink through the bottom, so the target is the higher of normal buoyancy and the
|
| 1137 |
-
// ground plus the draft
|
| 1138 |
-
// the
|
| 1139 |
-
|
|
|
|
|
|
|
| 1140 |
const hs = wetted < 0.5 ? GROUND.heave : HEAVE;
|
| 1141 |
const ts = wetted < 0.5 ? GROUND.tilt : TILT;
|
| 1142 |
[boat.bob, boat.bobV] = spring(boat.bob, boat.bobV, floatTarget, dt, hs.period, hs.zeta);
|
| 1143 |
-
[boat.tiltX, boat.tiltXV] = spring(boat.tiltX, boat.tiltXV, sx, dt, ts.period, ts.zeta);
|
| 1144 |
-
[boat.tiltZ, boat.tiltZV] = spring(boat.tiltZ, boat.tiltZV, sz, dt, ts.period, ts.zeta);
|
| 1145 |
const up = norm([-boat.tiltX, 1, -boat.tiltZ]);
|
| 1146 |
const heading = [Math.sin(boat.heading), 0, Math.cos(boat.heading)];
|
| 1147 |
const fwd = norm(sub(heading, scl(up, dot(heading, up))));
|
|
|
|
| 51 |
const cross = (a,b)=>[a[1]*b[2]-a[2]*b[1], a[2]*b[0]-a[0]*b[2], a[0]*b[1]-a[1]*b[0]];
|
| 52 |
const dot = (a,b)=>a[0]*b[0]+a[1]*b[1]+a[2]*b[2];
|
| 53 |
const norm = v=>{const l=Math.hypot(v[0],v[1],v[2])||1;return [v[0]/l,v[1]/l,v[2]/l];};
|
| 54 |
+
function clamp(v, a, b){ return Math.min(b, Math.max(a, v)); }
|
| 55 |
|
| 56 |
function perspective(fov, aspect, near, far){
|
| 57 |
const f = 1/Math.tan(fov/2);
|
|
|
|
| 103 |
is there at all. The profile below is mirrored in the shader, so the boat sits on the same
|
| 104 |
ground it is drawn on. */
|
| 105 |
const ICELL = 48, MAX_ISLANDS = 48, MAXR = 70; // a 170 m grid plus the reach of the largest island
|
| 106 |
+
const LAND = { beach: 0.02, shelf: 1.2, deep: 6.0 }; // beach: how far up the shore the boat may sit
|
| 107 |
let SEED = 1337;
|
| 108 |
|
| 109 |
function ihash(n){
|
|
|
|
| 137 |
const h = ihash((Math.imul(cx, 73856093) + Math.imul(cz, 19349153) + SEED) >>> 0);
|
| 138 |
const low = h & 255;
|
| 139 |
let type = 0;
|
| 140 |
+
if(low < 3) type = 1; // rare: a large flattish island
|
| 141 |
+
else if(low < 6) return null; // pinnacles removed; these cells stay open sea
|
| 142 |
else if(low >= 96) return null; // most of the world is open sea
|
| 143 |
const s = ihash(h) & 0xFFFFFF;
|
| 144 |
const rb = s & 63, pk = (s >> 6) & 15, sh = (s >> 10) & 15, rg = (s >> 14) & 15;
|
| 145 |
+
let r = F32(14 + rb*0.25); // common islet
|
| 146 |
+
if(type === 1) r = F32(50 + rb*0.31); // large flattish island
|
| 147 |
return {
|
| 148 |
// x, z and r are stored as float32 values because that is what the shader receives. Otherwise
|
| 149 |
// the two sides can land on different lattice cells and disagree about the island's shape.
|
|
|
|
| 281 |
const LOA = 4.06, BEAM = 1.15; // hull footprint, in world units
|
| 282 |
const SIT = 0.06; // how high the boat sits on the waterline
|
| 283 |
const DRAFT = 0.625; // keel depth below the origin - the boat must stay wet
|
| 284 |
+
// Heave: a light 4 m hull has a natural heave period well under a second, so it should follow the
|
| 285 |
+
// surface closely. The hull-averaged waterplane is already what removes the chop - the spring should
|
| 286 |
+
// not have to do it as well, which is why the period is short and the damping only moderate. A
|
| 287 |
+
// critically damped spring cannot overshoot, so it can never settle onto a crest the way a real
|
| 288 |
+
// light boat does; zeta under 1 is what lets it ring once or twice and come to rest.
|
| 289 |
+
const HEAVE = {period: 0.45, zeta: 0.40};
|
| 290 |
+
// Pitch and roll have different natural periods on a small hull. Roll is quick: the waterplane slope
|
| 291 |
+
// across a 1.15 m beam changes faster than the slope along the length. Pitch lags more.
|
| 292 |
+
const TILT = {roll:{period:0.6, zeta:0.40}, pitch:{period:1.2, zeta:0.45}};
|
| 293 |
// On land there is nothing to ring. Both springs go critically damped, and the tilt comes from the
|
| 294 |
// ground under the hull instead of from the waterplane.
|
| 295 |
+
const GROUND = {heave:{period:0.4, zeta:1.0}, tilt:{roll:{period:1.0, zeta:1.0}, pitch:{period:1.2, zeta:1.0}}};
|
| 296 |
|
| 297 |
const STATIONS = [];
|
| 298 |
(function(){
|
|
|
|
| 302 |
|
| 303 |
// what is actually under a point: the ground if it is above sea level, otherwise the sea.
|
| 304 |
// Same rule the mesh draws with, which is what keeps the boat on the surface it is sitting on.
|
| 305 |
+
// Land only counts where the ground is above the water, because that is the land you can see.
|
| 306 |
+
function surfaceH(x, z, t){ const g = landH(x, z), w = waveH(x, z, t); return Math.max(g, w); }
|
| 307 |
|
| 308 |
// average height under the footprint. u runs along the keel, v across the beam.
|
| 309 |
+
// The average is weighted towards the middle of the waterplane. A flat average over the whole
|
| 310 |
+
// footprint is what a rigid hull would do for long swell, but it cancels nearly everything for a wave
|
| 311 |
+
// about the length of the boat - that is what was making it read as half-buoyant. A real hull is not
|
| 312 |
+
// rigid: the middle carries the motion and the ends ride over the top of it, so the weight falls off
|
| 313 |
+
// towards the ends. Long swell still averages to the local height, so that part is unchanged.
|
| 314 |
function hullH(x, z, heading, t){
|
| 315 |
const ax = Math.sin(heading), az = Math.cos(heading);
|
| 316 |
+
let sum = 0, wsum = 0;
|
| 317 |
+
for(const s of STATIONS){
|
| 318 |
+
const w = (1 - (s[0]/(LOA/2))**2) * (1 - (s[1]/(BEAM/2))**2);
|
| 319 |
+
if(w <= 0) continue;
|
| 320 |
+
sum += w * surfaceH(x + ax*s[0] + az*s[1], z + az*s[0] - ax*s[1], t);
|
| 321 |
+
wsum += w;
|
| 322 |
+
}
|
| 323 |
+
return sum / wsum;
|
| 324 |
+
}
|
| 325 |
+
// The highest water over the footprint. A rigid hull rides the mean of the waterplane, but the deck
|
| 326 |
+
// is not allowed to be under the sea, so the heave target is pulled a little towards the high point
|
| 327 |
+
// instead of being the pure mean. That is what stops a crest under the stern swallowing the deck.
|
| 328 |
+
function hullTop(x, z, heading, t){
|
| 329 |
+
const ax = Math.sin(heading), az = Math.cos(heading);
|
| 330 |
+
let m = -1e9;
|
| 331 |
+
for(const s of STATIONS) m = Math.max(m, waveH(x + ax*s[0] + az*s[1], z + az*s[0] - ax*s[1], t));
|
| 332 |
+
return m;
|
| 333 |
}
|
| 334 |
// highest ground under the footprint. A rigid flat-bottomed hull rests on the high point of a
|
| 335 |
// slope rather than on the average of it, so grounding has to use the max, not the mean.
|
|
|
|
| 339 |
for(const s of STATIONS) m = Math.max(m, landH(x + ax*s[0] + az*s[1], z + az*s[0] - ax*s[1]));
|
| 340 |
return m;
|
| 341 |
}
|
| 342 |
+
// Is there land above the water at a point? Same test the mesh uses to decide what to draw, so the
|
| 343 |
+
// boat only meets land it can see. It is checked at the boat itself rather than as the highest point
|
| 344 |
+
// over the whole footprint - that stopped a 4 m hull about two metres before the water's edge. The
|
| 345 |
+
// boat can now nose onto the beach and slide back off.
|
| 346 |
+
function shoreAt(x, z, t){ return landH(x, z) - waveH(x, z, t); }
|
| 347 |
+
|
| 348 |
+
// same window, water only. A floating boat is tipped by the waterplane, not by ground under it - the
|
| 349 |
+
// version above mixes land into the average, so a corner that sees land pulls the whole tilt up. When
|
| 350 |
+
// the boat is actually aground the ground slope is used instead.
|
| 351 |
+
function hullWater(x, z, heading, t){
|
| 352 |
+
const ax = Math.sin(heading), az = Math.cos(heading);
|
| 353 |
+
let sum = 0, wsum = 0;
|
| 354 |
+
for(const s of STATIONS){
|
| 355 |
+
const w = (1 - (s[0]/(LOA/2))**2) * (1 - (s[1]/(BEAM/2))**2);
|
| 356 |
+
if(w <= 0) continue;
|
| 357 |
+
const px = x + ax*s[0] + az*s[1], pz = z + az*s[0] - ax*s[1];
|
| 358 |
+
sum += w * waveH(px, pz, t);
|
| 359 |
+
wsum += w;
|
| 360 |
+
}
|
| 361 |
+
return sum / wsum;
|
| 362 |
+
}
|
| 363 |
|
| 364 |
// slope of that averaged water: the boat tips by the difference across its own length/beam
|
| 365 |
function hullSlope(x, z, heading, t){
|
| 366 |
const ax = Math.sin(heading), az = Math.cos(heading);
|
| 367 |
+
const fore = hullWater(x + ax*LOA/2, z + az*LOA/2, heading, t);
|
| 368 |
+
const aft = hullWater(x - ax*LOA/2, z - az*LOA/2, heading, t);
|
| 369 |
+
const star = hullWater(x + az*BEAM/2, z - ax*BEAM/2, heading, t);
|
| 370 |
+
const port = hullWater(x - az*BEAM/2, z + ax*BEAM/2, heading, t);
|
| 371 |
return [(fore - aft) / LOA, (star - port) / BEAM];
|
| 372 |
}
|
| 373 |
|
|
|
|
| 420 |
uniform vec2 uOrigin;
|
| 421 |
uniform float uTime, uSpeed, uPopAge;
|
| 422 |
uniform vec3 uBoat, uFwd, uPopPos;
|
| 423 |
+
out vec3 vWorld; out vec3 vNormal; out float vFoam; out float vLand, vDepth; flat out int vIsLand;
|
| 424 |
${WAVES_GLSL}
|
| 425 |
${NOISE_GLSL}
|
| 426 |
${ISLANDS_GLSL}
|
|
|
|
| 453 |
// The sea and the ground are not the same kind of surface. The wave only exists where there is
|
| 454 |
// water: once the ground is above sea level it is solid and does not move with the swell. Using
|
| 455 |
// max(land, water) here made low land near the shore ride the waves like a liquid.
|
| 456 |
+
// Land only where the ground is above the water. Below the surface the ground is not drawn at all,
|
| 457 |
+
// so the boat must not be stopped by it, and the coastline becomes where the water actually meets
|
| 458 |
+
// the land rather than where a fixed height is crossed.
|
| 459 |
float land = landH(w);
|
| 460 |
float water = waveH(w, uTime) - dip + ripple;
|
| 461 |
+
bool isLand = land > water;
|
| 462 |
+
float h = isLand ? land : water;
|
| 463 |
vec3 wp = vec3(w.x, h, w.y);
|
| 464 |
vWorld = wp;
|
| 465 |
vLand = land;
|
| 466 |
vDepth = water - land; // depth of water over the seabed
|
| 467 |
+
vIsLand = isLand ? 1 : 0;
|
| 468 |
vNormal = waveN(w, uTime); // land normals come from the facets in the fragment stage
|
| 469 |
+
vFoam = clamp((wake + hullFoam + popFoam) * (isLand ? 0.0 : 1.0), 0.0, 1.0);
|
| 470 |
gl_Position = uProj * uView * vec4(wp, 1.0);
|
| 471 |
}`, `#version 300 es
|
| 472 |
precision highp float;
|
| 473 |
+
in vec3 vWorld; in vec3 vNormal; in float vFoam; in float vLand, vDepth; flat in int vIsLand;
|
| 474 |
uniform vec3 uCam;
|
| 475 |
uniform float uTime;
|
| 476 |
uniform vec3 uSand, uGrass, uRock;
|
|
|
|
| 484 |
// or water decision is made per fragment from interpolated values, so a triangle that straddles
|
| 485 |
// the shoreline is split diagonally between the two materials, and on a tall island that split
|
| 486 |
// runs most of the way up the cliff. Deciding from the facet keeps a cliff one material.
|
| 487 |
+
bool isLand = vIsLand > 0 || fn.y < 0.12;
|
| 488 |
if(isLand) n = fn;
|
| 489 |
if(n.y < 0.0) n = -n;
|
| 490 |
vec3 L = normalize(vec3(0.5, 0.75, -0.45));
|
|
|
|
| 703 |
const puffBuf = gl.createBuffer();
|
| 704 |
const skyBuf = gl.createBuffer();
|
| 705 |
const maxIndex = (N+1)*(N+1) - 1;
|
| 706 |
+
if (maxIndex > 65535) console.error("index buffer too big for Uint16");
|
| 707 |
|
| 708 |
// attribute state is applied per draw instead of cached in a VAO
|
| 709 |
function setAttribs(pr, layout, stride){
|
|
|
|
| 840 |
// the rub rail, which is what the plain stripe quad used to be standing in for.
|
| 841 |
const wAt = z => z < 0 ? 0.42 + 0.04*Math.sin(clamp((z+1.55)/1.55,0,1)*1.5708)
|
| 842 |
: 0.46*Math.pow(1 - clamp(z/1.45,0,1), 1.35);
|
| 843 |
+
const dAt = z => 0.22 + 0.14*Math.pow(clamp((z+1.55)/3.2,0,1), 1.7); // freeboard at the transom raised
|
| 844 |
const bAt = z => -0.50 + 0.34*Math.pow(clamp((z+1.55)/3.2,0,1), 2.2);
|
| 845 |
const ring = z => {
|
| 846 |
const w = wAt(z), d = dAt(z), b = bAt(z), sy = d - 0.07, by = b + 0.16*(d-b);
|
|
|
|
| 871 |
// the pipes are what hold the boiler up: they come out of its underside, dip below the
|
| 872 |
// waterline just aft of it - that dip is the U bend the water sits in - and the mouth
|
| 873 |
// stays submerged at the back.
|
| 874 |
+
poly([[side*0.12, by-0.01, bz-0.02], [side*0.15, 0.10, -0.14], [side*0.19, -0.16, -0.62],
|
| 875 |
+
[side*0.22, -0.26, -1.42], [side*0.24, -0.34, -1.82]], 0.035, PAL.metal, 0, 1);
|
| 876 |
}
|
| 877 |
|
| 878 |
// the candle stands on the deck under the boiler and slides forward towards it, so at full
|
|
|
|
| 1110 |
|
| 1111 |
function spawnPuff(model, origin, fwd, side, heat){
|
| 1112 |
if(puffs.length > 160) return;
|
| 1113 |
+
const lp = [side*0.24*BOAT_SCALE, -0.34*BOAT_SCALE, -1.82*BOAT_SCALE]; // the submerged tube mouth
|
| 1114 |
const wp = [
|
| 1115 |
origin[0] + model[0]*lp[0] + model[4]*lp[1] + model[8]*lp[2],
|
| 1116 |
origin[1] + model[1]*lp[0] + model[5]*lp[1] + model[9]*lp[2],
|
|
|
|
| 1148 |
const maxSpeed = 6.0, idleThrust = 0.35; // the candle is always heating, so it always pulls a little
|
| 1149 |
|
| 1150 |
/* --- what is under the hull --- */
|
| 1151 |
+
const landAt = landH(boat.x, boat.z); // ground under the boat itself
|
| 1152 |
+
const surf = hullWater(boat.x, boat.z, boat.heading, t); // the waterplane, not the ground under it
|
| 1153 |
+
// The boat only meets ground that is above the water, because that is the land the mesh draws. Under
|
| 1154 |
+
// the surface the seabed is invisible, so it must neither stop the boat nor tip it. A hull in half a
|
| 1155 |
+
// metre of water over an unseen bottom was being treated as aground, and the tilt came from the slope
|
| 1156 |
+
// of ground you cannot see - which is the lean you are seeing close to shores.
|
| 1157 |
+
const shore = shoreAt(boat.x, boat.z, t);
|
| 1158 |
+
const grounded = shore > LAND.beach;
|
| 1159 |
+
const wetted = grounded ? 0 : 1;
|
| 1160 |
+
boat.aground = grounded; // dry land: nothing to hiss against
|
| 1161 |
|
| 1162 |
// A putt-putt pushes against water. Out of the water it only spins, so the engine is scaled by how
|
| 1163 |
// much of the hull is still wet. What moves a grounded boat is gravity along the hull - and since
|
|
|
|
| 1178 |
let nz = boat.z + Math.cos(boat.heading) * boat.speed * dt;
|
| 1179 |
// It may sit on the beach, but it cannot climb. The motion into the land is removed and the rest
|
| 1180 |
// slides along the contour, which is what stops it sticking fast in a cove.
|
| 1181 |
+
if(shoreAt(nx, nz, t) > LAND.beach){
|
| 1182 |
const g = norm([landGrad(nx, nz)[0], 0, landGrad(nx, nz)[1]]);
|
| 1183 |
const mv = [nx - boat.x, 0, nz - boat.z];
|
| 1184 |
const into = dot(mv, g);
|
|
|
|
| 1191 |
|
| 1192 |
/* --- float: the hull feels the average surface over its footprint, not one point --- */
|
| 1193 |
// The boat cannot sink through the bottom, so the target is the higher of normal buoyancy and the
|
| 1194 |
+
// ground plus the draft - but only where the ground is above sea level. The seabed is never drawn:
|
| 1195 |
+
// the water is opaque and the surface mesh follows the waves, not the bottom, so a hull that grounds
|
| 1196 |
+
// on a seabed a metre below the surface reads as floating above invisible ground. Only land the mesh
|
| 1197 |
+
// actually draws is physical.
|
| 1198 |
+
const floatTarget = Math.max(surf + 0.12*(hullTop(boat.x, boat.z, boat.heading, t) - surf) + SIT, grounded ? landAt + DRAFT : -1e9);
|
| 1199 |
const hs = wetted < 0.5 ? GROUND.heave : HEAVE;
|
| 1200 |
const ts = wetted < 0.5 ? GROUND.tilt : TILT;
|
| 1201 |
[boat.bob, boat.bobV] = spring(boat.bob, boat.bobV, floatTarget, dt, hs.period, hs.zeta);
|
| 1202 |
+
[boat.tiltX, boat.tiltXV] = spring(boat.tiltX, boat.tiltXV, sx, dt, ts.roll.period, ts.roll.zeta);
|
| 1203 |
+
[boat.tiltZ, boat.tiltZV] = spring(boat.tiltZ, boat.tiltZV, sz, dt, ts.pitch.period, ts.pitch.zeta);
|
| 1204 |
const up = norm([-boat.tiltX, 1, -boat.tiltZ]);
|
| 1205 |
const heading = [Math.sin(boat.heading), 0, Math.cos(boat.heading)];
|
| 1206 |
const fwd = norm(sub(heading, scl(up, dot(heading, up))));
|