Skip to content

Instantly share code, notes, and snippets.

@savvythunder
Created June 13, 2025 02:41
Show Gist options
  • Select an option

  • Save savvythunder/fde35e8be6a14429cb0a4e5d5857c7be to your computer and use it in GitHub Desktop.

Select an option

Save savvythunder/fde35e8be6a14429cb0a4e5d5857c7be to your computer and use it in GitHub Desktop.
# Minecraft Skin Renderer A Python module to render Minecraft skins in 2D and pseudo-3D using PIL. Extracts face, head, and body parts from skins (64x64 or 64x32) with scaling options. Supports command-line usage to generate images for various skin parts.
"""
Minecraft Skin Renderer Module with 2D and 3D rendering
-------------------------------------------------------
Provides MinecraftSkinRenderer class to render skin parts in 2D or 3D styles using PIL.
Usage:
from minecraft_skin_renderer import MinecraftSkinRenderer
renderer = MinecraftSkinRenderer("skin.png")
head_2d = renderer.RenderHead(mode="2d")
head_3d = renderer.RenderHead(mode="3d")
head_2d.show()
head_3d.show()
Author: OpenAI / ChatGPT
"""
from PIL import Image, ImageEnhance
class MinecraftSkinRenderer:
"""
Renders parts of a Minecraft skin image using PIL.
Supports 2D (flat) and 3D (pseudo 3D with perspective & shading) rendering modes.
"""
def __init__(self, skin_source):
"""
Initialize the renderer with a skin image path or PIL Image.
Args:
skin_source (str or PIL.Image.Image): Path to skin PNG file or PIL Image.
Raises:
ValueError: If the skin image size is not supported (expect 64x64 or 64x32).
"""
if isinstance(skin_source, str):
self.skin = Image.open(skin_source).convert("RGBA")
elif isinstance(skin_source, Image.Image):
self.skin = skin_source.convert("RGBA")
else:
raise TypeError("skin_source must be a file path or PIL.Image.Image")
w, h = self.skin.size
if (w, h) not in [(64, 64), (64, 32)]:
raise ValueError("Unsupported skin size. Expected 64x64 or 64x32 pixels.")
# If older skin (64x32), convert to 64x64 by extending with transparency
if h == 32:
new_skin = Image.new("RGBA", (64, 64), (0, 0, 0, 0))
new_skin.paste(self.skin, (0, 0))
self.skin = new_skin
def _apply_shade(self, img, factor=0.7):
"""Return a shaded version of img by factor (0 < factor < 1)."""
enhancer = ImageEnhance.Brightness(img)
return enhancer.enhance(factor)
def _transform_perspective(self, img, quad):
"""
Apply a perspective transform defined by quad.
Args:
img: PIL.Image
quad: tuple with 8 floats (4 points x,y)
Returns:
Transformed PIL.Image with the same size as img.
"""
size = img.size
return img.transform(
size,
Image.QUAD,
data=quad,
resample=Image.BICUBIC,
fill=0,
)
def RenderFace(self, scale=8):
"""
Render the face (8x8 front head base layer) scaled.
Args:
scale (int, optional): Scale factor. Defaults to 8.
Returns:
PIL.Image: Scaled face image in 2D.
"""
face = self.skin.crop((8, 8, 16, 16))
return face.resize((8 * scale, 8 * scale), Image.NEAREST)
def RenderHead(self, scale=8, mode="2d"):
"""
Render the full head - 2D or 3D mode.
Args:
scale (int, optional): Scale factor. Defaults to 8.
mode (str, optional): "2d" or "3d". Defaults to "2d".
Returns:
PIL.Image: Scaled rendered head image.
"""
mode = mode.lower()
if mode == "2d":
# Flat composite with overlay
base_head = self.skin.crop((8, 8, 16, 16))
overlay = self.skin.crop((40, 8, 48, 16))
head = Image.alpha_composite(base_head, overlay)
return head.resize((8 * scale, 8 * scale), Image.NEAREST)
elif mode == "3d":
# Render head as a cube - visible faces: front, top, right
front = self.skin.crop((8, 8, 16, 16))
top = self.skin.crop((8, 0, 16, 8))
right = self.skin.crop((16, 8, 24, 16))
front_overlay = self.skin.crop((40, 8, 48, 16))
top_overlay = self.skin.crop((40, 0, 48, 8))
right_overlay = self.skin.crop((48, 8, 56, 16)) # right overlay layer
# Compose overlays on faces
front = Image.alpha_composite(front, front_overlay)
top = Image.alpha_composite(top, top_overlay)
right = Image.alpha_composite(right, right_overlay)
# Scale faces up before perspective transform for quality
face_scale = scale * 2
front = front.resize((8 * face_scale, 8 * face_scale), Image.NEAREST)
top = top.resize((8 * face_scale, 8 * face_scale), Image.NEAREST)
right = right.resize((8 * face_scale, 8 * face_scale), Image.NEAREST)
# Define quads for perspective (x0,y0,x1,y1,x2,y2,x3,y3)
# We define a simple isometric cube projection
offset = 8 * face_scale
# top face quadrilateral (diamond shaped)
top_quad = (
offset * 0.25, 0,
offset * 0.75, 0,
offset, offset * 0.25,
0, offset * 0.25,
)
# left(face front) is flat rectangle
front_quad = (
0, offset * 0.25,
offset, offset * 0.25,
offset, offset + offset * 0.25,
0, offset + offset * 0.25,
)
# right face parallelogram skewed to right
right_quad = (
offset, offset * 0.25,
offset + offset * 0.5, offset * 0.5,
offset + offset * 0.5, offset + offset * 0.5,
offset, offset + offset,
)
# Transform faces for 3d effect
top_tf = self._transform_perspective(top, top_quad)
front_tf = self._transform_perspective(front, front_quad)
right_tf = self._transform_perspective(right, right_quad)
# Shade the side faces for depth
right_tf = self._apply_shade(right_tf, 0.6)
top_tf = self._apply_shade(top_tf, 0.85)
# Create composed image large enough for all faces
width = int(offset + offset * 0.5 + 1)
height = int(offset + offset + 1)
canvas = Image.new("RGBA", (width, height), (0, 0, 0, 0))
# Paste faces in correct order: top, sides, front last
canvas.alpha_composite(top_tf, (0, 0))
canvas.alpha_composite(right_tf, (0, 0))
canvas.alpha_composite(front_tf, (0, 0))
# Downscale canvas to target scale for sharper edges
final_scale = scale
canvas = canvas.resize((width // (face_scale // final_scale), height // (face_scale // final_scale)), Image.NEAREST)
return canvas
else:
raise ValueError('Invalid mode: "{}". Use "2d" or "3d".'.format(mode))
def RenderFrontBody(self, scale=8, mode="2d"):
"""
Render the front torso - 2D or 3D mode.
Args:
scale (int, optional): Scale factor. Defaults to 8.
mode (str, optional): "2d" or "3d". Defaults to "2d".
Returns:
PIL.Image: Scaled rendered front torso image.
"""
mode = mode.lower()
if mode == "2d":
torso_front = self.skin.crop((20, 20, 28, 32))
return torso_front.resize((8 * scale, 12 * scale), Image.NEAREST)
elif mode == "3d":
# For torso 3d, render front face with slight perspective and shading
front = self.skin.crop((20, 20, 28, 32))
front_overlay = self.skin.crop((52, 20, 60, 32)) # overlay for torso front
front = Image.alpha_composite(front, front_overlay)
face_scale = scale * 2
front = front.resize((8 * face_scale, 12 * face_scale), Image.NEAREST)
# perspective quad (approx trapezoid to simulate 3D front face)
quad = (
8 * face_scale * 0.1, 0,
8 * face_scale * 0.9, 0,
8 * face_scale, 12 * face_scale,
0, 12 * face_scale,
)
front_tf = self._transform_perspective(front, quad)
# shading bottom slightly
front_tf = self._apply_shade(front_tf, 0.95)
# Downscale to scale for sharp edges
final_img = front_tf.resize((int(front_tf.width / (face_scale / scale)), int(front_tf.height / (face_scale / scale))), Image.NEAREST)
return final_img
else:
raise ValueError('Invalid mode: "{}". Use "2d" or "3d".'.format(mode))
def RenderBackBody(self, scale=8, mode="2d"):
"""
Render the back torso - 2D or 3D mode.
Args:
scale (int, optional): Scale factor. Defaults to 8.
mode (str, optional): "2d" or "3d". Defaults to "2d".
Returns:
PIL.Image: Scaled rendered back torso image.
"""
mode = mode.lower()
if mode == "2d":
torso_back = self.skin.crop((32, 20, 40, 32))
return torso_back.resize((8 * scale, 12 * scale), Image.NEAREST)
elif mode == "3d":
back = self.skin.crop((32, 20, 40, 32))
back_overlay = self.skin.crop((44, 20, 52, 32)) # overlay for torso back
back = Image.alpha_composite(back, back_overlay)
face_scale = scale * 2
back = back.resize((8 * face_scale, 12 * face_scale), Image.NEAREST)
quad = (
0, 0,
8 * face_scale, 0,
8 * face_scale * 0.8, 12 * face_scale,
8 * face_scale * 0.2, 12 * face_scale,
)
back_tf = self._transform_perspective(back, quad)
back_tf = self._apply_shade(back_tf, 0.8)
final_img = back_tf.resize((int(back_tf.width / (face_scale / scale)), int(back_tf.height / (face_scale / scale))), Image.NEAREST)
return final_img
else:
raise ValueError('Invalid mode: "{}". Use "2d" or "3d".'.format(mode))
def RenderLeftBody(self, scale=8, mode="2d"):
"""
Render the left torso - 2D or 3D mode.
Args:
scale (int, optional): Scale factor. Defaults to 8.
mode (str, optional): "2d" or "3d". Defaults to "2d".
Returns:
PIL.Image: Scaled rendered left torso image.
"""
mode = mode.lower()
if mode == "2d":
torso_left = self.skin.crop((16, 20, 20, 32))
return torso_left.resize((4 * scale, 12 * scale), Image.NEAREST)
elif mode == "3d":
left = self.skin.crop((16, 20, 20, 32))
left_overlay = self.skin.crop((48, 20, 52, 32)) # overlay for left
left = Image.alpha_composite(left, left_overlay)
face_scale = scale * 2
left = left.resize((4 * face_scale, 12 * face_scale), Image.NEAREST)
# skew for side panel
quad = (
0, 0,
4 * face_scale * 0.8, face_scale * 0.25,
4 * face_scale * 0.8, 12 * face_scale * 0.75,
0, 12 * face_scale,
)
left_tf = self._transform_perspective(left, quad)
left_tf = self._apply_shade(left_tf, 0.6)
final_img = left_tf.resize((int(left_tf.width / (face_scale / scale)), int(left_tf.height / (face_scale / scale))), Image.NEAREST)
return final_img
else:
raise ValueError('Invalid mode: "{}". Use "2d" or "3d".'.format(mode))
def RenderRightBody(self, scale=8, mode="2d"):
"""
Render the right torso - 2D or 3D mode.
Args:
scale (int, optional): Scale factor. Defaults to 8.
mode (str, optional): "2d" or "3d". Defaults to "2d".
Returns:
PIL.Image: Scaled rendered right torso image.
"""
mode = mode.lower()
if mode == "2d":
torso_right = self.skin.crop((28, 20, 32, 32))
return torso_right.resize((4 * scale, 12 * scale), Image.NEAREST)
elif mode == "3d":
right = self.skin.crop((28, 20, 32, 32))
right_overlay = self.skin.crop((52, 20, 56, 32)) # overlay for right
right = Image.alpha_composite(right, right_overlay)
face_scale = scale * 2
right = right.resize((4 * face_scale, 12 * face_scale), Image.NEAREST)
quad = (
4 * face_scale * 0.2, face_scale * 0.25,
4 * face_scale, 0,
4 * face_scale, 12 * face_scale,
4 * face_scale * 0.2, 12 * face_scale * 0.75,
)
right_tf = self._transform_perspective(right, quad)
right_tf = self._apply_shade(right_tf, 0.6)
final_img = right_tf.resize((int(right_tf.width / (face_scale / scale)), int(right_tf.height / (face_scale / scale))), Image.NEAREST)
return final_img
else:
raise ValueError('Invalid mode: "{}". Use "2d" or "3d".'.format(mode))
def RenderBody(self, scale=8, mode="2d"):
"""
Render torso composite - mode "2d" horizontal simple or "3d" as arranged cube.
Args:
scale (int, optional): Scale factor. Defaults to 8.
mode (str, optional): "2d" or "3d". Defaults to "2d".
Returns:
PIL.Image: Composite torso image.
"""
mode = mode.lower()
if mode == "2d":
left = self.RenderLeftBody(scale=scale, mode="2d")
front = self.RenderFrontBody(scale=scale, mode="2d")
right = self.RenderRightBody(scale=scale, mode="2d")
back = self.RenderBackBody(scale=scale, mode="2d")
width = left.width + front.width + right.width + back.width
height = max(left.height, front.height, right.height, back.height)
composite = Image.new("RGBA", (width, height), (0, 0, 0, 0))
x_offset = 0
for part in [left, front, right, back]:
composite.paste(part, (x_offset, 0))
x_offset += part.width
return composite
elif mode == "3d":
# Render a simplified torso cube with front, left, right, and top shading top is not in previous methods so let's fake it
front = self.RenderFrontBody(scale=scale, mode="3d")
back = self.RenderBackBody(scale=scale, mode="3d")
left = self.RenderLeftBody(scale=scale, mode="3d")
right = self.RenderRightBody(scale=scale, mode="3d")
# Compose simplified 3d torso cube showing front, right and left sides
# Arrange left, front and right vertically with a small top shade on front
# Define final canvas size - width as max width of parts + small margin
width = max(left.width, front.width, right.width)
height = left.height + front.height + right.height + scale * 4 # extra for spacing
canvas = Image.new("RGBA", (width, height), (0, 0, 0, 0))
# Paste with vertical stacking and some spacing
y = 0
canvas.alpha_composite(left, ( (width - left.width)//2, y))
y += left.height + scale * 2
canvas.alpha_composite(front, ((width - front.width)//2, y))
y += front.height + scale * 2
canvas.alpha_composite(right, ((width - right.width)//2, y))
return canvas
else:
raise ValueError('Invalid mode: "{}". Use "2d" or "3d".'.format(mode))
if __name__ == "__main__":
import sys
if len(sys.argv) != 2:
print("Usage: python minecraft_skin_renderer.py path_to_skin.png")
sys.exit(1)
skin_path = sys.argv[1]
try:
renderer = MinecraftSkinRenderer(skin_path)
except Exception as e:
print("Error loading skin:", e)
sys.exit(1)
# Render parts in both 2d and 3d and save results
try:
face_img = renderer.RenderFace()
face_img.save("face.png")
print("Rendered face saved to face.png")
head_2d = renderer.RenderHead(mode="2d")
head_2d.save("head_2d.png")
print("Rendered head (2D) saved to head_2d.png")
head_3d = renderer.RenderHead(mode="3d")
head_3d.save("head_3d.png")
print("Rendered head (3D) saved to head_3d.png")
front_body_2d = renderer.RenderFrontBody(mode="2d")
front_body_2d.save("front_body_2d.png")
print("Rendered front torso (2D) saved to front_body_2d.png")
front_body_3d = renderer.RenderFrontBody(mode="3d")
front_body_3d.save("front_body_3d.png")
print("Rendered front torso (3D) saved to front_body_3d.png")
back_body_2d = renderer.RenderBackBody(mode="2d")
back_body_2d.save("back_body_2d.png")
print("Rendered back torso (2D) saved to back_body_2d.png")
back_body_3d = renderer.RenderBackBody(mode="3d")
back_body_3d.save("back_body_3d.png")
print("Rendered back torso (3D) saved to back_body_3d.png")
left_body_2d = renderer.RenderLeftBody(mode="2d")
left_body_2d.save("left_body_2d.png")
print("Rendered left torso (2D) saved to left_body_2d.png")
left_body_3d = renderer.RenderLeftBody(mode="3d")
left_body_3d.save("left_body_3d.png")
print("Rendered left torso (3D) saved to left_body_3d.png")
right_body_2d = renderer.RenderRightBody(mode="2d")
right_body_2d.save("right_body_2d.png")
print("Rendered right torso (2D) saved to right_body_2d.png")
right_body_3d = renderer.RenderRightBody(mode="3d")
right_body_3d.save("right_body_3d.png")
print("Rendered right torso (3D) saved to right_body_3d.png")
body_composite_2d = renderer.RenderBody(mode="2d")
body_composite_2d.save("body_composite_2d.png")
print("Rendered composite torso (2D) saved to body_composite_2d.png")
body_composite_3d = renderer.RenderBody(mode="3d")
body_composite_3d.save("body_composite_3d.png")
print("Rendered composite torso (3D) saved to body_composite_3d.png")
except Exception as e:
print("Error during rendering or saving images:", e)
sys.exit(1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment