added multi-view support on full object texture
This commit is contained in:
@@ -401,6 +401,83 @@ def cmd_texture(revision_dir: Path, args: argparse.Namespace) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_texture_views(revision_dir: Path, args: argparse.Namespace) -> int:
|
||||
"""Generate one independent texture per production view.
|
||||
|
||||
Step 1 of the multi-angle work: each pair writes its own PNG/SVG/PDF from its own
|
||||
camera into its own UV layout. No ownership partition, no seam handling and no merge —
|
||||
two pairs may cover the same surface region and that overlap is left unresolved.
|
||||
"""
|
||||
from orisys_sensor_design import texture_views
|
||||
|
||||
root = repo_root(revision_dir)
|
||||
paths = RevisionPaths.from_revision_dir(revision_dir)
|
||||
errors = validate_revision_metadata(paths)
|
||||
if errors:
|
||||
for message in errors:
|
||||
print(f"ERROR: {message}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
overrides = texture_overrides_from_args(args)
|
||||
try:
|
||||
settings = texture_views.resolve_views_settings(paths, overrides)
|
||||
pairs = texture_views.resolve_view_pairs(paths, settings)
|
||||
output_dir = resolve_texture_output_dir(root, paths, args.output_dir)
|
||||
except TextureConfigError as exc:
|
||||
for message in str(exc).splitlines():
|
||||
print(f"ERROR: {message}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
targets = texture_views.multiview_output_paths(output_dir, pairs)
|
||||
job = texture_views.build_multiview_job(paths, settings, pairs, output_dir)
|
||||
job_path = texture_views.write_multiview_job(job, output_dir)
|
||||
print(f"Wrote multi-view texture job: {job_path}")
|
||||
print(f" {len(pairs)} view(s), step 1 (independent textures, no merge)")
|
||||
for pair, _artifacts in targets:
|
||||
print(f" [{pair.id}] camera={pair.camera!r} uv_map={pair.uv_map!r} seed={pair.seed}")
|
||||
|
||||
code = _run_blender_script(
|
||||
"random_patch_multiview.py",
|
||||
paths.source_blend,
|
||||
["--job", str(job_path)],
|
||||
)
|
||||
if code != 0:
|
||||
return code
|
||||
|
||||
missing = [
|
||||
(pair, format_name, path)
|
||||
for pair, artifacts in targets
|
||||
for format_name, path in artifacts.items()
|
||||
if not path.is_file() or path.stat().st_size == 0
|
||||
]
|
||||
if missing:
|
||||
for pair, format_name, path in missing:
|
||||
print(
|
||||
f"ERROR: Blender finished but the {format_name.upper()} artifact for view "
|
||||
f"{pair.id!r} is missing or empty: {path}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 3
|
||||
|
||||
artifact_count = sum(len(artifacts) for _, artifacts in targets)
|
||||
print(f"OK: wrote {artifact_count} multi-view artifact(s) to {output_dir}")
|
||||
for pair, artifacts in targets:
|
||||
print(f" [{pair.id}] camera={pair.camera!r} uv_map={pair.uv_map!r}")
|
||||
for format_name, path in artifacts.items():
|
||||
print(f" {format_name.upper()}: {path}")
|
||||
print(
|
||||
f" mode=patches-multiview {settings.width}x{settings.height} "
|
||||
f"grid={settings.nx}x{settings.ny} "
|
||||
f"vector={settings.vector_width_mm:g}x{settings.vector_height_mm:g}mm "
|
||||
f"mesh={settings.mesh!r}"
|
||||
)
|
||||
print(
|
||||
" NOTE: overlap between views is NOT resolved in this step; "
|
||||
"merging needs surface-space ownership (see texture_views.py)"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def _print_map_summary(entry: dict) -> None:
|
||||
"""Print the human-readable summary for one measured UV map."""
|
||||
record = entry.get("file") or {}
|
||||
@@ -502,12 +579,26 @@ def cmd_inspect(revision_dir: Path, args: argparse.Namespace) -> int:
|
||||
|
||||
maps: list[tuple[str, Path]] = []
|
||||
missing: list[Path] = []
|
||||
for target, artifacts in targets:
|
||||
svg_path = artifacts["svg"]
|
||||
if not svg_path.is_file() or svg_path.stat().st_size == 0:
|
||||
missing.append(svg_path)
|
||||
else:
|
||||
maps.append((target.uv_map, svg_path))
|
||||
explicit = list(getattr(args, "svg", None) or [])
|
||||
if explicit:
|
||||
# Grade artifacts that are not named by texture_patch.uv_outputs — the per-view
|
||||
# textures from `texture-views` deliberately do not extend that list, because
|
||||
# extending it would make `orisys-design texture` generate them too. Additive: the
|
||||
# configured-map path below is unchanged when --svg is absent. --svg wins over
|
||||
# --uv-map, since an explicit path is a more specific request.
|
||||
for raw in explicit:
|
||||
path = Path(raw)
|
||||
if not path.is_file() or path.stat().st_size == 0:
|
||||
missing.append(path)
|
||||
else:
|
||||
maps.append((path.stem, path.resolve()))
|
||||
else:
|
||||
for target, artifacts in targets:
|
||||
svg_path = artifacts["svg"]
|
||||
if not svg_path.is_file() or svg_path.stat().st_size == 0:
|
||||
missing.append(svg_path)
|
||||
else:
|
||||
maps.append((target.uv_map, svg_path))
|
||||
if missing:
|
||||
for path in missing:
|
||||
print(
|
||||
@@ -1826,6 +1917,40 @@ def main(argv: list[str] | None = None) -> None:
|
||||
help="Reference cross RGBA colour in 0-1",
|
||||
)
|
||||
|
||||
texture_views_p = sub.add_parser(
|
||||
"texture-views",
|
||||
help="Generate one independent texture per production view (no merge)",
|
||||
)
|
||||
texture_views_p.add_argument("revision_dir", type=Path)
|
||||
texture_views_p.add_argument(
|
||||
"--output-dir",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Output directory (default: build/textures/<sensor>/<revision>)",
|
||||
)
|
||||
texture_views_p.add_argument("--mesh", default=None, help="Sensing surface mesh object name")
|
||||
texture_views_p.add_argument("--width", type=int, default=None, help="Texture width in pixels")
|
||||
texture_views_p.add_argument("--height", type=int, default=None, help="Texture height in pixels")
|
||||
texture_views_p.add_argument("--nx", type=int, default=None, help="Patch columns across the frame")
|
||||
texture_views_p.add_argument("--ny", type=int, default=None, help="Patch rows across the frame")
|
||||
texture_views_p.add_argument(
|
||||
"--color-r",
|
||||
dest="color_r",
|
||||
type=float,
|
||||
default=None,
|
||||
help="Colour separation between neighbouring patches",
|
||||
)
|
||||
texture_views_p.add_argument(
|
||||
"--edge-sample-grid",
|
||||
dest="edge_sample_grid",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Edge sampling grid for partially covered patches",
|
||||
)
|
||||
texture_views_p.add_argument(
|
||||
"--seed", type=int, default=None, help="Base seed; pair i uses seed + i"
|
||||
)
|
||||
|
||||
manifest_p = sub.add_parser("manifest", help="Write release manifest with file hashes")
|
||||
manifest_p.add_argument("revision_dir", type=Path)
|
||||
manifest_p.add_argument(
|
||||
@@ -1851,6 +1976,12 @@ def main(argv: list[str] | None = None) -> None:
|
||||
"--uv-map", dest="uv_map", default=None,
|
||||
help="Measure only this UV map (default: every configured map)",
|
||||
)
|
||||
inspect_p.add_argument(
|
||||
"--svg", type=Path, action="append", default=None, metavar="PATH",
|
||||
help="Grade this SVG directly instead of the configured texture_patch.uv_outputs "
|
||||
"maps (repeatable; for artifacts such as the per-view textures written by "
|
||||
"`texture-views`). Takes precedence over --uv-map",
|
||||
)
|
||||
inspect_p.add_argument(
|
||||
"--out", type=Path, default=None,
|
||||
help=f"Report path (default: <output-dir>/{REPORT_FILENAME})",
|
||||
@@ -2211,6 +2342,8 @@ def main(argv: list[str] | None = None) -> None:
|
||||
code = cmd_export(args.revision_dir, args.profile, args.housing_collections)
|
||||
elif args.command == "texture":
|
||||
code = cmd_texture(args.revision_dir, args)
|
||||
elif args.command == "texture-views":
|
||||
code = cmd_texture_views(args.revision_dir, args)
|
||||
elif args.command == "inspect":
|
||||
code = cmd_inspect(args.revision_dir, args)
|
||||
elif args.command == "preview":
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
"""Multi-view production textures: one independent texture per production UV layout.
|
||||
|
||||
Scope — **step 1 only**. Each pair generates its own PNG/SVG/PDF, with:
|
||||
|
||||
* **no ownership partition** — two pairs may cover the same surface region, and the
|
||||
resulting overlap is *not* resolved here;
|
||||
* **no seam or merge logic** — nothing is stitched into a single texture;
|
||||
* **no change to the single-view path** — ``texture_patch``, ``random_patch_dd02c.py`` and
|
||||
the ``texture-job`` schema are untouched.
|
||||
|
||||
The pattern is grounded on the sensing camera, not on the production views
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
The pattern is a property of the **surface**, defined by ``texture_patch.camera``
|
||||
(``cam_sensor``) — the same camera that defines ``compute``, the runtime layout. A
|
||||
production entry therefore names only a **UV layout**: ``uv_1`` / ``uv_2`` are different
|
||||
*parameterisations* of one and the same surface pattern, not two different patterns.
|
||||
|
||||
This is what ``patch_projection.build_patch_geometry`` already supports: the camera
|
||||
argument produces the cell clips (``clips``) and the visibility answers, while the
|
||||
``uv_names`` argument only chooses which layers the fragment's barycentrics are
|
||||
interpolated into. Passing ``cam_sensor`` with ``uv_names=['uv_1']`` therefore yields
|
||||
cam_sensor's cells (and therefore cam_sensor's colours) written into ``uv_1``'s layout.
|
||||
|
||||
So ``cam_uv_1`` / ``cam_uv_2`` are the **authoring** cameras used to create the
|
||||
``uv_1`` / ``uv_2`` layouts (Blender's *Project from View (Bounds)*); they are not pattern
|
||||
sources. A pair may still override ``camera:`` to source the pattern elsewhere, but the
|
||||
default — and the reason these layouts exist — is the sensing camera.
|
||||
|
||||
Two consequences worth stating, because they are easy to get wrong:
|
||||
|
||||
1. **The colour seed is shared, not derived per view.** A per-view seed (base + index)
|
||||
would paint two different patterns onto one physical surface; the printed marker has to
|
||||
be one pattern.
|
||||
2. **No rigid or affine transform relates the layouts.** ``uv_1`` and ``uv_2`` are related
|
||||
by roughly a 90 degree rotation with a flip (per-triangle UV centroid correlation
|
||||
``corr(u1, v2) = +0.93`` / ``corr(v1, u2) = -0.93``), and each is a different projection
|
||||
of a curved surface, so no UV-space transform maps one onto the other. Any later overlap
|
||||
resolution therefore has to happen in **surface space**, not by overlaying the layouts.
|
||||
|
||||
``compute`` is the runtime contract: the shipped SDK unroll map was built from it. It is
|
||||
never addressed by this module.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping, Sequence
|
||||
|
||||
from orisys_sensor_design.manifest import RevisionPaths, load_yaml
|
||||
from orisys_sensor_design.texture import (
|
||||
TEXTURE_ARTIFACT_FORMATS,
|
||||
TextureConfigError,
|
||||
TextureOutputTarget,
|
||||
TexturePatchSettings,
|
||||
resolve_texture_settings,
|
||||
texture_output_dir,
|
||||
)
|
||||
|
||||
MULTIVIEW_JOB_SCHEMA = "orisys-sensor-design/texture-job-multiview-v1"
|
||||
MULTIVIEW_JOB_FILENAME = "_texture_views_job.json"
|
||||
VIEWS_BLOCK_KEY = "texture_patch_views"
|
||||
|
||||
_BLOCK_KEYS = frozenset({"enabled", "pattern_camera", "pairs"})
|
||||
_PAIR_KEYS = frozenset({"id", "camera", "uv_map", "filename", "seed"})
|
||||
|
||||
#: Default artifact stem per pair. Derived from the pair id so a pair never has to name its
|
||||
#: own files unless it wants to; ``{id}`` is substituted.
|
||||
DEFAULT_FILENAME_TEMPLATE = "patch_texture_{id}.png"
|
||||
|
||||
|
||||
class ViewPairConfigError(TextureConfigError):
|
||||
"""Raised when ``texture_patch_views`` is present but unusable."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ViewPair:
|
||||
"""One production UV layout, and the pattern camera whose cells colour it.
|
||||
|
||||
``camera`` is the **pattern source** (normally the sensing camera), not the view that
|
||||
authored the layout. ``uv_map`` is the layout the pattern is written into.
|
||||
"""
|
||||
|
||||
id: str
|
||||
camera: str
|
||||
uv_map: str
|
||||
target: TextureOutputTarget
|
||||
seed: int | None
|
||||
|
||||
def to_job_entry(self, output_dir: Path) -> dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"camera": self.camera,
|
||||
"uv_map": self.uv_map,
|
||||
"seed": self.seed,
|
||||
"artifacts": {
|
||||
format_name: str(path)
|
||||
for format_name, path in self.target.artifact_paths(output_dir).items()
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def load_views_block(paths: RevisionPaths) -> dict[str, Any] | None:
|
||||
"""Return the raw ``texture_patch_views`` block, or None when the revision has none."""
|
||||
design = load_yaml(paths.design_yaml)
|
||||
block = design.get(VIEWS_BLOCK_KEY)
|
||||
if block is None:
|
||||
return None
|
||||
if not isinstance(block, dict):
|
||||
raise ViewPairConfigError(
|
||||
f"{VIEWS_BLOCK_KEY}: must be a mapping (got {type(block).__name__})"
|
||||
)
|
||||
return block
|
||||
|
||||
|
||||
def validate_views_block(
|
||||
block: Mapping[str, Any],
|
||||
*,
|
||||
objects: Mapping[str, str] | None = None,
|
||||
) -> list[str]:
|
||||
"""Return human-readable problems with a ``texture_patch_views`` block.
|
||||
|
||||
``objects`` is unused today (Blender owns object existence) but is accepted so the
|
||||
signature does not have to change when camera validation moves host-side.
|
||||
"""
|
||||
errors: list[str] = []
|
||||
unknown = sorted(set(block) - _BLOCK_KEYS)
|
||||
if unknown:
|
||||
errors.append(
|
||||
f"{VIEWS_BLOCK_KEY}: unknown key(s) {unknown}; allowed: {sorted(_BLOCK_KEYS)}"
|
||||
)
|
||||
if "enabled" in block and not isinstance(block["enabled"], bool):
|
||||
errors.append(f"{VIEWS_BLOCK_KEY}.enabled: must be true or false")
|
||||
pattern_camera = block.get("pattern_camera")
|
||||
if pattern_camera is not None and (
|
||||
not isinstance(pattern_camera, str) or not pattern_camera.strip()
|
||||
):
|
||||
errors.append(
|
||||
f"{VIEWS_BLOCK_KEY}.pattern_camera: must be a non-empty object name "
|
||||
"(or omitted, to use texture_patch.camera)"
|
||||
)
|
||||
|
||||
pairs = block.get("pairs")
|
||||
if not isinstance(pairs, list) or not pairs:
|
||||
errors.append(f"{VIEWS_BLOCK_KEY}.pairs: must be a non-empty list")
|
||||
return errors
|
||||
|
||||
seen_ids: set[str] = set()
|
||||
seen_files: dict[str, str] = {}
|
||||
seen_camera_layout: dict[tuple[str, str], str] = {}
|
||||
for index, pair in enumerate(pairs):
|
||||
where = f"{VIEWS_BLOCK_KEY}.pairs[{index}]"
|
||||
if not isinstance(pair, Mapping):
|
||||
errors.append(f"{where}: must be a mapping")
|
||||
continue
|
||||
extra = sorted(set(pair) - _PAIR_KEYS)
|
||||
if extra:
|
||||
errors.append(
|
||||
f"{where}: unknown key(s) {extra}; allowed: {sorted(_PAIR_KEYS)}"
|
||||
)
|
||||
# `camera` is optional: the pattern source defaults to the sensing camera.
|
||||
for key in ("id", "uv_map"):
|
||||
value = pair.get(key)
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
errors.append(f"{where}.{key}: must be a non-empty string")
|
||||
if "camera" in pair:
|
||||
value = pair.get("camera")
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
errors.append(
|
||||
f"{where}.camera: must be a non-empty object name "
|
||||
"(omit it to use the pattern camera)"
|
||||
)
|
||||
pair_id = pair.get("id")
|
||||
if isinstance(pair_id, str) and pair_id.strip():
|
||||
if pair_id in seen_ids:
|
||||
errors.append(f"{where}.id: duplicate pair id {pair_id!r}")
|
||||
seen_ids.add(pair_id) # type: ignore[arg-type]
|
||||
filename = pair.get("filename", _default_filename(pair_id))
|
||||
if isinstance(filename, str) and filename:
|
||||
stem = str(Path(filename).with_suffix(".png")).lower()
|
||||
if stem in seen_files:
|
||||
errors.append(
|
||||
f"{where}.filename: {filename!r} collides with pair "
|
||||
f"{seen_files[stem]!r} (artifacts would overwrite each other)"
|
||||
)
|
||||
seen_files[stem] = str(pair_id)
|
||||
if Path(filename).suffix.lower() not in {".png", ""}:
|
||||
errors.append(f"{where}.filename: must be a .png name")
|
||||
seed = pair.get("seed")
|
||||
if seed is not None and (not isinstance(seed, int) or isinstance(seed, bool)):
|
||||
errors.append(f"{where}.seed: must be an integer or omitted")
|
||||
uv_map = pair.get("uv_map")
|
||||
# Compare on the *effective* camera, so two pairs that both default to the sensing
|
||||
# camera and write the same layout are still caught as a copy-paste.
|
||||
effective_camera = pair.get("camera") or pattern_camera or "?"
|
||||
if isinstance(uv_map, str) and uv_map:
|
||||
key = (str(effective_camera), uv_map)
|
||||
if key in seen_camera_layout:
|
||||
errors.append(
|
||||
f"{where}: pattern camera {effective_camera!r} with UV map {uv_map!r} is "
|
||||
f"already used by pair {seen_camera_layout[key]!r}; the two textures "
|
||||
"would be identical"
|
||||
)
|
||||
seen_camera_layout[key] = str(pair_id)
|
||||
return errors
|
||||
|
||||
|
||||
def _default_filename(pair_id: Any) -> str:
|
||||
return DEFAULT_FILENAME_TEMPLATE.format(id=pair_id)
|
||||
|
||||
|
||||
def resolve_view_pairs(
|
||||
paths: RevisionPaths,
|
||||
settings: TexturePatchSettings,
|
||||
block: Mapping[str, Any] | None = None,
|
||||
) -> tuple[ViewPair, ...]:
|
||||
"""Resolve the views block into concrete pairs, or raise ViewPairConfigError."""
|
||||
if block is None:
|
||||
block = load_views_block(paths)
|
||||
if block is None:
|
||||
raise ViewPairConfigError(
|
||||
f"this revision declares no {VIEWS_BLOCK_KEY} block, so there are no production "
|
||||
"views to generate; add it next to texture_patch (the single-view "
|
||||
"`orisys-design texture` path is unchanged)"
|
||||
)
|
||||
errors = validate_views_block(block)
|
||||
if errors:
|
||||
raise ViewPairConfigError("\n".join(errors))
|
||||
if not block.get("enabled", True):
|
||||
raise ViewPairConfigError(f"{VIEWS_BLOCK_KEY}.enabled is false")
|
||||
|
||||
# The pattern source. The production cameras authored the UV layouts (Project from
|
||||
# View); they are not pattern sources, so the default is the sensing camera that also
|
||||
# defines `compute`.
|
||||
pattern_camera = str(block.get("pattern_camera") or settings.camera)
|
||||
base_seed = settings.seed
|
||||
pairs: list[ViewPair] = []
|
||||
for raw in block["pairs"]:
|
||||
pair_id = str(raw["id"])
|
||||
# One shared seed, deliberately: the marker is a single physical pattern, and a
|
||||
# per-view seed would paint two different patterns onto one surface.
|
||||
seed = raw.get("seed")
|
||||
if seed is None:
|
||||
seed = base_seed
|
||||
pairs.append(
|
||||
ViewPair(
|
||||
id=pair_id,
|
||||
camera=str(raw.get("camera") or pattern_camera),
|
||||
uv_map=str(raw["uv_map"]),
|
||||
target=TextureOutputTarget(
|
||||
uv_map=str(raw["uv_map"]),
|
||||
filename=str(raw.get("filename") or _default_filename(pair_id)),
|
||||
),
|
||||
seed=None if seed is None else int(seed),
|
||||
)
|
||||
)
|
||||
return tuple(pairs)
|
||||
|
||||
|
||||
def build_multiview_job(
|
||||
paths: RevisionPaths,
|
||||
settings: TexturePatchSettings,
|
||||
pairs: Sequence[ViewPair],
|
||||
output_dir: Path,
|
||||
) -> dict[str, Any]:
|
||||
"""Build the JSON job ``random_patch_multiview.py`` consumes."""
|
||||
if not pairs:
|
||||
raise ViewPairConfigError("no view pairs to generate")
|
||||
pattern_cameras = sorted({pair.camera for pair in pairs})
|
||||
return {
|
||||
"schema": MULTIVIEW_JOB_SCHEMA,
|
||||
"mode": "patches-multiview",
|
||||
"revision_dir": str(paths.root.resolve()),
|
||||
"source_blend": str(paths.source_blend.resolve()),
|
||||
"objects": {"mesh": settings.mesh},
|
||||
# Recorded so a reader can see which camera defined the pattern, and that the
|
||||
# production cameras merely chose the layouts.
|
||||
"pattern_camera": pattern_cameras[0] if len(pattern_cameras) == 1 else pattern_cameras,
|
||||
"settings": settings.to_job_settings(),
|
||||
"pairs": [pair.to_job_entry(Path(output_dir)) for pair in pairs],
|
||||
}
|
||||
|
||||
|
||||
def write_multiview_job(job: dict[str, Any], output_dir: Path) -> Path:
|
||||
"""Write ``_texture_views_job.json`` into the output directory."""
|
||||
output_dir = Path(output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
path = output_dir / MULTIVIEW_JOB_FILENAME
|
||||
path.write_text(json.dumps(job, indent=2) + "\n", encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def multiview_output_paths(
|
||||
output_dir: Path,
|
||||
pairs: Sequence[ViewPair],
|
||||
) -> list[tuple[ViewPair, dict[str, Path]]]:
|
||||
"""Pair each view with its ordered PNG/SVG/PDF artifact paths."""
|
||||
resolved_dir = Path(output_dir).resolve()
|
||||
return [
|
||||
(pair, pair.target.artifact_paths(resolved_dir))
|
||||
for pair in pairs
|
||||
]
|
||||
|
||||
|
||||
def resolve_views_settings(
|
||||
paths: RevisionPaths,
|
||||
overrides: Mapping[str, Any] | None = None,
|
||||
) -> TexturePatchSettings:
|
||||
"""The rendering settings are shared with the single-view path, on purpose.
|
||||
|
||||
Page size, grid, palette and seed are decisions about the printed page, not about a
|
||||
particular view, so they come from ``texture_patch`` and only the *pairs* come from
|
||||
``texture_patch_views``.
|
||||
"""
|
||||
return resolve_texture_settings(paths, overrides)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_FILENAME_TEMPLATE",
|
||||
"MULTIVIEW_JOB_FILENAME",
|
||||
"MULTIVIEW_JOB_SCHEMA",
|
||||
"TEXTURE_ARTIFACT_FORMATS",
|
||||
"VIEWS_BLOCK_KEY",
|
||||
"ViewPair",
|
||||
"ViewPairConfigError",
|
||||
"build_multiview_job",
|
||||
"load_views_block",
|
||||
"multiview_output_paths",
|
||||
"resolve_view_pairs",
|
||||
"resolve_views_settings",
|
||||
"texture_output_dir",
|
||||
"validate_views_block",
|
||||
"write_multiview_job",
|
||||
]
|
||||
Reference in New Issue
Block a user