Documentation
¶
Overview ¶
Package bedrockskin renders Minecraft Bedrock skins to images.
It takes a skin texture and, optionally, the skin's geometry.json, and rasterizes them into a square PNG-ready image.Image — no GPU, no headless browser, no external process.
Rendering ¶
Render is the entry point. Only a texture is required:
tex, _, err := image.Decode(f)
if err != nil {
return err
}
img, err := bedrockskin.Render(bedrockskin.Options{Texture: tex})
That renders the full body of a standard humanoid, straight on, at 512x512. Options selects a different framing, angle, size, bone subset or explicit camera.
Callers holding encoded bytes can skip the decode and encode entirely with RenderBytes, which takes BytesOptions and returns PNG bytes. Both paths run the same renderer and produce identical output. Options and BytesOptions also render themselves, via Render, RenderPNG and BytesOptions.RenderPNG.
Skins taken off the wire arrive as raw RGBA rather than an encoded image; TextureFromRGBA wraps those without copying.
Geometry is optional ¶
Leaving Options.Geometry nil is not a shortcut — it is the correct input for most real skins. A Bedrock client sends no mesh at all for a skin that uses one of the built-in models: its login packet carries the literal JSON "null" in SkinGeometryData and names the model only in the skin's resource patch. Geometry travels the wire only for skins with a genuinely custom mesh. So a caller relaying a real skin very often has nothing to pass, and DefaultGeometry stands in with the model the client itself would use.
When a skin does carry geometry, parse it with ParseGeometry, which accepts both of Bedrock's on-the-wire formats — the modern "minecraft:geometry" array and the pre-1.12 flat top-level-key form — and pass the result:
geos, err := bedrockskin.ParseGeometry(raw)
if err != nil {
return err
}
img, err := bedrockskin.Render(bedrockskin.Options{
Texture: tex,
Geometry: geos,
Identifier: "geometry.humanoid.customSlim",
View: bedrockskin.ViewAvatar,
Angle: bedrockskin.AngleIso,
Size: 256,
})
Use IsEmpty to tell "the skin has no custom mesh" apart from "the upload was broken" before parsing: it reports true for empty input and for the literal "null" a client actually sends.
Custom models and persona skins ¶
Skins with extra bones — ears, tails, wings, party hats — render as a real 3D mesh with no special-casing; bone scoping is ancestry-based, so a custom bone parented under "head" is included by ViewHead automatically.
Persona (character creator) skins are poly meshes rather than cubes, spread over several geometry entries; their head is textured by the face animation, which Options.Animated carries. See docs/geometry-format.md#persona-skins.
Untrusted input ¶
The library enforces no size or complexity limits of its own, since what counts as too large is policy. It does supply the two measurements to set those limits from: Complexity bounds a geometry document before rendering, and ImageDimensions reads a texture's dimensions from its header before decoding — a small PNG can declare enormous dimensions and force a huge allocation.
Malformed input is survivable rather than fatal. Cubes whose size or origin arrays are too short are skipped rather than indexed, and every parser is fuzzed; see fuzz_test.go.
Reading a login packet ¶
A skin arrives as several fields that each need decoding. ParseResourcePatch reads the one that names the model — it is authoritative for wide-vs-slim, where the packet's ArmSize field is not — and ParseView/ParseAngle turn request parameters into the corresponding options, rejecting names they do not recognise instead of quietly rendering something else.
Detecting invisible skins ¶
Separately from rendering, Skin answers whether a skin is invisible or only half-visible, cross-referencing geometry against the texture's alpha. See NewSkin, or NewSkinWithOptions to judge by your own thresholds.
Further reading ¶
The docs directory covers the subject in depth, for anyone extending the library or debugging a render:
docs/skin-data.md what a Bedrock client actually sends docs/geometry-format.md the geometry.json format, both versions docs/rendering-pipeline.md how a bone tree becomes pixels docs/views-and-cameras.md bone scoping, framing and camera math docs/api-reference.md every exported symbol docs/recipes.md worked examples docs/design-decisions.md why the library works the way it does
Comments in this package stay brief and point into those files rather than repeating them.
Example ¶
A skin file in, a 3D head icon out. With no geometry the default model is drawn, which is right for most skins.
package main
import (
"os"
bedrockskin "github.com/THEBOSS9345/bedrock-skin-go"
)
func main() {
raw, err := os.ReadFile("skin.png")
if err != nil {
return // a skin file of your own
}
tex, err := bedrockskin.DecodeImage(raw)
if err != nil {
panic(err)
}
f, err := os.Create("avatar.png")
if err != nil {
panic(err)
}
defer f.Close()
opts := bedrockskin.Options{Texture: tex, View: bedrockskin.ViewAvatar, Angle: bedrockskin.AngleIso, Size: 256}
if err := opts.WritePNG(f); err != nil {
panic(err)
}
}
Output:
Index ¶
- Constants
- Variables
- func Complexity(geos []Geometry) (bones, cubes int)
- func DecodeImage(data []byte) (image.Image, error)
- func EncodePNG(img image.Image) ([]byte, error)
- func ExampleAnimations() map[string]*Animation
- func ImageDimensions(data []byte) (width, height int, err error)
- func IsEmpty(raw []byte) bool
- func IsSkinInvisible(texture image.Image) bool
- func IsSkinTiny(geomData []byte) bool
- func ParseAnimations(raw []byte) (map[string]*Animation, error)
- func ParseParts(raw string) []string
- func Render(opts Options) (image.Image, error)
- func Render2D(texture image.Image, view View, size int) image.Image
- func RenderBytes(opts BytesOptions) ([]byte, error)
- func RenderFrames(opts AnimationOptions) ([]image.Image, error)
- func RenderFramesPNG(opts AnimationBytesOptions) ([][]byte, error)
- func RenderGIF(opts AnimationOptions) ([]byte, error)
- func RenderGIFBytes(opts AnimationBytesOptions) ([]byte, error)
- func RenderItem(opts ItemOptions) (image.Image, error)
- func RenderItemBytes(opts ItemBytesOptions) ([]byte, error)
- func RenderItemFrames(opts ItemAnimationOptions) ([]image.Image, error)
- func RenderItemGIF(opts ItemAnimationOptions) ([]byte, error)
- func RenderItemGIFBytes(opts ItemAnimationBytesOptions) ([]byte, error)
- func TextureFromRGBA(pix []byte, width, height int) (image.Image, error)
- func WriteGIF(w io.Writer, opts AnimationOptions) error
- type Angle
- type AnimatedTexture
- type AnimatedTextureBytes
- type AnimatedType
- type Animation
- type AnimationBytesOptions
- type AnimationOptions
- type Animator
- type Armor
- type ArmorBytes
- type Bone
- type BonePose
- type BytesOptions
- type Camera
- type Cube
- type FaceUV
- type Frames
- type Geometry
- type GeometrySizeResult
- type GeometryTree
- type GeometryValue
- type GeometryViolation
- type Held
- type HeldBytes
- type ItemAdjust
- type ItemAnimationBytesOptions
- type ItemAnimationOptions
- type ItemBytesOptions
- type ItemOptions
- type Locator
- type Motion
- type Options
- type PartReport
- type PartVisibility
- type PolyMesh
- type PolyVertex
- type Pose
- type ResourcePatch
- type Scale
- type Skin
- type SkinOptions
- type SkinPartResult
- type SkinReport
- type SkinVisibilityResult
- type Verdict
- type View
- type WireAnimation
- type WireSkin
Examples ¶
Constants ¶
const ( // DefaultMinVisibleAlpha is the minimum alpha value for a pixel to count // as visible, matching the renderer's alpha-test threshold. DefaultMinVisibleAlpha = 0.5 / 255.0 // DefaultMinVisibleFraction is the default minimum fraction of // non-transparent pixels a body part must have to count as VISIBLE. A // part that is more than half transparent is effectively invisible to a // viewer, so the default requires a majority of a part's sampled pixels // to be opaque. Override it per Skin with SkinOptions. DefaultMinVisibleFraction = 0.5 // DefaultMinGeometrySize is the size a geometry bone must reach to be // considered non-tiny: the largest axis of the box enclosing its cubes, // so a bone qualifies by being big enough in any one dimension. A flat // plane is visible; a bone that is small on every axis is not. DefaultMinGeometrySize = 0.5 // DefaultMinVisibleParts is the default minimum number of standard body // parts that must be meaningfully visible for the skin to pass. This // catches half-invisible skins where a player hides one or more body // parts while keeping the rest opaque. Override it per Skin with // SkinOptions. DefaultMinVisibleParts = 4 )
const DefaultSize = 512
DefaultSize is the output edge length used when Options.Size is zero.
Variables ¶
var ( // ErrNoTexture is returned by Render when Options.Texture is nil. ErrNoTexture = errors.New("bedrockskin: texture is required") // ErrNoGeometry means Options.Geometry held no entry that could be // rendered. Leaving Geometry nil is not this error - it selects // DefaultGeometry. ErrNoGeometry = errors.New("bedrockskin: geometry has no usable entries") // ErrNoMatchingParts means no bone in the geometry matched Options.Parts, // usually a misspelled bone name. ErrNoMatchingParts = errors.New("bedrockskin: no bones matched the requested parts") // ErrEmptyView means the chosen View scoped to bones that have no cubes - // for example ViewHead on geometry with no head bone. ErrEmptyView = errors.New("bedrockskin: nothing to render for this view") // ErrUnknownView is returned by ParseView for an unrecognised name. ErrUnknownView = errors.New("bedrockskin: unknown view") // ErrUnknownAngle is returned by ParseAngle for an unrecognised name. ErrUnknownAngle = errors.New("bedrockskin: unknown angle") )
Errors returned by Render and the option parsers. Every one describes bad caller input rather than an internal failure, so a service can map the whole set to a 4xx with errors.Is and without matching on message text.
var ErrNoAnimation = errors.New("bedrockskin: no animation to render")
ErrNoAnimation is returned by RenderFrames and RenderGIF without an Animation.
var ErrNoAnimations = errors.New("bedrockskin: no animations in the file")
ErrNoAnimations is returned by ParseAnimations for a file with no animations in it.
var ErrNoGeometryModels = errors.New("bedrockskin: no geometry models in the file")
ErrNoGeometryModels is returned by ParseGeometryTree for valid JSON that holds no geometry.
var ErrUnknownMotion = errors.New("bedrockskin: unknown motion")
ErrUnknownMotion is returned by ParseMotion for an unrecognised name.
Functions ¶
func Complexity ¶
Complexity reports total bones and cubes across every entry in geos, which is roughly what mesh-building costs. It sums the whole document, not just the entry that will be rendered, because SelectGeometry's fallback and FindCape both walk all of it.
The library enforces no limit itself - what counts as too large is policy. See docs/recipes.md#handling-untrusted-uploads.
func DecodeImage ¶
DecodeImage decodes PNG or JPEG bytes into an image.
The result is always an *image.NRGBA: straight-alpha RGBA, whatever kind of PNG it was.
It applies no size limit. Decoding is where a malicious image does its damage: a few-KB file can declare enormous dimensions and force a huge allocation. Callers handling untrusted input should call ImageDimensions first, which reads only the header.
func ExampleAnimations ¶
ExampleAnimations returns the bundled example animations by name, e.g. "animation.player.dance" - emotes, moves and fighting, made for the player model. The map is new on each call; the animations are shared and must not be changed.
Example ¶
One of the 33 bundled animations.
package main
import (
"fmt"
"image"
"image/color"
bedrockskin "github.com/THEBOSS9345/bedrock-skin-go"
)
// skinTexture stands in for a decoded skin file: 64x64, every pixel opaque.
func skinTexture() *image.NRGBA {
img := image.NewNRGBA(image.Rect(0, 0, 64, 64))
for y := 0; y < 64; y++ {
for x := 0; x < 64; x++ {
img.Set(x, y, color.NRGBA{R: uint8(x * 4), G: uint8(y * 4), B: 128, A: 255})
}
}
return img
}
func main() {
dance := bedrockskin.ExampleAnimations()["animation.player.dance"]
frames, err := bedrockskin.RenderFrames(bedrockskin.AnimationOptions{
Options: bedrockskin.Options{Texture: skinTexture(), Size: 48},
Animation: dance,
FPS: 5,
})
if err != nil {
panic(err)
}
fmt.Println(len(frames) > 1)
}
Output: true
func ImageDimensions ¶
ImageDimensions reports an encoded image's pixel dimensions by reading only its header, without decoding the pixels.
This is the check DecodeImage's documentation asks callers handling untrusted uploads to make first, and the reason it matters: decoding is where a malicious image does its damage. A few-KB PNG can declare enormous dimensions and force a multi-gigabyte allocation the moment it is decoded. Bounding the header first costs nothing.
w, h, err := bedrockskin.ImageDimensions(data)
if err != nil {
return err
}
if w > 512 || h > 512 {
return errors.New("skin texture too large")
}
tex, err := bedrockskin.DecodeImage(data)
It is to a texture what Complexity is to geometry: the measurement, with the ceiling left to the caller. See docs/recipes.md#handling-untrusted-uploads.
func IsEmpty ¶
IsEmpty reports whether raw carries no geometry at all: either nothing, or the literal JSON null a Bedrock client sends for a skin whose model is built into the client. Both mean "no mesh supplied", not "broken upload" - use it to tell those apart before calling ParseGeometry.
See docs/skin-data.md#most-skins-send-no-geometry-at-all.
func IsSkinInvisible ¶
IsSkinInvisible reports whether the skin is invisible — every body part defined by the geometry (or the standard UV layout) has transparent pixels.
func IsSkinTiny ¶
IsSkinTiny reports whether the geometry defines body parts too small to see.
func ParseAnimations ¶
ParseAnimations reads a Bedrock animation file, returning its animations by name. Every Molang expression in it is compiled, so a syntax error is reported here, naming the animation, bone and channel it is in; names an expression reads that this library does not model are simply 0 when it runs.
func ParseParts ¶
ParseParts splits a comma-separated bone list into trimmed, non-empty names. Blank input returns nil, which Options.Parts reads as "everything".
func Render ¶
Render rasterizes a skin into a square image.
The zero-ish case is the common one: with only a Texture set, this renders the full body of a standard humanoid, straight on, at 512x512.
img, err := bedrockskin.Render(bedrockskin.Options{Texture: tex})
Persona skins render in 3D from their poly meshes; pass their animation images as Animated to get the head. Geometry whose bones draw nothing falls back to a flat crop of the texture (see Render2D) rather than failing.
Example ¶
package main
import (
"fmt"
"image"
"image/color"
bedrockskin "github.com/THEBOSS9345/bedrock-skin-go"
)
// skinTexture stands in for a decoded skin file: 64x64, every pixel opaque.
func skinTexture() *image.NRGBA {
img := image.NewNRGBA(image.Rect(0, 0, 64, 64))
for y := 0; y < 64; y++ {
for x := 0; x < 64; x++ {
img.Set(x, y, color.NRGBA{R: uint8(x * 4), G: uint8(y * 4), B: 128, A: 255})
}
}
return img
}
func main() {
img, err := bedrockskin.Render(bedrockskin.Options{
Texture: skinTexture(),
View: bedrockskin.ViewHead,
Size: 128,
})
if err != nil {
panic(err)
}
fmt.Println(img.Bounds().Dx(), "x", img.Bounds().Dy())
}
Output: 128 x 128
func Render2D ¶
Render2D composites a flat front-view "paper doll" by cropping the standard vanilla box-UV regions straight out of the texture. It needs no geometry at all, which is why Render falls back to it for geometry that draws nothing: bones with neither cubes nor a poly mesh.
Coordinates are relative to a 64-wide texture and scaled proportionally for other widths. See docs/rendering-pipeline.md#the-2d-fallback.
func RenderBytes ¶
func RenderBytes(opts BytesOptions) ([]byte, error)
RenderBytes renders from encoded bytes and returns encoded PNG bytes.
out, err := bedrockskin.RenderBytes(bedrockskin.BytesOptions{
Texture: textureBytes,
View: bedrockskin.ViewAvatar,
})
It is exactly Render with decoding and PNG encoding folded in, so it behaves identically for geometry defaults, persona fallback and identifier selection.
Note that this decodes whatever it is given. A service accepting untrusted uploads should bound image dimensions first — see docs/recipes.md#handling-untrusted-uploads.
func RenderFrames ¶
func RenderFrames(opts AnimationOptions) ([]image.Image, error)
RenderFrames renders the motion frame by frame. Every frame shares one camera, fitted around the motion's whole sweep, so the model moves within a still frame rather than the frame chasing it. For a viewer that draws frames as its own camera moves, PrepareFrames keeps the same scenes and camera and draws them one at a time.
func RenderFramesPNG ¶ added in v0.2.0
func RenderFramesPNG(opts AnimationBytesOptions) ([][]byte, error)
RenderFramesPNG renders an animation from encoded bytes and returns every frame as PNG bytes, in order: RenderFrames with decoding and encoding folded in, for callers that build their own animation format.
func RenderGIF ¶
func RenderGIF(opts AnimationOptions) ([]byte, error)
RenderGIF renders the motion as a looping animated GIF. GIF holds 256 colours a frame, so the frames share a palette of the colours they use most (exact for most skins, which use fewer), and transparency is on or off per pixel, as the renderer's alpha test already makes it.
func RenderGIFBytes ¶ added in v0.2.0
func RenderGIFBytes(opts AnimationBytesOptions) ([]byte, error)
RenderGIFBytes renders an animation from encoded bytes and returns GIF bytes: RenderGIF with decoding folded in.
gif, err := bedrockskin.RenderGIFBytes(bedrockskin.AnimationBytesOptions{
BytesOptions: bedrockskin.BytesOptions{Texture: textureBytes, Size: 256},
Animation: bedrockskin.MotionWalk,
})
func RenderItem ¶ added in v0.2.3
func RenderItem(opts ItemOptions) (image.Image, error)
RenderItem renders an item on its own: the sprite extruded one texel deep, as the game draws a held item, centred and framed by the camera.
func RenderItemBytes ¶ added in v0.2.3
func RenderItemBytes(opts ItemBytesOptions) ([]byte, error)
RenderItemBytes renders an item on its own from encoded bytes and returns PNG bytes: RenderItem with decoding and encoding folded in.
func RenderItemFrames ¶ added in v0.2.3
func RenderItemFrames(opts ItemAnimationOptions) ([]image.Image, error)
RenderItemFrames renders the spinning item frame by frame. Every frame shares one camera, fitted around the whole turn, so the item turns in a still frame.
func RenderItemGIF ¶ added in v0.2.3
func RenderItemGIF(opts ItemAnimationOptions) ([]byte, error)
RenderItemGIF renders the spinning item as a looping animated GIF, as RenderGIF encodes one.
func RenderItemGIFBytes ¶ added in v0.2.3
func RenderItemGIFBytes(opts ItemAnimationBytesOptions) ([]byte, error)
RenderItemGIFBytes spins an item on its own from encoded bytes and returns GIF bytes: RenderItemGIF with decoding folded in.
func TextureFromRGBA ¶
TextureFromRGBA wraps raw non-premultiplied RGBA pixel data as an image, which is the form Bedrock actually sends a skin in: SkinData decodes to width*height*4 bytes with no header, and the dimensions arrive separately in SkinImageWidth and SkinImageHeight.
raw, err := base64.StdEncoding.DecodeString(data.SkinData)
if err != nil {
return err
}
tex, err := bedrockskin.TextureFromRGBA(raw, data.SkinImageWidth, data.SkinImageHeight)
The byte slice backs the image directly rather than being copied, so it must not be modified afterwards. A length that disagrees with the declared dimensions is an error rather than a silently garbled image.
See docs/skin-data.md#the-texture-is-not-an-image-file.
func WriteGIF ¶ added in v0.2.1
func WriteGIF(w io.Writer, opts AnimationOptions) error
WriteGIF renders the animation and writes the GIF to w - an HTTP response, a file - without holding the encoded bytes first. It writes the same bytes RenderGIF returns.
Example ¶
A looping GIF, written as it is encoded - to a file here, to an HTTP response just the same.
package main
import (
"bytes"
"fmt"
"image"
"image/color"
"image/gif"
bedrockskin "github.com/THEBOSS9345/bedrock-skin-go"
)
// skinTexture stands in for a decoded skin file: 64x64, every pixel opaque.
func skinTexture() *image.NRGBA {
img := image.NewNRGBA(image.Rect(0, 0, 64, 64))
for y := 0; y < 64; y++ {
for x := 0; x < 64; x++ {
img.Set(x, y, color.NRGBA{R: uint8(x * 4), G: uint8(y * 4), B: 128, A: 255})
}
}
return img
}
func main() {
var out bytes.Buffer
err := bedrockskin.WriteGIF(&out, bedrockskin.AnimationOptions{
Options: bedrockskin.Options{Texture: skinTexture(), Size: 64},
Animation: bedrockskin.MotionWalk,
FPS: 10,
})
if err != nil {
panic(err)
}
g, err := gif.DecodeAll(&out)
if err != nil {
panic(err)
}
fmt.Println(len(g.Image), "frames")
}
Output: 10 frames
Types ¶
type Angle ¶
type Angle string
Angle selects one of the two named camera presets. Options.Camera bypasses these entirely and takes explicit yaw/pitch instead.
func ParseAngle ¶
ParseAngle resolves an angle name to an Angle, the same way ParseView resolves a view. Blank input returns the zero Angle, which Options reads as "the default for the chosen view". An unrecognised name wraps ErrUnknownAngle.
type AnimatedTexture ¶
type AnimatedTexture struct {
Type AnimatedType
Texture image.Image
}
AnimatedTexture is one skin animation's image: its frames stacked top to bottom, as the client sends it.
type AnimatedTextureBytes ¶ added in v0.2.0
type AnimatedTextureBytes struct {
Type AnimatedType
Texture []byte // an encoded PNG or JPEG
}
AnimatedTextureBytes is AnimatedTexture with the image encoded.
type AnimatedType ¶
type AnimatedType int
AnimatedType is the kind of a skin animation, numbered as the Bedrock protocol numbers them.
const ( AnimatedFace AnimatedType = 1 // the face: eyes that blink AnimatedBody32 AnimatedType = 2 // a 32x32 animated body part AnimatedBody128 AnimatedType = 3 // a 128x128 animated body part )
type Animation ¶
type Animation struct {
// Name is its key in the file, e.g. "animation.player.wave".
Name string
// Loop is whether it starts over at the end; HoldOnLastFrame keeps the
// last pose instead (as does not looping, once it has finished).
Loop bool
HoldOnLastFrame bool
// Length is how long it runs, in seconds: animation_length, or the last
// keyframe's time when the file leaves it out.
Length float64
// contains filtered or unexported fields
}
Animation is one animation from a Bedrock animation file - the format Blockbench exports and Minecraft's own resource packs use. See docs/animation.md#animation-files.
func (*Animation) Duration ¶
Duration is how long one loop lasts: Length, or a second for an animation with no keyframes (all expressions), which has no natural end.
func (*Animation) MissingBones ¶
MissingBones lists the bones the animation moves that g does not have, so those parts of it will do nothing on that model - typically an animation made for another entity (a wing, a tail). Names match case-insensitively, as in game. Empty means every part of it applies.
type AnimationBytesOptions ¶ added in v0.2.0
type AnimationBytesOptions struct {
BytesOptions
Animation Animator // required; ErrNoAnimation without one
FPS int
Frames int
Workers int
}
AnimationBytesOptions is AnimationOptions with encoded bytes in place of images: BytesOptions plus the animation fields, which behave as their AnimationOptions counterparts.
func (AnimationBytesOptions) RenderFramesPNG ¶ added in v0.2.0
func (o AnimationBytesOptions) RenderFramesPNG() ([][]byte, error)
RenderFramesPNG is the method form of RenderFramesPNG.
func (AnimationBytesOptions) RenderGIF ¶ added in v0.2.0
func (o AnimationBytesOptions) RenderGIF() ([]byte, error)
RenderGIF is the method form of RenderGIFBytes.
type AnimationOptions ¶
type AnimationOptions struct {
Options
// Animation is what moves the model: a built-in Motion, or an Animation
// from ParseAnimations. Required.
Animation Animator
// FPS is frames per second; zero means 20.
FPS int
// Frames is how many frames to render; zero means one loop.
Frames int
// Workers is how many frames are rasterized at once; zero means
// GOMAXPROCS, one means one at a time. Frames are independent, so the
// images are the same either way; a server already rendering on every
// core may want 1. See docs/design-decisions.md#why-animation-frames-render-in-parallel.
Workers int
}
AnimationOptions configures RenderFrames and RenderGIF: every field of Options (its Pose is ignored), plus the animation and its timing.
type Animator ¶
type Animator interface {
// Duration is the length of one loop, in seconds.
Duration() float64
// Pose is the pose t seconds in.
Pose(t float64) Pose
}
Animator is anything that poses a model over time: a built-in Motion, or an Animation loaded from a Bedrock animation file.
type Armor ¶ added in v0.2.3
type Armor struct {
Helmet image.Image
Chestplate image.Image
Leggings image.Image
Boots image.Image
// Elytra is the elytra's texture (textures/models/armor/elytra.png),
// worn on the back. It takes the chestplate's slot, as in game: with
// both set, only the elytra is worn. See docs/equipment.md#elytra.
Elytra image.Image
}
Armor is the armor a skin wears: one texture per piece, as a resource pack lays them out. The helmet, chestplate and boots take the set's first layer (e.g. textures/models/armor/diamond_1.png), the leggings its second (diamond_2.png). A nil piece is not worn, so pieces from different sets mix freely. See docs/equipment.md.
type ArmorBytes ¶ added in v0.2.3
type ArmorBytes struct {
Helmet []byte
Chestplate []byte
Leggings []byte
Boots []byte
Elytra []byte
}
ArmorBytes is Armor with each piece's texture encoded as PNG or JPEG; a nil piece is not worn.
func ArmorSetBytes ¶ added in v0.2.3
func ArmorSetBytes(layer1, layer2 []byte) ArmorBytes
ArmorSetBytes is ArmorSet for encoded textures.
type Bone ¶
type Bone struct {
Name string `json:"name"`
Parent string `json:"parent,omitempty"`
Pivot []float64 `json:"pivot,omitempty"`
Rotation []float64 `json:"rotation,omitempty"`
Inflate float64 `json:"inflate,omitempty"`
Mirror bool `json:"mirror,omitempty"`
Cubes []Cube `json:"cubes,omitempty"`
// The rest of the schema, kept so a whole file can be read. Poly
// meshes are drawn (read one with Mesh); texture meshes are parsed but
// not drawn. See docs/geometry-format.md#everything-else-in-a-bone.
BindPoseRotation []float64 `json:"bind_pose_rotation,omitempty"`
Locators map[string]Locator `json:"locators,omitempty"`
PolyMesh json.RawMessage `json:"poly_mesh,omitempty"`
TextureMeshes json.RawMessage `json:"texture_meshes,omitempty"`
}
Bone and Cube mirror Bedrock's geometry.json schema. UV stays raw JSON because a cube's "uv" field is either a [u,v] pair or a per-face object, resolved in mesh.go. See docs/geometry-format.md.
type BonePose ¶
BonePose moves one bone from where its geometry puts it: Rotation is added to the bone's own rotation (degrees, the geometry's convention), Position to its offset from its parent (model units), and, when Scaled, Scale multiplies it (about its pivot, carrying its children; 0 hides it).
type BytesOptions ¶
type BytesOptions struct {
// Texture is an encoded PNG or JPEG. Required.
//
// Bedrock sends skins as raw RGBA rather than an encoded image; use
// TextureFromRGBA for those and the Options/Render path instead.
Texture []byte
// Geometry is a raw geometry.json. Nil, empty, or the literal "null" a
// Bedrock client sends for a built-in model all fall back to
// DefaultGeometry, so a skin's field can be passed straight through.
Geometry []byte
// Cape is an encoded PNG or JPEG cape texture, or nil.
Cape []byte
// Animated holds a persona skin's animation images, encoded; see
// Options.Animated.
Animated []AnimatedTextureBytes
// Armor is Options.Armor with each piece's texture encoded.
Armor ArmorBytes
// RightHand and LeftHand are Options' hands with the item encoded.
RightHand HeldBytes
LeftHand HeldBytes
Identifier string
View View
Angle Angle
Parts []string
Camera *Camera
Size int
// Scale and HideSkin are Options' fields. With HideSkin, Texture may be
// empty.
Scale Scale
HideSkin bool
}
BytesOptions is Options with encoded byte slices in place of decoded images, for callers who already hold file or wire bytes and want PNG bytes back. It is the same render with the decode and encode steps folded in.
Every field behaves exactly as its Options counterpart; see Options for what each one means.
func (BytesOptions) RenderPNG ¶
func (o BytesOptions) RenderPNG() ([]byte, error)
RenderPNG renders these options and returns encoded PNG bytes. It is the method form of RenderBytes.
type Camera ¶
type Camera struct {
// Yaw rotates the camera around the vertical axis, in degrees. 0 is
// straight-on front; positive values swing the camera around to bring
// more of the subject's left side into view.
Yaw float64
// Pitch raises the camera, in degrees. 0 is level with the subject's
// centre; positive values look down and reveal top-facing surfaces.
Pitch float64
// FOV is the field of view in degrees. Zero means 35. Small values
// (~15-20) read as flat and zoomed in; large ones (~50+) add perspective
// distortion, which suits dramatic close framing.
FOV float64
// Margin is how much room to leave around the subject. Zero means 1.5.
// 1.0 frames as tightly as possible without clipping; larger values pull
// the camera back.
Margin float64
}
Camera positions the view explicitly, instead of letting View and Angle pick a framing. Set Options.Camera only when one of the presets won't do — the presets already handle the common cases.
type Cube ¶
type Cube struct {
Origin []float64 `json:"origin"`
Size []float64 `json:"size"`
UV json.RawMessage `json:"uv"`
Inflate *float64 `json:"inflate,omitempty"`
Mirror bool `json:"mirror,omitempty"`
// Rotation turns the cube about Pivot, in degrees; Pivot is in model
// space and defaults to the cube's centre. See
// docs/geometry-format.md#rotation.
Rotation []float64 `json:"rotation,omitempty"`
Pivot []float64 `json:"pivot,omitempty"`
}
type FaceUV ¶
type FaceUV struct {
UV []float64 `json:"uv"`
UVSize []float64 `json:"uv_size,omitempty"`
UVRotation float64 `json:"uv_rotation,omitempty"`
MaterialInstance string `json:"material_instance,omitempty"`
}
FaceUV is one face's texture area in a cube's per-face uv form.
type Frames ¶ added in v0.2.4
type Frames struct {
// contains filtered or unexported fields
}
Frames is an animation prepared once: its per-frame scenes and the bounding box they share, kept so a viewer can draw frames one at a time as its camera moves. RenderFrames builds the same thing and discards it after drawing every frame; Frames keeps it, so one frame costs one rasterization and every frame of an angle is framed by one camera - root and whole-body motion stay on screen instead of the camera chasing each pose. See docs/animation.md.
func PrepareFrames ¶ added in v0.2.4
func PrepareFrames(opts AnimationOptions) (*Frames, error)
PrepareFrames builds every frame of an animation, and the one camera they share, without rasterizing anything. It is RenderFrames split in two; draw the result with Frames.Draw.
func (*Frames) Draw ¶ added in v0.2.4
Draw rasterizes frame i at size, using the camera the frames were prepared with unless cam is set, when it refits the shared framing to cam. Refitting is what turns a draw into an orbit: every frame at one camera still shares a single framing. i wraps into range. A persona skin's flat crop is returned as prepared, whatever size was asked for.
type Geometry ¶
type Geometry struct {
Identifier string
TextureWidth float64
TextureHeight float64
Bones []Bone
// The visible bounds: the box, in blocks, the game uses to decide the
// model is on screen. Zero when the file leaves them out.
VisibleBoundsWidth float64
VisibleBoundsHeight float64
VisibleBoundsOffset []float64
}
Geometry is one normalized model - a body, a cape - regardless of which of Bedrock's two wire formats it came from. See ParseGeometry.
func DefaultGeometry ¶
func DefaultGeometry() []Geometry
DefaultGeometry returns the standard vanilla humanoid geometry: the wide ("geometry.humanoid.custom") and slim ("geometry.humanoid.customSlim") body variants, plus "geometry.cape". Set Options.Identifier to choose between them; with no identifier the wide variant wins.
This is the right model for most real skins, not merely a fallback: a Bedrock client sends no mesh at all for a skin using a built-in model. See docs/skin-data.md#most-skins-send-no-geometry-at-all.
The returned slice is shared across calls and must not be modified.
func FindCape ¶
FindCape returns the entry holding a bone literally named "cape" that has a cube. Capes always live in their own entry, never merged into the body.
func ParseGeometry ¶
ParseGeometry parses raw into normalized entries, detecting whichever of Bedrock's two formats it is - bone and cube fields are identical between them, only the wrapper differs.
Valid JSON carrying no geometry, including the literal "null" a client sends for a built-in model, returns zero entries and no error. An error means genuinely malformed input.
Entry order is stable for the same input: modern keeps document order, legacy sorts by identifier.
func SelectGeometry ¶
SelectGeometry picks the entry matching identifier, falling back to the one with the most cubes when identifier is empty or matches nothing.
The fallback is deliberately not geos[0]: bundles commonly list the sparse cape entry first, so taking the first would select it and leave every head-scoped view empty. See docs/design-decisions.md#why-select-by-cube-count.
func (*Geometry) BoneByName ¶
BoneByName returns the bone with the given name, and whether it exists.
func (*Geometry) Children ¶
Children returns the bones whose parent is the named bone, in file order.
func (*Geometry) Locator ¶
Locator finds a locator by name on any bone, returning it and the bone it is on.
func (*Geometry) TotalCubes ¶
TotalCubes is the number of cubes across every bone in the entry. A persona skin has none - its mesh is poly meshes; HasMesh counts both.
type GeometrySizeResult ¶
type GeometrySizeResult struct {
Pass bool
Violations []GeometryViolation
}
GeometrySizeResult holds the result of ValidateGeometrySize.
func ValidateGeometrySize ¶
func ValidateGeometrySize(geomData []byte, minSize float64) GeometrySizeResult
ValidateGeometrySize checks whether the geometry defines body parts that are large enough to be visible. Every bone with cubes is checked: the largest axis of the box enclosing its cubes, inflate included, must meet minSize. Bones with no cubes are ignored (they produce no rendered geometry).
Violations are ordered by bone name.
type GeometryTree ¶
type GeometryTree struct {
// FormatVersion is the file's format_version, e.g. "1.12.0".
FormatVersion string
// contains filtered or unexported fields
}
GeometryTree is a whole geometry file, every field kept - including ones this library has no type for - so any value in it can be picked out by a path. ParseGeometry gives the typed models the renderer uses; a tree is for reading a file. See docs/geometry-format.md#picking-values-out-of-a-file.
tree, err := bedrockskin.ParseGeometryTree(raw)
pivot, ok := tree.Get("geometry.humanoid.custom/bones/rightArm/pivot")
for _, v := range tree.Select("*/bones/*/cubes/*/size") { ... }
Both of Bedrock's wire formats read into the same shape, the modern one: each model is an object with a "description" (identifier, texture_width, texture_height, visible_bounds_*) and "bones", so one path works on either.
func ParseGeometryTree ¶
func ParseGeometryTree(raw []byte) (*GeometryTree, error)
ParseGeometryTree reads a geometry file of either format into a tree.
func (*GeometryTree) Geometries ¶
func (t *GeometryTree) Geometries() ([]Geometry, error)
Geometries returns the typed models: the same as ParseGeometry on the file.
func (*GeometryTree) Get ¶
func (t *GeometryTree) Get(path string) (GeometryValue, bool)
Get returns the first value the path picks out, and whether there was one.
func (*GeometryTree) Identifiers ¶
func (t *GeometryTree) Identifiers() []string
Identifiers lists the models' identifiers, in the same order as ParseGeometry.
func (*GeometryTree) Select ¶
func (t *GeometryTree) Select(path string) []GeometryValue
Select returns every value the path picks out, in file order; none when nothing matches.
A path is segments separated by "/". The first picks the model by identifier, the rest walk into it:
- an object's field by name: "description", "bones", "locators", "pivot"
- an array element by index, from 0 ("-1" is the last), or - for a list of named things, such as bones - by its name: "bones/rightArm"
- "*" for every model, field or element at that level
Names match exactly, else case-insensitively, as the game matches bones. A legacy model named "geometry.a:geometry.b" (one inheriting from another) is also picked by "geometry.a". An empty path returns every model.
type GeometryValue ¶
GeometryValue is one value a path picked out: its canonical path, with bones and other named entries by name, and the value itself - a map[string]any, []any, float64, string, bool or nil, as encoding/json reads JSON.
func (GeometryValue) Decode ¶
func (v GeometryValue) Decode(dst any) error
Decode stores the value in dst as encoding/json would, so a bone decodes into a Bone, a cube into a Cube, a locator into a Locator.
func (GeometryValue) Float ¶
func (v GeometryValue) Float() (float64, bool)
Float returns the value as a number.
func (GeometryValue) Floats ¶
func (v GeometryValue) Floats() ([]float64, bool)
Floats returns the value as a list of numbers - a pivot, an origin, a size.
func (GeometryValue) Text ¶
func (v GeometryValue) Text() (string, bool)
Text returns the value as a string - a name, a parent, an identifier.
type GeometryViolation ¶
GeometryViolation records a bone whose world-space size is below the minimum threshold.
type Held ¶ added in v0.2.3
type Held struct {
// Item is the item's sprite, e.g. textures/items/diamond_sword.png,
// drawn extruded and placed where the game places it, with the arm
// held forward. Nil holds nothing; nor does geometry without the arm.
Item image.Image
// Flat holds it as the game holds an item that is not a tool or
// weapon - food, materials. False holds it upright, as a sword.
Flat bool
// Adjust moves the item from where the game puts it, for an item that
// placement does not suit.
Adjust ItemAdjust
}
Held is an item held in one hand. The zero value holds nothing. See docs/equipment.md#held-items.
type HeldBytes ¶ added in v0.2.3
type HeldBytes struct {
Item []byte
Flat bool
Adjust ItemAdjust
}
HeldBytes is Held with the item's sprite an encoded PNG or JPEG; nil holds nothing.
type ItemAdjust ¶ added in v0.2.3
type ItemAdjust struct {
// Offset moves the item, in model units along the model's axes, as a
// bone's position moves a bone.
Offset [3]float64
// Rotation turns the item about its grip, in degrees, as a bone's
// rotation turns a bone: a positive X tips its top forward.
Rotation [3]float64
// Scale resizes the item about its grip. Zero means 1.
Scale float64
}
ItemAdjust moves a held item from the game's placement, about its grip and in the hand's frame, so it follows the arm. The zero value moves nothing. See docs/equipment.md#adjusting-an-item.
type ItemAnimationBytesOptions ¶ added in v0.2.3
type ItemAnimationBytesOptions struct {
ItemBytesOptions
Duration float64
FPS int
Frames int
}
ItemAnimationBytesOptions is ItemAnimationOptions with the item's sprite encoded.
type ItemAnimationOptions ¶ added in v0.2.3
type ItemAnimationOptions struct {
ItemOptions
// Duration is how long one turn takes, in seconds. Zero means 3.
Duration float64
// FPS is frames per second; zero means 20.
FPS int
// Frames is how many frames to render; zero means one turn.
Frames int
}
ItemAnimationOptions spins an item on its own: one full turn about its upright axis each loop, as a dropped item turns. See docs/equipment.md#an-item-on-its-own.
type ItemBytesOptions ¶ added in v0.2.3
type ItemBytesOptions struct {
Item []byte
Angle Angle
Camera *Camera
Size int
Adjust ItemAdjust
}
ItemBytesOptions is ItemOptions with the item's sprite an encoded PNG or JPEG.
type ItemOptions ¶ added in v0.2.3
type ItemOptions struct {
// Item is the item's sprite, e.g. textures/items/diamond_sword.png.
Item image.Image
// Angle picks the camera preset: AngleFront faces the sprite, as an
// inventory icon; AngleIso turns it to show its depth. Zero means
// AngleFront. Ignored when Camera is set.
Angle Angle
// Camera overrides Angle with an explicit position.
Camera *Camera
// Size is the output edge length; the image is square. Zero means
// DefaultSize.
Size int
// Adjust turns and resizes the item about its centre. The camera frames
// the item whatever its size or offset, so Scale and Offset change only
// its shape against other parts, not how big it looks.
Adjust ItemAdjust
}
ItemOptions describes a render of one item on its own, extruded as a held item is. Only Item is required. See docs/equipment.md#an-item-on-its-own.
type Locator ¶
type Locator struct {
Offset []float64 `json:"offset"`
Rotation []float64 `json:"rotation,omitempty"`
IgnoreInheritedScale bool `json:"ignore_inherited_scale,omitempty"`
}
Locator is a named point on a bone - where an item is held, a lead ties, particles start. A file writes one as just an offset, [x, y, z], or as an object with an offset and a rotation; both read into this.
func (*Locator) UnmarshalJSON ¶
type Motion ¶
type Motion string
Motion is one of the built-in player animations: Minecraft's own movements, recreated as poses over time. They move bones by their standard names (head, body, rightArm, leftArm, rightLeg, leftLeg), so they work on any model that uses them, custom ones included; whatever is parented under a bone - a sleeve, a hat, a backpack - moves with it. See docs/animation.md.
const ( MotionWalk Motion = "walk" // arms and legs swing, each leg opposite its arm MotionIdle Motion = "idle" // the gentle breathing sway of an idle player MotionWave Motion = "wave" // the right arm raised, waving MotionSneak Motion = "sneak" // leaning forward, creeping )
The built-in motions.
func ParseMotion ¶
ParseMotion returns the Motion named s.
func (Motion) Pose ¶
Pose is the motion's pose t seconds in; it loops every Duration.
Rotations follow the geometry's convention, in which a positive X turn tips a bone's top toward the front: a limb hanging from its shoulder or hip then swings backward, so swinging it forward is a negative X. Raising an arm out to its side is a positive Z for the right arm, negative for the left.
type Options ¶
type Options struct {
// Texture is the decoded skin image. Required. Bedrock skins are
// normally 64x64 or 128x128, but any size works.
Texture image.Image
// Geometry is the skin's model, as returned by ParseGeometry. Leave it
// nil for a skin that uses a built-in model — DefaultGeometry stands in,
// which is what a Bedrock client would render. See DefaultGeometry for
// why this is the common case rather than a convenience.
Geometry []Geometry
// Identifier picks which entry of Geometry to render when it holds more
// than one, which is usual: a bundle typically carries a cape entry plus
// both arm variants. It matches the identifier named in the skin's
// resource patch, e.g. "geometry.humanoid.customSlim".
//
// The resource patch is the authoritative wide-vs-slim selector. The
// login packet's ArmSize field is not: real captures show the two
// disagreeing, with ArmSize reporting "wide" for a skin whose patch
// names customSlim.
//
// Empty, or naming an entry that isn't present, falls back to the entry
// with the most cubes. See SelectGeometry.
Identifier string
// Cape is an equipped cape texture, or nil.
//
// The cape mesh comes from a "cape" bone in Geometry, or from the
// built-in geometry.cape when Geometry has none — which is the usual
// case, since capes live in their own entry and never travel merged into
// a custom skin's body. Not drawn for ViewHead or ViewAvatar, which do
// not show one.
Cape image.Image
// View selects the framing: body, chest, head or avatar. Zero means
// ViewBody. Ignored when Parts is set.
View View
// Angle picks the camera preset for View: straight-on or the angled
// 3-quarter "head icon" look. Zero means the default for the chosen
// View, which is AngleIso for ViewHead and AngleFront elsewhere.
// Ignored when Camera is set.
Angle Angle
// Parts names exactly which bones to render, e.g.
// []string{"head", "leftArm"}. Each name pulls in everything parented
// under it, so "head" also brings a hat or hair bone. Empty means use
// View instead.
Parts []string
// Camera overrides the View/Angle framing with an explicit position.
Camera *Camera
// Size is the output edge length in pixels; the image is always square.
// Zero means DefaultSize.
Size int
// Pose moves bones from where the geometry puts them, e.g. a frame of a
// Motion (see Motion.Pose). Nil is the rest pose.
Pose Pose
// Animated holds the extra textures a persona skin's animations carry -
// the face, and animated body parts. Each draws the geometry entry made
// for it alongside the main one; a persona skin's head lives only in its
// face entry. See docs/geometry-format.md#persona-skins.
Animated []AnimatedTexture
// Armor is the armor worn over the skin. The zero value wears none.
// See docs/equipment.md.
Armor Armor
// RightHand and LeftHand are the items held in each hand. The zero
// value holds nothing. See docs/equipment.md#held-items.
RightHand Held
LeftHand Held
// Scale resizes the figure or any bone. The zero value changes
// nothing. See docs/equipment.md#scale.
Scale Scale
// HideSkin draws the equipment alone - armor, elytra, held items and
// cape - posed and framed as it would be on the skin, so any piece can
// be rendered by itself; Parts and View pick which. Texture may then be
// nil. ErrEmptyView or ErrNoMatchingParts means no equipment was left
// to draw. See docs/equipment.md#equipment-on-its-own.
HideSkin bool
}
Options describes one render. Only Texture is required.
func (Options) Render ¶
Render renders these options, as the package-level Render function does. It is the method form, for callers who prefer to build options and render them in one expression.
type PartReport ¶
type PartReport struct {
// Name is the body part or overlay/geometry bone name (head, hat, cape, ...).
Name string `json:"name"`
// Visibility is why this part counts as rendering or not. Visible reports
// the same thing as a bool when the distinction does not matter.
Visibility PartVisibility `json:"visibility"`
// OpaqueRatio is the share of the part's sampled pixels that are opaque,
// from 0 to 1.
OpaqueRatio float64 `json:"opaque_ratio"`
// Pixels is how many texture pixels were sampled for this part, and
// Transparent how many of those were see-through.
Pixels int `json:"pixels"`
Transparent int `json:"transparent_pixels"`
// FromGeometry is true when this part was resolved from real geometry
// cube UVs rather than the standard-layout fallback.
FromGeometry bool `json:"from_geometry"`
}
PartReport describes the visibility of one body part after analysis.
func (PartReport) Visible ¶
func (p PartReport) Visible() bool
Visible reports whether the part renders. It is derived from Visibility rather than stored, so the two can never disagree.
type PartVisibility ¶
type PartVisibility int
PartVisibility is the visibility classification of a single body part.
const ( // PartVisible means the part has enough opaque pixels to render normally. PartVisible PartVisibility = iota // PartInvisible means the part has no usable opaque pixels. PartInvisible // PartSuspicious means the part is partially visible but below the // minimum visible fraction. PartSuspicious // PartTiny means the geometry defines the part below the minimum size. PartTiny )
func (PartVisibility) MarshalJSON ¶
func (p PartVisibility) MarshalJSON() ([]byte, error)
MarshalJSON encodes the level as its lowercase name rather than its integer value, so a SkinReport handed straight to json.Marshal reads as "visibility":"invisible" instead of "visibility":1.
func (PartVisibility) String ¶
func (p PartVisibility) String() string
String returns a stable lowercase name for the visibility level.
func (*PartVisibility) UnmarshalJSON ¶
func (p *PartVisibility) UnmarshalJSON(data []byte) error
UnmarshalJSON accepts the names MarshalJSON produces, and the bare integers an older client may still send.
type PolyMesh ¶
type PolyMesh struct {
// NormalizedUVs means UVs are 0..1 across the texture; otherwise they
// are texture pixels against the entry's declared texture size.
NormalizedUVs bool `json:"normalized_uvs"`
Positions [][]float64 `json:"positions"`
Normals [][]float64 `json:"normals"`
UVs [][]float64 `json:"uvs"`
// Polys is a list of polygons, each a list of [position, normal, uv]
// index triples, or the string "tri_list" / "quad_list" for vertices
// taken in order. Polygons resolves them.
Polys json.RawMessage `json:"polys"`
}
PolyMesh is a bone's free-form mesh: the shape Bedrock sends for persona (character creator) skins instead of cubes. Positions are in model space, like a cube's origin. See docs/geometry-format.md#poly-meshes.
func (PolyMesh) Polygons ¶ added in v0.2.1
func (m PolyMesh) Polygons() [][]PolyVertex
Polygons resolves every polygon to its corners, "tri_list" and "quad_list" included. A polygon with fewer than three corners, or one whose position or UV index points outside the mesh's lists, is skipped - the renderer and the detector skip it too. Normals are not drawn, so a missing one leaves the corner's normal zero rather than dropping the polygon.
Example ¶
Reading a persona skin's poly mesh: every polygon, each corner looked up.
package main
import (
"fmt"
bedrockskin "github.com/THEBOSS9345/bedrock-skin-go"
)
func main() {
geos, err := bedrockskin.ParseGeometry([]byte(`{"minecraft:geometry":[{"description":{"identifier":"geometry.persona_x"},
"bones":[{"name":"body","poly_mesh":{"normalized_uvs":true,
"positions":[[-4,12,-2],[4,12,-2],[4,24,-2],[-4,24,-2]],
"normals":[[0,0,-1]],
"uvs":[[0.25,0.5],[0.375,0.5],[0.375,0.6875],[0.25,0.6875]],
"polys":[[[0,0,0],[1,0,1],[2,0,2],[3,0,3]]]}}]}]}`))
if err != nil {
panic(err)
}
mesh, _ := geos[0].Bones[0].Mesh()
for _, poly := range mesh.Polygons() {
fmt.Println(len(poly), "corners, first at", poly[0].Position, "facing", poly[0].Normal)
}
}
Output: 4 corners, first at [-4 12 -2] facing [0 0 -1]
type PolyVertex ¶ added in v0.2.1
type PolyVertex struct {
Position [3]float64
Normal [3]float64 // zero when the corner names no usable normal
UV [2]float64
}
PolyVertex is one corner of a polygon, its indices looked up: a position in model space, a normal, and a texture coordinate - 0..1 with V counting up when the mesh's NormalizedUVs is set, else texture pixels.
type Pose ¶
Pose is a BonePose for each bone it moves, by name. Bones not in it stay at rest. A name matches a bone exactly, or failing that case-insensitively, as animation files are matched in game.
type ResourcePatch ¶
type ResourcePatch struct {
// Default is the identifier of the body geometry, e.g.
// "geometry.humanoid.customSlim". Pass it as Options.Identifier.
Default string
// Cape is the cape geometry identifier when the patch names one, and
// empty otherwise. Most patches name only Default.
Cape string
}
ResourcePatch is a skin's decoded SkinResourcePatch: the mapping from render slot to geometry identifier that a Bedrock client sends alongside the texture. See docs/skin-data.md#the-resource-patch-is-the-authoritative-model-selector.
func ParseResourcePatch ¶
func ParseResourcePatch(raw []byte) (ResourcePatch, error)
ParseResourcePatch decodes a skin's resource patch and returns the geometry identifiers it names.
patch, err := bedrockskin.ParseResourcePatch(raw)
if err != nil {
return err
}
img, err := bedrockskin.Render(bedrockskin.Options{
Texture: tex,
Geometry: geos,
Identifier: patch.Default,
})
The patch is the authoritative wide-vs-slim selector, which is why this exists rather than leaving callers to hand-roll the struct: the login packet's ArmSize field disagrees with it on real captures, reporting "wide" for a skin whose patch names customSlim.
Empty input, or the literal "null", returns a zero ResourcePatch and no error - the same "nothing was sent" case IsEmpty covers for geometry. A patch that parses but names no default is not an error either; check Default against "" if you need one, and fall back to SelectGeometry's cube-count heuristic when it is absent.
type Scale ¶ added in v0.2.3
type Scale struct {
// Model is the figure's size in the image: 2 draws it twice as large,
// cropping what no longer fits; 0.5 half as large. Zero means 1. The
// camera frames the model whatever its size, so only this changes how
// big it looks.
Model float64
// Parts scales bones by name, ignoring case, each about its own pivot
// and carrying everything parented under it: the armor on it, and an
// arm's held item. 0 hides a bone.
Parts map[string]float64
}
Scale resizes the figure or any of its bones. The zero value changes nothing. A held item has its own scale, in ItemAdjust. See docs/equipment.md#scale.
type Skin ¶
type Skin struct {
// contains filtered or unexported fields
}
Skin bundles a decoded texture with its optional geometry so callers can question body-part visibility in one place.
A Skin must not be copied after first use, and must be used through the pointer NewSkin returns.
func NewSkin ¶
NewSkin builds a Skin for analysis with the default thresholds. geometry is raw geometry.json or nil.
Example ¶
Blocking the invisible-skin trick: a fully transparent skin is caught.
package main
import (
"fmt"
"image"
"image/color"
bedrockskin "github.com/THEBOSS9345/bedrock-skin-go"
)
// skinTexture stands in for a decoded skin file: 64x64, every pixel opaque.
func skinTexture() *image.NRGBA {
img := image.NewNRGBA(image.Rect(0, 0, 64, 64))
for y := 0; y < 64; y++ {
for x := 0; x < 64; x++ {
img.Set(x, y, color.NRGBA{R: uint8(x * 4), G: uint8(y * 4), B: 128, A: 255})
}
}
return img
}
func main() {
clear := image.NewNRGBA(image.Rect(0, 0, 64, 64))
fmt.Println(bedrockskin.NewSkin(clear, nil).IsInvisible())
fmt.Println(bedrockskin.NewSkin(skinTexture(), nil).IsInvisible())
}
Output: true false
func NewSkinWithOptions ¶
func NewSkinWithOptions(texture image.Image, geometry []byte, opts SkinOptions) *Skin
NewSkinWithOptions builds a Skin that judges by opts rather than the defaults. It is otherwise identical to NewSkin, caching and all.
func (*Skin) InvisibleParts ¶
InvisibleParts returns the names of the invisible body parts.
func (*Skin) IsInvisible ¶
IsInvisible reports whether the whole skin is effectively invisible.
func (*Skin) IsSuspicious ¶
IsSuspicious reports whether the skin is half-invisible (not fully invisible, but several body parts are missing).
func (*Skin) OK ¶
OK reports whether the skin is acceptable, the single question most callers have. Equivalent to Report().OK().
func (*Skin) Parts ¶
func (s *Skin) Parts() []PartReport
Parts returns the per-part breakdown, equivalent to Report().Parts.
func (*Skin) Report ¶
func (s *Skin) Report() SkinReport
Report runs the full visibility analysis and returns the breakdown.
The result is computed once and reused, so asking several questions of one Skin costs a single pass over the texture. It is safe to call concurrently from multiple goroutines, which a service sharing one Skin across requests will do. Each call returns its own copy of the slices, so a caller is free to sort or filter them without disturbing the cached report.
type SkinOptions ¶
type SkinOptions struct {
// MinVisibleFraction is the share of a part's sampled pixels that must be
// opaque for it to count as visible. Zero means DefaultMinVisibleFraction.
MinVisibleFraction float64
// MinGeometrySize is the world-space size a bone must reach to avoid
// being judged too small to see. Zero means DefaultMinGeometrySize. It
// applies only when geometry is supplied.
MinGeometrySize float64
// MinVisibleParts is how many of the six standard body parts must be
// visible before the skin stops being suspicious. Zero means
// DefaultMinVisibleParts.
MinVisibleParts int
}
SkinOptions tunes the thresholds one Skin judges by. A zero field takes its Default* value, so the zero SkinOptions is exactly what NewSkin uses.
The knobs exist because what counts as unacceptable is policy, the same reasoning that keeps size and complexity limits out of the library. A lobby that only ever shows head icons might care solely about the head; a strict server might demand every part be near-opaque.
type SkinPartResult ¶
type SkinPartResult struct {
Name string
Visible bool
Fraction float64
Pixels int
Transparent int
FromGeo bool // true when the part came from geometry, not the fallback layout
Tiny bool // true when the geometry defines the part below the minimum size
}
SkinPartResult holds the visibility result for one body part. When geometry is provided, results reflect the actual cube UV regions resolved from the geometry. Without geometry, the standard vanilla UV layout is assumed.
type SkinReport ¶
type SkinReport struct {
// Verdict is the overall judgement.
Verdict Verdict `json:"verdict"`
// VisibleParts is how many of the standard body parts render, out of
// TotalParts. Overlay layers fold into the part they cover and
// accessories are ignored, so an opaque cape cannot mask an invisible
// body.
VisibleParts int `json:"visible_parts"`
TotalParts int `json:"total_parts"`
// Parts is the per-part breakdown: the standard body parts first, in a
// fixed order, then every other bone sorted by name.
Parts []PartReport `json:"parts"`
}
SkinReport is the result of analyzing a skin for invisible or suspicious body parts. It is safe to marshal straight to JSON from an API.
Everything derivable is a method rather than a field, so there is exactly one place each fact is stated and no way for the JSON to contradict itself.
func (SkinReport) InvisibleParts ¶
func (r SkinReport) InvisibleParts() []string
InvisibleParts returns the names of the standard body parts that do not render, in the order they appear in Parts. It is derived from Parts on each call rather than stored.
func (SkinReport) OK ¶
func (r SkinReport) OK() bool
OK reports whether the skin is acceptable. It is false for a zero SkinReport, so a report that was never filled in does not read as a pass.
type SkinVisibilityResult ¶
type SkinVisibilityResult struct {
IsInvisible bool
Pass bool
Suspicious bool
Parts []SkinPartResult
VisibleParts int
InvisibleParts int
}
SkinVisibilityResult holds the result of ValidateSkinVisibility.
IsInvisible is true when no body part is visible. Suspicious flags a half-invisible skin: at least one part is visible, but fewer of the standard body parts than the configured minimum, DefaultMinVisibleParts by default.
func ValidateSkinInvisibility ¶
func ValidateSkinInvisibility(texture image.Image, geomData []byte) SkinVisibilityResult
ValidateSkinInvisibility is the main entry point. It cross-references the geometry against the texture: for every bone the geometry defines, the corresponding UV regions are checked in the actual texture pixels. A skin is invisible if every rendered body part is either transparent in the texture or defined by geometry too small to see.
geomData is the raw geometry.json bytes. Pass nil for standard skins (most real skins send no geometry — the default geometry is assumed).
func ValidateSkinVisibility ¶
func ValidateSkinVisibility(texture image.Image, geomData []byte, minVisibleFraction float64) SkinVisibilityResult
ValidateSkinVisibility checks whether a skin texture has visible body parts. When geomData is provided (raw geometry.json), every bone with cubes is examined: its UV rectangles are resolved from the geometry and the actual texture pixels are checked for alpha. This cross-references geometry against the texture so a player cannot hide behind geometry that maps to transparent regions.
Without geometry (nil geomData), the standard vanilla humanoid UV layout is used as the fallback — correct for most real skins.
Persona geometry - geometry that parses into bones, none of which carry cubes - has no usable box UVs to cross-reference and comes from Minecraft's curated skin market, so it is always treated as visible and never flagged suspicious. Geometry that fails to parse is not that: it falls through to the texture-only check, exactly as nil geomData does.
A skin is invisible if every body part has fewer than minVisibleFraction non-transparent pixels at its rendered UV regions.
type Verdict ¶
type Verdict int
Verdict is the overall judgement on a skin.
It is one value with a fixed set of states rather than the three independent booleans this used to carry (Pass, IsInvisible, IsSuspicious), which could express five combinations that mean nothing - invisible and suspicious at once, or passing while invisible.
const ( // VerdictUnknown is the zero value: no analysis has been run. It exists // so an uninitialized SkinReport does not claim a skin is fine. OK // reports false for it, so the zero value fails closed. VerdictUnknown Verdict = iota // VerdictOK means the skin renders normally. VerdictOK // VerdictSuspicious means some standard body parts do not render, but // enough do that the skin is not simply invisible. A soft signal: worth // logging or reviewing, not necessarily worth rejecting. VerdictSuspicious // VerdictInvisible means nothing renders, or only a stray limb does. VerdictInvisible )
func (Verdict) MarshalJSON ¶
MarshalJSON encodes the verdict as its lowercase name.
func (*Verdict) UnmarshalJSON ¶
UnmarshalJSON accepts the names MarshalJSON produces.
type View ¶
type View string
View selects which part of the model to render. Bone inclusion is ancestry-based rather than a fixed list, so a custom skin's extra bones (ears, tails, wings, party hats) are picked up automatically as long as they are parented somewhere under a standard anchor.
See docs/views-and-cameras.md.
func ParseView ¶
ParseView resolves a view name, as it would arrive in a query string or a config file, to a View. Matching ignores case and surrounding space, and blank input returns ViewBody.
An unrecognised name is an error wrapping ErrUnknownView rather than a silent fallback. Options.View is a bare string type that accepts anything, so a request for "avatr" would otherwise render a full body and look like the service ignoring its caller.
type WireAnimation ¶ added in v0.2.1
type WireAnimation struct {
Type AnimatedType
Data []byte
Width, Height int
}
WireAnimation is one entry of a skin's animation list: its image as raw RGBA, and its type as the protocol numbers it.
type WireSkin ¶ added in v0.2.1
type WireSkin struct {
// SkinData is the skin's pixels, raw non-premultiplied RGBA,
// SkinWidth*SkinHeight*4 bytes. Required.
SkinData []byte
SkinWidth, SkinHeight int
// CapeData is the cape's pixels in the same form; empty for no cape.
CapeData []byte
CapeWidth, CapeHeight int
// Geometry is SkinGeometryData. Empty or the literal "null" - what a
// client sends for a built-in model - draws the default model.
Geometry []byte
// ResourcePatch is SkinResourcePatch: it names which entry of Geometry
// the skin uses, wide or slim among them.
ResourcePatch []byte
// Animations is the skin's animation list. A persona skin's face, and
// some of its body, are textured by these.
Animations []WireAnimation
}
WireSkin is a skin as a Bedrock client sends it - in the login packet, a PlayerList entry or a PlayerSkin packet: images as raw RGBA with their sizes alongside, and the model as JSON. The field names follow the protocol's, so a proxy or bot can fill one straight from the packet.
opts, err := bedrockskin.WireSkin{
SkinData: s.SkinData, SkinWidth: int(s.SkinImageWidth), SkinHeight: int(s.SkinImageHeight),
Geometry: s.SkinGeometry, ResourcePatch: s.SkinResourcePatch,
}.Options()
See docs/skin-data.md for what each field holds.
func (WireSkin) Options ¶ added in v0.2.1
Options turns the wire fields into render Options: the images wrapped without copying, the geometry parsed, the entry the resource patch names selected, and the animation images attached so persona heads draw. Set View, Size and the rest on the result.
A resource patch that does not parse is not an error - the patch only picks an entry, and without it the entry with the most cubes is used, as for an empty patch. Malformed geometry and images whose data does not match their size are errors. An animation of a type the renderer does not draw is kept and ignored.
Example ¶
A skin straight from a Bedrock packet: raw RGBA, "null" geometry for a built-in model, and the resource patch naming the slim arms.
package main
import (
"fmt"
"image"
"image/color"
bedrockskin "github.com/THEBOSS9345/bedrock-skin-go"
)
// skinTexture stands in for a decoded skin file: 64x64, every pixel opaque.
func skinTexture() *image.NRGBA {
img := image.NewNRGBA(image.Rect(0, 0, 64, 64))
for y := 0; y < 64; y++ {
for x := 0; x < 64; x++ {
img.Set(x, y, color.NRGBA{R: uint8(x * 4), G: uint8(y * 4), B: 128, A: 255})
}
}
return img
}
func main() {
pix := skinTexture().Pix
opts, err := bedrockskin.WireSkin{
SkinData: pix,
SkinWidth: 64,
SkinHeight: 64,
Geometry: []byte("null"),
ResourcePatch: []byte(`{"geometry":{"default":"geometry.humanoid.customSlim"}}`),
}.Options()
if err != nil {
panic(err)
}
opts.View = bedrockskin.ViewChest
opts.Size = 96
img, err := opts.Render()
if err != nil {
panic(err)
}
fmt.Println(opts.Identifier, img.Bounds().Dx())
}
Output: geometry.humanoid.customSlim 96
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
bedrock-skin
command
Command bedrock-skin renders a skin file from the command line, to try the library before writing any code:
|
Command bedrock-skin renders a skin file from the command line, to try the library before writing any code: |