// Package template reads a painted world map and turns it into the fields the geology solve needs. // // The map is an image; the legend beside it says what each colour means. The image is cylindrical: X // wraps, Y does not, so the left and right edges are the same meridian and the top and bottom rows are // the poles. Nothing here knows how big the world is - that is the planet package's job. This package // only answers "what did the author paint here". // // The one rule that governs the whole design is Docs/Terrain-Next.md 3.2: paint the uplift, never the // height. A painted heightmap is handed to a solver that erodes it into something else and throws away // the drainage network, which is the reason the generator exists. So a class carries an uplift rate and // an erodibility, and the solve makes the terrain. package template import ( "bufio" "fmt" "image" "os" // Registered for image.Decode. JPEG is here because the first template anyone painted was a JPEG; // PNG is what a template should be, because JPEG bleeds colour across every class boundary and the // classifier then has to clean up after it. _ "image/jpeg" _ "image/png" ) // DecodeRGB reads an image and returns tightly packed 8-bit RGB, three bytes a pixel, row-major. // // field.ReadHeightmap cannot be used for this and deliberately is not extended: it decodes PNG only, and // its fallback branch collapses colour to luma, which is right for a DEM and destroys a painted map - // two different classes can share a luma and here several nearly do. func DecodeRGB(path string) (px []uint8, w, h int, err error) { px, _, w, h, err = decode(path, false) return px, w, h, err } // DecodeRGBA is DecodeRGB with the alpha channel kept alongside. // // It exists for the annotation layer and only for it. A class template is opaque by definition - every pixel // is some class - so throwing alpha away there costs nothing. An overlay is the opposite: it is a transparent // sheet with strokes on it, most of it is nothing, and "nothing" is exactly what alpha records. An image with // no alpha comes back fully opaque, which is the right reading of a flattened export. func DecodeRGBA(path string) (px, alpha []uint8, w, h int, err error) { return decode(path, true) } func decode(path string, wantAlpha bool) (px, alpha []uint8, w, h int, err error) { f, err := os.Open(path) if err != nil { return nil, nil, 0, 0, err } defer f.Close() img, _, err := image.Decode(bufio.NewReaderSize(f, 1<<20)) if err != nil { return nil, nil, 0, 0, fmt.Errorf("%s: %w", path, err) } b := img.Bounds() w, h = b.Dx(), b.Dy() if w <= 0 || h <= 0 { return nil, nil, 0, 0, fmt.Errorf("%s: empty image", path) } px = make([]uint8, w*h*3) if wantAlpha { alpha = make([]uint8, w*h) for i := range alpha { alpha[i] = 255 } } // The fast paths matter: a 7738x3761 template is 29 million pixels, and going through the At() // interface for every one of them costs seconds rather than milliseconds. switch src := img.(type) { case *image.RGBA: // Premultiplied: the RGB bytes are already scaled by alpha, so a half-transparent red reads as a // darker red. Nothing here un-multiplies it, because every consumer that cares about alpha treats a // non-opaque pixel as blank and never looks at its colour. for y := 0; y < h; y++ { row := src.Pix[(y+b.Min.Y-src.Rect.Min.Y)*src.Stride:] off := (b.Min.X - src.Rect.Min.X) * 4 for x := 0; x < w; x++ { o := (y*w + x) * 3 px[o], px[o+1], px[o+2] = row[off+x*4], row[off+x*4+1], row[off+x*4+2] if alpha != nil { alpha[y*w+x] = row[off+x*4+3] } } } case *image.NRGBA: for y := 0; y < h; y++ { row := src.Pix[(y+b.Min.Y-src.Rect.Min.Y)*src.Stride:] off := (b.Min.X - src.Rect.Min.X) * 4 for x := 0; x < w; x++ { o := (y*w + x) * 3 px[o], px[o+1], px[o+2] = row[off+x*4], row[off+x*4+1], row[off+x*4+2] if alpha != nil { alpha[y*w+x] = row[off+x*4+3] } } } case *image.YCbCr: // What image/jpeg returns. for y := 0; y < h; y++ { for x := 0; x < w; x++ { yi := src.YOffset(b.Min.X+x, b.Min.Y+y) ci := src.COffset(b.Min.X+x, b.Min.Y+y) r, g, bl := ycbcrToRGB(src.Y[yi], src.Cb[ci], src.Cr[ci]) o := (y*w + x) * 3 px[o], px[o+1], px[o+2] = r, g, bl } } default: for y := 0; y < h; y++ { for x := 0; x < w; x++ { r, g, bl, a := img.At(b.Min.X+x, b.Min.Y+y).RGBA() o := (y*w + x) * 3 px[o], px[o+1], px[o+2] = uint8(r>>8), uint8(g>>8), uint8(bl>>8) if alpha != nil { alpha[y*w+x] = uint8(a >> 8) } } } } return px, alpha, w, h, nil } // ycbcrToRGB is image/color's conversion, inlined so the YCbCr path does not allocate a color.Color per // pixel. Same arithmetic, same rounding. func ycbcrToRGB(y, cb, cr uint8) (uint8, uint8, uint8) { yy := int32(y) * 0x10101 cb1 := int32(cb) - 128 cr1 := int32(cr) - 128 r := yy + 91881*cr1 if uint32(r)&0xff000000 == 0 { r >>= 16 } else { r = ^(r >> 31) & 0xffff >> 8 } g := yy - 22554*cb1 - 46802*cr1 if uint32(g)&0xff000000 == 0 { g >>= 16 } else { g = ^(g >> 31) & 0xffff >> 8 } b := yy + 116130*cb1 if uint32(b)&0xff000000 == 0 { b >>= 16 } else { b = ^(b >> 31) & 0xffff >> 8 } return uint8(r), uint8(g), uint8(b) }