A hue per object, and faces that cannot be confused with their opposite

Untextured, every model came out the same red. The hue was the box's index
within its model and almost every model here is one box, so a scene of eight
was eight objects sharing hue zero - the one thing the colour was supposed to
tell you was the one thing it did not. So `face_colour` takes a hue now, and
the quickstart hands each model its own: successive golden-ratio turns around
the wheel, so any two are far apart and the third is not squeezed between them,
indexed on the model's place in the whole list rather than on what happens to
be visible. The boxes of one model spread a tenth of a turn either side, near
enough to read as one object and far enough to count. Left alone the hue is
what it always was - the wheel split per shape - so ARMOUR_GUI and the unwrap
templates are unchanged.

The shades were the other half. Six faces were sharing three saturations, and
east and west were separated by nothing but a fifth of a value - which is the
worst place to be thin, because opposite faces are the pair you can only ever
see one of at a time and so have nothing to compare against. Each face now sits
at its own rung of both ladders, and each opposite pair is far apart on both:
up is pale and brightest against a down that is vivid and darkest, north vivid
and bright against a washed, dim south, west the palest against a vivid,
mid-dark east.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main
Amir Alexander Abdelbaki 2026-08-24 21:23:45 +02:00
parent 44bd5e597c
commit 8e5178ece6
3 changed files with 69 additions and 25 deletions

View File

@ -348,7 +348,7 @@ def _texture_is_blank(image, uv):
return alpha.getextrema()[1] < 128
def build_meshes(res, quads, shape_count=None, by_colour=False):
def build_meshes(res, quads, shape_count=None, by_colour=False, hue=None):
"""Quads -> [(PolyData, texture id, colour)], grouped so one draw is one look.
Textured, that means one mesh per texture. Untextured, `by_colour` splits
@ -361,6 +361,10 @@ def build_meshes(res, quads, shape_count=None, by_colour=False):
`shape_count` is how many boxes the model has. Pass it when you know it,
because a box whose every face was culled would otherwise shift the hues
off the ones the unwrap template was drawn with.
`hue` hands the whole model a colour of its own instead, for a scene of
several models where the question is which model a face belongs to rather
than which box.
"""
import pyvista as pv
@ -378,7 +382,7 @@ def build_meshes(res, quads, shape_count=None, by_colour=False):
if image is not None and _texture_is_blank(image, q.uv):
continue
tex = q.texture if image is not None else None
colour = mc.face_colour(q.shape, shape_count, q.face) if by_colour else None
colour = mc.face_colour(q.shape, shape_count, q.face, hue) if by_colour else None
groups.setdefault((tex, colour), []).append(q)
out = []

View File

@ -781,15 +781,18 @@ class Quickstart(QtWidgets.QMainWindow):
# on from: the rest of the list is still readable.
print(f'{entry.ref}: {exc}', file=sys.stderr)
continue
# Untextured, a face is coloured the way its unwrap template is:
# hue per box, saturation and brightness per side. That is what
# makes the viewport a legend for the texture sheet - the green top
# of box two in here is the green top of box two on the sheet. The
# palette is only there for a model whose faces were all culled.
# Untextured, every model gets a hue of its own and every side of
# a box its own saturation and brightness - so the colour says two
# things at once, which model this is and which way the face
# points, and opposite sides never read alike. The hue is indexed
# on the model's place in the whole list rather than on what is
# visible, or hiding one model would repaint the others.
colour = ae.PALETTE[i % len(ae.PALETTE)]
hue = mc.object_hue(self.entries.index(entry))
count = entry.shapes(self.res, self.args.variant)
for j, (mesh, tex, face) in enumerate(
ae.build_meshes(self.res, quads, count, by_colour=not textured)):
ae.build_meshes(self.res, quads, count,
by_colour=not textured, hue=hue)):
kw = dict(smooth_shading=False, ambient=0.42, diffuse=0.78,
specular=0.0)
if textured and tex is not None:

View File

@ -275,18 +275,31 @@ class Quad:
# the model - and a face drawn on the wrong patch shows up as the wrong shade of
# the right colour rather than as something that looks fine.
# Saturation and value per face. Front and back are the vivid pair because they
# are what you look at most; up and down are pushed to the ends of the value
# range so a box read from above or below is never ambiguous. Entity cubes name
# their vertical faces differently, and both names are here rather than
# translated, so neither convention has to know about the other.
# Saturation and value per face, arranged so that no two faces of a box read as
# the same colour and, above all, that opposite faces do not: a face is told
# from its opposite by both axes at once, never by brightness alone, because
# the pair you most need to tell apart is the one you can only ever see one of
# at a time. Every face sits at its own rung of the value ladder as well, so a
# box seen against a bright background is still read the same way.
#
# up pale and brightest down vivid and darkest
# north vivid and bright south washed and dim
# west palest and mid-bright east vivid and mid-dark
#
# Entity cubes name their vertical faces differently, and both names are here
# rather than translated, so neither convention has to know about the other.
FACE_SHADES = {
'north': (0.90, 0.98), 'south': (0.90, 0.60),
'east': (0.55, 0.90), 'west': (0.55, 0.68),
'up': (0.26, 1.00), 'down': (1.00, 0.42),
'top': (0.26, 1.00), 'bottom': (1.00, 0.42),
'up': (0.45, 1.00), 'down': (1.00, 0.24),
'north': (1.00, 0.86), 'south': (0.35, 0.44),
'west': (0.30, 0.78), 'east': (0.90, 0.50),
'top': (0.45, 1.00), 'bottom': (1.00, 0.24),
}
# How far the boxes of one object spread from its hue. Enough to tell one box
# from the next, far less than the gap between objects, so a box is never
# mistaken for something else on screen.
BOX_BAND = 0.10
def shape_hues(count):
"""The colour wheel split evenly, one slice per shape."""
@ -294,10 +307,31 @@ def shape_hues(count):
return [i / count for i in range(count)]
def face_colour(shape, count, face):
"""The colour of one face of one shape, as floats in 0..1."""
hues = shape_hues(count)
hue = hues[int(shape) % len(hues)]
def object_hue(index):
"""A hue of its own for each object in a scene, however many there are.
Successive golden-ratio turns around the wheel: any two are far apart, the
third is not squeezed between the first two, and it never runs out. What it
is not is stable against reordering - an object keeps its colour only as
long as it keeps its index, so index by something that does not move.
"""
return (int(index) * 0.6180339887498949) % 1.0
def face_colour(shape, count, face, hue=None):
"""The colour of one face of one shape, as floats in 0..1.
Left alone, the hue says which box of one model this is - the wheel split
per shape, which is what an unwrap template is drawn against. Given a
`hue`, it says which *object* instead, and the boxes of that object sit in
a narrow band around it. Either way the saturation and value say which of
the six faces it is.
"""
count = max(1, int(count))
if hue is None:
hue = shape_hues(count)[int(shape) % count]
else:
hue = (float(hue) + BOX_BAND * (int(shape) % count) / count) % 1.0
sat, val = FACE_SHADES.get(face, (0.70, 0.80))
return colorsys.hsv_to_rgb(hue, sat, val)
@ -648,10 +682,13 @@ def unwrap(elements, texture=None, padding=0, atlas_width=None):
def unwrap_template(size, nets, elements, labelled=True):
"""A painting guide for an unwrap: one coloured, labelled patch per face.
The colours are the ones the untextured viewport uses - hue per shape,
shade per face - so the model on screen is the legend for this sheet.
Find the colour on the model, find the same colour here, and that is the
patch to paint.
The colours are the ones the untextured viewport uses when it is drawing
one model - hue per shape, shade per face - so the model on screen is the
legend for this sheet. Find the colour on the model, find the same colour
here, and that is the patch to paint. A scene of several models hands each
one a hue of its own instead, since there the question is which model a
face belongs to; the shades are the same either way, so the sheet is still
read by them.
Doubles as the check on the unwrap itself. Render a model with this as its
texture and every face should show its own colour, its own letter, and the