""" The ice the station stands on: one big floe, 916 x 956 m (east-west x north-south) and 10 to 13 m thick, with the vessel's berth cut straight along its south edge. (Until 2026-10-14 it was about 190 x 120 m; the user: "make the ice 916meter x 956 meter with random deformations like we did before. I want ice bigger". Round the station the ground is the same as before, cell for cell; the floe's first recipe is models/_backups/ice_pre-big-ice_2026-10-14.py.) add_ice(b) adds (or replaces) the collection "09 ICE floe" in the open file, in place What is built * ANCH_Ice_Mass: the floe as one closed mesh, all quads: a top that follows a height function, an edge that drops as a cliff to below the waterline, flanks that draw in under water, and an underside. The top is in the pack's clear ice (MI_Ice_A-2), the rest in its snow material (M_Cave_SimpleSnow). The pack's ice is translucent in Unreal, and its own showcase map lays it over a snow surface. So the FBX for Unreal gets one more layer that this file does not have: a copy of the top, 3 cm lower, in the snow material (build/export_custom.py makes it; the object's "anchorage_underlay_*" properties say so). In Blender two layers 3 cm apart would only flicker when seen from far away. * Seracs, ice blocks and drift ice from the pack's own outdoor meshes (the ones its showcase stands in), set out by hand: three groups on the floe's high ground and a lane of blocks that marks the landing ground outside the arrivals door. Each large block is the pack's pair: a snow core and a slightly larger shell of ice. The top, in numbers -0.62 m under the station and on the worked ground round it, along the berth, on the landing ground. The station's floors are at 0.05, its lowest pieces at -0.51; the shell's walls go down to -0.80. -1.84 m under Relay Control's window drum, whose sunken ring goes down to -1.70, and in a moat round the drum. That is still above the sea (-1.90), so the moat is dry. rising away from the worked ground: gentle swell, mounds of 1 to 2.6 m where the serac groups stand. The berth edge is straight at y = -35.70 for the vessel's whole length and beyond; at the height of the ice the hull is 34 cm away from it, and further below. Everything here carries ["anchorage_surroundings"]: the sea level guard and the outside check leave it out. """ import bisect import math import bpy from mathutils import Matrix, Vector from bl_kit import GAME, get_collection from exterior import MeshBuilder COLLECTION = "09 ICE floe" ZONE = "09 ICE floe" ICE_TOP = -0.62 UNDERLAY = 0.03 # Unreal only: the snow surface lies this far under the clear ice PIT_Z = -1.84 # under and round Relay Control's window drum PIT_R = 13.00 # the drum's footing ring is at 11.50, the sunken ring's outer wall at 10.58 PIT_RISE = 2.40 # width of the moat's slope UV_TOP = 504.5 # the pack's ice plane is this wide in its showcase; the material tiles it 8 times (63 m) UV_SIDE = 30.0 MAT_BODY = GAME + "UE4_Assets/Environment_B/Materials/M_Cave_SimpleSnow" MAT_GLAZE = GAME + "NatureMaterials_Ice/Materials/MI_Ice_A-2" BODY, ICE = 0, 1 ROCK_L = GAME + "UE4_Assets/Environment_A/Meshes/SM_Cave_Rock_Large_01a" ROCK_M = GAME + "UE4_Assets/Environment_A/Meshes/SM_Cave_Rock_Med_01a" CHUNKS = (GAME + "UE4_Assets/Environment_B/Meshes/SM_IceChunks_01", GAME + "UE4_Assets/Environment_B/Meshes/SM_IceChunks_02") FLOE_W, FLOE_L = 916.0, 956.0 # east-west, north-south (from the berth) BERTH_Y = -35.70 # where build_floe lays the berth out (b.BRIDGE_END_Y - 0.20): the pads below go by it CELL, CELL_FAR = 2.0, 12.0 # the top's grid: cells of 2 m round the station, growing to 12 m far out CELL_GROW = 1.12 # by this much from one cell to the next CELL_EDGE = 3.0 # and 3 m along the floe's own edge, so that the band the rim takes is about as wide all round FINE_X = (-130.0, 175.0) # 2 m cells between these x ... FINE_Y = 115.0 # ... and from the berth up to this y: the station's ground and what is seen from it OLD_FLOE = (-85.0, 107.0, -36.0, 86.0) # the first floe: in it and 25 m round it the ground is as it was DEBUG = None # a dict here: build_floe leaves its rings in it (for the checks' pictures) BAND = 3 # cells of that grid given over to the rim (6 m round the station, 9 m elsewhere) FINE = 3 # the rim has three points for every one of the grid: one every 0.7 m round the station # Worked ground: flat at ICE_TOP inside these boxes (x0, x1, y0, y1), swell and mounds only beyond them FLAT = ( (-44.0, 44.0, -36.5, -17.0), # the quays, the dock hub and the quayside in front of them (-66.0, 100.0, -36.5, -29.5), # the berth: a level strip along the whole edge (-41.0, -13.0, -17.0, 17.0), # Relay Control (-15.0, 32.0, -9.0, 9.0), # crossing, cargo hall and the apron outside the maintenance door (-9.0, 9.0, -20.0, -6.0), # mooring gallery (-12.0, 15.0, 6.0, 54.0), # processing, arrivals lock, the landing deck and its ramp (-9.0, 9.0, 54.0, 66.0), # the landing ground # the outpost's site works (build/outpost_site.py, 2026-10-14) (30.0, 62.0, -11.5, 10.5), # the cargo yard, east of the apron (20.0, 43.0, 9.0, 30.0), # the tank farm, north of the cargo hall (-30.0, -12.0, 27.0, 39.0), # the comms mast ) # The four ice coolers (the user, 2026-10-14: "there are 4 big machines spread over on the Ice to cool it so ice never # melts"): at the floe's quarter points, south-west, south-east, north-west, north-east, each on a level pad that # takes its platform and the cloth it carries (build/coolers.py stands them there) COOLERS = tuple((sx * FLOE_W / 4.0, BERTH_Y + FLOE_L * fy) for fy in (0.25, 0.75) for sx in (-1.0, 1.0)) COOLER_PAD = (-16.0, 17.0, -13.0, 17.0) FLAT = FLAT + tuple((x + COOLER_PAD[0], x + COOLER_PAD[1], y + COOLER_PAD[2], y + COOLER_PAD[3]) for x, y in COOLERS) # Mounds of the floe's high ground (x, y, height, radius): the serac groups stand on them MOUNDS = ( (-58.0, 50.0, 2.2, 11.0), # north-west (52.0, 66.0, 2.6, 12.0), # north-east (86.0, 14.0, 2.0, 10.0), # east (-30.0, 76.0, 1.2, 9.0), # the bank along the north rim (22.0, 78.0, 1.4, 10.0), ) # --------------------------------------------------------------------------------------------------------------- # a small value noise of its own: the same floe on every run and in every Blender def _hash(ix, iy, seed): n = (ix * 374761393 + iy * 668265263 + seed * 362437) & 0xFFFFFFFF n = ((n ^ (n >> 13)) * 1274126177) & 0xFFFFFFFF return ((n ^ (n >> 16)) & 0xFFFF) / 65535.0 def _vnoise(x, y, seed): ix, iy = int(math.floor(x)), int(math.floor(y)) fx, fy = x - ix, y - iy fx, fy = fx * fx * (3.0 - 2.0 * fx), fy * fy * (3.0 - 2.0 * fy) a = _hash(ix, iy, seed) + (_hash(ix + 1, iy, seed) - _hash(ix, iy, seed)) * fx c = _hash(ix, iy + 1, seed) + (_hash(ix + 1, iy + 1, seed) - _hash(ix, iy + 1, seed)) * fx return a + (c - a) * fy def fbm(x, y, octaves, seed): """-1 .. 1""" s, amp, tot = 0.0, 1.0, 0.0 for o in range(octaves): s += amp * (_vnoise(x, y, seed + o * 17) * 2.0 - 1.0) tot += amp amp *= 0.5 x, y = x * 2.03, y * 2.03 return s / tot def _smooth(t): t = min(1.0, max(0.0, t)) return t * t * (3.0 - 2.0 * t) def _dist_flat(x, y): best = 1e9 for x0, x1, y0, y1 in FLAT: dx = max(x0 - x, 0.0, x - x1) dy = max(y0 - y, 0.0, y - y1) best = min(best, math.hypot(dx, dy)) return best def _dist_box(x, y, box): x0, x1, y0, y1 = box return math.hypot(max(x0 - x, 0.0, x - x1), max(y0 - y, 0.0, y - y1)) def ice_height(x, y, edge=1.0): """Top of the snow surface at (x, y). `edge`: 0 at the floe's rim, 1 from 8 m inside it.""" rel = 0.30 * fbm(x / 38.0, y / 38.0, 4, 11) + 0.08 * fbm(x / 9.0, y / 9.0, 2, 5) for gx, gy, gh, gs in MOUNDS: rel += gh * math.exp(-((x - gx) ** 2 + (y - gy) ** 2) / (2.0 * gs * gs)) # far out a slower, wider swell: none of it in the first floe or 25 m round it far = _smooth((_dist_box(x, y, OLD_FLOE) - 25.0) / 90.0) if far > 0.0: rel += far * (0.85 * fbm(x / 150.0, y / 150.0, 3, 301) + 0.30 * fbm(x / 48.0, y / 48.0, 2, 307)) rel = max(rel, -0.25 - 0.35 * far) h = ICE_TOP + _smooth((_dist_flat(x, y) - 1.5) / 13.0) * rel * (0.35 + 0.65 * edge) # the pit under Relay Control's drum and the moat round it: a half disc west of the drum's axis d = max(math.hypot(x + 25.0, y) - PIT_R, x + 24.0, 0.0) return PIT_Z + (h - PIT_Z) * _smooth(d / PIT_RISE) # --------------------------------------------------------------------------------------------------------------- # The outline's corners and bulges (m). Set so that the broken edge reaches x -458 and 458 and y FLOE_L north of the # berth (build_floe reports the floe's extent; build/check_site.py checks it). OUT_SW, OUT_SE = -428.0, 430.0 # the south corners OUT_NW, OUT_NE = -431.0, 433.0 # the north corners OUT_N = (FLOE_L - 50.0, FLOE_L - 38.0, 38.9) # the north corners' distance from the berth, and the north edge's bow OUT_W, OUT_E = 31.0, 21.0 # how far the west and east edges bow out RECESS = (92.0, 74.0) # how far the south edge has fallen back at its west and east corners def _outline(berth_y): """The floe's four edges as curves of t in 0..1; they meet at the corners. Returns (patch, sides): `patch` maps the unit square onto the floe, `sides` are the edges in the order one walks round it counter-clockwise from the south-west corner, each with the direction its parameter runs. The berth is the middle of the south edge, straight from x -70 to 104 as it was. Beyond it the edge falls back to the north in a wide curve, so the vessel has open water to the south, east and west. The other three edges bulge out and wander. The numbers are set so that the broken edge (_Rim) spans FLOE_W x FLOE_L.""" xb0, xb1 = -70.0, 104.0 # the straight berth xsw, xse = OUT_SW, OUT_SE xnw, xne = OUT_NW, OUT_NE ynw, yne = berth_y + OUT_N[0], berth_y + OUT_N[1] def recess(x): if xb0 <= x <= xb1: return 0.0 r, a = ((xb0 - x) / (xb0 - xsw), RECESS[0]) if x < xb0 else ((x - xb1) / (xse - xb1), RECESS[1]) r = min(1.0, max(0.0, r)) return max(0.0, a * r ** 1.7 + 9.0 * fbm(x / 70.0, 0.37, 3, 211) * _smooth(r * 3.0)) def south(t): x = xsw + (xse - xsw) * t return (x, berth_y + recess(x)) ysw, yse = south(0.0)[1], south(1.0)[1] def north(t): s = math.sin(math.pi * t) return (xnw + (xne - xnw) * t, ynw + (yne - ynw) * t + s * (OUT_N[2] + 8.0 * math.sin(2 * math.pi * 1.4 * t + 0.9)) + 10.0 * fbm(t * 7.0, 0.53, 3, 231) * s) # (the bows start as a plain sine: steeper starts bend the grid near the sides away from the edge it follows) def west(v): s = math.sin(math.pi * v) return (xsw + (xnw - xsw) * v - s * (OUT_W + 6.0 * math.sin(2 * math.pi * 1.3 * v + 1.1)) - 9.0 * fbm(v * 6.0, 0.61, 3, 241) * s, ysw + (ynw - ysw) * v) def east(v): s = math.sin(math.pi * v) return (xse + (xne - xse) * v + s * (OUT_E + 6.0 * math.sin(2 * math.pi * 1.7 * v + 0.4)) + 9.0 * fbm(v * 6.0, 0.83, 3, 251) * s, yse + (yne - yse) * v) def patch(u, v): s, n, w, e = south(u), north(u), west(v), east(v) c00, c10, c01, c11 = south(0.0), south(1.0), north(0.0), north(1.0) return tuple((1 - v) * s[k] + v * n[k] + (1 - u) * w[k] + u * e[k] - ((1 - u) * (1 - v) * c00[k] + u * (1 - v) * c10[k] + (1 - u) * v * c01[k] + u * v * c11[k]) for k in range(2)) return patch, ((south, True), (east, True), (north, False), (west, False)) def _bump(u): u = abs(u) return (1.0 - u * u) ** 1.5 if u < 1.0 else 0.0 def _lerp(a, c, t): return a + (c - a) * t class _Rim(object): """The floe's edge as it really runs: the smooth outline, broken up. The outline is sampled three times as finely as the top's grid (a point every 0.7 m). Every point gets its own way out from the floe (the outline's normal) and a distance `d` along it by which the edge is moved: outwards where the ice has pushed out, inwards where a piece has broken away. What moves the edge * a slow wander of up to 1.3 m (34 m long), a quicker one of 0.55 m (9 m), and a jaggedness of some 20 cm from point to point * bites: every 15 to 40 m a piece is missing, 1 to 3 m deep and 4 to 12 m long; the lip slumps there * the two corners of the berth are rounded off What holds it back * along the vessel: the edge keeps to within 15 cm of the line the berth was laid out on and otherwise gives way inwards (to 1.7 m), so the hull has at least half a metre everywhere. Under water nothing stands further out than the edge above it. At the stern, where the tail fins reach closest, it is set back 40 cm. * under the boarding bridge the edge is dressed: straight and level, so the bridge stands on ice to its end. The noise runs round the floe on a circle, so it closes without a jump. Since the floe is 916 x 956 m (2026-10-14) the points follow the top's grid, three to a cell: 0.7 m apart round the station as before, up to 4 m far out. There the breaks are as many points long as near the station, so they are larger (bites of up to 15 m by 8 m), and the edge wanders more (5 m at 150 m). The four corners of the outline are rounded off. An edge that gives way inwards stops 4 m short of the grid's own edge (`coarse`: its points round the floe), so rim and grid never cross.""" def __init__(self, sides, params, coarse, berth_y): self.pts = [] self.south = [] starts = [] for (curve, forward), ts in zip(sides, params): starts.append(len(self.pts)) # each side begins at a corner: SW, SE, NE, NW for t in ts: self.pts.append(curve(t)) self.south.append(curve is sides[0][0]) n = self.n = len(self.pts) # the outline's corners rounded off: a parabola from up to 40 m before each corner to as far after it, so that # the rings inside the edge do not cross there. As much as the grid's edge leaves room for: the rounded edge # keeps 4 m outside it. self.rounded = [] for kc in starts: orig = list(self.pts) for reach in (40.0, 30.0, 22.0, 16.0, 11.0, 7.0, 0.0): self.pts = list(orig) if reach > 0.0: seq = self._window(kc, reach) pa, pc, pb = self.pts[seq[0]], self.pts[kc], self.pts[seq[-1]] dist = [0.0] for a, c in zip(seq[:-1], seq[1:]): dist.append(dist[-1] + math.hypot(self.pts[c][0] - self.pts[a][0], self.pts[c][1] - self.pts[a][1])) for k, dd in zip(seq, dist): s = dd / dist[-1] self.pts[k] = tuple((1 - s) ** 2 * pa[i] + 2 * (1 - s) * s * pc[i] + s * s * pb[i] for i in range(2)) if reach == 0.0 or min(self._room_at(k, coarse) for k in self._window(kc, reach + 6.0)) >= 4.0: self.rounded.append(reach) break self.run = [0.0] for k in range(1, n + 1): a, c = self.pts[k - 1], self.pts[k % n] self.run.append(self.run[-1] + math.hypot(c[0] - a[0], c[1] - a[1])) self.length = self.run[-1] self.nrm = [] for k in range(n): a, c = self.pts[k - 1], self.pts[(k + 1) % n] tx, ty = c[0] - a[0], c[1] - a[1] ln = math.hypot(tx, ty) or 1.0 self.nrm.append((ty / ln, -tx / ln)) # counter-clockwise loop: outwards # the points' spacing, and how much room each has inwards before the grid's edge self.step = [0.5 * (self.run[k + 1] - self.run[k] + self.run[k] - self.run[k - 1]) if k else self.run[1] for k in range(n)] self.room = [self._room_at(k, coarse) for k in range(n)] # how far in the edge may give way: so far that the apron ring (0.5 d - 3.4, or d - 1.8) keeps 0.5 m from # the grid's edge. Smoothed along the rim (the grid's edge has corners), never below what the rule asks. raw = [max(7.8 - 2.0 * r, 2.3 - r) for r in self.room] top = [max(raw[(k + j) % n] for j in range(-3, 4)) for k in range(n)] self.limit = [sum(top[(k + j) % n] for j in range(-3, 4)) / 7.0 for k in range(n)] self.limit = [max(a, c) for a, c in zip(self.limit, raw)] # where pieces have broken away: (place along the edge, half length, depth) self.bites = [] s, i = 9.0, 0 while s < self.length - 9.0: k = min(n - 1, bisect.bisect_left(self.run, s)) sc = max(1.0, self.step[k] / 0.67) self.bites.append((s, max((2.2 + 3.6 * _hash(i, 1, 61)) * sc ** 0.8, 2.5 * self.step[k]), (1.0 + 2.2 * _hash(i, 2, 61)) * sc ** 0.5)) s += (15.0 + 26.0 * _hash(i, 3, 61)) * sc ** 0.8 i += 1 self.d, self.bite, self.berth, self.dressed = [], [], [], [] for k in range(n): x, y = self.pts[k] s = self.run[k] bite = 0.0 for s0, hw, depth in self.bites: ds = abs(s - s0) if ds < hw or self.length - ds < hw: bite += depth * _bump(min(ds, self.length - ds) / hw) sc = max(1.0, self.step[k] / 0.67) ** 0.5 farw = _smooth((math.hypot(x - 15.0, y - 10.0) - 180.0) / 220.0) # 0 round the station big = 1.3 * self.noise(k, 34.0, 3, 41) * sc + farw * (5.0 * self.noise(k, 150.0, 3, 141) + 2.0 * self.noise(k, 55.0, 2, 143)) mid = 0.55 * self.noise(k, 9.0, 2, 43) * sc small = (0.16 * self.noise(k, 1.9, 1, 47) + 0.07 * (2.0 * _hash(k, 3, 53) - 1.0)) * sc free = max(-3.3 * sc - 7.0 * farw, min(1.6 * sc + 3.0 * farw, big + mid + small - bite)) # along the vessel (it lies from x -41.8 to 80.2) wb = _smooth((x + 54.0) / 8.0) * _smooth((92.0 - x) / 8.0) if self.south[k] else 0.0 stern = _smooth((x + 45.0) / 2.0) * _smooth((-30.0 - x) / 2.0) hi = _lerp(0.15, -0.40, stern) held = max(-1.7, min(hi, 0.35 * big + 0.6 * mid + small - 0.45 * bite - 0.30)) d = _lerp(free, held, wb) # under the boarding bridge (and its hatch seal, x -7.7 to 7.7) wa = (1.0 - _smooth((abs(x) - 6.5) / 3.0)) if self.south[k] else 0.0 d = max(_lerp(d, -0.04, wa), self.limit[k]) self.d.append(d) self.bite.append(min(1.0, bite / 1.5) * (1.0 - wa)) self.berth.append(wb) self.dressed.append(wa) def _room_at(self, k, coarse): """How far it is from point k straight inwards to the grid's edge (`coarse`: the ring of grid points the rim is joined to, FINE rim points to each of its steps).""" n, m = len(self.pts), len(coarse) a, c = self.pts[k - 1], self.pts[(k + 1) % n] tx, ty = c[0] - a[0], c[1] - a[1] ln = math.hypot(tx, ty) or 1.0 nx, ny = -ty / ln, tx / ln # inwards (the loop runs counter-clockwise) ci, e = divmod(k, FINE) px, py = self.pts[k] best = None for cc in range(ci - 8, ci + 9): p0, p1 = coarse[cc % m], coarse[(cc + 1) % m] ex, ey = p1[0] - p0[0], p1[1] - p0[1] det = ex * ny - nx * ey if abs(det) < 1e-9: continue wx, wy = p0[0] - px, p0[1] - py t = (ex * wy - wx * ey) / det s = (nx * wy - ny * wx) / det if t > 0.0 and -1e-6 <= s <= 1.0 + 1e-6: best = t if best is None else min(best, t) if best is None: p0, p1 = coarse[ci % m], coarse[(ci + 1) % m] best = math.hypot(px - (p0[0] + (p1[0] - p0[0]) * e / FINE), py - (p0[1] + (p1[1] - p0[1]) * e / FINE)) return best def _window(self, kc, reach): """The points within `reach` metres of point kc along the edge, in order round it.""" n = len(self.pts) out = {} for direction in (-1, 1): acc, k, path = 0.0, kc, [] while acc < reach: k2 = (k + direction) % n acc += math.hypot(self.pts[k2][0] - self.pts[k][0], self.pts[k2][1] - self.pts[k][1]) path.append(k2) k = k2 out[direction] = path return out[-1][::-1] + [kc] + out[1] def noise(self, k, wavelength, octaves, seed): """Noise along the edge, -1 .. 1, the same at the end of the round as at its start.""" a = 2.0 * math.pi * self.run[k] / self.length r = self.length / (2.0 * math.pi * wavelength) return fbm(r * math.cos(a) + 31.7, r * math.sin(a) + 17.3, octaves, seed) def at(self, k, out): """The point `out` metres outside (negative: inside) the outline at point k.""" return (self.pts[k][0] + self.nrm[k][0] * out, self.pts[k][1] + self.nrm[k][1] * out) def _lines(a0, a1, f0, f1): """Grid lines from a0 to a1: CELL apart between f0 and f1, CELL_EDGE at a0 and a1 (the floe's own edge), and from either growing by CELL_GROW a cell up to CELL_FAR. (Cells growing by a factor q have a size that grows by q - 1 times the distance gone.)""" g = CELL_GROW - 1.0 def size(x): df = max(f0 - x, 0.0, x - f1) de = min(x - a0, a1 - x) return min(CELL_FAR, CELL + g * df, CELL_EDGE + g * de) xs = [a0] while True: x = xs[-1] step = size(x + 0.5 * size(x)) if x + 1.5 * step >= a1: break xs.append(x + step) xs.append(a1) return xs def build_floe(mb, b): berth_y = b.BRIDGE_END_Y - 0.20 sea = b.SEA_LEVEL_Z patch, sides = _outline(berth_y) # the grid's lines: 2 m apart round the station, wider far out. u runs with x along the south edge, v with y xs = _lines(OUT_SW, OUT_SE, FINE_X[0], FINE_X[1]) yn = sides[2][0](0.5)[1] ys = _lines(berth_y, yn, berth_y, FINE_Y) U = [(x - OUT_SW) / (OUT_SE - OUT_SW) for x in xs] V = [(y - berth_y) / (yn - berth_y) for y in ys] NX, NY = len(U) - 1, len(V) - 1 grid = [[patch(U[i], V[j]) for i in range(NX + 1)] for j in range(NY + 1)] xs_all = [p[0] for row in grid for p in row] ys_all = [p[1] for row in grid for p in row] cx, cy = (min(xs_all) + max(xs_all)) / 2.0, (min(ys_all) + max(ys_all)) / 2.0 B = BAND def edge_factor(i, j): # metres from the rim, roughly: to the grid's nearest boundary line p = grid[j][i] d = min(math.hypot(p[0] - q[0], p[1] - q[1]) for q in (grid[j][0], grid[j][NX], grid[0][i], grid[NY][i])) return _smooth(d / 8.0) top = [[(p[0], p[1], ice_height(p[0], p[1], edge_factor(i, j))) for i, p in enumerate(row)] for j, row in enumerate(grid)] # underside: the same grid drawn in a little towards the middle (4.5 m and more), deepest in the middle bot = [[(cx + (p[0] - cx) * 0.99, cy + (p[1] - cy) * 0.99, -9.5 - 3.2 * math.sin(math.pi * U[i]) * math.sin(math.pi * V[j]) - 0.8 * fbm(p[0] / 20.0, p[1] / 20.0, 3, 23)) for i, p in enumerate(row)] for j, row in enumerate(grid)] def uv_top(p): return (p[0] / UV_TOP, p[1] / UV_TOP) def uv_bot(p): return (p[0] / UV_SIDE, p[1] / UV_SIDE) up, down = (0.0, 0.0, 1.0), (0.0, 0.0, -1.0) # ---- the middle of the floe: the grid, without the band of cells along its edge ------------------------------ for j in range(B, NY - B): for i in range(B, NX - B): q = [top[j][i], top[j][i + 1], top[j + 1][i + 1], top[j + 1][i]] mb.poly(q, [uv_top(p) for p in q], ICE, up) q = [bot[j][i], bot[j][i + 1], bot[j + 1][i + 1], bot[j + 1][i]] mb.poly(q, [uv_bot(p) for p in q], BODY, down) # the grid's own edge, once round, counter-clockwise from the south-west: south, east, north, west ring_c = [(i, B) for i in range(B, NX - B)] + [(NX - B, j) for j in range(B, NY - B)] + \ [(i, NY - B) for i in range(NX - B, B, -1)] + [(B, j) for j in range(NY - B, B, -1)] coarse_top = [top[j][i] for i, j in ring_c] coarse_bot = [bot[j][i] for i, j in ring_c] # ---- the rim: rings of points round the floe, three for every one of the grid -------------------------------- def steps(seq, back, K=8): """The sub-steps along the grid's edge, in the order the ring walks it, as the side's parameter 0..1. The grid's edge stands B cells inside the outline; the rim runs from corner to corner. Over the first and the last K cells the parameter is stretched to make up the difference; in between a rim point lies where its grid point lies (round the station, where the cells are small, that matters).""" last = len(seq) - 1 lo, hi = seq[B], seq[last - B] a1, b1 = seq[min(last, B + K)], seq[max(0, last - B - K)] def f(u): w0 = _smooth((a1 - u) / (a1 - lo)) if u < a1 else 0.0 w1 = _smooth((u - b1) / (hi - b1)) if u > b1 else 0.0 return u - lo * w0 + (1.0 - hi) * w1 idx = range(last - B, B, -1) if back else range(B, last - B) out = [] for i in idx: j = i - 1 if back else i + 1 for e in range(FINE): out.append(f(seq[i] + (seq[j] - seq[i]) * e / float(FINE))) return out rim = _Rim(sides, (steps(U, False), steps(V, False), steps(U, True), steps(V, True)), [(p[0], p[1]) for p in coarse_top], berth_y) n = rim.n def h01(k, ring): return _hash(k, ring, 71) ring_a, ring_s, ring_l = [], [], [] k0, k1, wl, u1, u2, u3 = [], [], [], [], [], [] for k in range(n): d, wb, wa, bite = rim.d[k], rim.berth[k], rim.dressed[k], rim.bite[k] free = 1.0 - wb # on top: an apron, a shoulder, and the lip, which hangs a little and slumps where a piece broke away (the # apron at least 1.8 m inside the lip, where the edge has given way far: up to 15 m far out) x, y = rim.at(k, min(0.5 * d - 3.4, d - 1.8)) ring_a.append((x, y, ice_height(x, y, 0.35) + 0.03 * rim.noise(k, 4.0, 1, 73) * (1.0 - wa))) drop = (0.05 + 0.15 * h01(k, 1) + 0.25 * bite) * (1.0 - 0.9 * wa) + 0.02 * wa x, y = rim.at(k, d - (0.75 + 0.45 * h01(k, 2))) ring_s.append((x, y, ice_height(x, y, 0.12) - 0.3 * drop + 0.04 * rim.noise(k, 2.6, 1, 79) * (1.0 - wa))) x, y = rim.at(k, d) lip = (x, y, ice_height(x, y, 0.0) - drop) ring_l.append(lip) # the cliff: two rings between lip and water that stand in and out, then the notch the sea cuts at the # waterline. Towards the vessel nothing bulges more than 8 cm. zw = sea - 0.03 - 0.05 * h01(k, 3) out0 = min(0.04 + 0.12 * rim.noise(k, 2.2, 1, 83), _lerp(9.0, 0.08, wb)) * (1.0 - wa) out1 = min(0.02 + 0.16 * rim.noise(k, 3.1, 1, 89), _lerp(9.0, 0.08, wb)) * (1.0 - wa) f0 = 0.28 + 0.06 * (2.0 * h01(k, 4) - 1.0) f1 = 0.64 + 0.07 * (2.0 * h01(k, 5) - 1.0) x, y = rim.at(k, d + out0) k0.append((x, y, _lerp(lip[2], zw, f0))) x, y = rim.at(k, d + out1) k1.append((x, y, _lerp(lip[2], zw, f1))) notch = -(0.22 + 0.26 * h01(k, 6)) + 0.06 * rim.noise(k, 1.7, 1, 97) x, y = rim.at(k, d + _lerp(notch, -0.10, wa)) wl.append((x, y, zw)) # under water: on the open sides a foot of ice stands out below the notch; towards the vessel it does not foot = _lerp(0.15 + 0.55 * h01(k, 7) * (0.5 + 0.5 * rim.noise(k, 6.0, 1, 101)), -0.18 - 0.20 * h01(k, 7), wb) x, y = rim.at(k, d + foot) u1.append((x, y, sea - 0.95 - 0.25 * h01(k, 8))) # and the flanks draw in towards the underside: 2.2 and 4.0 m in from the lip (what the first floe's flanks # drew in by along its sides) lx, ly = lip[0], lip[1] w2 = rim.noise(k, 7.0, 2, 103) off = _lerp(0.5 * w2, -0.4 * abs(w2), wb) - 2.2 u2.append((lx + rim.nrm[k][0] * off, ly + rim.nrm[k][1] * off, -5.6 + 0.35 * rim.noise(k, 5.0, 1, 107))) off = 0.6 * rim.noise(k, 8.0, 2, 109) * free - 4.0 u3.append((lx + rim.nrm[k][0] * off, ly + rim.nrm[k][1] * off, -8.0 + 0.4 * rim.noise(k, 6.0, 1, 113))) def strip(r0, r1, mat, kind): """Quads between two rings of the rim.""" for k in range(n): k2 = (k + 1) % n q = [r0[k], r0[k2], r1[k2], r1[k]] if kind == "top": mb.poly(q, [uv_top(p) for p in q], mat, up) else: uv = [(rim.run[k] / UV_SIDE, q[0][2] / UV_SIDE), (rim.run[k + 1] / UV_SIDE, q[1][2] / UV_SIDE), (rim.run[k + 1] / UV_SIDE, q[2][2] / UV_SIDE), (rim.run[k] / UV_SIDE, q[3][2] / UV_SIDE)] mb.poly(q, uv, mat, (rim.nrm[k][0] + rim.nrm[k2][0], rim.nrm[k][1] + rim.nrm[k2][1], -0.3 if kind == "under" else 0.0)) def join(coarse, fine, mat, kind): """Three quads of the rim to one edge of the grid, all in quads: the grid's edge and the rim's ring get two points between them, and four quads go round those.""" m = len(coarse) for c in range(m): c2 = (c + 1) % m a = [fine[(FINE * c + e) % n] for e in range(FINE + 1)] p0, p1 = coarse[c], coarse[c2] mid = [] for e in (1, 2): t = e / 3.0 g = tuple(_lerp(p0[i], p1[i], t) for i in range(3)) x, y = (g[0] + a[e][0]) / 2.0, (g[1] + a[e][1]) / 2.0 mid.append((x, y, ice_height(x, y, 0.6) if kind == "top" else (g[2] + a[e][2]) / 2.0)) for q in ([a[0], a[1], mid[0], p0], [a[1], a[2], mid[1], mid[0]], [a[2], a[3], p1, mid[1]], [mid[0], mid[1], p1, p0]): if kind == "top": mb.poly(q, [uv_top(p) for p in q], mat, up) else: mb.poly(q, [uv_bot(p) for p in q], mat, down) join(coarse_top, ring_a, ICE, "top") strip(ring_a, ring_s, ICE, "top") strip(ring_s, ring_l, ICE, "top") for r0, r1 in ((ring_l, k0), (k0, k1), (k1, wl), (wl, u1), (u1, u2)): strip(r0, r1, BODY, "side") strip(u2, u3, BODY, "under") join(coarse_bot, u3, BODY, "under") if DEBUG is not None: DEBUG.update(coarse=coarse_top, ring_a=ring_a, ring_s=ring_s, ring_l=ring_l, outline=list(rim.pts), u3=u3, coarse_bot=coarse_bot, room=list(rim.room), d=list(rim.d)) xs = [p[0] for p in ring_l] ys = [p[1] for p in ring_l] along = [ring_l[k][1] for k in range(n) if rim.south[k] and -41.8 <= ring_l[k][0] <= 80.2] tops = [p[2] for row in top for p in row] + [p[2] for p in ring_l] return {"x": (min(xs), max(xs)), "y": (min(ys), max(ys)), "berth_y": berth_y, "corners_rounded": rim.rounded, "berth": (min(along), max(along)), "rim_points": n, "rim_step": rim.length / n, "bites": len(rim.bites), "top": (min(tops), max(tops)), "bottom": min(p[2] for row in bot for p in row)} # --------------------------------------------------------------------------------------------------------------- def _rz(deg): return Matrix.Rotation(math.radians(deg), 4, "Z") def _rx(deg): return Matrix.Rotation(math.radians(deg), 4, "X") def _scale(sx, sy, sz): return Matrix.Diagonal((sx, sy, sz, 1.0)) def drift_matrix(x, y, s, yaw): """Where a patch of loose ice lies: on the ice at (x, y), on the slope of the ground there.""" e = 1.0 nrm = Vector((-(ice_height(x + e, y) - ice_height(x - e, y)) / (2 * e), -(ice_height(x, y + e) - ice_height(x, y - e)) / (2 * e), 1.0)).normalized() lay = Vector((0.0, 0.0, 1.0)).rotation_difference(nrm).to_matrix().to_4x4() return Matrix.Translation((x, y, ice_height(x, y) + 0.01)) @ lay @ _rz(yaw) @ _scale(s, s, s) # The one patch of drift ice that lay too near the floe's edge for an edge that breaks: where it was, where it is DRIFT_MOVED = ((9.5, 75.5), (9.5, 70.5), 5.0, -140.0) def dress(b, col): """The pack's outdoor pieces, set out by hand. Every large block is the pack's pair: a snow core and, 6 % larger, a shell of ice (in the pack's showcase the pairs are 5 to 11 % apart).""" kit = b.kit n = {"seracs": 0, "blocks": 0, "drift ice": 0} def tag(ob, module): ob["anchorage_zone"] = ZONE ob["anchorage_module"] = module ob["anchorage_surroundings"] = True return ob def half_height(path): rec = kit.meshes[path] return rec["max"][2] / 100.0, rec["min"][2] / 100.0 def serac(x, y, show, s, yaw, lean=0.0): """A large block, `s` times the pack's size across: `show` metres of it stand above the ice, the rest is in the floe. It is made less tall than wide where it would otherwise come out of the floe's underside (the pack's showcase scales its own blocks unevenly in the same way).""" hi, lo = half_height(ROCK_L) sz = min(s, (show + 9.0) / (hi - lo)) z = ice_height(x, y) + show - hi * sz for k, (grow, over, what) in enumerate(((1.0, {0: MAT_BODY}, "core"), (1.06, None, "ice"))): m = Matrix.Translation((x, y, z)) @ _rz(yaw) @ _rx(lean) @ _scale(s * grow, s * grow, sz * grow) tag(kit.place(ROCK_L, m, col, name="Serac (%s)" % what, overrides=over), "ice: seracs") n["seracs"] += 1 def block(x, y, show, s, yaw): hi, lo = half_height(ROCK_M) z = ice_height(x, y) + show - hi * s m = Matrix.Translation((x, y, z)) @ _rz(yaw) @ _scale(s, s, s) tag(kit.place(ROCK_M, m, col, name="Ice block", overrides={0: MAT_BODY}), "ice: blocks") n["blocks"] += 1 def drift(x, y, s, yaw, which=0): """A patch of loose ice, laid on the slope of the ground it lies on.""" tag(kit.place(CHUNKS[which], drift_matrix(x, y, s, yaw), col, name="Drift ice"), "ice: drift ice") n["drift ice"] += 1 # ---- north-west group: frames the right-hand side of the view from Relay Control --------------------------- serac(-58.0, 50.0, 8.5, 1.6, 45.0, lean=4.0) serac(-65.5, 44.0, 5.0, 1.0, -65.0, lean=-6.0) serac(-51.5, 57.0, 4.0, 0.9, 121.5, lean=5.0) for x, y, s, yaw in ((-49.0, 47.5, 1.5, 5.3), (-61.5, 57.5, 1.2, -44.2), (-55.0, 41.5, 1.0, 55.3)): block(x, y, 2.2 * s, s, yaw) # ---- north-east group: the tallest, behind the landing ground -------------------------------------------- serac(52.0, 66.0, 10.0, 1.9, -129.6, lean=3.0) serac(61.0, 60.5, 6.0, 1.2, 105.0, lean=-5.0) serac(44.5, 72.0, 4.5, 1.0, -10.0, lean=6.0) serac(57.5, 74.0, 3.5, 0.8, -38.5, lean=-4.0) for x, y, s, yaw in ((42.0, 63.0, 1.5, 19.9), (64.0, 68.5, 1.2, -29.8), (49.0, 57.5, 1.0, 11.5), (37.5, 69.5, 1.0, 90.0)): block(x, y, 2.2 * s, s, yaw) # ---- east group: seen from the cargo apron, well clear of the vessel's bow ---------------------------------- serac(86.0, 14.0, 7.5, 1.5, 27.9, lean=-4.0) serac(92.0, 21.0, 4.5, 1.0, -56.1, lean=5.0) serac(79.5, 20.5, 3.5, 0.8, 64.9, lean=3.0) for x, y, s, yaw in ((80.0, 9.0, 1.5, -20.0), (90.5, 7.5, 1.0, 55.3), (84.0, 24.5, 1.2, -90.0)): block(x, y, 2.2 * s, s, yaw) # (the pack's SM_Cliff_01 was tried as a bank along the north rim and taken out again: at this size it reads as # a row of slabs. The rim's swell is in the floe itself: MOUNDS.) # ---- the landing ground: a lane of blocks, five a side, the way the showcase lines its approach -------------- yaws = (-29.8, 11.5, 90.0, -90.0, -20.0) for k in range(5): for sx in (-1.0, 1.0): block(sx * 7.6, 55.2 + 2.4 * k, 2.2, 1.0, yaws[k] * sx) # ---- drift ice at the foot of the groups and along the bank ------------------------------------------------ for x, y, s, yaw, w in ( (-52.5, 44.0, 7.5, -155.0, 1), (-62.0, 51.5, 5.0, 75.0, 0), (-47.0, 53.0, 5.0, -15.0, 1), (-57.5, 60.5, 5.0, 175.0, 0), (46.5, 60.0, 7.5, 135.0, 1), (58.0, 55.5, 5.0, -105.0, 0), (39.5, 74.5, 5.0, 65.0, 1), (65.5, 64.0, 5.0, 175.0, 0), (82.5, 5.0, 7.5, -35.0, 1), (93.5, 14.0, 5.0, 40.0, 1), (76.5, 15.5, 5.0, 145.0, 1), (-18.0, 72.5, 5.0, 15.0, 1), (9.5, 70.5, 5.0, -140.0, 1), (33.0, 73.5, 5.0, 165.0, 1)): drift(x, y, s, yaw, w) return n # --------------------------------------------------------------------------------------------------------------- def floe_mesh(b): """The floe's mesh, new, and what build_floe says about it.""" mb = MeshBuilder() info = build_floe(mb, b) me = mb.build("ANCH_Ice_Mass", b.kit, mats=(MAT_BODY, MAT_GLAZE)) me.polygons.foreach_set("use_smooth", [True] * len(me.polygons)) try: me.set_sharp_from_angle(angle=math.radians(40.0)) # the swell stays soft, breaks in the ice sharp except Exception: # noqa: BLE001 pass me.update() info["faces"] = len(me.polygons) info["tris"] = sum(len(p.vertices) - 2 for p in me.polygons) return me, info def _report(info, extra=""): print("[anchorage] ice: floe x %.1f..%.1f, y %.1f..%.1f, top %.2f..%.2f, underside down to %.1f; rim: a point every " "%.2f m (%d round the floe), %d bites; along the vessel the edge runs between y %.2f and %.2f (laid out at " "%.2f); %d faces, %d tris%s" % ( info["x"][0], info["x"][1], info["y"][0], info["y"][1], info["top"][0], info["top"][1], info["bottom"], info["rim_step"], info["rim_points"], info["bites"], info["berth"][1], info["berth"][0], info["berth_y"], info["faces"], info["tris"], extra)) def update_floe(b): """Gives the floe that is in the open file a new mesh, in place: the object, its place in the collections, its properties and everything that stands on the ice stay as they are. For changes to the floe's shape in a file that has been worked on by hand.""" ob = bpy.data.objects.get("ANCH_Ice_Mass") if ob is None or ob.type != "MESH": raise RuntimeError("no ANCH_Ice_Mass in this file: use add_ice") old = ob.data me, info = floe_mesh(b) ob.data = me if old.users == 0: bpy.data.meshes.remove(old) me.name = "ANCH_Ice_Mass" # the patch of drift ice that the recipe now lays further in: moved here too, if it still lies where it was laid (ox, oy), (nx, ny), s, yaw = DRIFT_MOVED moved = "" for d in bpy.data.objects: if d.get("anchorage_module") == "ice: drift ice" and \ math.hypot(d.matrix_world.translation.x - ox, d.matrix_world.translation.y - oy) < 0.05: d.matrix_world = drift_matrix(nx, ny, s, yaw) moved = "; one patch of drift ice moved 5 m in from the edge" _report(info, "; mesh replaced in place" + moved) return info def add_ice(b): """Adds or replaces the ice in the open file. `b` is the Builder (it carries the kit and the plan's numbers).""" station = get_collection("ANCHORAGE STATION") col = get_collection(COLLECTION, station) for ob in list(col.objects): me = ob.data bpy.data.objects.remove(ob) if me is not None and me.users == 0: bpy.data.meshes.remove(me) me, info = floe_mesh(b) ob = bpy.data.objects.new("ANCH_Ice_Mass", me) col.objects.link(ob) ob["anchorage_zone"] = ZONE ob["anchorage_module"] = "ice" ob["anchorage_custom_mesh"] = "ANCH_Ice_Mass" ob["anchorage_surroundings"] = True ob["anchorage_collision"] = "complex" # it is walked on: Unreal uses the mesh itself as collision # for Unreal only (see the docstring): a snow surface under the translucent top ob["anchorage_underlay_slot"] = ICE ob["anchorage_underlay_material"] = MAT_BODY ob["anchorage_underlay_depth"] = UNDERLAY ob["anchorage_note"] = ("the floe: top in the pack's clear ice, cliff, flanks and underside in its snow material; " "for Unreal a snow surface is added 3 cm under the top") counts = dress(b, col) _report(info, "; %s" % counts) return col