Spaces:
Running
Running
Terrain round 32: water repaint skips water-coloured ground, colour map keeps callers' colours, small-ground preview cameras, running elapsed time
Browse files- app/llms.txt +4 -2
- app/server.py +10 -3
- projtex/terrain.py +70 -11
app/llms.txt
CHANGED
|
@@ -204,8 +204,10 @@ Request (JSON; only the ground and a prompt are required):
|
|
| 204 |
(paths get an even surface automatically). "from_above" (optional) says how a layer looks in the colour map when
|
| 205 |
that differs from its close-up; forests and woods default to their tree crowns. If you place your own 3D trees,
|
| 206 |
make the forest layer their floor: from_above "pine-needle forest floor, no tree crowns" and a brown colour (a
|
| 207 |
-
green patch is painted as crowns); then no trees are painted anywhere.
|
| 208 |
-
|
|
|
|
|
|
|
| 209 |
- uv: planar (default: terrain.glb gets top-down UVs) | keep (the colour map is baked into your own UVs).
|
| 210 |
- backend: qwen-turbo (default) | qwen-space | mock (free dry run). Cost: layers + 1 + tiles images (2048: about
|
| 211 |
15 images, ~2 min of GPU quota and ~2-3 min in all; 4096: ~31 images).
|
|
|
|
| 204 |
(paths get an even surface automatically). "from_above" (optional) says how a layer looks in the colour map when
|
| 205 |
that differs from its close-up; forests and woods default to their tree crowns. If you place your own 3D trees,
|
| 206 |
make the forest layer their floor: from_above "pine-needle forest floor, no tree crowns" and a brown colour (a
|
| 207 |
+
green patch is painted as crowns); then no trees are painted anywhere. A layer's "color" [r, g, b] is kept:
|
| 208 |
+
the colour map is turned to its hue (the image model drifts, e.g. red autumn leaves toward olive). Presets (when
|
| 209 |
+
layers is omitted, chosen from the prompt's words): alpine, temperate, desert, arctic, winter, volcanic (GET /api
|
| 210 |
+
lists them).
|
| 211 |
- uv: planar (default: terrain.glb gets top-down UVs) | keep (the colour map is baked into your own UVs).
|
| 212 |
- backend: qwen-turbo (default) | qwen-space | mock (free dry run). Cost: layers + 1 + tiles images (2048: about
|
| 213 |
15 images, ~2 min of GPU quota and ~2-3 min in all; 4096: ~31 images).
|
app/server.py
CHANGED
|
@@ -51,6 +51,12 @@ def identity_from(headers, token: str | None = None) -> Identity:
|
|
| 51 |
return Identity(token=tok or None, ip_token=headers.get("x-ip-token") or None)
|
| 52 |
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
def terrain_status(job, base: str, events: int = 12) -> dict:
|
| 55 |
"""The status of a terrain job: progress, the files, what each layer covers, and what to do next."""
|
| 56 |
url = f"{base}/api/jobs/{job.id}"
|
|
@@ -63,7 +69,7 @@ def terrain_status(job, base: str, events: int = 12) -> dict:
|
|
| 63 |
"job_id": job.id, "kind": "terrain", "state": job.state, "operation": job.op, "progress": job.progress,
|
| 64 |
"backend": job.params.get("backend"),
|
| 65 |
"quota_identity": job.identity.kind if BACKENDS[job.params["backend"]]["uses_quota"] else "not needed",
|
| 66 |
-
"elapsed_seconds":
|
| 67 |
"recent_events": job.events[-events:],
|
| 68 |
}
|
| 69 |
warnings = list(job.result.get("warnings") or []) + list(st.get("warnings") or [])
|
|
@@ -211,7 +217,7 @@ def status_payload(job, base: str, events: int = 12) -> dict:
|
|
| 211 |
"job_id": job.id, "state": job.state, "operation": job.op, "progress": job.progress,
|
| 212 |
"backend": job.params.get("backend"), "mode": job.params.get("mode"),
|
| 213 |
"quota_identity": job.identity.kind if BACKENDS[job.params["backend"]]["uses_quota"] else "not needed",
|
| 214 |
-
"elapsed_seconds":
|
| 215 |
"views": views, "error": job.error, "recent_events": job.events[-events:],
|
| 216 |
}
|
| 217 |
last = dict(job.result.get("last") or {})
|
|
@@ -544,7 +550,8 @@ class TerrainLayer(BaseModel):
|
|
| 544 |
breakup: float | None = Field(None, ge=0, le=1, description="How irregular the layer's edges are (default 0.5)")
|
| 545 |
tile_m: float = Field(4.0, gt=0.2, le=50, description="Size in metres of one repeat of the detail texture")
|
| 546 |
color: list[float] | None = Field(None, description="Optional [r, g, b] 0-255 the layer shows in the overview "
|
| 547 |
-
"guide (default: its detail texture's mean colour)"
|
|
|
|
| 548 |
triplanar: bool | None = Field(None, description="Project the detail texture from the sides on steep slopes "
|
| 549 |
"(default: layers whose rules start at 30 degrees or more)")
|
| 550 |
from_above: str | None = Field(None, description="How the layer looks from high above in the colour map, if "
|
|
|
|
| 51 |
return Identity(token=tok or None, ip_token=headers.get("x-ip-token") or None)
|
| 52 |
|
| 53 |
|
| 54 |
+
def elapsed(job) -> float:
|
| 55 |
+
"""Seconds since the job was created: up to now while it runs (the last event's time stood still through a
|
| 56 |
+
long step, round 32), else up to its last event."""
|
| 57 |
+
return round((time.time() if job.state in ("queued", "running") else job.updated) - job.created, 1)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
def terrain_status(job, base: str, events: int = 12) -> dict:
|
| 61 |
"""The status of a terrain job: progress, the files, what each layer covers, and what to do next."""
|
| 62 |
url = f"{base}/api/jobs/{job.id}"
|
|
|
|
| 69 |
"job_id": job.id, "kind": "terrain", "state": job.state, "operation": job.op, "progress": job.progress,
|
| 70 |
"backend": job.params.get("backend"),
|
| 71 |
"quota_identity": job.identity.kind if BACKENDS[job.params["backend"]]["uses_quota"] else "not needed",
|
| 72 |
+
"elapsed_seconds": elapsed(job), "files": files, "error": job.error,
|
| 73 |
"recent_events": job.events[-events:],
|
| 74 |
}
|
| 75 |
warnings = list(job.result.get("warnings") or []) + list(st.get("warnings") or [])
|
|
|
|
| 217 |
"job_id": job.id, "state": job.state, "operation": job.op, "progress": job.progress,
|
| 218 |
"backend": job.params.get("backend"), "mode": job.params.get("mode"),
|
| 219 |
"quota_identity": job.identity.kind if BACKENDS[job.params["backend"]]["uses_quota"] else "not needed",
|
| 220 |
+
"elapsed_seconds": elapsed(job), "summary": summary, "files": files,
|
| 221 |
"views": views, "error": job.error, "recent_events": job.events[-events:],
|
| 222 |
}
|
| 223 |
last = dict(job.result.get("last") or {})
|
|
|
|
| 550 |
breakup: float | None = Field(None, ge=0, le=1, description="How irregular the layer's edges are (default 0.5)")
|
| 551 |
tile_m: float = Field(4.0, gt=0.2, le=50, description="Size in metres of one repeat of the detail texture")
|
| 552 |
color: list[float] | None = Field(None, description="Optional [r, g, b] 0-255 the layer shows in the overview "
|
| 553 |
+
"guide (default: its detail texture's mean colour); the "
|
| 554 |
+
"colour map is turned to its hue")
|
| 555 |
triplanar: bool | None = Field(None, description="Project the detail texture from the sides on steep slopes "
|
| 556 |
"(default: layers whose rules start at 30 degrees or more)")
|
| 557 |
from_above: str | None = Field(None, description="How the layer looks from high above in the colour map, if "
|
projtex/terrain.py
CHANGED
|
@@ -1053,6 +1053,44 @@ def _lab(rgb) -> np.ndarray:
|
|
| 1053 |
return cv2.cvtColor(np.asarray(rgb, np.float32).reshape(1, 1, 3), cv2.COLOR_RGB2LAB)[0, 0]
|
| 1054 |
|
| 1055 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1056 |
def lift_darks(colors: np.ndarray, floor: float = 18.0, knee: float = 30.0) -> np.ndarray:
|
| 1057 |
"""Guide colours (sRGB 0..1) darker than ``knee`` L* pressed into ``floor``..``knee``, their order kept: flat
|
| 1058 |
near-black reads as ink drawn on the picture, not as ground (round 31: black lava flows, L* 6, were painted
|
|
@@ -1257,14 +1295,18 @@ _WATER_LAYER = r"\b(?:water|rivers?|streams?|creeks?|brooks?|ponds?|lakes?|sea|o
|
|
| 1257 |
|
| 1258 |
|
| 1259 |
def dry_painted_water(img: np.ndarray, H: np.ndarray, level: float, W: np.ndarray, layers: list[dict],
|
| 1260 |
-
relief: float, water_rgb, cell=(1.0, 1.0)
|
|
|
|
| 1261 |
"""Water the image model painted on ground above the caller's water plane, repainted in the colour its layers
|
| 1262 |
have elsewhere in the image. Measured (round 29): an 18 m pond in an 800 m desert was painted 33 to 52 times
|
| 1263 |
its area whatever the prompt said; under the caller's water plane the extra became dark blue "water" on dry
|
| 1264 |
sand. Only gentle ground near the level counts (under 30 degrees, within max(10 m, 2 % of the relief)): shaded
|
| 1265 |
fjord cliffs hundreds of metres up are as dark and blue as water (round 30; the oasis's extra water lay on
|
| 1266 |
-
slopes up to 30 degrees, at most 6 m up).
|
| 1267 |
-
|
|
|
|
|
|
|
|
|
|
| 1268 |
import cv2
|
| 1269 |
import re
|
| 1270 |
|
|
@@ -1283,12 +1325,17 @@ def dry_painted_water(img: np.ndarray, H: np.ndarray, level: float, W: np.ndarra
|
|
| 1283 |
fallback = np.median(L[plain], 0) if plain.sum() >= 30 else ground
|
| 1284 |
if fallback is None:
|
| 1285 |
return img, 0
|
| 1286 |
-
|
|
|
|
|
|
|
|
|
|
| 1287 |
E = np.einsum("lhw,lc->hwc", W, np.stack(refs).astype(np.float32))
|
|
|
|
| 1288 |
de = np.linalg.norm(L - E, axis=-1)
|
| 1289 |
gz, gx = np.gradient(H.astype(np.float64), float(cell[0]), float(cell[1]))
|
| 1290 |
low = (np.degrees(np.arctan(np.hypot(gx, gz))) < 30) & (H < level + max(10.0, 0.02 * relief))
|
| 1291 |
wet = dry & low & (dw < de) & (dw < np.maximum(12.0, 0.5 * np.linalg.norm(E - mu, axis=-1)))
|
|
|
|
| 1292 |
keep = [i for i, Lr in enumerate(layers) if re.search(_WATER_LAYER, Lr.get("name", "").replace("_", " "), re.I)]
|
| 1293 |
if keep:
|
| 1294 |
wet &= W[keep].sum(0) < 0.3
|
|
@@ -1716,7 +1763,8 @@ class Terrain:
|
|
| 1716 |
fixed, n = dry_painted_water(ov, H, float(self.prm["water_level"]), self.weights(ov.shape[0]),
|
| 1717 |
self.state["layers"], self.state["relief_m"],
|
| 1718 |
water_look(self.prm.get("prompt", ""))[0],
|
| 1719 |
-
(self.state["depth_m"] / H.shape[0], self.state["width_m"] / H.shape[1])
|
|
|
|
| 1720 |
if n:
|
| 1721 |
_save(self.root / "macro" / "overview_raw.png", ov)
|
| 1722 |
ov = fixed
|
|
@@ -1790,6 +1838,12 @@ class Terrain:
|
|
| 1790 |
tiles = {(i, j): _load(d / f"tile_{i}_{j}.png") for i in range(lay["n"]) for j in range(lay["n"])}
|
| 1791 |
macro = merge_tiles(tiles, lay, ov)
|
| 1792 |
F = macro.shape[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1793 |
_save(self.root / "macro" / "macro.jpg", macro, quality=92)
|
| 1794 |
self.state["macro_file"] = "macro/macro.jpg"
|
| 1795 |
(x0, _), (x1, _) = self.bounds
|
|
@@ -1797,7 +1851,6 @@ class Terrain:
|
|
| 1797 |
self.state["macro_soft_file"] = "macro/macro_soft.jpg"
|
| 1798 |
# colour modulation for engines that multiply splatted layers by a colour map (Unity/Godot style):
|
| 1799 |
# lowpass(macro) / lowpass(sum of w_i * layer mean), linear, stored halved (0.5 = x1)
|
| 1800 |
-
W = self.weights()
|
| 1801 |
means = np.stack([np.asarray(L.get("mean_linear") or [0.2, 0.2, 0.2], np.float32)
|
| 1802 |
for L in self.state["layers"]])
|
| 1803 |
S = W.shape[1]
|
|
@@ -1955,19 +2008,25 @@ class Terrain:
|
|
| 1955 |
if drawn:
|
| 1956 |
near_drawn = ndi.uniform_filter(W[drawn].sum(0), max(3, S // 30))
|
| 1957 |
score = score + 0.8 * near_drawn / max(float(near_drawn.max()), 1e-6)
|
|
|
|
|
|
|
| 1958 |
wl = self.prm.get("water_level")
|
| 1959 |
if wl is not None: # look at the land, not at the bed of the caller's lake (round 28)
|
| 1960 |
Hs = resample(H, S)
|
| 1961 |
-
|
| 1962 |
-
|
| 1963 |
-
|
|
|
|
|
|
|
|
|
|
| 1964 |
r, cc = np.unravel_index(int(np.argmax(score)), score.shape)
|
| 1965 |
px, pz = x0 + (cc + 0.5) / S * (x1 - x0), z0 + (r + 0.5) / S * (z1 - z0)
|
| 1966 |
toward = np.array([c[0] - px, c[2] - pz])
|
| 1967 |
toward = toward / (np.linalg.norm(toward) + 1e-9)
|
| 1968 |
-
for name, dd, hh, fov in (("mid", float(np.clip(0.12 * span, 40, 350)), 0.45, 50),
|
| 1969 |
-
("near", float(np.clip(0.004 * span, 4, 12)), 0.4, 60)):
|
| 1970 |
ex, ez = px - toward[0] * dd, pz - toward[1] * dd
|
|
|
|
| 1971 |
ey = max(self._ground_y(H, ex, ez), self._ground_y(H, px, pz), float(wl) if wl is not None else -1e18) \
|
| 1972 |
+ hh * dd
|
| 1973 |
tgt = [px, self._ground_y(H, px, pz), pz]
|
|
|
|
| 1053 |
return cv2.cvtColor(np.asarray(rgb, np.float32).reshape(1, 1, 3), cv2.COLOR_RGB2LAB)[0, 0]
|
| 1054 |
|
| 1055 |
|
| 1056 |
+
def match_colours(img: np.ndarray, W: np.ndarray, layers: list[dict], keep: np.ndarray | None = None,
|
| 1057 |
+
chroma: float = 0.5, cap: float = 15.0) -> np.ndarray:
|
| 1058 |
+
"""The colour map with each layer the caller gave a ``color`` turned toward that colour's hue (and half way to
|
| 1059 |
+
its chroma, at most ``cap`` in a*b*), lightness and texture as painted. The image model drifts toward its idea
|
| 1060 |
+
of the place: October leaf litter asked for in rust (126, 74, 38) came back olive brown, a* 19 -> 6, and red
|
| 1061 |
+
blueberry scrub olive, hue 55 -> 94 degrees (round 32): the tester recoloured the materials by hand.
|
| 1062 |
+
``W`` and ``keep`` (0..1, where to change it: not the caller's water) are at the splat map's size; the shift is
|
| 1063 |
+
measured there and resized to the image."""
|
| 1064 |
+
import cv2
|
| 1065 |
+
|
| 1066 |
+
S = W.shape[1]
|
| 1067 |
+
lab = lambda a: cv2.cvtColor(np.clip(a, 0, 1).astype(np.float32), cv2.COLOR_RGB2LAB) # noqa: E731
|
| 1068 |
+
small = lab(cv2.resize(img, (S, S), interpolation=cv2.INTER_AREA) if img.shape[0] != S else img)
|
| 1069 |
+
shift = np.zeros((S, S, 2), np.float32)
|
| 1070 |
+
inner = np.ones((S, S), bool) if keep is None else keep > 0.5
|
| 1071 |
+
for i, L in enumerate(layers):
|
| 1072 |
+
if not L.get("color"):
|
| 1073 |
+
continue
|
| 1074 |
+
m = cv2.erode(((W[i] > 0.7) & inner).astype(np.uint8), np.ones((5, 5), np.uint8)) > 0
|
| 1075 |
+
if m.sum() < max(100, 1e-4 * m.size):
|
| 1076 |
+
continue
|
| 1077 |
+
ab = small[m][:, 1:].mean(0)
|
| 1078 |
+
want = _lab(guide_colour(L))[1:]
|
| 1079 |
+
c, wc = float(np.hypot(*ab)), float(np.hypot(*want))
|
| 1080 |
+
tc = c + chroma * (wc - c)
|
| 1081 |
+
target = want * tc / wc if wc > 3 else ab * tc / max(c, 1e-6) # a grey has no hue to turn to
|
| 1082 |
+
d = target - ab
|
| 1083 |
+
d *= min(1.0, cap / max(float(np.linalg.norm(d)), 1e-6))
|
| 1084 |
+
shift += W[i][..., None] * d.astype(np.float32)
|
| 1085 |
+
if keep is not None:
|
| 1086 |
+
shift *= keep[..., None]
|
| 1087 |
+
if not shift.any():
|
| 1088 |
+
return img
|
| 1089 |
+
out = lab(img)
|
| 1090 |
+
out[..., 1:] += cv2.resize(shift, img.shape[1::-1], interpolation=cv2.INTER_LINEAR) if img.shape[0] != S else shift
|
| 1091 |
+
return np.clip(cv2.cvtColor(out, cv2.COLOR_LAB2RGB), 0, 1)
|
| 1092 |
+
|
| 1093 |
+
|
| 1094 |
def lift_darks(colors: np.ndarray, floor: float = 18.0, knee: float = 30.0) -> np.ndarray:
|
| 1095 |
"""Guide colours (sRGB 0..1) darker than ``knee`` L* pressed into ``floor``..``knee``, their order kept: flat
|
| 1096 |
near-black reads as ink drawn on the picture, not as ground (round 31: black lava flows, L* 6, were painted
|
|
|
|
| 1295 |
|
| 1296 |
|
| 1297 |
def dry_painted_water(img: np.ndarray, H: np.ndarray, level: float, W: np.ndarray, layers: list[dict],
|
| 1298 |
+
relief: float, water_rgb, cell=(1.0, 1.0), guide: np.ndarray | None = None
|
| 1299 |
+
) -> tuple[np.ndarray, int]:
|
| 1300 |
"""Water the image model painted on ground above the caller's water plane, repainted in the colour its layers
|
| 1301 |
have elsewhere in the image. Measured (round 29): an 18 m pond in an 800 m desert was painted 33 to 52 times
|
| 1302 |
its area whatever the prompt said; under the caller's water plane the extra became dark blue "water" on dry
|
| 1303 |
sand. Only gentle ground near the level counts (under 30 degrees, within max(10 m, 2 % of the relief)): shaded
|
| 1304 |
fjord cliffs hundreds of metres up are as dark and blue as water (round 30; the oasis's extra water lay on
|
| 1305 |
+
slopes up to 30 degrees, at most 6 m up). Nor does ground asked for in a colour like water's (``guide``, the
|
| 1306 |
+
colour map the model was given): Iceland's black sand and lava were within 12 of it, and repainting them in
|
| 1307 |
+
the only colour left, the glacier's, ringed the lava with white (round 32). ``H``, ``W`` and ``guide`` are at
|
| 1308 |
+
the image's size, ``cell`` its metres per pixel (along Z, along X); returns the image and the number of
|
| 1309 |
+
pixels repainted."""
|
| 1310 |
import cv2
|
| 1311 |
import re
|
| 1312 |
|
|
|
|
| 1325 |
fallback = np.median(L[plain], 0) if plain.sum() >= 30 else ground
|
| 1326 |
if fallback is None:
|
| 1327 |
return img, 0
|
| 1328 |
+
G = None if guide is None else lab(guide)
|
| 1329 |
+
asked = [np.median(G[w > 0.6], 0) if G is not None and (w > 0.6).sum() >= 30 else None for w in W]
|
| 1330 |
+
refs = [np.median(L[(w > 0.6) & plain], 0) if ((w > 0.6) & plain).sum() >= 30 else
|
| 1331 |
+
a if a is not None else fallback for w, a in zip(W, asked)]
|
| 1332 |
E = np.einsum("lhw,lc->hwc", W, np.stack(refs).astype(np.float32))
|
| 1333 |
+
A = np.einsum("lhw,lc->hwc", W, np.stack([r if a is None else a for a, r in zip(asked, refs)]).astype(np.float32))
|
| 1334 |
de = np.linalg.norm(L - E, axis=-1)
|
| 1335 |
gz, gx = np.gradient(H.astype(np.float64), float(cell[0]), float(cell[1]))
|
| 1336 |
low = (np.degrees(np.arctan(np.hypot(gx, gz))) < 30) & (H < level + max(10.0, 0.02 * relief))
|
| 1337 |
wet = dry & low & (dw < de) & (dw < np.maximum(12.0, 0.5 * np.linalg.norm(E - mu, axis=-1)))
|
| 1338 |
+
wet &= np.linalg.norm(A - mu, axis=-1) > 20 # ground asked for in a colour like water's: cannot tell them apart
|
| 1339 |
keep = [i for i, Lr in enumerate(layers) if re.search(_WATER_LAYER, Lr.get("name", "").replace("_", " "), re.I)]
|
| 1340 |
if keep:
|
| 1341 |
wet &= W[keep].sum(0) < 0.3
|
|
|
|
| 1763 |
fixed, n = dry_painted_water(ov, H, float(self.prm["water_level"]), self.weights(ov.shape[0]),
|
| 1764 |
self.state["layers"], self.state["relief_m"],
|
| 1765 |
water_look(self.prm.get("prompt", ""))[0],
|
| 1766 |
+
(self.state["depth_m"] / H.shape[0], self.state["width_m"] / H.shape[1]),
|
| 1767 |
+
guide=resample(g, ov.shape[0]))
|
| 1768 |
if n:
|
| 1769 |
_save(self.root / "macro" / "overview_raw.png", ov)
|
| 1770 |
ov = fixed
|
|
|
|
| 1838 |
tiles = {(i, j): _load(d / f"tile_{i}_{j}.png") for i in range(lay["n"]) for j in range(lay["n"])}
|
| 1839 |
macro = merge_tiles(tiles, lay, ov)
|
| 1840 |
F = macro.shape[0]
|
| 1841 |
+
W = self.weights()
|
| 1842 |
+
keep = None
|
| 1843 |
+
if self.prm.get("water_level") is not None: # the water painted below the caller's plane stays water
|
| 1844 |
+
H = resample(np.load(self.root / "mesh" / "height.npy").astype(np.float32), W.shape[1])
|
| 1845 |
+
keep = cv2.GaussianBlur((H >= float(self.prm["water_level"])).astype(np.float32), (0, 0), 1)
|
| 1846 |
+
macro = match_colours(macro, W, self.state["layers"], keep)
|
| 1847 |
_save(self.root / "macro" / "macro.jpg", macro, quality=92)
|
| 1848 |
self.state["macro_file"] = "macro/macro.jpg"
|
| 1849 |
(x0, _), (x1, _) = self.bounds
|
|
|
|
| 1851 |
self.state["macro_soft_file"] = "macro/macro_soft.jpg"
|
| 1852 |
# colour modulation for engines that multiply splatted layers by a colour map (Unity/Godot style):
|
| 1853 |
# lowpass(macro) / lowpass(sum of w_i * layer mean), linear, stored halved (0.5 = x1)
|
|
|
|
| 1854 |
means = np.stack([np.asarray(L.get("mean_linear") or [0.2, 0.2, 0.2], np.float32)
|
| 1855 |
for L in self.state["layers"]])
|
| 1856 |
S = W.shape[1]
|
|
|
|
| 2008 |
if drawn:
|
| 2009 |
near_drawn = ndi.uniform_filter(W[drawn].sum(0), max(3, S // 30))
|
| 2010 |
score = score + 0.8 * near_drawn / max(float(near_drawn.max()), 1e-6)
|
| 2011 |
+
m = S // 8
|
| 2012 |
+
score[:m], score[-m:], score[:, :m], score[:, -m:] = -1e9, -1e9, -1e9, -1e9
|
| 2013 |
wl = self.prm.get("water_level")
|
| 2014 |
if wl is not None: # look at the land, not at the bed of the caller's lake (round 28)
|
| 2015 |
Hs = resample(H, S)
|
| 2016 |
+
# a zen garden's gravel was 6 cm above its pond's level: all of it "lake bed", and the views went to
|
| 2017 |
+
# the corner (round 32)
|
| 2018 |
+
for above in (max(0.3, 0.002 * self.state["relief_m"]), 0.0):
|
| 2019 |
+
if (score[Hs >= float(wl) + above] > -1e8).any():
|
| 2020 |
+
score[Hs < float(wl) + above] = -1e9
|
| 2021 |
+
break
|
| 2022 |
r, cc = np.unravel_index(int(np.argmax(score)), score.shape)
|
| 2023 |
px, pz = x0 + (cc + 0.5) / S * (x1 - x0), z0 + (r + 0.5) / S * (z1 - z0)
|
| 2024 |
toward = np.array([c[0] - px, c[2] - pz])
|
| 2025 |
toward = toward / (np.linalg.norm(toward) + 1e-9)
|
| 2026 |
+
for name, dd, hh, fov in (("mid", float(np.clip(0.12 * span, min(40, 0.3 * span), 350)), 0.45, 50),
|
| 2027 |
+
("near", float(np.clip(0.004 * span, min(4, 0.1 * span), 12)), 0.4, 60)):
|
| 2028 |
ex, ez = px - toward[0] * dd, pz - toward[1] * dd
|
| 2029 |
+
ex, ez = np.clip(ex, x0, x1), np.clip(ez, z0, z1) # over the ground, not beyond its edge
|
| 2030 |
ey = max(self._ground_y(H, ex, ez), self._ground_y(H, px, pz), float(wl) if wl is not None else -1e18) \
|
| 2031 |
+ hh * dd
|
| 2032 |
tgt = [px, self._ground_y(H, px, pz), pz]
|