diff --git a/tools/ARMOUR_EDITOR.py b/tools/ARMOUR_EDITOR.py index 94cae49..ae49548 100644 --- a/tools/ARMOUR_EDITOR.py +++ b/tools/ARMOUR_EDITOR.py @@ -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 = [] diff --git a/tools/ARMOUR_QUICKSTART.py b/tools/ARMOUR_QUICKSTART.py index 827d56b..836ab5b 100755 --- a/tools/ARMOUR_QUICKSTART.py +++ b/tools/ARMOUR_QUICKSTART.py @@ -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: diff --git a/tools/mcmodel.py b/tools/mcmodel.py index 2af9f31..e852004 100644 --- a/tools/mcmodel.py +++ b/tools/mcmodel.py @@ -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