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
parent
44bd5e597c
commit
8e5178ece6
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue