155 lines
5.2 KiB
Go
155 lines
5.2 KiB
Go
// 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)
|
|
}
|