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
|
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.
|
"""Quads -> [(PolyData, texture id, colour)], grouped so one draw is one look.
|
||||||
|
|
||||||
Textured, that means one mesh per texture. Untextured, `by_colour` splits
|
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,
|
`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
|
because a box whose every face was culled would otherwise shift the hues
|
||||||
off the ones the unwrap template was drawn with.
|
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
|
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):
|
if image is not None and _texture_is_blank(image, q.uv):
|
||||||
continue
|
continue
|
||||||
tex = q.texture if image is not None else None
|
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)
|
groups.setdefault((tex, colour), []).append(q)
|
||||||
|
|
||||||
out = []
|
out = []
|
||||||
|
|
|
||||||
|
|
@ -781,15 +781,18 @@ class Quickstart(QtWidgets.QMainWindow):
|
||||||
# on from: the rest of the list is still readable.
|
# on from: the rest of the list is still readable.
|
||||||
print(f'{entry.ref}: {exc}', file=sys.stderr)
|
print(f'{entry.ref}: {exc}', file=sys.stderr)
|
||||||
continue
|
continue
|
||||||
# Untextured, a face is coloured the way its unwrap template is:
|
# Untextured, every model gets a hue of its own and every side of
|
||||||
# hue per box, saturation and brightness per side. That is what
|
# a box its own saturation and brightness - so the colour says two
|
||||||
# makes the viewport a legend for the texture sheet - the green top
|
# things at once, which model this is and which way the face
|
||||||
# of box two in here is the green top of box two on the sheet. The
|
# points, and opposite sides never read alike. The hue is indexed
|
||||||
# palette is only there for a model whose faces were all culled.
|
# 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)]
|
colour = ae.PALETTE[i % len(ae.PALETTE)]
|
||||||
|
hue = mc.object_hue(self.entries.index(entry))
|
||||||
count = entry.shapes(self.res, self.args.variant)
|
count = entry.shapes(self.res, self.args.variant)
|
||||||
for j, (mesh, tex, face) in enumerate(
|
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,
|
kw = dict(smooth_shading=False, ambient=0.42, diffuse=0.78,
|
||||||
specular=0.0)
|
specular=0.0)
|
||||||
if textured and tex is not None:
|
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 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.
|
# the right colour rather than as something that looks fine.
|
||||||
|
|
||||||
# Saturation and value per face. Front and back are the vivid pair because they
|
# Saturation and value per face, arranged so that no two faces of a box read as
|
||||||
# are what you look at most; up and down are pushed to the ends of the value
|
# the same colour and, above all, that opposite faces do not: a face is told
|
||||||
# range so a box read from above or below is never ambiguous. Entity cubes name
|
# from its opposite by both axes at once, never by brightness alone, because
|
||||||
# their vertical faces differently, and both names are here rather than
|
# the pair you most need to tell apart is the one you can only ever see one of
|
||||||
# translated, so neither convention has to know about the other.
|
# 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 = {
|
FACE_SHADES = {
|
||||||
'north': (0.90, 0.98), 'south': (0.90, 0.60),
|
'up': (0.45, 1.00), 'down': (1.00, 0.24),
|
||||||
'east': (0.55, 0.90), 'west': (0.55, 0.68),
|
'north': (1.00, 0.86), 'south': (0.35, 0.44),
|
||||||
'up': (0.26, 1.00), 'down': (1.00, 0.42),
|
'west': (0.30, 0.78), 'east': (0.90, 0.50),
|
||||||
'top': (0.26, 1.00), 'bottom': (1.00, 0.42),
|
'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):
|
def shape_hues(count):
|
||||||
"""The colour wheel split evenly, one slice per shape."""
|
"""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)]
|
return [i / count for i in range(count)]
|
||||||
|
|
||||||
|
|
||||||
def face_colour(shape, count, face):
|
def object_hue(index):
|
||||||
"""The colour of one face of one shape, as floats in 0..1."""
|
"""A hue of its own for each object in a scene, however many there are.
|
||||||
hues = shape_hues(count)
|
|
||||||
hue = hues[int(shape) % len(hues)]
|
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))
|
sat, val = FACE_SHADES.get(face, (0.70, 0.80))
|
||||||
return colorsys.hsv_to_rgb(hue, sat, val)
|
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):
|
def unwrap_template(size, nets, elements, labelled=True):
|
||||||
"""A painting guide for an unwrap: one coloured, labelled patch per face.
|
"""A painting guide for an unwrap: one coloured, labelled patch per face.
|
||||||
|
|
||||||
The colours are the ones the untextured viewport uses - hue per shape,
|
The colours are the ones the untextured viewport uses when it is drawing
|
||||||
shade per face - so the model on screen is the legend for this sheet.
|
one model - hue per shape, shade per face - so the model on screen is the
|
||||||
Find the colour on the model, find the same colour here, and that is the
|
legend for this sheet. Find the colour on the model, find the same colour
|
||||||
patch to paint.
|
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
|
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
|
texture and every face should show its own colour, its own letter, and the
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue