"""A forged metal ring, plus the matcap needed to shade it as chrome. The Asheron's Call mark is a broken, hand-forged silver band enclosing a hooked glyph. A plain torus reads as a donut, so the tube radius is noise-modulated along the major angle and tapered to points at the break. Chrome needs environment reflection, which a Lambert rasterizer cannot give. A matcap (material capture) solves it: one image of a lit sphere, sampled by the camera-space normal. It is the standard cheap stand-in for a full env map. """ import numpy as np def _fbm(theta, seed=7, octaves=4): rng = np.random.default_rng(seed) out = np.zeros_like(theta) amp, freq = 1.0, 3.0 for _ in range(octaves): phase = rng.uniform(0, 2 * np.pi) out += amp * np.sin(freq * theta + phase) amp *= 0.5 freq *= 2.0 return out / 1.9 def forged_ring(R=1.0, r=0.135, nu=320, nv=44, gap_deg=26.0, gap_center_deg=90.0, rough=0.30, taper=2.2, seed=7, end_frac=0.16): """Broken forged band in the XZ plane (so it faces a -Y camera).""" span = 360.0 - gap_deg start = gap_center_deg + gap_deg / 2.0 u = np.radians(start + np.linspace(0.0, span, nu)) v = np.linspace(0.0, 2 * np.pi, nv) t = np.linspace(0.0, 1.0, nu) # taper both ends of the band to points, and rough up the middle # only taper the last end_frac at each end, so the band stays a band ramp = np.clip(np.minimum(t, 1.0 - t) / max(end_frac, 1e-6), 0.0, 1.0) ends = ramp ** (1.0 / taper) tube = r * ends * (1.0 + rough * _fbm(u * 1.7, seed)) tube = np.maximum(tube, r * 0.05) U, V = np.meshgrid(u, v, indexing="ij") T = np.broadcast_to(tube[:, None], U.shape) # slight out-of-plane wobble so it reads hand-made, not machined wob = 0.035 * _fbm(u * 2.3, seed + 3)[:, None] cx, cy = np.cos(U), np.sin(U) px = (R + T * np.cos(V)) * cx pz = (R + T * np.cos(V)) * cy py = T * np.sin(V) + wob P = np.stack([px, py, pz], axis=-1).reshape(-1, 3) # analytic-ish normals: outward from the tube centreline ccx = R * cx ccz = R * cy ccy = np.zeros_like(ccx) + wob C = np.stack([ccx, ccy, ccz], axis=-1).reshape(-1, 3) N = P - C ln = np.linalg.norm(N, axis=1, keepdims=True) N = N / np.where(ln < 1e-9, 1, ln) UV = np.stack([U / (2 * np.pi), V / (2 * np.pi)], axis=-1).reshape(-1, 2) tri = [] for i in range(nu - 1): for j in range(nv - 1): a = i * nv + j b = (i + 1) * nv + j c = i * nv + (j + 1) d = (i + 1) * nv + (j + 1) tri.append((a, b, c)) tri.append((b, d, c)) return P, N, UV, np.array(tri, dtype=np.int64) def hook_glyph(scale=0.62, thick=0.115, nu=200, nv=28, seed=11): """A tapering crescent hook, echoing the glyph inside the AC ring.""" t = np.linspace(0.0, 1.0, nu) ang = np.radians(118.0 + t * 250.0) rad = scale * (1.0 - 0.30 * t) cx = np.cos(ang) * rad cz = np.sin(ang) * rad # taper: fat at the shoulder, needle at the tip tube = thick * (np.sin(np.pi * (0.18 + 0.82 * t)) ** 0.85) * (1.0 - 0.55 * t) tube = np.maximum(tube, thick * 0.04) v = np.linspace(0.0, 2 * np.pi, nv) U, V = np.meshgrid(t, v, indexing="ij") T = np.broadcast_to(tube[:, None], U.shape) # local frame along the curve dx = np.gradient(cx); dz = np.gradient(cz) tl = np.sqrt(dx * dx + dz * dz); tl = np.where(tl < 1e-9, 1, tl) tx, tz = dx / tl, dz / tl nx_, nz_ = -tz, tx # in-plane normal P = np.stack([ (cx[:, None] + T * np.cos(V) * nx_[:, None]), (T * np.sin(V)), (cz[:, None] + T * np.cos(V) * nz_[:, None]), ], axis=-1).reshape(-1, 3) C = np.stack([ np.broadcast_to(cx[:, None], U.shape), np.zeros_like(U), np.broadcast_to(cz[:, None], U.shape), ], axis=-1).reshape(-1, 3) N = P - C ln = np.linalg.norm(N, axis=1, keepdims=True) N = N / np.where(ln < 1e-9, 1, ln) UV = np.stack([U, V / (2 * np.pi)], axis=-1).reshape(-1, 2) tri = [] for i in range(nu - 1): for j in range(nv - 1): a = i * nv + j; b = (i + 1) * nv + j c = i * nv + (j + 1); d = (i + 1) * nv + (j + 1) tri.append((a, b, c)); tri.append((b, d, c)) return P, N, UV, np.array(tri, dtype=np.int64) def chrome_matcap(size=512, tint=(1.0, 1.0, 1.06), warm=(0.62, 0.55, 0.42)): """Polished-silver matcap: bright sky above, dark horizon, warm ground.""" y, x = np.mgrid[0:size, 0:size].astype(np.float32) x = (x / (size - 1)) * 2 - 1 y = 1 - (y / (size - 1)) * 2 r2 = x * x + y * y inside = r2 <= 1.0 z = np.sqrt(np.clip(1 - r2, 0, 1)) sky = np.clip(y * 0.5 + 0.5, 0, 1) # sharp horizon band -- what makes metal read as metal horizon = np.exp(-((y + 0.06) ** 2) / 0.0026) ground = np.clip(-y * 0.9 + 0.15, 0, 1) base = (0.16 + 0.72 * sky ** 1.7)[..., None] * np.array(tint, dtype=np.float32) base = base + 0.55 * horizon[..., None] * np.array([0.85, 0.90, 1.0], dtype=np.float32) base = base + 0.42 * (ground ** 1.6)[..., None] * np.array(warm, dtype=np.float32) # key specular + a secondary glint spec = np.exp(-(((x + 0.36) ** 2 + (y - 0.46) ** 2)) / 0.020) spec2 = np.exp(-(((x - 0.44) ** 2 + (y - 0.16) ** 2)) / 0.055) base = base + 1.5 * spec[..., None] + 0.40 * spec2[..., None] # rim brightening at grazing angles base = base + 0.55 * np.clip((r2 - 0.72) / 0.28, 0, 1)[..., None] rgb = np.clip(base, 0, 1.6) a = inside.astype(np.float32) return np.concatenate([rgb, a[..., None]], axis=2).astype(np.float32) def verdigris_matcap(size=512): """Same form, aged bronze-green -- ties the ring to the mosswart palette.""" m = chrome_matcap(size, tint=(0.72, 0.86, 0.58), warm=(0.50, 0.44, 0.18)) m[..., 0] *= 0.78 m[..., 1] *= 0.96 m[..., 2] *= 0.62 return m def merge(*meshes): """Concatenate (P,N,UV,TRI,TEX) tuples into one mesh.""" P, N, UV, TRI, TEX = [], [], [], [], [] base = 0 for p, n, uv, tri, tex in meshes: P.append(p); N.append(n); UV.append(uv) TRI.append(tri + base) TEX += tex base += len(p) return (np.concatenate(P), np.concatenate(N), np.concatenate(UV), np.concatenate(TRI), TEX)