""" The ice the station stands on: one big floe, about 175 x 120 m and 10 to 13 m thick, with the vessel's berth cut straight along its south edge. 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 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") NX, NY = 82, 60 # the top's grid: cells of about 2 m BAND = 3 # cells of that grid given over to the rim: the edge may wander 6 m FINE = 3 # the rim has three points for every one of the grid: one every 0.7 m # 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 ) # 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 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)) rel = max(rel, -0.25) 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) # --------------------------------------------------------------------------------------------------------------- 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.""" xsw, xse = -64.0, 98.0 xnw, xne, yn = -70.0, 94.0, 84.0 def south(t): return (xsw + (xse - xsw) * t, berth_y) def north(t): s = math.sin(math.pi * t) return (xnw + (xne - xnw) * t, yn + s * (4.5 * math.sin(2 * math.pi * 1.3 * t + 0.7) + 2.5 * math.sin(2 * math.pi * 3.1 * t + 2.0))) def west(v): s = math.sin(math.pi * v) return (xsw + (xnw - xsw) * v - s ** 0.8 * (15.0 + 3.0 * math.sin(2 * math.pi * 2.2 * v + 1.0)), berth_y + (yn - berth_y) * v) def east(v): s = math.sin(math.pi * v) return (xse + (xne - xse) * v + s ** 0.9 * (11.0 + 3.0 * math.sin(2 * math.pi * 1.7 * v + 0.4)), berth_y + (yn - berth_y) * 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.""" def __init__(self, sides, n_x, n_y, berth_y): self.pts = [] self.south = [] for (curve, forward), n in zip(sides, (n_x, n_y, n_x, n_y)): for e in range(n * FINE): t = e / float(n * FINE) self.pts.append(curve(t if forward else 1.0 - t)) self.south.append(curve is sides[0][0]) n = self.n = len(self.pts) 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 # 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: self.bites.append((s, 2.2 + 3.6 * _hash(i, 1, 61), 1.0 + 2.2 * _hash(i, 2, 61))) s += 15.0 + 26.0 * _hash(i, 3, 61) i += 1 sw, se = sides[0][0](0.0), sides[0][0](1.0) 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) bite += depth * _bump(min(ds, self.length - ds) / hw) big = 1.3 * self.noise(k, 34.0, 3, 41) mid = 0.55 * self.noise(k, 9.0, 2, 43) small = 0.16 * self.noise(k, 1.9, 1, 47) + 0.07 * (2.0 * _hash(k, 3, 53) - 1.0) corner = 2.2 * (math.exp(-(math.hypot(x - sw[0], y - sw[1]) / 4.5) ** 2) + math.exp(-(math.hypot(x - se[0], y - se[1]) / 4.5) ** 2)) free = max(-3.3, min(1.6, big + mid + small - bite - corner)) # 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 self.d.append(_lerp(d, -0.04, wa)) self.bite.append(min(1.0, bite / 1.5) * (1.0 - wa)) self.berth.append(wb) self.dressed.append(wa) 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 build_floe(mb, b): berth_y = b.BRIDGE_END_Y - 0.20 sea = b.SEA_LEVEL_Z patch, sides = _outline(berth_y) grid = [[patch(i / NX, j / NY) for i in range(NX + 1)] for j in range(NY + 1)] cx = sum(p[0] for row in grid for p in row) / ((NX + 1) * (NY + 1)) cy = sum(p[1] for row in grid for p in row) / ((NX + 1) * (NY + 1)) B = BAND def edge_factor(i, j): # metres from the rim, roughly, along the grid; the south edge is the berth and stays level anyway d = min(i, NX - i, NY - j) * 2.0 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 towards the middle, deepest in the middle bot = [[(cx + (p[0] - cx) * 0.93, cy + (p[1] - cy) * 0.93, -9.5 - 3.2 * math.sin(math.pi * i / NX) * math.sin(math.pi * j / NY) - 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 -------------------------------- rim = _Rim(sides, NX - 2 * B, NY - 2 * B, 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 x, y = rim.at(k, 0.5 * d - 3.4) 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 lx, ly = lip[0], lip[1] w2 = rim.noise(k, 7.0, 2, 103) off = _lerp(0.5 * w2, -0.4 * abs(w2), wb) u2.append((cx + (lx - cx) * 0.972 + rim.nrm[k][0] * off, cy + (ly - cy) * 0.972 + 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 u3.append((cx + (lx - cx) * 0.95 + rim.nrm[k][0] * off, cy + (ly - cy) * 0.95 + 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") 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, "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