One commited on
Commit
a5b4831
·
verified ·
1 Parent(s): 5f99515

Upload index.html

Browse files
Files changed (1) hide show
  1. 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
- const clamp = (v,a,b)=>v<a?a:v>b?b:v;
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.35, 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,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 < 2) type = 1; // rare: a large flattish island
141
- else if(low < 5) type = 2; // rarer: a tall narrow pinnacle
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
- const r = F32(type === 1 ? 50 + rb*0.31 : type === 2 ? 9 + rb*0.14 : 14 + rb*0.25);
 
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: slow enough to bridge the small ripples, but not so slow that the boat
284
- // visibly floats above the water it is supposed to be sitting in. A little ringing
285
- // (zeta < 1) is what gives the settle you were missing.
286
- const HEAVE = {period: 1.2, zeta: 0.7};
287
- const TILT = {period: 3.0, zeta: 0.9}; // lags the water: rides the swell, ignores the chop
 
 
 
 
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.5, zeta:1.0}, tilt:{period:1.2, zeta:1.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
- function surfaceH(x, z, t){ const g = landH(x, z); return g > 0 ? g : waveH(x, z, t); }
 
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) sum += surfaceH(x + ax*s[0] + az*s[1], z + az*s[0] - ax*s[1], t);
307
- return sum / STATIONS.length;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = hullH(x + ax*LOA/2, z + az*LOA/2, heading, t);
322
- const aft = hullH(x - ax*LOA/2, z - az*LOA/2, heading, t);
323
- const star = hullH(x + az*BEAM/2, z - ax*BEAM/2, heading, t);
324
- const port = hullH(x - az*BEAM/2, z + ax*BEAM/2, heading, t);
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
- float h = land > 0.0 ? land : water;
 
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) * (land > 0.0 ? 0.0 : 1.0), 0.0, 1.0);
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 = vLand > 0.0 || fn.y < 0.12;
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.log("index buffer too big for Uint16");
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.14 + 0.22*Math.pow(clamp((z+1.55)/3.2,0,1), 1.7);
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.18, -0.08], [side*0.17, -0.02, -0.45],
824
- [side*0.17, -0.06, -1.25], [side*0.17, -0.14, -1.58]], 0.035, PAL.metal, 0, 1);
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.17*BOAT_SCALE, -0.14*BOAT_SCALE, -1.58*BOAT_SCALE]; // the submerged tube mouth
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 ground = hullLand(boat.x, boat.z, boat.heading); // highest ground under the footprint
1101
- const surf = hullH(boat.x, boat.z, boat.heading, t); // the same surface the mesh draws
1102
- const wetted = clamp((surf - ground) / DRAFT, 0, 1); // 1 = floating, 0 = hull sitting on the bottom
1103
- boat.aground = ground > 0.02; // dry land: nothing to hiss against
 
 
 
 
 
 
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(hullLand(nx, nz, boat.heading) > LAND.beach){
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. On open water the second term is far below the first and this is exactly
1138
- // the old buoyancy; it only bites when the water is shallower than the draft.
1139
- const floatTarget = Math.max(surf + SIT, ground + DRAFT);
 
 
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))));