Created
June 13, 2025 02:41
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| 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