Upload index.html
Browse files- index.html +105 -50
index.html
CHANGED
|
@@ -105,8 +105,8 @@ function waveH(x, z, t){
|
|
| 105 |
shader is handed the nearby ones as data, so the two can never disagree about whether land
|
| 106 |
is there at all. The profile below is mirrored in the shader, so the boat sits on the same
|
| 107 |
ground it is drawn on. */
|
| 108 |
-
const ICELL = 48, MAX_ISLANDS =
|
| 109 |
-
const LAND = { beach: 0.35, shelf: 1.2, deep: 6.0 }; // beach: how far up the shore the boat may sit
|
| 110 |
let SEED = 1337;
|
| 111 |
|
| 112 |
function ihash(n){
|
|
@@ -131,37 +131,56 @@ function lnoise(x, z){
|
|
| 131 |
return u + (v-u)*sz;
|
| 132 |
}
|
| 133 |
|
| 134 |
-
/* code packs
|
| 135 |
-
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
| 137 |
function islandAt(cx, cz){
|
| 138 |
const h = ihash((Math.imul(cx, 73856093) + Math.imul(cz, 19349153) + SEED) >>> 0);
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
const s = ihash(h) & 0xFFFFFF;
|
|
|
|
|
|
|
| 141 |
return {
|
| 142 |
x: (cx + 0.5 + ((h >>> 8 & 255) - 127.5)/256) * ICELL,
|
| 143 |
z: (cz + 0.5 + ((h >>> 16 & 255) - 127.5)/256) * ICELL,
|
| 144 |
-
r:
|
| 145 |
-
code: ((s >> 6) & 15) + ((s >> 10) & 15)*16 + ((s >> 14) & 15)*256
|
| 146 |
};
|
| 147 |
}
|
| 148 |
function decode(code){
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
}
|
| 151 |
|
| 152 |
function landH(x, z){
|
| 153 |
const cx = Math.floor(x/ICELL), cz = Math.floor(z/ICELL);
|
| 154 |
// starting below sea level is what makes the depth readable: outside an island there is no
|
| 155 |
// seabed, so it reads as deep water, and the shallow band around one is the shelf.
|
|
|
|
|
|
|
| 156 |
let h = -LAND.deep;
|
| 157 |
-
|
|
|
|
| 158 |
const isl = islandAt(cx+i, cz+j);
|
| 159 |
if(!isl) continue;
|
| 160 |
const d = Math.hypot(x - isl.x, z - isl.z);
|
| 161 |
if(d > isl.r) continue;
|
| 162 |
const p = decode(isl.code);
|
| 163 |
const k = 1 - d/isl.r; // monotone: downhill always leads back to the sea
|
| 164 |
-
const
|
|
|
|
| 165 |
h = Math.max(h, p.peak*terr - LAND.shelf); // the shelf is the shallow ring around an island
|
| 166 |
}
|
| 167 |
return h;
|
|
@@ -170,11 +189,12 @@ function landGrad(x, z){
|
|
| 170 |
const e = 0.30;
|
| 171 |
return [(landH(x+e, z) - landH(x-e, z))/(2*e), (landH(x, z+e) - landH(x, z-e))/(2*e)];
|
| 172 |
}
|
| 173 |
-
// every island that can reach a vertex of the water grid
|
| 174 |
function islandsNear(minX, maxX, minZ, maxZ){
|
| 175 |
const out = [];
|
| 176 |
-
|
| 177 |
-
|
|
|
|
| 178 |
const isl = islandAt(cx, cz);
|
| 179 |
if(isl) out.push(isl);
|
| 180 |
}
|
|
@@ -209,6 +229,7 @@ uint ihash(uint n){
|
|
| 209 |
}
|
| 210 |
float lhash(int x, int y){ return float(ihash(uint(x)*3419u + uint(y)*2147483647u) & 1023u) / 1023.0; }
|
| 211 |
float lnoise(vec2 p){
|
|
|
|
| 212 |
ivec2 i = ivec2(floor(p));
|
| 213 |
vec2 f = p - vec2(float(i.x), float(i.y));
|
| 214 |
f = f*f*(3.0-2.0*f);
|
|
@@ -222,11 +243,15 @@ float landH(vec2 p){
|
|
| 222 |
float d = length(p - isl.xy);
|
| 223 |
if(d > isl.z) continue;
|
| 224 |
float code = isl.w;
|
| 225 |
-
float
|
| 226 |
-
float
|
| 227 |
-
float
|
|
|
|
|
|
|
|
|
|
| 228 |
float k = 1.0 - d/isl.z;
|
| 229 |
-
float
|
|
|
|
| 230 |
h = max(h, peak*terr - 1.2);
|
| 231 |
}
|
| 232 |
return h;
|
|
@@ -388,7 +413,14 @@ out vec4 frag;
|
|
| 388 |
${NOISE_GLSL}
|
| 389 |
void main(){
|
| 390 |
vec3 n = normalize(vNormal);
|
| 391 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
if(n.y < 0.0) n = -n;
|
| 393 |
vec3 L = normalize(vec3(0.5, 0.75, -0.45));
|
| 394 |
float ndl = max(dot(n, L), 0.0);
|
|
@@ -398,13 +430,14 @@ void main(){
|
|
| 398 |
float tone = mix(floor(q)/4.0, q/4.0, 0.40);
|
| 399 |
|
| 400 |
vec3 col;
|
| 401 |
-
|
|
|
|
| 402 |
float flatness = clamp(n.y, 0.0, 1.0); // "flat" is a reserved word in GLSL ES 3.0
|
| 403 |
-
col = mix(uSand * 0.62, uSand, smoothstep(0.0, 0.30,
|
| 404 |
-
col = mix(col, uGrass, smoothstep(0.40, 1.10,
|
| 405 |
col = mix(col, uRock, smoothstep(0.55, 0.80, 1.0 - flatness));
|
| 406 |
col = col * (0.55 + 0.70 * tone);
|
| 407 |
-
col = mix(col, col * 1.10, (vnoise(vWorld.xz * 2.2) - 0.5) * 0.35 * smoothstep(0.2, 1.0,
|
| 408 |
} else {
|
| 409 |
vec3 deep = vec3(0.04,0.17,0.38);
|
| 410 |
vec3 mid = vec3(0.09,0.34,0.53);
|
|
@@ -436,8 +469,8 @@ void main(){
|
|
| 436 |
|
| 437 |
// wispy foam, built the same way as the clouds, plus a band where shallow water meets the shore
|
| 438 |
float wisps = smoothstep(0.42, 0.58, fbm(vWorld.xz * 1.2 + uTime * 0.02, 2));
|
| 439 |
-
float shore =
|
| 440 |
-
float f = clamp(vFoam * (0.35 + 0.55 * wisps) + shore, 0.0, 1.0);
|
| 441 |
col = mix(col, vec3(0.93,0.98,1.0), f);
|
| 442 |
|
| 443 |
float fog = smoothstep(14.0, 70.0, length(vWorld - uCam));
|
|
@@ -713,6 +746,27 @@ function buildBoat(rudder, t, heat){
|
|
| 713 |
};
|
| 714 |
const rod=(a,b,th,col,glow=0,mat=0)=>tube(a,b,th,6,col,glow,mat,true);
|
| 715 |
const limb=(a,b,r,col,mat=0)=>tube(a,b,r,6,col,0,mat,true);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 716 |
|
| 717 |
// ---- hull ----
|
| 718 |
// The profile is a function of position along the boat instead of a list of stations, so the
|
|
@@ -746,19 +800,26 @@ function buildBoat(rudder, t, heat){
|
|
| 746 |
for(let i=0;i<rr.length;i++) tri(rr[i][0], rr[(i+1)%rr.length][0], tip, rr[i][1], 0, 0, true);
|
| 747 |
|
| 748 |
// ---- boiler, candle, exhaust ----
|
| 749 |
-
const bz = 0.
|
| 750 |
-
|
| 751 |
-
cone(0, 0.42, bz, 0.12, 0.06, 9, PAL.brass, 0, 1);
|
| 752 |
-
rod([0,0.215,-0.40],[0,0.215,0.05], 0.012, [0.30,0.28,0.24], 0, 1); // the rail the candle slides along
|
| 753 |
-
const cz = -0.34 + 0.37*heat; // W/S pushes the candle towards the boiler
|
| 754 |
-
const flame = 0.06 + 0.05*heat + 0.012*Math.sin(t*11.0) + 0.008*Math.sin(t*23.0);
|
| 755 |
-
tube([0,0.22,cz],[0,0.34,cz], 0.04, 7, PAL.wax, 0, 0, true);
|
| 756 |
-
cone(0, 0.34, cz, 0.04, flame, 7, PAL.flame, 1.0, 0);
|
| 757 |
for(const side of [-1, 1]){
|
| 758 |
-
|
| 759 |
-
|
|
|
|
|
|
|
|
|
|
| 760 |
}
|
| 761 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 762 |
// ---- tiller and rudder ----
|
| 763 |
const ang = -rudder * 0.45;
|
| 764 |
const head = [0, 0.16, -1.58];
|
|
@@ -782,19 +843,11 @@ function buildBoat(rudder, t, heat){
|
|
| 782 |
sphere(PX, 0.70, -0.55, 0.095, PAL.skin, 0, 10, 6); // head
|
| 783 |
disc(PX, 0.79, -0.55, 0.15, 10, PAL.hat); // brim
|
| 784 |
cone(PX, 0.79, -0.55, 0.115, 0.10, 10, PAL.hat); // crown
|
| 785 |
-
//
|
| 786 |
-
//
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
const u = scl(d, 1/len);
|
| 791 |
-
const ca = Math.acos(clamp((L1*L1 + len*len - L2*L2)/(2*L1*len), -1, 1));
|
| 792 |
-
let bend = sub([-0.55,-0.55,-0.1], scl(u, dot([-0.55,-0.55,-0.1], u)));
|
| 793 |
-
if(Math.hypot(bend[0],bend[1],bend[2]) < 1e-4) bend = [0,-1,0]; else bend = norm(bend);
|
| 794 |
-
const elbow = add(shoulder, add(scl(u, L1*Math.cos(ca)), scl(bend, L1*Math.sin(ca))));
|
| 795 |
-
limb(shoulder, elbow, 0.05, PAL.jacket);
|
| 796 |
-
limb(elbow, hand, 0.042, PAL.skin);
|
| 797 |
-
sphere(hand[0], hand[1], hand[2], 0.045, PAL.skin, 0, 8, 4);
|
| 798 |
|
| 799 |
// ---- pennant ----
|
| 800 |
// the pole starts at the deck, not above it
|
|
@@ -981,7 +1034,7 @@ function reset(){
|
|
| 981 |
|
| 982 |
function spawnPuff(model, origin, fwd, side, heat){
|
| 983 |
if(puffs.length > 160) return;
|
| 984 |
-
const lp = [side*0.
|
| 985 |
const wp = [
|
| 986 |
origin[0] + model[0]*lp[0] + model[4]*lp[1] + model[8]*lp[2],
|
| 987 |
origin[1] + model[1]*lp[0] + model[5]*lp[1] + model[9]*lp[2],
|
|
@@ -1196,7 +1249,9 @@ function frame(nowMs){
|
|
| 1196 |
const decay = Math.exp(-dt*2.2);
|
| 1197 |
pf.v[0] *= decay; pf.v[2] *= decay;
|
| 1198 |
if(pf.kind === 1 && pf.life < pf.max*0.55 && landH(pf.p[0], pf.p[2]) <= 0.02){
|
| 1199 |
-
|
|
|
|
|
|
|
| 1200 |
} else {
|
| 1201 |
pf.v[1] *= decay; // then it lifts off as steam
|
| 1202 |
pf.p[1] += pf.v[1]*dt;
|
|
|
|
| 105 |
shader is handed the nearby ones as data, so the two can never disagree about whether land
|
| 106 |
is there at all. The profile below is mirrored in the shader, so the boat sits on the same
|
| 107 |
ground it is drawn on. */
|
| 108 |
+
const ICELL = 48, MAX_ISLANDS = 48, MAXR = 70; // a 170 m grid plus the reach of the largest island
|
| 109 |
+
const LAND = { beach: 0.35, shelf: 1.2, deep: 6.0 }; // beach: how far up the shore the boat may sit
|
| 110 |
let SEED = 1337;
|
| 111 |
|
| 112 |
function ihash(n){
|
|
|
|
| 131 |
return u + (v-u)*sz;
|
| 132 |
}
|
| 133 |
|
| 134 |
+
/* code packs four 4-bit fields, because that is the part both sides can decode identically:
|
| 135 |
+
bits 0-3 peak, 4-7 profile, 8-11 roughness, 12-15 type. Powers of two are exact in float32,
|
| 136 |
+
so the decode cannot drift between JS and the GPU.
|
| 137 |
+
type 0 is the common islet: radius 14-30 m, peak 1.2-4.5 m, about 36 cells in 256.
|
| 138 |
+
type 1 is a large flattish island: radius 50-70 m, peak 2-7.2 m, about 2 cells in 256.
|
| 139 |
+
type 2 is a tall narrow pinnacle: radius 9-18 m, peak 14-38 m, about 3 cells in 256. */
|
| 140 |
function islandAt(cx, cz){
|
| 141 |
const h = ihash((Math.imul(cx, 73856093) + Math.imul(cz, 19349153) + SEED) >>> 0);
|
| 142 |
+
const low = h & 255;
|
| 143 |
+
let type = 0;
|
| 144 |
+
if(low < 2) type = 1; // rare: a large flattish island
|
| 145 |
+
else if(low < 5) type = 2; // rarer: a tall narrow pinnacle
|
| 146 |
+
else if(low >= 96) return null; // most of the world is open sea
|
| 147 |
const s = ihash(h) & 0xFFFFFF;
|
| 148 |
+
const rb = s & 63, pk = (s >> 6) & 15, sh = (s >> 10) & 15, rg = (s >> 14) & 15;
|
| 149 |
+
const r = type === 1 ? 50 + rb*0.31 : type === 2 ? 9 + rb*0.14 : 14 + rb*0.25;
|
| 150 |
return {
|
| 151 |
x: (cx + 0.5 + ((h >>> 8 & 255) - 127.5)/256) * ICELL,
|
| 152 |
z: (cz + 0.5 + ((h >>> 16 & 255) - 127.5)/256) * ICELL,
|
| 153 |
+
r, code: pk + sh*16 + rg*256 + type*4096
|
|
|
|
| 154 |
};
|
| 155 |
}
|
| 156 |
function decode(code){
|
| 157 |
+
const pk = code & 15, sh = (code >> 4) & 15, rg = (code >> 8) & 15, type = code >> 12;
|
| 158 |
+
let peak = 1.2 + pk*0.22, shape = 1.2 + sh*0.03, ramp = 0;
|
| 159 |
+
// shape is the exponent on the falloff, so a small shape is nearly flat: a pinnacle gets near
|
| 160 |
+
// vertical sides. A flat island instead uses a ramp - flat for most of the radius, then a
|
| 161 |
+
// straight fall over the last fraction of it, which is the beach the boat can actually sit on.
|
| 162 |
+
if(type === 1){ peak = 2.0 + pk*0.35; ramp = 0.10 + sh*0.04; }
|
| 163 |
+
else if(type === 2){ peak = 14.0 + pk*1.6; shape = 0.04 + sh*0.012; }
|
| 164 |
+
return { peak, shape, rough: rg/15, type, ramp };
|
| 165 |
}
|
| 166 |
|
| 167 |
function landH(x, z){
|
| 168 |
const cx = Math.floor(x/ICELL), cz = Math.floor(z/ICELL);
|
| 169 |
// starting below sea level is what makes the depth readable: outside an island there is no
|
| 170 |
// seabed, so it reads as deep water, and the shallow band around one is the shelf.
|
| 171 |
+
// The search has to reach as far as the largest island, otherwise the boat could sit on water
|
| 172 |
+
// that the shader is drawing as land.
|
| 173 |
let h = -LAND.deep;
|
| 174 |
+
const span = Math.ceil(MAXR/ICELL);
|
| 175 |
+
for(let j=-span;j<=span;j++) for(let i=-span;i<=span;i++){
|
| 176 |
const isl = islandAt(cx+i, cz+j);
|
| 177 |
if(!isl) continue;
|
| 178 |
const d = Math.hypot(x - isl.x, z - isl.z);
|
| 179 |
if(d > isl.r) continue;
|
| 180 |
const p = decode(isl.code);
|
| 181 |
const k = 1 - d/isl.r; // monotone: downhill always leads back to the sea
|
| 182 |
+
const nz = 0.55 + 0.45*p.rough*lnoise(x, z);
|
| 183 |
+
const terr = (p.ramp > 0 ? Math.min(1, k/p.ramp) : Math.pow(k, p.shape)) * nz;
|
| 184 |
h = Math.max(h, p.peak*terr - LAND.shelf); // the shelf is the shallow ring around an island
|
| 185 |
}
|
| 186 |
return h;
|
|
|
|
| 189 |
const e = 0.30;
|
| 190 |
return [(landH(x+e, z) - landH(x-e, z))/(2*e), (landH(x, z+e) - landH(x, z-e))/(2*e)];
|
| 191 |
}
|
| 192 |
+
// every island that can reach a vertex of the water grid, extended by the reach of the largest one
|
| 193 |
function islandsNear(minX, maxX, minZ, maxZ){
|
| 194 |
const out = [];
|
| 195 |
+
const a = minX - MAXR, b = maxX + MAXR, c = minZ - MAXR, d = maxZ + MAXR;
|
| 196 |
+
for(let cz = Math.floor(c/ICELL); cz <= Math.floor(d/ICELL) && out.length < MAX_ISLANDS; cz++)
|
| 197 |
+
for(let cx = Math.floor(a/ICELL); cx <= Math.floor(b/ICELL) && out.length < MAX_ISLANDS; cx++){
|
| 198 |
const isl = islandAt(cx, cz);
|
| 199 |
if(isl) out.push(isl);
|
| 200 |
}
|
|
|
|
| 229 |
}
|
| 230 |
float lhash(int x, int y){ return float(ihash(uint(x)*3419u + uint(y)*2147483647u) & 1023u) / 1023.0; }
|
| 231 |
float lnoise(vec2 p){
|
| 232 |
+
p = p * 0.08; // same lattice scale as the JS copy
|
| 233 |
ivec2 i = ivec2(floor(p));
|
| 234 |
vec2 f = p - vec2(float(i.x), float(i.y));
|
| 235 |
f = f*f*(3.0-2.0*f);
|
|
|
|
| 243 |
float d = length(p - isl.xy);
|
| 244 |
if(d > isl.z) continue;
|
| 245 |
float code = isl.w;
|
| 246 |
+
float pk = mod(code, 16.0), sh = floor(mod(code/16.0, 16.0)), rg = floor(mod(code/256.0, 16.0));
|
| 247 |
+
float type = floor(code/4096.0);
|
| 248 |
+
float peak = 1.2 + pk*0.22, shape = 1.2 + sh*0.03;
|
| 249 |
+
if(type == 1.0){ peak = 2.0 + pk*0.35; }
|
| 250 |
+
else if(type == 2.0){ peak = 14.0 + pk*1.6; shape = 0.04 + sh*0.012; }
|
| 251 |
+
float rough = rg/15.0;
|
| 252 |
float k = 1.0 - d/isl.z;
|
| 253 |
+
float nz = 0.55 + 0.45*rough*lnoise(p);
|
| 254 |
+
float terr = (type == 1.0) ? min(1.0, k/(0.10 + sh*0.04)) * nz : pow(k, shape) * nz;
|
| 255 |
h = max(h, peak*terr - 1.2);
|
| 256 |
}
|
| 257 |
return h;
|
|
|
|
| 413 |
${NOISE_GLSL}
|
| 414 |
void main(){
|
| 415 |
vec3 n = normalize(vNormal);
|
| 416 |
+
vec3 fn = normalize(cross(dFdy(vWorld), dFdx(vWorld))); // the facet actually drawn
|
| 417 |
+
if(fn.y < 0.0) fn = -fn;
|
| 418 |
+
// A facet this steep can only be a cliff face - the sea surface never gets that steep. The land
|
| 419 |
+
// or water decision is made per fragment from interpolated values, so a triangle that straddles
|
| 420 |
+
// the shoreline is split diagonally between the two materials, and on a tall island that split
|
| 421 |
+
// runs most of the way up the cliff. Deciding from the facet keeps a cliff one material.
|
| 422 |
+
bool isLand = vLand > 0.0 || fn.y < 0.12;
|
| 423 |
+
if(isLand) n = fn;
|
| 424 |
if(n.y < 0.0) n = -n;
|
| 425 |
vec3 L = normalize(vec3(0.5, 0.75, -0.45));
|
| 426 |
float ndl = max(dot(n, L), 0.0);
|
|
|
|
| 430 |
float tone = mix(floor(q)/4.0, q/4.0, 0.40);
|
| 431 |
|
| 432 |
vec3 col;
|
| 433 |
+
float lv = max(vLand, 0.0);
|
| 434 |
+
if(isLand){
|
| 435 |
float flatness = clamp(n.y, 0.0, 1.0); // "flat" is a reserved word in GLSL ES 3.0
|
| 436 |
+
col = mix(uSand * 0.62, uSand, smoothstep(0.0, 0.30, lv)); // wet sand at the waterline
|
| 437 |
+
col = mix(col, uGrass, smoothstep(0.40, 1.10, lv) * smoothstep(0.50, 0.75, flatness));
|
| 438 |
col = mix(col, uRock, smoothstep(0.55, 0.80, 1.0 - flatness));
|
| 439 |
col = col * (0.55 + 0.70 * tone);
|
| 440 |
+
col = mix(col, col * 1.10, (vnoise(vWorld.xz * 2.2) - 0.5) * 0.35 * smoothstep(0.2, 1.0, lv));
|
| 441 |
} else {
|
| 442 |
vec3 deep = vec3(0.04,0.17,0.38);
|
| 443 |
vec3 mid = vec3(0.09,0.34,0.53);
|
|
|
|
| 469 |
|
| 470 |
// wispy foam, built the same way as the clouds, plus a band where shallow water meets the shore
|
| 471 |
float wisps = smoothstep(0.42, 0.58, fbm(vWorld.xz * 1.2 + uTime * 0.02, 2));
|
| 472 |
+
float shore = isLand ? 0.0 : smoothstep(0.50, 0.02, max(vDepth, 0.0)) * 0.22;
|
| 473 |
+
float f = clamp((isLand ? 0.0 : vFoam) * (0.35 + 0.55 * wisps) + shore, 0.0, 1.0);
|
| 474 |
col = mix(col, vec3(0.93,0.98,1.0), f);
|
| 475 |
|
| 476 |
float fog = smoothstep(14.0, 70.0, length(vWorld - uCam));
|
|
|
|
| 746 |
};
|
| 747 |
const rod=(a,b,th,col,glow=0,mat=0)=>tube(a,b,th,6,col,glow,mat,true);
|
| 748 |
const limb=(a,b,r,col,mat=0)=>tube(a,b,r,6,col,0,mat,true);
|
| 749 |
+
const ellipsoid=(cx,cy,cz,rx,ry,rz,col,mat=0,seg=10,rows=5)=>{
|
| 750 |
+
const P=(a,b)=>[cx+Math.cos(a)*Math.cos(b)*rx, cy+Math.sin(a)*ry, cz+Math.cos(a)*Math.sin(b)*rz];
|
| 751 |
+
for(let j=0;j<rows;j++) for(let i=0;i<seg;i++){
|
| 752 |
+
const a0=Math.PI*(-0.5+j/rows), a1=Math.PI*(-0.5+(j+1)/rows), b0=i/seg*TAU, b1=(i+1)/seg*TAU;
|
| 753 |
+
quad(P(a0,b0),P(a0,b1),P(a1,b1),P(a1,b0),col,0,mat,true);
|
| 754 |
+
}
|
| 755 |
+
};
|
| 756 |
+
const poly=(pts,r,col,glow=0,mat=0)=>{ for(let i=0;i<pts.length-1;i++) tube(pts[i], pts[i+1], r, 7, col, glow, mat, true); };
|
| 757 |
+
// a two link arm solved for where the hand has to be, so the elbow bends as the thing being
|
| 758 |
+
// held moves. Straight rods from shoulder to handle were what read as an awkward arm.
|
| 759 |
+
const arm=(sh, target, L1, L2, bendDir)=>{
|
| 760 |
+
const d = sub(target, sh), len = Math.max(1e-4, Math.hypot(d[0],d[1],d[2]));
|
| 761 |
+
const u = scl(d, 1/len);
|
| 762 |
+
const ca = Math.acos(clamp((L1*L1 + len*len - L2*L2)/(2*L1*len), -1, 1));
|
| 763 |
+
let b = sub(bendDir, scl(u, dot(bendDir, u)));
|
| 764 |
+
if(Math.hypot(b[0],b[1],b[2]) < 1e-4) b = [0,-1,0]; else b = norm(b);
|
| 765 |
+
const elbow = add(sh, add(scl(u, L1*Math.cos(ca)), scl(b, L1*Math.sin(ca))));
|
| 766 |
+
limb(sh, elbow, 0.05, PAL.jacket);
|
| 767 |
+
limb(elbow, target, 0.042, PAL.skin);
|
| 768 |
+
sphere(target[0], target[1], target[2], 0.045, PAL.skin, 0, 8, 4);
|
| 769 |
+
};
|
| 770 |
|
| 771 |
// ---- hull ----
|
| 772 |
// The profile is a function of position along the boat instead of a list of stations, so the
|
|
|
|
| 800 |
for(let i=0;i<rr.length;i++) tri(rr[i][0], rr[(i+1)%rr.length][0], tip, rr[i][1], 0, 0, true);
|
| 801 |
|
| 802 |
// ---- boiler, candle, exhaust ----
|
| 803 |
+
const bz = 0.08, by = 0.36;
|
| 804 |
+
ellipsoid(0, by, bz, 0.16, 0.05, 0.16, PAL.brass, 1, 10, 4);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 805 |
for(const side of [-1, 1]){
|
| 806 |
+
// the pipes are what hold the boiler up: they come out of its underside, dip below the
|
| 807 |
+
// waterline just aft of it - that dip is the U bend the water sits in - and the mouth
|
| 808 |
+
// stays submerged at the back.
|
| 809 |
+
poly([[side*0.12, by-0.01, bz-0.02], [side*0.15, 0.18, -0.08], [side*0.17, -0.02, -0.45],
|
| 810 |
+
[side*0.17, -0.06, -1.25], [side*0.17, -0.14, -1.58]], 0.035, PAL.metal, 0, 1);
|
| 811 |
}
|
| 812 |
|
| 813 |
+
// the candle stands on the deck under the boiler and slides forward towards it, so at full
|
| 814 |
+
// throttle the flame is just under the boiler's underside, where the heat actually does something
|
| 815 |
+
const cz = -0.32 + 0.30*heat;
|
| 816 |
+
const cy = dAt(cz); // on the deck, not floating above it
|
| 817 |
+
const flame = 0.03 + 0.02*heat + 0.008*Math.sin(t*11.0) + 0.005*Math.sin(t*23.0);
|
| 818 |
+
tube([0, cy, cz], [0, cy+0.035, cz], 0.035, 7, PAL.wax, 0, 0, true);
|
| 819 |
+
cone(0, cy+0.035, cz, 0.035, flame, 7, PAL.flame, 1.0, 0);
|
| 820 |
+
const knob = [-0.05, cy+0.022, cz]; // the little handle on the candle's side
|
| 821 |
+
sphere(knob[0], knob[1], knob[2], 0.018, [0.30,0.28,0.24], 0, 6, 3);
|
| 822 |
+
|
| 823 |
// ---- tiller and rudder ----
|
| 824 |
const ang = -rudder * 0.45;
|
| 825 |
const head = [0, 0.16, -1.58];
|
|
|
|
| 843 |
sphere(PX, 0.70, -0.55, 0.095, PAL.skin, 0, 10, 6); // head
|
| 844 |
disc(PX, 0.79, -0.55, 0.15, 10, PAL.hat); // brim
|
| 845 |
cone(PX, 0.79, -0.55, 0.115, 0.10, 10, PAL.hat); // crown
|
| 846 |
+
// right hand on the tiller, left hand on the candle handle. Both are solved for wherever the
|
| 847 |
+
// thing being held currently is, so pushing the throttle straightens the left arm as the candle
|
| 848 |
+
// moves forward, and steering swings the right one.
|
| 849 |
+
arm([PX+0.06, 0.52, -0.48], [hx, hy, hz], 0.29, 0.29, [-0.55,-0.55,-0.1]);
|
| 850 |
+
arm([PX-0.06, 0.52, -0.44], knob, 0.29, 0.29, [-0.5,-0.6,0.05]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 851 |
|
| 852 |
// ---- pennant ----
|
| 853 |
// the pole starts at the deck, not above it
|
|
|
|
| 1034 |
|
| 1035 |
function spawnPuff(model, origin, fwd, side, heat){
|
| 1036 |
if(puffs.length > 160) return;
|
| 1037 |
+
const lp = [side*0.17*BOAT_SCALE, -0.14*BOAT_SCALE, -1.58*BOAT_SCALE]; // the submerged tube mouth
|
| 1038 |
const wp = [
|
| 1039 |
origin[0] + model[0]*lp[0] + model[4]*lp[1] + model[8]*lp[2],
|
| 1040 |
origin[1] + model[1]*lp[0] + model[5]*lp[1] + model[9]*lp[2],
|
|
|
|
| 1249 |
const decay = Math.exp(-dt*2.2);
|
| 1250 |
pf.v[0] *= decay; pf.v[2] *= decay;
|
| 1251 |
if(pf.kind === 1 && pf.life < pf.max*0.55 && landH(pf.p[0], pf.p[2]) <= 0.02){
|
| 1252 |
+
const surf = waveH(pf.p[0], pf.p[2], t);
|
| 1253 |
+
// the mouth is submerged now, so a bubble has to rise to the surface before it reads as foam
|
| 1254 |
+
if(pf.p[1] >= surf - 0.02) pf.p[1] = surf + 0.02;
|
| 1255 |
} else {
|
| 1256 |
pf.v[1] *= decay; // then it lifts off as steam
|
| 1257 |
pf.p[1] += pf.v[1]*dt;
|