├── setup.cfg ├── pyproject.toml ├── minepi ├── errors.py ├── __init__.py ├── player.py ├── skin.py ├── utils.py └── skin_render.py ├── .readthedocs.yml ├── docs ├── example_usage.rst ├── index.rst ├── installation.rst ├── Makefile ├── make.bat ├── player.rst └── conf.py ├── examples ├── player.py └── multiple_players.py ├── README.rst ├── .github └── workflows │ └── python-publish.yml ├── LICENSE.txt ├── setup.py └── .gitignore /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=61.0.0", 4 | "wheel" 5 | ] 6 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /minepi/errors.py: -------------------------------------------------------------------------------- 1 | class InvalidPlayer(Exception): 2 | """The given player does not seem to exist""" 3 | 4 | class NoRenderedSkin(Exception): 5 | """There is no rendered skin in cache""" -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | image: latest 5 | 6 | python: 7 | version: 3.8 8 | install: 9 | - method: pip 10 | path: . 11 | extra_requirements: 12 | - docs 13 | -------------------------------------------------------------------------------- /docs/example_usage.rst: -------------------------------------------------------------------------------- 1 | ************* 2 | Example Usage 3 | ************* 4 | 5 | ~~~~~ 6 | Basic 7 | ~~~~~ 8 | 9 | .. literalinclude:: ../examples/player.py 10 | :language: python 11 | 12 | ~~~~~~~~ 13 | Advanced 14 | ~~~~~~~~ 15 | 16 | .. literalinclude:: ../examples/multiple_players.py 17 | :language: python -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to MinePI's documentation! 2 | ================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Documentation: 7 | 8 | installation 9 | example_usage 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | :caption: API Reference: 14 | 15 | player 16 | 17 | -------------------------------------------------------------------------------- /minepi/__init__.py: -------------------------------------------------------------------------------- 1 | from .player import Player 2 | from .skin import Skin 3 | 4 | from .utils import ( 5 | uuid_to_dashed, 6 | uuid_to_undashed, 7 | name_to_uuid, 8 | uuid_to_name, 9 | fetch_skin, 10 | fetch_optifine_cape, 11 | fetch_labymod_cape, 12 | fetch_5zig_cape, 13 | fetch_minecraftcapes_cape, 14 | get_players_by_name, 15 | ) 16 | -------------------------------------------------------------------------------- /examples/player.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from minepi import Player 3 | 4 | async def main(): 5 | p = Player(uuid="1cb4b37f623d439d9528d17e3a452f0a") # create a Player object by UUID 6 | await p.initialize() # initialize the Player object 7 | 8 | await p.skin.render_skin(hr=180, vr=0) 9 | await p.skin.render_head() 10 | p.skin.skin.show() 11 | p.skin.head.show() 12 | 13 | asyncio.run(main()) -------------------------------------------------------------------------------- /examples/multiple_players.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import aiohttp 3 | import minepi 4 | 5 | async def main(): 6 | session = aiohttp.ClientSession() # creating our own ClientSession since we can reuse it 7 | usernames = [ 8 | "sucr_kolli", 9 | "Herobrine", 10 | "Technoblade" 11 | ] 12 | 13 | players = await minepi.get_players_by_name(usernames, session=session) 14 | await asyncio.gather(*[p.initialize() for p in players]) # initializes all Player objects 15 | 16 | print(players) 17 | 18 | await session.close() 19 | 20 | asyncio.run(main()) -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ************ 2 | Installation 3 | ************ 4 | 5 | ~~~~~~ 6 | Stable 7 | ~~~~~~ 8 | 9 | ^^^^^^^^^ 10 | From PYPI 11 | ^^^^^^^^^ 12 | 13 | .. prompt:: bash 14 | 15 | pip install MinePI -U 16 | 17 | ^^^^^^^^^^^ 18 | From Github 19 | ^^^^^^^^^^^ 20 | 21 | .. prompt:: bash 22 | 23 | pip install git+https://github.com/benno1237/MinePI.git#egg=MinePI 24 | 25 | ~~~~~~~ 26 | Develop 27 | ~~~~~~~ 28 | 29 | ^^^^^^^^^^^ 30 | From Github 31 | ^^^^^^^^^^^ 32 | 33 | .. prompt:: bash 34 | 35 | pip install git+https://github.com/benno1237/MinePI.git@develop#egg=MinePI 36 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | MinePI 2 | ====== 3 | 4 | .. image:: https://readthedocs.org/projects/minepi/badge/?version=latest 5 | 6 | MinePI is an all in one Minecraft skin render library written in Python. 7 | 8 | Docs 9 | ---- 10 | 11 | https://minepi.readthedocs.io/en/latest/ 12 | 13 | Contribute 14 | ---------- 15 | 16 | - Issue Tracker: github.com/benno1237/MinePI/issues 17 | - Source Code: github.com/benno1237/MinePI 18 | 19 | Support 20 | ------- 21 | 22 | If you are having issues, please let us know. 23 | Either open an Issue or contact me (Benno#8969) on Discord 24 | 25 | License 26 | ------- 27 | 28 | The project is licensed under the MIT license. 29 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | python setup.py sdist 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Copyright 2021 minepi developers 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | setup( 4 | name="MinePI", 5 | packages=["minepi"], 6 | version="0.5.1", 7 | license="MIT", 8 | description="Minecraft utility library.", 9 | author="benno1237, honigkuchen", 10 | author_email="benno.kollreider@gmail.com", 11 | url="https://github.com/benno1237/MinePI", 12 | download_url="https://github.com/benno1237/MinePI/archive/refs/tags/0.5.1.tar.gz", 13 | keywords=["Minecraft", "Skin", "Render", "Head", "UUID"], 14 | install_requires=[ 15 | "aiohttp", 16 | "Pillow", 17 | "numpy", 18 | ], 19 | extras_require={ 20 | "docs": [ 21 | "sphinx", 22 | "sphinx-rtd-theme", 23 | "sphinx-prompt", 24 | "autodocsumm" 25 | ] 26 | }, 27 | classifiers=[ 28 | 'Development Status :: 3 - Alpha', 29 | 'Intended Audience :: Developers', 30 | 'Topic :: Software Development :: Build Tools', 31 | 'License :: OSI Approved :: MIT License', 32 | 'Programming Language :: Python :: 3', 33 | 'Programming Language :: Python :: 3.7', 34 | 'Programming Language :: Python :: 3.8', 35 | 'Programming Language :: Python :: 3.9', 36 | 'Programming Language :: Python :: 3.10', 37 | 'Programming Language :: Python :: 3.11', 38 | ], 39 | ) 40 | -------------------------------------------------------------------------------- /docs/player.rst: -------------------------------------------------------------------------------- 1 | ****** 2 | Player 3 | ****** 4 | 5 | Class representing a Minecraft player. 6 | This class is used to fetch and store player data from the Mojang API. 7 | 8 | .. note:: This class needs an active network connection. 9 | 10 | ~~~~~~~~~~~ 11 | Basic Usage 12 | ~~~~~~~~~~~ 13 | .. code-block:: python 14 | 15 | from minepi import Player 16 | 17 | p = Player(name="sucr_kolli") # get player by name 18 | await p.initialize() # fetch all data 19 | 20 | ~~~~~~~~~~~~~ 21 | API Reference 22 | ~~~~~~~~~~~~~ 23 | .. autoclass:: minepi.Player 24 | :autosummary: 25 | :autosummary-nosignatures: 26 | :members: 27 | 28 | **** 29 | Skin 30 | **** 31 | 32 | Class representing a Minecraft skin 33 | 34 | .. note:: Everything in this class works completely offline, even 35 | the render itself. 36 | 37 | ~~~~~~~~~~~ 38 | Basic Usage 39 | ~~~~~~~~~~~ 40 | .. code-block:: python 41 | 42 | from minepi import Skin 43 | from PIL import Image 44 | 45 | raw_skin = Image.open(path_to_skin) 46 | raw_cape = Image.open(path_to_cape) # Optional 47 | s = Skin(raw_skin=raw_skin, raw_cape=raw_cape) 48 | 49 | await s.render_skin() 50 | s.skin.show() 51 | 52 | ~~~~~~~~~~~~~ 53 | API Reference 54 | ~~~~~~~~~~~~~ 55 | .. autoclass:: minepi.Skin 56 | :autosummary: 57 | :autosummary-nosignatures: 58 | :members: 59 | 60 | ***** 61 | Utils 62 | ***** 63 | 64 | ~~~~~~~~~~~~~ 65 | API Reference 66 | ~~~~~~~~~~~~~ 67 | .. automodule:: minepi.utils 68 | :autosummary: 69 | :autosummary-nosignatures: 70 | :members: -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | 14 | # import os 15 | # import sys 16 | # sys.path.insert(0, os.path.abspath('..')) 17 | 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = 'MinePI' 22 | copyright = '2022, benno1237' 23 | author = 'benno1237' 24 | 25 | # The full version, including alpha/beta/rc tags 26 | release = '0.5.0' 27 | 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | "sphinx.ext.autodoc", 36 | "sphinx.ext.napoleon", 37 | "sphinx.ext.extlinks", 38 | "sphinx.ext.intersphinx", 39 | "sphinx-prompt", 40 | "autodocsumm", 41 | ] 42 | 43 | autodoc_member_order = "bysource" 44 | 45 | # Add any paths that contain templates here, relative to this directory. 46 | templates_path = ['_templates'] 47 | 48 | # List of patterns, relative to source directory, that match files and 49 | # directories to ignore when looking for source files. 50 | # This pattern also affects html_static_path and html_extra_path. 51 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 52 | 53 | 54 | # -- Options for HTML output ------------------------------------------------- 55 | 56 | # The theme to use for HTML and HTML Help pages. See the documentation for 57 | # a list of builtin themes. 58 | # 59 | html_theme = 'sphinx_rtd_theme' 60 | 61 | # Add any paths that contain custom static files (such as style sheets) here, 62 | # relative to this directory. They are copied after the builtin static files, 63 | # so a file named "default.css" will overwrite the builtin "default.css". 64 | # html_static_path = ['_static'] 65 | 66 | add_module_names = False 67 | 68 | # -- Options for extensions ----------------------------------------------- 69 | 70 | # Intersphinx 71 | intersphinx_mapping = { 72 | "python": ("https://docs.python.org/3", None), 73 | "pillow": ("https://pillow.readthedocs.io/en/stable/", None), 74 | "aiohttp": ("https://aiohttp.readthedocs.io/en/stable/", None), 75 | } 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.dll 3 | *.pot 4 | .data 5 | !/tests/cogs/dataconverter/data/**/*.json 6 | Pipfile 7 | Pipfile.lock 8 | .directory 9 | 10 | ### JetBrains template 11 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 12 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 13 | 14 | # User-specific stuff: 15 | .idea/ 16 | *.iws 17 | .vscode/ 18 | *.sublime-project 19 | *.sublime-workspace 20 | 21 | ## Plugin-specific files: 22 | 23 | # IntelliJ 24 | out/ 25 | 26 | # mpeltonen/sbt-idea plugin 27 | .idea_modules/ 28 | 29 | # JIRA plugin 30 | atlassian-ide-plugin.xml 31 | 32 | # Cursive Clojure plugin 33 | .idea/replstate.xml 34 | 35 | # Crashlytics plugin (for Android Studio and IntelliJ) 36 | com_crashlytics_export_strings.xml 37 | crashlytics.properties 38 | crashlytics-build.properties 39 | fabric.properties 40 | ### Python template 41 | # Byte-compiled / optimized / DLL files 42 | __pycache__/ 43 | *.py[cod] 44 | *$py.class 45 | 46 | # C extensions 47 | *.so 48 | 49 | # Distribution / packaging 50 | .Python 51 | build/ 52 | develop-eggs/ 53 | dist/ 54 | downloads/ 55 | eggs/ 56 | .eggs/ 57 | lib/ 58 | lib64/ 59 | parts/ 60 | sdist/ 61 | var/ 62 | wheels/ 63 | pip-wheel-metadata/ 64 | *.egg-info/ 65 | .installed.cfg 66 | *.egg 67 | 68 | # PyInstaller 69 | # Usually these files are written by a python script from a template 70 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 71 | *.manifest 72 | *.spec 73 | 74 | # Installer logs 75 | pip-log.txt 76 | pip-delete-this-directory.txt 77 | 78 | # Unit test / coverage reports 79 | htmlcov/ 80 | .tox/ 81 | .coverage 82 | .coverage.* 83 | .cache 84 | nosetests.xml 85 | coverage.xml 86 | *.cover 87 | .hypothesis/ 88 | 89 | # Translations 90 | *.mo 91 | 92 | # Django stuff: 93 | *.log 94 | local_settings.py 95 | 96 | # Flask stuff: 97 | instance/ 98 | .webassets-cache 99 | 100 | # Scrapy stuff: 101 | .scrapy 102 | 103 | # Sphinx documentation 104 | docs/_build/ 105 | 106 | # PyBuilder 107 | target/ 108 | 109 | # Jupyter Notebook 110 | .ipynb_checkpoints 111 | 112 | # pyenv 113 | .python-version 114 | 115 | # celery beat schedule file 116 | celerybeat-schedule 117 | 118 | # SageMath parsed files 119 | *.sage.py 120 | 121 | # Environments 122 | .env 123 | .venv 124 | env/ 125 | venv/ 126 | ENV/ 127 | 128 | # Spyder project settings 129 | .spyderproject 130 | .spyproject 131 | 132 | # Rope project settings 133 | .ropeproject 134 | 135 | # mkdocs documentation 136 | /site 137 | 138 | # mypy 139 | .mypy_cache/ 140 | 141 | # pytest 142 | .pytest_cache/ 143 | 144 | # Pre-commit hooks 145 | /.pre-commit-config.yaml 146 | 147 | ### macOS template 148 | # General 149 | .DS_Store 150 | .AppleDouble 151 | .LSOverride 152 | 153 | # Icon must end with two \r 154 | Icon 155 | 156 | # Thumbnails 157 | ._* 158 | 159 | # Files that might appear in the root of a volume 160 | .DocumentRevisions-V100 161 | .fseventsd 162 | .Spotlight-V100 163 | .TemporaryItems 164 | .Trashes 165 | .VolumeIcon.icns 166 | .com.apple.timemachine.donotpresent 167 | 168 | # Directories potentially created on remote AFP share 169 | .AppleDB 170 | .AppleDesktop 171 | Network Trash Folder 172 | Temporary Items 173 | .apdisk 174 | 175 | ### Windows template 176 | # Windows thumbnail cache files 177 | Thumbs.db 178 | Thumbs.db:encryptable 179 | ehthumbs.db 180 | ehthumbs_vista.db 181 | 182 | # Dump file 183 | *.stackdump 184 | 185 | # Folder config file 186 | [Dd]esktop.ini 187 | 188 | # Recycle Bin used on file shares 189 | $RECYCLE.BIN/ 190 | 191 | # Windows Installer files 192 | *.cab 193 | *.msi 194 | *.msix 195 | *.msm 196 | *.msp 197 | 198 | # Windows shortcuts 199 | *.lnk 200 | 201 | ### SublimeText template 202 | # Cache files for Sublime Text 203 | *.tmlanguage.cache 204 | *.tmPreferences.cache 205 | *.stTheme.cache 206 | 207 | # Workspace files are user-specific 208 | 209 | # SFTP configuration file 210 | sftp-config.json 211 | sftp-config-alt*.json 212 | 213 | # Package control specific files 214 | Package Control.last-run 215 | Package Control.ca-list 216 | Package Control.ca-bundle 217 | Package Control.system-ca-bundle 218 | Package Control.cache/ 219 | Package Control.ca-certs/ 220 | Package Control.merged-ca-bundle 221 | Package Control.user-ca-bundle 222 | oscrypto-ca-bundle.crt 223 | bh_unicode_properties.cache 224 | 225 | # Sublime-github package stores a github token in this file 226 | # https://packagecontrol.io/packages/sublime-github 227 | GitHub.sublime-settings -------------------------------------------------------------------------------- /minepi/player.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import base64 3 | import json 4 | from io import BytesIO 5 | from typing import Optional 6 | 7 | import aiohttp 8 | from PIL import Image 9 | 10 | from .skin import Skin 11 | from .utils import ( 12 | fetch_optifine_cape, 13 | fetch_labymod_cape, 14 | fetch_5zig_cape, 15 | fetch_minecraftcapes_cape, 16 | fetch_tlauncher_cape, 17 | name_to_uuid, 18 | uuid_to_undashed, 19 | ) 20 | from .errors import InvalidPlayer 21 | 22 | 23 | class Player: 24 | """Class representing a minecraft player 25 | This has to be created before a skin can be rendered 26 | 27 | Parameters 28 | ---------- 29 | uuid: str 30 | UUID of the player (Not needed if name is given 31 | name: str 32 | Username of the player (Not needed if UUID is given) 33 | raw_skin: PIL.Image.Image 34 | Raw skin image of the player (64x64px) 35 | raw_cape: PIL.Image.Image 36 | Raw cape image of the player (64x32px) 37 | session: aiohttp.ClientSession 38 | ClientSession to use for requests 39 | """ 40 | def __init__( 41 | self, 42 | uuid: str = None, 43 | name: str = None, 44 | raw_skin: Image.Image = None, 45 | raw_cape: Image.Image = None, 46 | session: aiohttp.ClientSession = None 47 | ): 48 | if uuid is None and name is None: 49 | raise ValueError("Pass a username or UUID") 50 | 51 | self._uuid: Optional[str] = uuid 52 | self._username: Optional[str] = name 53 | 54 | self._skin: Optional[Skin] = None 55 | self._raw_skin: Optional[Image.Image] = raw_skin 56 | self._raw_capes: dict = { 57 | "default": raw_cape, 58 | "mojang": None, 59 | "optifine": None, 60 | "labymod": None, 61 | "5zig": None, 62 | "minecraftcapes": None, 63 | "tlauncher": None, 64 | } 65 | 66 | self._session: Optional[aiohttp.ClientSession] = session 67 | self._close_session: bool = False 68 | 69 | self._ready: asyncio.Event = asyncio.Event() 70 | if self._uuid: 71 | self._uuid = uuid_to_undashed(self._uuid) 72 | 73 | if len(self._uuid) != 32: 74 | raise ValueError("UUID seems to be invalid.") 75 | 76 | def __repr__(self): 77 | return f"" 78 | 79 | @property 80 | def uuid(self): 81 | """The players UUID\n 82 | Either passed when creating this class or fetched from the MojangAPI""" 83 | return self._uuid 84 | 85 | @property 86 | def name(self): 87 | """The players name""" 88 | return self._username 89 | 90 | @property 91 | def skin(self): 92 | """The players :class:`Skin`""" 93 | return self._skin 94 | 95 | @property 96 | def capes(self): 97 | """A dict representing this player's capes""" 98 | return self._raw_capes 99 | 100 | @property 101 | def mojang_cape(self): 102 | """The player's mojang cape""" 103 | return self._raw_capes["mojang"] 104 | 105 | @property 106 | def optifine_cape(self): 107 | """The player's optifine cape""" 108 | return self._raw_capes["optifine"] 109 | 110 | @property 111 | def labymod_cape(self): 112 | """The player's labymod cape""" 113 | return self._raw_capes["labymod"] 114 | 115 | @property 116 | def minecraftcapes_cape(self): 117 | """The player's MinecraftCapes cape""" 118 | return self._raw_capes["minecraftcapes"] 119 | 120 | @property 121 | def zig_cape(self): 122 | """The player's 5Zig cape""" 123 | return self._raw_capes["5zig"] 124 | 125 | @property 126 | def tlauncher_cape(self): 127 | """The player's TLauncher cape""" 128 | return self._raw_capes["tlauncher"] 129 | 130 | def set_skin(self, skin: "Skin"): 131 | """Manually overwrite/set this players skin 132 | 133 | Parameters 134 | ---------- 135 | skin: Skin 136 | The new skin 137 | """ 138 | self._skin = skin 139 | 140 | async def initialize(self): 141 | """Initializes the player class 142 | 143 | This function is an initializer which helps to get various details about the player with just one method. 144 | Once this function has finished running, the corresponding :py:class:`Player` object is 145 | guaranteed to have a name, UUID and skin (includes the cape if available) associated to it 146 | (if the given username and/or UUID is valid of course). 147 | 148 | Warning 149 | ------- 150 | This function does one to four API calls to the mojang API: 151 | -> (1.) Obtain the players UUID by name (Only if no UUID is given)\n 152 | -> 2. Get the players profile\n 153 | -> (3.) Get the players skin (Only if no skin is given)\n 154 | -> (4.) Get the players cape (Only if the player actually has a cape)\n 155 | Rate limits of the API are unknown but expected to be somewhere close to 6000 requests per 10 minutes. 156 | 157 | Raises 158 | ------ 159 | errors.InvalidPlayer 160 | Player does not seem to be valid 161 | """ 162 | if not self._session: 163 | self._session = aiohttp.ClientSession() 164 | self._close_session = True 165 | 166 | if self._uuid is None: 167 | uuid = await name_to_uuid(self._username, self._session) 168 | if uuid: 169 | self._uuid = uuid_to_undashed(uuid) 170 | 171 | textures = None 172 | if self._uuid is not None and (self._raw_skin is None or self._raw_capes["default"] is None): 173 | async with self._session.get( 174 | f"https://sessionserver.mojang.com/session/minecraft/profile/{self._uuid}" 175 | ) as resp: 176 | if resp.status == 200: 177 | resp_dict = await resp.json() 178 | 179 | self._username = resp_dict["name"] if self._username is None else self._username 180 | 181 | for p in resp_dict["properties"]: 182 | if p["name"] == "textures": 183 | textures = json.loads(base64.b64decode(p["value"]))["textures"] 184 | break 185 | 186 | else: 187 | raise InvalidPlayer() 188 | 189 | if textures is not None: 190 | _raw_skin_url = textures["SKIN"]["url"] 191 | _raw_cape_url = textures["CAPE"]["url"] if "CAPE" in textures.keys() else None 192 | 193 | if not self._raw_skin: 194 | async with self._session.get(_raw_skin_url) as resp: 195 | self._raw_skin = Image.open(BytesIO(await resp.read())) 196 | 197 | if not self._raw_capes["default"]: 198 | if _raw_cape_url is not None: 199 | async with self._session.get(_raw_cape_url) as resp: 200 | self._raw_capes["default"] = Image.open(BytesIO(await resp.read())) 201 | self._raw_capes["mojang"] = self._raw_capes["default"] 202 | else: 203 | self._raw_capes["default"] = None 204 | 205 | self._skin = Skin( 206 | raw_skin=self._raw_skin, 207 | raw_skin_url=_raw_skin_url, 208 | raw_cape=self._raw_capes["default"], 209 | raw_cape_url=_raw_cape_url 210 | ) 211 | 212 | if self._close_session: 213 | await self._session.close() 214 | 215 | self._ready.set() 216 | 217 | async def wait_for_fully_constructed(self): 218 | """Returns as soon as the initialize function finished 219 | 220 | Waiting for this guarantees that the skin has been fetched from the api""" 221 | try: 222 | await asyncio.wait_for(self._ready.wait(), timeout=60) 223 | except asyncio.TimeoutError: 224 | raise asyncio.TimeoutError 225 | 226 | async def fetch_optifine_cape(self): 227 | """Fetches the players optifine cape and stores it to :py:attr:`Player.optifine_cape` 228 | 229 | This is basically just an alias for :py:func:`utils.fetch_optifine_cape`""" 230 | cape = await fetch_optifine_cape(self) 231 | if cape is not None: 232 | self._raw_capes["optifine"] = cape 233 | 234 | async def fetch_labymod_cape(self): 235 | """Fetches the players labymod cape and stores it to :py:attr:`Player.labymod_cape` 236 | 237 | This is basically just an alias for :py:func:`utils.fetch_labymod_cape`""" 238 | cape = await fetch_labymod_cape(self) 239 | if cape is not None: 240 | self._raw_capes["labymod"] = cape 241 | 242 | async def fetch_minecraftcapes_cape(self): 243 | """Fetches the players MinecraftCapes cape and stores it to :py:attr:`Player.minecraftcapes_cape` 244 | 245 | This is basically just an alias for :py:func:`utils.fetch_minecraftcapes_cape`""" 246 | cape = await fetch_minecraftcapes_cape(self) 247 | if cape is not None: 248 | self._raw_capes["minecraftcapes"] = cape 249 | 250 | async def fetch_5zig_cape(self): 251 | """Fetches the players 5Zig cape and stores it to :py:attr:`Player.zig_cape` 252 | 253 | This is basically just an alias for :py:func:`utils.fetch_5zig_cape`""" 254 | cape = await fetch_5zig_cape(self) 255 | if cape is not None: 256 | self._raw_capes["5zig"] = cape 257 | 258 | async def fetch_tlauncher_cape(self): 259 | """Fetches the players TLauncher cape and stores it to :py:attr:`Player.tlauncher_cape` 260 | 261 | This is basically just an alias for :py:func:`utils.fetch_tlauncher_cape`""" 262 | cape = await fetch_tlauncher_cape() 263 | if cape is not None: 264 | self._raw_capes["tlauncher"] = cape 265 | -------------------------------------------------------------------------------- /minepi/skin.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | from typing import Optional 4 | from PIL import Image, ImageOps 5 | from io import BytesIO 6 | 7 | from .skin_render import Render 8 | from .errors import NoRenderedSkin 9 | 10 | class Skin: 11 | """ 12 | Tip 13 | ---- 14 | It's best practice to save the players skin to a database using :py:func:`encodeb64` and then 15 | reinitialize this class using :py:func:`decodeb64`. This way no API call is needed.\n 16 | Alternatively you can manually instantiate by passing a raw skin image. 17 | 18 | Parameters 19 | ---------- 20 | raw_skin: PIL.Image.Image 21 | The raw skin image 22 | raw_cape: PIL.Image.Image 23 | The raw cape image 24 | """ 25 | 26 | def __init__( 27 | self, 28 | raw_skin, 29 | raw_cape=None, 30 | raw_skin_url=None, 31 | raw_cape_url=None, 32 | name=None, 33 | ): 34 | self._raw_skin: Image.Image = raw_skin 35 | self._raw_skin_url: Optional[str] = raw_skin_url 36 | self._raw_cape: Optional[Image.Image] = None 37 | self._raw_cape_url: Optional[str] = raw_cape_url 38 | self._name: Optional[str] = name 39 | 40 | self._skin: Optional[Image.Image] = None 41 | self._head: Optional[Image.Image] = None 42 | 43 | if raw_cape is not None: 44 | self.set_cape(raw_cape) 45 | 46 | if self._raw_skin.mode != "RGBA": # Converting skins to RGBA 47 | self._raw_skin = self._raw_skin.convert(mode="RGBA") 48 | 49 | if self._raw_skin.height == 32: # old skin format 50 | new_skin_im = Image.new("RGBA", (64, 64), (0, 0, 0, 0)) 51 | new_skin_im.paste(self._raw_skin, (0, 0)) 52 | 53 | for i in range(2): 54 | f1 = 40 if i != 0 else 0 55 | f2 = 16 if i != 0 else 0 56 | 57 | new_skin_im.paste(ImageOps.mirror(self._raw_skin.crop((4+f1, 16, 8+f1, 20))), (20+f2, 48)) 58 | new_skin_im.paste(ImageOps.mirror(self._raw_skin.crop((8+f1, 16, 12+f1, 20))), (24+f2, 48)) 59 | new_skin_im.paste(ImageOps.mirror(self._raw_skin.crop((8+f1, 20, 12+f1, 32))), (16+f2, 52)) 60 | new_skin_im.paste(ImageOps.mirror(self._raw_skin.crop((12+f1, 20, 16+f1, 32))), (20+f2, 52)) 61 | new_skin_im.paste(ImageOps.mirror(self._raw_skin.crop((4+f1, 20, 8+f1, 32))), (24+f2, 52)) 62 | new_skin_im.paste(ImageOps.mirror(self._raw_skin.crop((0+f1, 20, 4+f1, 32))), (28+f2, 52)) 63 | 64 | self._raw_skin = new_skin_im 65 | 66 | def __repr__(self): 67 | return f"" 68 | 69 | @property 70 | def skin(self): 71 | """The last skin which has been rendered using :py:func:`render_skin`""" 72 | return self._skin 73 | 74 | @property 75 | def head(self): 76 | """The last head which has been rendered using :py:func:`render_head`""" 77 | return self._head 78 | 79 | @property 80 | def raw_skin(self): 81 | """The players raw skin image""" 82 | return self._raw_skin 83 | 84 | @property 85 | def raw_skin_url(self): 86 | """The players skins url. Returns None if the skin has been manually passed""" 87 | return self._raw_skin_url 88 | 89 | @property 90 | def raw_cape(self): 91 | """The players raw cape image""" 92 | return self._raw_cape 93 | 94 | @property 95 | def raw_cape_url(self): 96 | """The players capes url. Returns None if the player doesn't have a cape or the cape has been manually passed""" 97 | return self._raw_cape_url 98 | 99 | @property 100 | def has_cape(self): 101 | """Whether the player has a cape""" 102 | return bool(self._raw_cape) 103 | 104 | @property 105 | def is_slim(self): 106 | """Whether the skin is slim (Alex type) or classic (Steve type) 107 | 108 | Only difference being the width of the arms (3px - 4px)""" 109 | return not bool(self._raw_skin.getpixel((46, 52))[3]) 110 | 111 | def set_cape(self, cape: Image.Image): 112 | """Change the players cape 113 | 114 | Parameters 115 | ---------- 116 | cape: PIL.Image.Image 117 | The new cape image (64x32px) 118 | """ 119 | # attempt to detect wrongly scaled mojang capes 120 | if cape.width / cape.height == 2 and (cape.width != 64 or cape.height != 32): 121 | cape.resize((64, 32), resample=Image.LANCZOS) 122 | 123 | if cape.width == 22 and cape.height == 17: # Labymod 124 | pass 125 | 126 | if cape.width == 64 and cape.height > 32: # MinecraftCapes animated 127 | pass 128 | 129 | if cape.mode != "RGBA": # Converting capes to RGBA 130 | cape = cape.convert(mode="RGBA") 131 | 132 | self._raw_cape = cape 133 | 134 | def show(self): 135 | """Shows the last rendered skin 136 | 137 | Alias for :py:func:`Skin.skin.show()` 138 | 139 | Raises 140 | ------ 141 | NoRenderedSkin 142 | No skin present. Generate a render using :py:func:`render_skin` 143 | """ 144 | if self._skin: 145 | self._skin.show() 146 | else: 147 | raise NoRenderedSkin() 148 | 149 | def encodeb64(self): 150 | """Base64 encodes the players skin and cape 151 | 152 | This allows for better caching and persistent storage in a database 153 | A :class:`Skin` class can then be recreated using :py:func:`decodeb64` 154 | 155 | Returns 156 | ------- 157 | str 158 | The players skin and cape in format {raw_skin}-{raw_cape}""" 159 | with BytesIO() as buffered: 160 | self._raw_skin.save(buffered, format="PNG") 161 | buffered.seek(0) 162 | im_skin = buffered.getvalue() 163 | if self._raw_cape: 164 | with BytesIO() as buffered: 165 | self._raw_cape.save(buffered, format="PNG") 166 | buffered.seek(0) 167 | im_cape = buffered.getvalue() 168 | else: 169 | im_cape = None 170 | 171 | bytelist = [base64.b64encode(im_skin), base64.b64encode(im_cape)] if im_cape else [base64.b64encode(im_skin)] 172 | return b';'.join(bytelist).decode() 173 | 174 | @classmethod 175 | def decodeb64(cls, b64: str): 176 | """Create an instance of this class from a saved b64 string 177 | 178 | Parameters 179 | ---------- 180 | b64: str 181 | The base64 encoded raw_skin image or raw_skin and raw_cape separated with a ";". 182 | Can be obtained from :py:func:`encodeb64` 183 | 184 | Returns 185 | ------- 186 | :class:`Skin` 187 | """ 188 | skin, cape = b64.split(";") 189 | skin.encode() 190 | if cape: 191 | cape.encode() 192 | 193 | im_str = base64.b64decode(skin) 194 | buffered = BytesIO(im_str) 195 | im_skin = Image.open(buffered) 196 | 197 | if cape: 198 | im_str = base64.b64decode(cape) 199 | buffered = BytesIO(im_str) 200 | im_cape = Image.open(buffered) 201 | else: 202 | im_cape = None 203 | 204 | return cls(raw_skin=im_skin, raw_cape=im_cape) 205 | 206 | async def render_skin( 207 | self, 208 | vr: int = 25, 209 | hr: int = 35, 210 | hrh: int = 0, 211 | vrll: int = 0, 212 | vrrl: int = 0, 213 | vrla: int = 0, 214 | hrla: int = 0, 215 | vrra: int = 0, 216 | hrra: int = 0, 217 | vrc: int = 30, 218 | ratio: int = 12, 219 | display_hair: bool = True, 220 | display_second_layer: bool = True, 221 | display_cape: bool = True, 222 | aa: bool = False, 223 | ) -> Optional[Image.Image]: 224 | """Render a full body skin 225 | 226 | Parameters 227 | ---------- 228 | vr: int 229 | Vertical rotation of the output image 230 | hr: int 231 | Horizontal rotation of the output image 232 | hrh: int 233 | Horizontal head rotation 234 | vrll: int 235 | Vertical rotation of the left leg 236 | vrrl: int 237 | Vertical rotation of the right leg 238 | vrla: int 239 | Vertical rotation of the left arm 240 | hrla: int 241 | Horizontal rotation of the left arm 242 | vrra: int 243 | Vertical rotation of the right arm 244 | hrra: int 245 | Horizontal rotation of the right arm 246 | vrc: int 247 | Vertical rotation of the cape 248 | Not actually in degrees, use random values please until you find one you like 249 | ratio: int 250 | Resolution of the returned image 251 | display_hair: bool 252 | Whether the second head layer is displayed 253 | display_second_layer: bool 254 | Whether the second skin layer is displayed 255 | display_cape: bool 256 | Whether the player's cape is shown 257 | aa: bool 258 | Antialiasing: smoothens the corners a bit 259 | 260 | Returns 261 | ------- 262 | PIL.Image.Image 263 | The rendered skin 264 | """ 265 | render = Render( 266 | player=self, 267 | vr=vr, 268 | hr=hr, 269 | hrh=hrh, 270 | vrll=vrll, 271 | vrrl=vrrl, 272 | vrla=vrla, 273 | hrla=hrla, 274 | vrra=vrra, 275 | hrra=hrra, 276 | vrc=vrc, 277 | ratio=ratio, 278 | head_only=False, 279 | display_hair=display_hair, 280 | display_layers=display_second_layer, 281 | display_cape=display_cape, 282 | aa=aa, 283 | ) 284 | im = await render.get_render() 285 | self._skin = im 286 | return im 287 | 288 | async def render_head( 289 | self, 290 | vr: int = 25, 291 | hr: int = 35, 292 | ratio: int = 12, 293 | display_hair: bool = True, 294 | aa: bool = False, 295 | ) -> Optional[Image.Image]: 296 | """Render the players head 297 | 298 | Parameters 299 | ---------- 300 | vr: int 301 | Vertical rotation of the output image 302 | hr: int 303 | Horizontal rotation of the output image 304 | ratio: int 305 | Resolution of the returned image 306 | display_hair: bool 307 | Whether or not the second head layer should be displayed 308 | aa: bool 309 | Antialiasing: smoothens the corners a bit 310 | 311 | Returns 312 | ------- 313 | PIL.Image.Image 314 | The rendered head 315 | """ 316 | render = Render( 317 | player=self, 318 | vr=vr, 319 | hr=hr, 320 | ratio=ratio, 321 | head_only=True, 322 | display_hair=display_hair, 323 | aa=aa, 324 | ) 325 | im = await render.get_render() 326 | self._head = im 327 | return im 328 | -------------------------------------------------------------------------------- /minepi/utils.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import typing 3 | import aiohttp 4 | import json 5 | 6 | from PIL import Image 7 | from io import BytesIO 8 | from typing import Optional 9 | 10 | from .skin import Skin 11 | 12 | if typing.TYPE_CHECKING: 13 | from .player import Player 14 | 15 | 16 | __all__ = [ 17 | "uuid_to_dashed", 18 | "uuid_to_undashed", 19 | "name_to_uuid", 20 | "uuid_to_name", 21 | "fetch_skin", 22 | "fetch_optifine_cape", 23 | "fetch_labymod_cape", 24 | "fetch_5zig_cape", 25 | "fetch_minecraftcapes_cape", 26 | "fetch_tlauncher_cape", 27 | "get_players_by_name", 28 | ] 29 | 30 | 31 | def uuid_to_dashed(uuid: str) -> str: 32 | """Converts a not dashed UUID to a dashed one 33 | 34 | Parameters 35 | ---------- 36 | uuid: str 37 | The UUID to convert 38 | 39 | Returns 40 | ------- 41 | str 42 | The converted UUID 43 | """ 44 | return f"{uuid[:8]}-{uuid[8:12]}-{uuid[12:16]}-{uuid[16:20]}-{uuid[20:]}" 45 | 46 | 47 | def uuid_to_undashed(uuid: str) -> str: 48 | """Converts a dashed UUID to a not dashed one 49 | 50 | Parameters 51 | ---------- 52 | uuid: str 53 | The UUID to convert 54 | 55 | Returns 56 | ------- 57 | str 58 | The converted UUID 59 | """ 60 | return uuid.replace("-", "") 61 | 62 | 63 | async def name_to_uuid(name: str, session: aiohttp.ClientSession = None) -> Optional[str]: 64 | """Convert a minecraft name to a UUID 65 | 66 | Parameters 67 | ---------- 68 | name: str 69 | The minecraft name to get the UUID for 70 | session: aiohttp.ClientSession 71 | The ClientSession to use for requests 72 | Defaults to a new session which is closed again after handling all requests 73 | 74 | Returns 75 | ------- 76 | Optional[str] 77 | None if the given name is invalid 78 | """ 79 | if session is None: 80 | session = aiohttp.ClientSession() 81 | close = True 82 | else: 83 | close = False 84 | 85 | async with session.get(f"https://api.mojang.com/users/profiles/minecraft/{name}") as resp: 86 | if resp.status == 200: 87 | uuid = (await resp.json())["id"] 88 | else: 89 | uuid = None 90 | 91 | if close: 92 | await session.close() 93 | 94 | return uuid 95 | 96 | 97 | async def uuid_to_name(uuid: str, session: aiohttp.ClientSession = None) -> Optional[str]: 98 | """Convert a UUID to a minecraft name 99 | 100 | Parameters 101 | ---------- 102 | uuid: str 103 | The UUID to get the minecraft name for 104 | session: aiohttp.ClientSession 105 | The ClientSession to use for requests 106 | Defaults to a new session which is closed again after handling all requests 107 | 108 | Returns 109 | ------- 110 | Optional[str] 111 | None if the given UUID is invalid 112 | """ 113 | if session is None: 114 | session = aiohttp.ClientSession() 115 | close = True 116 | else: 117 | close = False 118 | 119 | async with session.get( 120 | f"https://sessionserver.mojang.com/session/minecraft/profile/{uuid}" 121 | ) as resp: 122 | if resp.status == 200: 123 | name = (await resp.json())["name"] 124 | else: 125 | name = None 126 | 127 | if close: 128 | await session.close() 129 | 130 | return name 131 | 132 | 133 | async def fetch_skin( 134 | player: "Player" = None, 135 | name: str = None, 136 | uuid: str = None, 137 | session: aiohttp.ClientSession = None 138 | ) -> Optional[Skin]: 139 | """Fetch a players skin 140 | 141 | Note 142 | ---- 143 | This function also returns the players mojang cape (if available). 144 | 145 | Tip 146 | ---- 147 | Passing a :py:class:`minepi.Player` is recommended. If none is available, a name or UUID can be passed. 148 | UUIDs are preferred over names in this case since they require one API call less. 149 | 150 | Parameters 151 | ---------- 152 | player: Player 153 | The player to fetch the skin for 154 | name: str 155 | Minecraft username to fetch the skin for 156 | uuid: str 157 | UUID to fetch the skin for 158 | session: aiohttp.ClientSession 159 | The ClientSession to use for requests 160 | Defaults to a new session which is closed again after handling all requests 161 | 162 | Returns 163 | ------- 164 | Skin 165 | A fully functional :py:class:`minepi.Skin` class 166 | 167 | Raises 168 | ------ 169 | ValueError 170 | No :py:class:`minepi.Player`, name or UUID has been passed 171 | """ 172 | if player is None and name is None and uuid is None: 173 | raise ValueError("At least one parameter must be passed") 174 | 175 | if session is None: 176 | session = aiohttp.ClientSession() 177 | close = True 178 | else: 179 | close = False 180 | 181 | if player and player.uuid is not None: 182 | uuid = player.uuid 183 | 184 | if uuid is None and name is not None: 185 | uuid = await name_to_uuid(name, session) 186 | 187 | if uuid is not None: 188 | async with session.get( 189 | f"https://sessionserver.mojang.com/session/minecraft/profile/{uuid}" 190 | ) as resp: 191 | if resp.status == 200: 192 | cape = None 193 | skin = None 194 | resp_dict = await resp.json() 195 | for p in resp_dict["properties"]: 196 | if p["name"] == "textures": 197 | textures = json.loads(base64.b64decode(p["value"]))["textures"] 198 | skin_url = textures["SKIN"]["url"] 199 | cape_url = textures["CAPE"]["url"] if "CAPE" in textures.keys() else None 200 | 201 | if skin_url: 202 | async with session.get(skin_url) as resp_skin: 203 | if resp.status == 200: 204 | skin = Image.open(BytesIO(await resp_skin.read())) 205 | 206 | if cape_url: 207 | async with session.get(cape_url) as resp_cape: 208 | if resp.status == 200: 209 | cape = Image.open(BytesIO(await resp_cape.read())) 210 | break 211 | 212 | if close is True: 213 | await session.close() 214 | 215 | if skin is None: 216 | raise ValueError 217 | 218 | return Skin( 219 | raw_skin=skin, 220 | raw_cape=cape, 221 | raw_skin_url=skin_url, 222 | raw_cape_url=cape_url, 223 | name=resp_dict["name"] 224 | ) 225 | 226 | 227 | async def fetch_mojang_cape( 228 | player: "Player" = None, 229 | name: str = None, 230 | uuid: str = None, 231 | session: aiohttp.ClientSession = None 232 | ) -> Optional[Image.Image]: 233 | """Fetch a players mojang cape 234 | 235 | Tip 236 | ---- 237 | Passing a :py:class:`minepi.Player` object is recommended. If none is available, a name or UUID can be passed. 238 | UUIDs are preferred over names in this case since they require one API call less. 239 | 240 | Parameters 241 | ---------- 242 | player: Player 243 | The player to fetch the mojang cape for 244 | name: str 245 | Minecraft username to fetch the mojang cape for 246 | uuid: str 247 | UUID to fetch the mojang cape for 248 | session: aiohttp.ClientSession 249 | The ClientSession to use for requests 250 | Defaults to a new session which is closed again after handling all requests 251 | 252 | Returns 253 | ------- 254 | Optional[PIL.Image.Image] 255 | None if the given player has no mojang cape 256 | 257 | Raises 258 | ------ 259 | ValueError 260 | No :py:class:`minepi.Player`, name or UUID has been passed 261 | """ 262 | s = await fetch_skin(player=player, name=name, uuid=uuid, session=session) 263 | return s.raw_cape if s is not None else None 264 | 265 | 266 | async def fetch_optifine_cape( 267 | player: "Player" = None, 268 | name: str = None, 269 | uuid: str = None, 270 | session: aiohttp.ClientSession = None 271 | ) -> Optional[Image.Image]: 272 | """Fetch a players optifine cape 273 | 274 | Tip 275 | ---- 276 | Passing a :py:class:`minepi.Player` object is recommended. If none is available, a name or UUID can be passed. 277 | Names are preferred over UUIDs in this case since they require one API call less. 278 | 279 | Parameters 280 | ---------- 281 | player: Player 282 | The player to fetch the optifine cape for 283 | name: str 284 | Minecraft username to fetch the optifine cape for 285 | uuid: str 286 | UUID to fetch the optifine cape for 287 | session: aiohttp.ClientSession 288 | The ClientSession to use for requests 289 | Defaults to a new session which is closed again after handling all requests 290 | 291 | Returns 292 | ------- 293 | Optional[PIL.Image.Image] 294 | None if the given player has no optifine cape 295 | 296 | Raises 297 | ------ 298 | ValueError 299 | No :py:class:`minepi.Player`, name or UUID has been passed 300 | """ 301 | if player is None and name is None and uuid is None: 302 | raise ValueError("At least one parameter must be passed") 303 | 304 | if session is None: 305 | session = aiohttp.ClientSession() 306 | close = True 307 | else: 308 | close = False 309 | 310 | if player is not None: 311 | name = player.name 312 | elif name is not None: 313 | pass 314 | else: 315 | name = await uuid_to_name(uuid, session) 316 | 317 | if name is not None: 318 | async with session.get(f"http://s.optifine.net/capes/{name}.png") as resp: 319 | if resp.status == 200: 320 | cape = Image.open(BytesIO(await resp.read())) 321 | else: 322 | cape = None 323 | else: 324 | cape = None 325 | 326 | if close: 327 | await session.close() 328 | 329 | return cape 330 | 331 | 332 | async def fetch_labymod_cape( 333 | player: "Player" = None, 334 | name: str = None, 335 | uuid: str = None, 336 | session: aiohttp.ClientSession = None 337 | ) -> Optional[Image.Image]: 338 | """Fetch a players labymod cape 339 | 340 | Tip 341 | ---- 342 | Passing a :py:class:`minepi.Player` object is recommended. If none is available, a name or UUID can be passed. 343 | UUIDs are preferred over names in this case since they require one API call less. 344 | 345 | Parameters 346 | ---------- 347 | player: Player 348 | The player to fetch the labymod cape for 349 | name: str 350 | Minecraft username to fetch the labymod cape for 351 | uuid: str 352 | UUID to fetch the labymod cape for 353 | session: aiohttp.ClientSession 354 | The ClientSession to use for requests 355 | Defaults to a new session which is closed again after handling all requests 356 | 357 | Returns 358 | ------- 359 | Optional[PIL.Image.Image] 360 | None if the given player has no labymod cape 361 | 362 | Raises 363 | ------ 364 | ValueError 365 | No :py:class:`minepi.Player`, name or UUID has been passed 366 | """ 367 | if player is None and name is None and uuid is None: 368 | raise ValueError("At least one parameter must be passed") 369 | 370 | if session is None: 371 | session = aiohttp.ClientSession() 372 | close = True 373 | else: 374 | close = False 375 | 376 | if player is not None: 377 | uuid = player.uuid 378 | elif uuid is not None: 379 | pass 380 | else: 381 | uuid = await name_to_uuid(name, session) 382 | 383 | if uuid is not None: 384 | if len(uuid) == 32: 385 | uuid = uuid_to_dashed(uuid) 386 | 387 | async with session.get(f"https://dl.labymod.net/capes/{uuid}") as resp: 388 | if resp.status == 200: 389 | cape = Image.open(BytesIO(await resp.read())) 390 | else: 391 | cape = None 392 | else: 393 | cape = None 394 | 395 | if close: 396 | await session.close() 397 | 398 | return cape 399 | 400 | 401 | async def fetch_5zig_cape( 402 | player: "Player" = None, 403 | name: str = None, 404 | uuid: str = None, 405 | session: aiohttp.ClientSession = None 406 | ) -> Optional[Image.Image]: 407 | """Fetch a players 5Zig cape 408 | 409 | Tip 410 | ---- 411 | Passing a :class:`minepi.Player` object is recommended. If none is available, a name or UUID can be passed. 412 | UUIDs are preferred over names in this case since they require one API call less. 413 | 414 | Warning 415 | ------- 416 | 5zig capes are not guaranteed to work yet. 417 | If you own an account with 5zigreborn cape, it would be very helpful if you could send us the 418 | username. 419 | 420 | Parameters 421 | ---------- 422 | player: Player 423 | The player to fetch the 5Zig cape for 424 | name: str 425 | Minecraft username to fetch the 5Zig cape for 426 | uuid: str 427 | UUID to fetch the 5Zig cape for 428 | session: aiohttp.ClientSession 429 | The ClientSession to use for requests 430 | Defaults to a new session which is closed again after handling all requests 431 | 432 | Returns 433 | ------- 434 | Optional[PIL.Image.Image] 435 | None if the given player has no 5Zig cape 436 | 437 | Raises 438 | ------ 439 | ValueError 440 | No :py:class:`minepi.Player`, name or UUID has been passed 441 | """ 442 | if player is None and name is None and uuid is None: 443 | raise ValueError("At least one parameter must be passed") 444 | 445 | if session is None: 446 | session = aiohttp.ClientSession() 447 | close = True 448 | else: 449 | close = False 450 | 451 | if player is not None: 452 | uuid = player.uuid 453 | elif uuid is not None: 454 | pass 455 | else: 456 | uuid = await name_to_uuid(name, session) 457 | 458 | if uuid is not None: 459 | if len(uuid) == 32: 460 | uuid = uuid_to_dashed(uuid) 461 | 462 | async with session.get(f"https://textures.5zigreborn.eu/profile/{uuid}") as resp: 463 | if resp.status == 200: 464 | cape = Image.open(BytesIO(await resp.read())) 465 | else: 466 | cape = None 467 | else: 468 | cape = None 469 | 470 | if close: 471 | await session.close() 472 | 473 | return cape 474 | 475 | 476 | async def fetch_minecraftcapes_cape( 477 | player: "Player" = None, 478 | name: str = None, 479 | uuid: str = None, 480 | session: aiohttp.ClientSession = None 481 | ) -> Optional[Image.Image]: 482 | """Fetch a players MinecraftCapes cape 483 | 484 | Tip 485 | ---- 486 | Passing a :py:class:`minepi.Player` object is recommended. If none is available, a name or UUID can be passed. 487 | UUIDs are preferred over names in this case since they require one API call less. 488 | 489 | Warning 490 | ------- 491 | Animated MinecraftCapes capes are not supported yet 492 | 493 | Parameters 494 | ---------- 495 | player: Player 496 | The player to fetch the MinecraftCapes cape for 497 | name: str 498 | Minecraft username to fetch the MinecraftCapes cape for 499 | uuid: str 500 | UUID to fetch the MinecraftCapes cape for 501 | session: aiohttp.ClientSession 502 | The ClientSession to use for requests 503 | Defaults to a new session which is closed again after handling all requests 504 | 505 | Returns 506 | ------- 507 | Optional[PIL.Image.Image] 508 | None if the given player has no MinecraftCapes cape 509 | 510 | Raises 511 | ------ 512 | ValueError 513 | No :py:class:`minepi.Player`, name or UUID has been passed 514 | """ 515 | if player is None and name is None and uuid is None: 516 | raise ValueError("At least one parameter must be passed") 517 | 518 | if not session: 519 | session = aiohttp.ClientSession() 520 | close = True 521 | else: 522 | close = False 523 | 524 | if player is not None: 525 | uuid = player.uuid 526 | elif uuid is not None: 527 | pass 528 | else: 529 | uuid = await name_to_uuid(name, session) 530 | 531 | if uuid is not None: 532 | if len(uuid) == 36: 533 | uuid = uuid_to_undashed(uuid) 534 | 535 | async with session.get(f"https://minecraftcapes.net/profile/{uuid}/cape") as resp: 536 | if resp.status == 200: 537 | cape = Image.open(BytesIO(await resp.read())) 538 | else: 539 | cape = None 540 | else: 541 | cape = None 542 | 543 | if close: 544 | await session.close() 545 | 546 | return cape 547 | 548 | 549 | async def fetch_tlauncher_cape( 550 | player: "Player" = None, 551 | name: str = None, 552 | uuid: str = None, 553 | session: aiohttp.ClientSession = None 554 | ) -> Optional[Image.Image]: 555 | """Fetch a players TLauncher cape 556 | 557 | Tip 558 | ---- 559 | Passing a :py:class:`minepi.Player` object is recommended. If none is available, a name or UUID can be passed. 560 | Names are preferred over UUIDs in this case since they require one API call less. 561 | 562 | 563 | Warning 564 | ------- 565 | TLauncher capes are still experimental and not guaranteed to work. 566 | If you want to help implement them further, please get in touch with me. 567 | If you could share a few account names with TLauncher capes (especially animated ones), 568 | that would help us a lot. 569 | 570 | Parameters 571 | ---------- 572 | player: Player 573 | The player to fetch the TLauncher cape for 574 | name: str 575 | Minecraft username to fetch the TLauncher cape for 576 | uuid: str 577 | UUID to fetch the TLauncher cape for 578 | session: aiohttp.ClientSession 579 | The ClientSession to use for requests 580 | Defaults to a new session which is closed again after handling all requests 581 | 582 | Returns 583 | ------- 584 | Optional[PIL.Image.Image] 585 | None if the given player has no TLauncher cape 586 | 587 | Raises 588 | ------ 589 | ValueError 590 | No :py:class:`minepi.Player`, name or UUID has been passed 591 | """ 592 | if player is None and name is None and uuid is None: 593 | raise ValueError() 594 | 595 | if session is None: 596 | session = aiohttp.ClientSession() 597 | close = True 598 | else: 599 | close = False 600 | 601 | if player is not None: 602 | name = player.name 603 | elif name is not None: 604 | pass 605 | else: 606 | name = await uuid_to_name(uuid, session) 607 | 608 | cape = None 609 | if name is not None: 610 | async with session.get(f"https://auth.tlauncher.org/skin/profile/texture/login/{name}") as resp: 611 | if resp.status == 200: 612 | resp_dict = await resp.json() 613 | cape_url = resp_dict["CAPE"]["url"] if "CAPE" in resp_dict.keys() else None 614 | else: 615 | cape_url = None 616 | 617 | if cape_url is not None: 618 | if not cape_url.startswith("http://textures.minecraft.net/"): 619 | async with session.get(cape_url) as resp: 620 | if resp.status == 200: 621 | cape = Image.open(BytesIO(await resp.read())) 622 | 623 | if close: 624 | await session.close() 625 | 626 | return cape 627 | 628 | 629 | async def get_players_by_name(names: list, session: aiohttp.ClientSession = None): 630 | """Useful helper function to get multiple :py:class:`minepi.Player` objects 631 | 632 | Only does one API call for the entire list instead of one per player 633 | This is recommended to be used if you have a list of usernames 634 | 635 | Parameters 636 | ---------- 637 | names: list 638 | A list of minecraft usernames 639 | session: aiohttp.ClientSession 640 | The ClientSession to use for requests 641 | Defaults to a new session which is closed again after handling all requests 642 | 643 | Returns 644 | ------- 645 | list 646 | A list of :py:class:`minepi.Player` objects 647 | """ 648 | if session is None: 649 | session = aiohttp.ClientSession() 650 | close = True 651 | else: 652 | close = False 653 | 654 | players = [] 655 | async with session.post("https://api.mojang.com/profiles/minecraft", json=names) as resp: 656 | if resp.status == 200: 657 | for entry in await resp.json(): 658 | players.append(Player(uuid=entry["id"], name=entry["name"], session=session if not close else None)) 659 | 660 | if close: 661 | await session.close() 662 | 663 | return players 664 | -------------------------------------------------------------------------------- /minepi/skin_render.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import numpy as np 3 | 4 | from math import radians, sin, cos 5 | from PIL import Image, ImageDraw 6 | from typing import Optional, List, TYPE_CHECKING 7 | 8 | if TYPE_CHECKING: 9 | from . import Skin 10 | 11 | 12 | def is_not_existing(dic, key1=None, key2=None, key3=None): 13 | try: 14 | if key1 is None: 15 | dic 16 | if key2 is None: 17 | dic[key1] 18 | elif key3 is None: 19 | dic[key1][key2] 20 | else: 21 | dic[key1][key2][key3] 22 | return False 23 | except KeyError: 24 | return True 25 | 26 | 27 | def append_dict(dic, key1, key2, key3, value): 28 | if is_not_existing(dic, key1, key2, key3): 29 | try: 30 | dic[key1][key2][key3] = value 31 | except KeyError: 32 | try: 33 | dic[key1][key2] = {} 34 | dic[key1][key2][key3] = value 35 | except KeyError: 36 | dic[key1] = {} 37 | dic[key1][key2] = {} 38 | dic[key1][key2][key3] = value 39 | return dic 40 | 41 | 42 | class Render: 43 | def __init__( 44 | self, 45 | player: "Skin", 46 | vr: int = 0, 47 | hr: int = 0, 48 | hrh: int = 0, 49 | vrll: int = 0, 50 | vrrl: int = 0, 51 | vrla: int = 0, 52 | hrla: int = 0, 53 | vrra: int = 0, 54 | hrra: int = 0, 55 | vrc: int = 30, 56 | ratio: int = 12, 57 | head_only: bool = False, 58 | display_hair: bool = False, 59 | display_layers: bool = False, 60 | display_cape: bool = False, 61 | aa: bool = False, 62 | ): 63 | self.vr = vr 64 | self.hr = hr 65 | self.hrh = hrh 66 | self.vrll = vrll 67 | self.vrrl = vrrl 68 | self.vrla = vrla 69 | self.hrla = hrla 70 | self.vrra = vrra 71 | self.hrra = hrra 72 | self.vrc = vrc 73 | self.head_only = head_only 74 | self.ratio = ratio 75 | self.display_hair = display_hair 76 | self.display_cape = display_cape if player.raw_cape else False 77 | self.layers = display_layers 78 | self.player = player 79 | self.aa = aa 80 | self.rendered_image = None 81 | 82 | self.loop = asyncio.get_event_loop() 83 | self.cube_points = self.set_cube_points() 84 | self.polygons = {} 85 | self.body_angles = {} 86 | self.visible_faces = {} 87 | self.front_faces = {} 88 | self.back_faces = {} 89 | 90 | self.min_x = 0 91 | self.max_x = 0 92 | self.min_y = 0 93 | self.max_y = 0 94 | 95 | @staticmethod 96 | def rotation_x(angle): 97 | return np.array([ 98 | [1, 0, 0], 99 | [0, cos(angle), sin(angle)], 100 | [0, -sin(angle), cos(angle)], 101 | ]) 102 | 103 | @staticmethod 104 | def rotation_y(angle): 105 | return np.array([ 106 | [cos(angle), 0, sin(angle)], 107 | [0, 1, 0], 108 | [-sin(angle), 0, cos(angle)], 109 | ]) 110 | 111 | @staticmethod 112 | def rotation_z(angle): 113 | return np.array([ 114 | [cos(angle), -sin(angle), 0], 115 | [sin(angle), cos(angle), 0], 116 | [0, 0, 1], 117 | ]) 118 | 119 | @classmethod 120 | def calculate_rotation_matrix(cls, ry, rx): 121 | return np.dot(cls.rotation_y(ry), cls.rotation_x(rx)) 122 | 123 | async def get_render(self): 124 | skin = self.player.raw_skin 125 | hd_ratio = int(skin.size[0] / 64) 126 | 127 | def render_skin(skin): 128 | self.calculate_angles() 129 | self.determine_faces() 130 | self.generate_polygons(hd_ratio, skin, self.player.raw_cape) 131 | self.member_rotation(hd_ratio) 132 | 133 | im = self.display_image() 134 | return im 135 | 136 | im = await self.loop.run_in_executor( 137 | None, 138 | render_skin, 139 | skin 140 | ) 141 | 142 | return im 143 | 144 | def calculate_angles(self): 145 | alpha = radians(self.vr) 146 | beta = -radians(self.hr) 147 | 148 | # general rotation matrix (around x and z axis) 149 | r = np.dot(self.rotation_y(beta), self.rotation_x(alpha)) 150 | self.body_angles["general"] = r 151 | 152 | # ToDo: think of a better way to do this 153 | # currently: apply part specific rotation -> apply offset -> apply general rotation 154 | self.body_angles["torso"] = np.dot(self.rotation_y(0), self.rotation_x(0)) 155 | self.body_angles["torso_layer"] = self.body_angles["torso"] 156 | 157 | # cape has an additional x-axis rotation 158 | alpha_cape = (-radians(self.vrc)) 159 | self.body_angles["cape"] = self.rotation_x(alpha_cape) 160 | 161 | # head has an additional z-axis rotation 162 | beta_head = radians(self.hrh) 163 | self.body_angles["head"] = self.rotation_y(beta_head) 164 | self.body_angles["helmet"] = self.body_angles["head"] 165 | 166 | # arms have an additional x and z-axis rotation 167 | alpha_r_arm = radians(self.vrra) 168 | beta_r_arm = radians(self.hrra) 169 | self.body_angles["r_arm"] = np.dot(self.rotation_x(alpha_r_arm), self.rotation_z(beta_r_arm)) 170 | self.body_angles["r_arm_layer"] = self.body_angles["r_arm"] 171 | 172 | alpha_l_arm = radians(self.vrla) 173 | beta_l_arm = radians(self.hrla) 174 | self.body_angles["l_arm"] = np.dot(self.rotation_x(alpha_l_arm), self.rotation_z(beta_l_arm)) 175 | self.body_angles["l_arm_layer"] = self.body_angles["l_arm"] 176 | 177 | # legs have an additional x-axis rotation 178 | alpha_r_leg = radians(self.vrrl) 179 | self.body_angles["r_leg"] = self.rotation_x(alpha_r_leg) 180 | self.body_angles["r_leg_layer"] = self.body_angles["r_leg"] 181 | 182 | alpha_l_leg = radians(self.vrll) 183 | self.body_angles["l_leg"] = self.rotation_x(alpha_l_leg) 184 | self.body_angles["l_leg_layer"] = self.body_angles["l_leg"] 185 | 186 | def determine_faces(self): 187 | self.visible_faces = { 188 | "head": {"front": [], "back": {}}, 189 | "torso": {"front": [], "back": {}}, 190 | "torso_layer": {"front": [], "back": {}}, 191 | "r_arm": {"front": [], "back": {}}, 192 | "r_arm_layer": {"front": [], "back": {}}, 193 | "l_arm": {"front": [], "back": {}}, 194 | "l_arm_layer": {"front": [], "back": {}}, 195 | "r_leg": {"front": [], "back": {}}, 196 | "r_leg_layer": {"front": [], "back": {}}, 197 | "l_leg": {"front": [], "back": {}}, 198 | "l_leg_layer": {"front": [], "back": {}}, 199 | "cape": {"front": [], "back": {}} 200 | } 201 | 202 | all_faces = ["top", "bottom", "back", "front", "left", "right"] 203 | 204 | for k, v in self.visible_faces.items(): 205 | cube_max_depth_faces = None 206 | 207 | for cube_point in self.cube_points: 208 | cube_point[0].project(np.array([0, 0, 0]), self.body_angles[k]) # torso is always level with the plane 209 | 210 | if (cube_max_depth_faces is None) or (cube_max_depth_faces[0].depth > cube_point[0].depth): 211 | cube_max_depth_faces = cube_point 212 | 213 | v["back"] = cube_max_depth_faces[1] 214 | v["front"] = [face for face in all_faces if face not in v["back"]] 215 | self.front_faces = self.visible_faces["torso"]["front"] 216 | self.back_faces = [face for face in all_faces if face not in self.front_faces] 217 | 218 | def set_cube_points(self): 219 | cube_points = [] 220 | cube_points.append( 221 | ( 222 | Point( 223 | self, 224 | np.array([0, 0, 0]) 225 | ), 226 | ["back", "right", "top"] 227 | ) 228 | ) 229 | 230 | cube_points.append( 231 | ( 232 | Point( 233 | self, 234 | np.array([0, 0, 1]) 235 | ), 236 | ["front", "right", "top"] 237 | ) 238 | ) 239 | 240 | cube_points.append( 241 | ( 242 | Point( 243 | self, 244 | np.array([0, 1, 0]) 245 | ), 246 | ["back", "right", "bottom"] 247 | ) 248 | ) 249 | 250 | cube_points.append( 251 | ( 252 | Point( 253 | self, 254 | np.array([0, 1, 1]) 255 | ), 256 | ["front", "right", "bottom"] 257 | ) 258 | ) 259 | 260 | cube_points.append( 261 | ( 262 | Point( 263 | self, 264 | np.array([1, 0, 0]) 265 | ), 266 | ["back", "left", "top"] 267 | ) 268 | ) 269 | 270 | cube_points.append( 271 | ( 272 | Point( 273 | self, 274 | np.array([1, 0, 1]) 275 | ), 276 | ["front", "left", "top"] 277 | ) 278 | ) 279 | 280 | cube_points.append( 281 | ( 282 | Point( 283 | self, 284 | np.array([1, 1, 0]) 285 | ), 286 | ["back", "left", "bottom"] 287 | ) 288 | ) 289 | 290 | cube_points.append( 291 | ( 292 | Point( 293 | self, 294 | np.array([1, 1, 1]) 295 | ), 296 | ["front", "left", "bottom"] 297 | ) 298 | ) 299 | return cube_points 300 | 301 | def generate_polygons(self, hd_ratio, skin, im_cape): 302 | self.polygons = { 303 | "helmet": {"front": [], "back": [], "top": [], "bottom": [], "right": [], "left": []}, 304 | "head": {"front": [], "back": [], "top": [], "bottom": [], "right": [], "left": []}, 305 | "torso": {"front": [], "back": [], "top": [], "bottom": [], "right": [], "left": []}, 306 | "torso_layer": {"front": [], "back": [], "top": [], "bottom": [], "right": [], "left": []}, 307 | "r_arm": {"front": [], "back": [], "top": [], "bottom": [], "right": [], "left": []}, 308 | "r_arm_layer": {"front": [], "back": [], "top": [], "bottom": [], "right": [], "left": []}, 309 | "l_arm": {"front": [], "back": [], "top": [], "bottom": [], "right": [], "left": []}, 310 | "l_arm_layer": {"front": [], "back": [], "top": [], "bottom": [], "right": [], "left": []}, 311 | "r_leg": {"front": [], "back": [], "top": [], "bottom": [], "right": [], "left": []}, 312 | "r_leg_layer": {"front": [], "back": [], "top": [], "bottom": [], "right": [], "left": []}, 313 | "l_leg": {"front": [], "back": [], "top": [], "bottom": [], "right": [], "left": []}, 314 | "l_leg_layer": {"front": [], "back": [], "top": [], "bottom": [], "right": [], "left": []}, 315 | "cape": {"front": [], "back": [], "top": [], "bottom": [], "right": [], "left": []} 316 | } 317 | 318 | """Head""" 319 | volume_points = {} 320 | for i in range(0, 9 * hd_ratio): 321 | for j in range(0, 9 * hd_ratio): 322 | volume_points = append_dict(volume_points, i, j, -2 * hd_ratio, 323 | Point(self, np.array([i, j, -2 * hd_ratio]))) 324 | volume_points = append_dict(volume_points, i, j, 6 * hd_ratio, 325 | Point(self, np.array([i, j, 6 * hd_ratio]))) 326 | 327 | for j in range(0, 9 * hd_ratio): 328 | for k in range(-2 * hd_ratio, 7 * hd_ratio): 329 | volume_points = append_dict(volume_points, 0, j, k, Point(self, np.array([0, j, k]))) 330 | volume_points = append_dict(volume_points, 8 * hd_ratio, j, k, 331 | Point(self, np.array([8 * hd_ratio, j, k]))) 332 | 333 | for i in range(0, 9 * hd_ratio): 334 | for k in range(-2 * hd_ratio, 7 * hd_ratio): 335 | volume_points = append_dict(volume_points, i, 0, k, Point(self, np.array([i, 0, k]))) 336 | volume_points = append_dict(volume_points, i, 8 * hd_ratio, k, 337 | Point(self, np.array([i, 8 * hd_ratio, k]))) 338 | 339 | if "back" in self.visible_faces["head"]["front"]: 340 | for i in range(0, 8 * hd_ratio): 341 | for j in range(0, 8 * hd_ratio): 342 | color = skin.getpixel((32 * hd_ratio - 1 - i, 8 * hd_ratio + j)) 343 | if color[3] != 0: 344 | self.polygons["head"]["back"].append(Polygon([ 345 | volume_points[i][j][-2 * hd_ratio], 346 | volume_points[i + 1][j][-2 * hd_ratio], 347 | volume_points[i + 1][j + 1][-2 * hd_ratio], 348 | volume_points[i][j + 1][-2 * hd_ratio]], 349 | color)) 350 | 351 | if "front" in self.visible_faces["head"]["front"]: 352 | for i in range(0, 8 * hd_ratio): 353 | for j in range(0, 8 * hd_ratio): 354 | color = skin.getpixel((8 * hd_ratio + i, 8 * hd_ratio + j)) 355 | if color[3] != 0: 356 | self.polygons["head"]["front"].append(Polygon([ 357 | volume_points[i][j][6 * hd_ratio], 358 | volume_points[i + 1][j][6 * hd_ratio], 359 | volume_points[i + 1][j + 1][6 * hd_ratio], 360 | volume_points[i][j + 1][6 * hd_ratio]], 361 | color)) 362 | 363 | if "right" in self.visible_faces["head"]["front"]: 364 | for j in range(0, 8 * hd_ratio): 365 | for k in range(-2 * hd_ratio, 6 * hd_ratio): 366 | color = skin.getpixel((k + 2 * hd_ratio, 8 * hd_ratio + j)) 367 | if color[3] != 0: 368 | self.polygons["head"]["right"].append(Polygon([ 369 | volume_points[0][j][k], 370 | volume_points[0][j][k + 1], 371 | volume_points[0][j + 1][k + 1], 372 | volume_points[0][j + 1][k]], 373 | color)) 374 | 375 | if "left" in self.visible_faces["head"]["front"]: 376 | for j in range(0, 8 * hd_ratio): 377 | for k in range(-2 * hd_ratio, 6 * hd_ratio): 378 | color = skin.getpixel(((24 * hd_ratio - 1) - k - 2 * hd_ratio, 8 * hd_ratio + j)) 379 | if color[3] != 0: 380 | self.polygons["head"]["left"].append(Polygon([ 381 | volume_points[8 * hd_ratio][j][k], 382 | volume_points[8 * hd_ratio][j][k + 1], 383 | volume_points[8 * hd_ratio][j + 1][k + 1], 384 | volume_points[8 * hd_ratio][j + 1][k]], 385 | color)) 386 | 387 | if "top" in self.visible_faces["head"]["front"]: 388 | for i in range(0, 8 * hd_ratio): 389 | for k in range(-2 * hd_ratio, 6 * hd_ratio): 390 | color = skin.getpixel((8 * hd_ratio + i, 2 * hd_ratio + k)) 391 | if color[3] != 0: 392 | self.polygons["head"]["top"].append(Polygon([ 393 | volume_points[i][0][k], 394 | volume_points[i + 1][0][k], 395 | volume_points[i + 1][0][k + 1], 396 | volume_points[i][0][k + 1]], 397 | color)) 398 | 399 | if "bottom" in self.visible_faces["head"]["front"]: 400 | for i in range(0, 8 * hd_ratio): 401 | for k in range(-2 * hd_ratio, 6 * hd_ratio): 402 | color = skin.getpixel((16 * hd_ratio + i, 2 * hd_ratio + k)) 403 | if color[3] != 0: 404 | self.polygons["head"]["bottom"].append(Polygon([ 405 | volume_points[i][8 * hd_ratio][k], 406 | volume_points[i + 1][8 * hd_ratio][k], 407 | volume_points[i + 1][8 * hd_ratio][k + 1], 408 | volume_points[i][8 * hd_ratio][k + 1]], 409 | color)) 410 | 411 | """Helmet / Hair""" 412 | if self.display_hair: 413 | volume_points = {} 414 | for i in range(0, 9 * hd_ratio): 415 | for j in range(0, 9 * hd_ratio): 416 | volume_points = append_dict(volume_points, i, j, -2 * hd_ratio, 417 | Point(self, np.array([i * 8.5 / 8 - 0.25 * hd_ratio, 418 | j * 8.5 / 8 - 0.25 * hd_ratio, 419 | -2.25 * hd_ratio]))) 420 | volume_points = append_dict(volume_points, i, j, 6 * hd_ratio, 421 | Point(self, np.array([i * 8.5 / 8 - 0.25 * hd_ratio, 422 | j * 8.5 / 8 - 0.25 * hd_ratio, 423 | 6.25 * hd_ratio]))) 424 | 425 | for j in range(0, 9 * hd_ratio): 426 | for k in range(-2 * hd_ratio, 7 * hd_ratio): 427 | volume_points = append_dict(volume_points, 0, j, k, 428 | Point(self, np.array([-0.25 * hd_ratio, 429 | j * 8.5 / 8 - 0.25 * hd_ratio, 430 | k * 8.5 / 8 - 0.25 * hd_ratio]))) 431 | volume_points = append_dict(volume_points, 8 * hd_ratio, j, k, 432 | Point(self, np.array([8.25 * hd_ratio, 433 | j * 8.5 / 8 - 0.25 * hd_ratio, 434 | k * 8.5 / 8 - 0.25 * hd_ratio]))) 435 | 436 | for i in range(0, 9 * hd_ratio): 437 | for k in range(-2 * hd_ratio, 7 * hd_ratio): 438 | volume_points = append_dict(volume_points, i, 0, k, 439 | Point(self, np.array([i * 8.5 / 8 - 0.25 * hd_ratio, 440 | -0.25 * hd_ratio, 441 | k * 8.5 / 8 - 0.25 * hd_ratio]))) 442 | volume_points = append_dict(volume_points, i, 8 * hd_ratio, k, 443 | Point(self, np.array([i * 8.5 / 8 - 0.25 * hd_ratio, 444 | 8.25 * hd_ratio, 445 | k * 8.5 / 8 - 0.25 * hd_ratio]))) 446 | 447 | for i in range(0, 8 * hd_ratio): 448 | for j in range(0, 8 * hd_ratio): 449 | color = skin.getpixel((64 * hd_ratio - 1 - i, 8 * hd_ratio + j)) 450 | if color[3] != 0: 451 | self.polygons["helmet"]["back"].append(Polygon([ 452 | volume_points[i][j][-2 * hd_ratio], 453 | volume_points[i + 1][j][-2 * hd_ratio], 454 | volume_points[i + 1][j + 1][-2 * hd_ratio], 455 | volume_points[i][j + 1][-2 * hd_ratio]], 456 | color)) 457 | 458 | for i in range(0, 8 * hd_ratio): 459 | for j in range(0, 8 * hd_ratio): 460 | color = skin.getpixel((40 * hd_ratio + i, 8 * hd_ratio + j)) 461 | if color[3] != 0: 462 | self.polygons["helmet"]["front"].append(Polygon([ 463 | volume_points[i][j][6 * hd_ratio], 464 | volume_points[i + 1][j][6 * hd_ratio], 465 | volume_points[i + 1][j + 1][6 * hd_ratio], 466 | volume_points[i][j + 1][6 * hd_ratio]], 467 | color)) 468 | 469 | for j in range(0, 8 * hd_ratio): 470 | for k in range(-2 * hd_ratio, 6 * hd_ratio): 471 | color = skin.getpixel((34 * hd_ratio + k, 8 * hd_ratio + j)) 472 | if color[3] != 0: 473 | self.polygons["helmet"]["right"].append(Polygon([ 474 | volume_points[0][j][k], 475 | volume_points[0][j][k + 1], 476 | volume_points[0][j + 1][k + 1], 477 | volume_points[0][j + 1][k]], 478 | color)) 479 | 480 | for j in range(0, 8 * hd_ratio): 481 | for k in range(-2 * hd_ratio, 6 * hd_ratio): 482 | color = skin.getpixel((54 * hd_ratio - k - 1, 8 * hd_ratio + j)) 483 | if color[3] != 0: 484 | self.polygons["helmet"]["left"].append(Polygon([ 485 | volume_points[8 * hd_ratio][j][k], 486 | volume_points[8 * hd_ratio][j][k + 1], 487 | volume_points[8 * hd_ratio][j + 1][k + 1], 488 | volume_points[8 * hd_ratio][j + 1][k]], 489 | color)) 490 | 491 | for i in range(0, 8 * hd_ratio): 492 | for k in range(-2 * hd_ratio, 6 * hd_ratio): 493 | color = skin.getpixel((40 * hd_ratio + i, 2 * hd_ratio + k)) 494 | if color[3] != 0: 495 | self.polygons["helmet"]["top"].append(Polygon([ 496 | volume_points[i][0][k], 497 | volume_points[i + 1][0][k], 498 | volume_points[i + 1][0][k + 1], 499 | volume_points[i][0][k + 1]], 500 | color)) 501 | 502 | for i in range(0, 8 * hd_ratio): 503 | for k in range(-2 * hd_ratio, 6 * hd_ratio): 504 | color = skin.getpixel((48 * hd_ratio + 1, 2 * hd_ratio + k)) 505 | if color[3] != 0: 506 | self.polygons["helmet"]["bottom"].append(Polygon([ 507 | volume_points[i][8 * hd_ratio][k], 508 | volume_points[i + 1][8 * hd_ratio][k], 509 | volume_points[i + 1][8 * hd_ratio][k + 1], 510 | volume_points[i][8 * hd_ratio][k + 1]], 511 | color)) 512 | 513 | if not self.head_only: 514 | """Torso""" 515 | volume_points = {} 516 | for i in range(0, 9 * hd_ratio): 517 | for j in range(0, 13 * hd_ratio): 518 | volume_points = append_dict(volume_points, i, j, 0, 519 | Point(self, np.array([i, j + 8 * hd_ratio, 0]))) 520 | volume_points = append_dict(volume_points, i, j, 4 * hd_ratio, 521 | Point(self, np.array([i, j + 8 * hd_ratio, 4 * hd_ratio]))) 522 | 523 | for j in range(0, 13 * hd_ratio): 524 | for k in range(0, 5 * hd_ratio): 525 | volume_points = append_dict(volume_points, 0, j, k, 526 | Point(self, np.array([0, j + 8 * hd_ratio, k]))) 527 | volume_points = append_dict(volume_points, 8 * hd_ratio, j, k, 528 | Point(self, np.array([8 * hd_ratio, j + 8 * hd_ratio, k]))) 529 | 530 | for i in range(0, 9 * hd_ratio): 531 | for k in range(0, 5 * hd_ratio): 532 | volume_points = append_dict(volume_points, i, 0, k, 533 | Point(self, np.array([i, 8 * hd_ratio, k]))) 534 | volume_points = append_dict(volume_points, i, 12 * hd_ratio, k, 535 | Point(self, np.array([i, 20 * hd_ratio, k]))) 536 | 537 | if "back" in self.visible_faces["torso"]["front"]: 538 | for i in range(0, 8 * hd_ratio): 539 | for j in range(0, 12 * hd_ratio): 540 | color = skin.getpixel(((40 * hd_ratio - 1) - i, 20 * hd_ratio + j)) 541 | if color[3] != 0: 542 | self.polygons["torso"]["back"].append(Polygon([ 543 | volume_points[i][j][0], 544 | volume_points[i + 1][j][0], 545 | volume_points[i + 1][j + 1][0], 546 | volume_points[i][j + 1][0]], 547 | color)) 548 | 549 | if "front" in self.visible_faces["torso"]["front"]: 550 | for i in range(0, 8 * hd_ratio): 551 | for j in range(0, 12 * hd_ratio): 552 | color = skin.getpixel((20 * hd_ratio + i, 20 * hd_ratio + j)) 553 | if color[3] != 0: 554 | self.polygons["torso"]["front"].append(Polygon([ 555 | volume_points[i][j][4 * hd_ratio], 556 | volume_points[i + 1][j][4 * hd_ratio], 557 | volume_points[i + 1][j + 1][4 * hd_ratio], 558 | volume_points[i][j + 1][4 * hd_ratio]], 559 | color)) 560 | 561 | if "right" in self.visible_faces["torso"]["front"]: 562 | for j in range(0, 12 * hd_ratio): 563 | for k in range(0 * hd_ratio, 4 * hd_ratio): 564 | color = skin.getpixel((16 * hd_ratio + k, 20 * hd_ratio + j)) 565 | if color[3] != 0: 566 | self.polygons["torso"]["right"].append(Polygon([ 567 | volume_points[0][j][k], 568 | volume_points[0][j][k + 1], 569 | volume_points[0][j + 1][k + 1], 570 | volume_points[0][j + 1][k]], 571 | color)) 572 | 573 | if "left" in self.visible_faces["torso"]["front"]: 574 | for j in range(0, 12 * hd_ratio): 575 | for k in range(0 * hd_ratio, 4 * hd_ratio): 576 | color = skin.getpixel(((32 * hd_ratio - 1) - k, 20 * hd_ratio + j)) 577 | if color[3] != 0: 578 | self.polygons["torso"]["left"].append(Polygon([ 579 | volume_points[8 * hd_ratio][j][k], 580 | volume_points[8 * hd_ratio][j][k + 1], 581 | volume_points[8 * hd_ratio][j + 1][k + 1], 582 | volume_points[8 * hd_ratio][j + 1][k]], 583 | color)) 584 | 585 | if "top" in self.visible_faces["torso"]["front"]: 586 | for i in range(0, 8 * hd_ratio): 587 | for k in range(0 * hd_ratio, 4 * hd_ratio): 588 | color = skin.getpixel((20 * hd_ratio + i, 16 * hd_ratio + k)) 589 | if color[3] != 0: 590 | self.polygons["torso"]["top"].append(Polygon([ 591 | volume_points[i][0][k], 592 | volume_points[i + 1][0][k], 593 | volume_points[i + 1][0][k + 1], 594 | volume_points[i][0][k + 1]], 595 | color)) 596 | 597 | if "bottom" in self.visible_faces["torso"]["front"]: 598 | for i in range(0, 8 * hd_ratio): 599 | for k in range(0 * hd_ratio, 4 * hd_ratio): 600 | color = skin.getpixel((28 * hd_ratio + i, (20 * hd_ratio - 1) - k)) 601 | if color[3] != 0: 602 | self.polygons["torso"]["bottom"].append(Polygon([ 603 | volume_points[i][12 * hd_ratio][k], 604 | volume_points[i + 1][12 * hd_ratio][k], 605 | volume_points[i + 1][12 * hd_ratio][k + 1], 606 | volume_points[i][12 * hd_ratio][k + 1]], 607 | color)) 608 | 609 | """Torso 2nd layer""" 610 | if self.layers: 611 | volume_points = {} 612 | for i in range(0, 9 * hd_ratio): 613 | for j in range(0, 13 * hd_ratio): 614 | volume_points = append_dict(volume_points, i, j, 0, 615 | Point(self, np.array([i * 8.25 / 8 - 0.125 * hd_ratio, 616 | (j * 12.25 / 12 - 0.125 * hd_ratio) + 8 * hd_ratio, 617 | -0.125 * hd_ratio]))) 618 | volume_points = append_dict(volume_points, i, j, 4 * hd_ratio, 619 | Point(self, np.array([i * 8.25 / 8 - 0.125 * hd_ratio, 620 | (j * 12.25 / 12 - 0.125 * hd_ratio) + 8 * hd_ratio, 621 | 4.125 * hd_ratio]))) 622 | for j in range(0, 13 * hd_ratio): 623 | for k in range(0, 5 * hd_ratio): 624 | volume_points = append_dict(volume_points, 0, j, k, 625 | Point(self, np.array([-0.125 * hd_ratio, 626 | (j * 12.25 / 12 - 0.125 * hd_ratio) + 8 * hd_ratio, 627 | k * 4.25 / 4 - 0.125 * hd_ratio]))) 628 | volume_points = append_dict(volume_points, 8 * hd_ratio, j, k, 629 | Point(self, np.array([8.125 * hd_ratio, 630 | (j * 12.25 / 12 - 0.125 * hd_ratio) + 8 * hd_ratio, 631 | k * 4.25 / 4 - 0.125 * hd_ratio]))) 632 | 633 | for i in range(0, 9 * hd_ratio): 634 | for k in range(0, 5 * hd_ratio): 635 | volume_points = append_dict(volume_points, i, 0, k, 636 | Point(self, np.array([i * 8.25 / 8 - 0.125 * hd_ratio, 637 | 7.875 * hd_ratio, 638 | k * 4.25 / 4 - 0.125 * hd_ratio]))) 639 | volume_points = append_dict(volume_points, i, 12 * hd_ratio, k, 640 | Point(self, np.array([i * 8.25 / 8 - 0.125 * hd_ratio, 641 | 12.125 * hd_ratio, 642 | k * 4.25 / 4 - 0.125 * hd_ratio]))) 643 | 644 | if "back" in self.visible_faces["torso_layer"]["front"]: 645 | for i in range(0, 8 * hd_ratio): 646 | for j in range(0, 12 * hd_ratio): 647 | color = skin.getpixel(((40 * hd_ratio - 1) - i, 20 * hd_ratio + j + 16)) 648 | if color[3] != 0: 649 | self.polygons["torso_layer"]["back"].append(Polygon([ 650 | volume_points[i][j][0], 651 | volume_points[i + 1][j][0], 652 | volume_points[i + 1][j + 1][0], 653 | volume_points[i][j + 1][0]], 654 | color)) 655 | 656 | if "front" in self.visible_faces["torso_layer"]["front"]: 657 | for i in range(0, 8 * hd_ratio): 658 | for j in range(0, 12 * hd_ratio): 659 | color = skin.getpixel((20 * hd_ratio + i, 20 * hd_ratio + j + 16)) 660 | if color[3] != 0: 661 | self.polygons["torso_layer"]["front"].append(Polygon([ 662 | volume_points[i][j][4 * hd_ratio], 663 | volume_points[i + 1][j][4 * hd_ratio], 664 | volume_points[i + 1][j + 1][4 * hd_ratio], 665 | volume_points[i][j + 1][4 * hd_ratio]], 666 | color)) 667 | 668 | if "right" in self.visible_faces["torso_layer"]["front"]: 669 | for j in range(0, 12 * hd_ratio): 670 | for k in range(0 * hd_ratio, 4 * hd_ratio): 671 | color = skin.getpixel((16 * hd_ratio + k, 20 * hd_ratio + j + 16)) 672 | if color[3] != 0: 673 | self.polygons["torso_layer"]["right"].append(Polygon([ 674 | volume_points[0][j][k], 675 | volume_points[0][j][k + 1], 676 | volume_points[0][j + 1][k + 1], 677 | volume_points[0][j + 1][k]], 678 | color)) 679 | 680 | if "left" in self.visible_faces["torso_layer"]["front"]: 681 | for j in range(0, 12 * hd_ratio): 682 | for k in range(0 * hd_ratio, 4 * hd_ratio): 683 | color = skin.getpixel(((32 * hd_ratio - 1) - k, 20 * hd_ratio + j + 16)) 684 | if color[3] != 0: 685 | self.polygons["torso_layer"]["left"].append(Polygon([ 686 | volume_points[8 * hd_ratio][j][k], 687 | volume_points[8 * hd_ratio][j][k + 1], 688 | volume_points[8 * hd_ratio][j + 1][k + 1], 689 | volume_points[8 * hd_ratio][j + 1][k]], 690 | color)) 691 | 692 | if "top" in self.visible_faces["torso_layer"]["front"]: 693 | for i in range(0, 8 * hd_ratio): 694 | for k in range(0 * hd_ratio, 4 * hd_ratio): 695 | color = skin.getpixel((20 * hd_ratio + i, 16 * hd_ratio + k + 16)) 696 | if color[3] != 0: 697 | self.polygons["torso_layer"]["top"].append(Polygon([ 698 | volume_points[i][0][k], 699 | volume_points[i + 1][0][k], 700 | volume_points[i + 1][0][k + 1], 701 | volume_points[i][0][k + 1]], 702 | color)) 703 | 704 | if "bottom" in self.visible_faces["torso_layer"]["front"]: 705 | for i in range(0, 8 * hd_ratio): 706 | for k in range(0 * hd_ratio, 4 * hd_ratio): 707 | color = skin.getpixel((28 * hd_ratio + i, (20 * hd_ratio - 1) - k + 16)) 708 | if color[3] != 0: 709 | self.polygons["torso_layer"]["bottom"].append(Polygon([ 710 | volume_points[i][12 * hd_ratio][k], 711 | volume_points[i + 1][12 * hd_ratio][k], 712 | volume_points[i + 1][12 * hd_ratio][k + 1], 713 | volume_points[i][12 * hd_ratio][k + 1]], 714 | color)) 715 | 716 | """Cape""" 717 | if self.display_cape: 718 | volume_points = {} 719 | for i in range(0, 11 * hd_ratio): 720 | for j in range(0, 17 * hd_ratio): 721 | volume_points = append_dict(volume_points, i, j, 0, 722 | Point(self, np.array([i - 1, j + 8 * hd_ratio, -1]))) 723 | volume_points = append_dict(volume_points, i, j, 1 * hd_ratio, 724 | Point(self, np.array([i - 1, j + 8 * hd_ratio, 0]))) 725 | 726 | for j in range(0, 17 * hd_ratio): 727 | for k in range(0, 2 * hd_ratio): 728 | volume_points = append_dict(volume_points, 0, j, k, 729 | Point(self, np.array([0, j + 8 * hd_ratio, k]))) 730 | volume_points = append_dict(volume_points, 8 * hd_ratio, j, k, 731 | Point(self, np.array([8 * hd_ratio, j + 8 * hd_ratio, k]))) 732 | 733 | for i in range(0, 11 * hd_ratio): 734 | for k in range(0, 2 * hd_ratio): 735 | volume_points = append_dict(volume_points, i, 0, k, 736 | Point(self, np.array([i, 8 * hd_ratio, k]))) 737 | volume_points = append_dict(volume_points, i, 12 * hd_ratio, k, 738 | Point(self, np.array([i, 20 * hd_ratio, k]))) 739 | 740 | if "back" in self.visible_faces["cape"]["front"]: 741 | for i in range(0, 10 * hd_ratio): 742 | for j in range(0, 16 * hd_ratio): 743 | color = im_cape.getpixel(((11 * hd_ratio - 1) - i, 1 * hd_ratio + j)) 744 | if color[3] != 0: 745 | self.polygons["cape"]["back"].append(Polygon([ 746 | volume_points[i][j][0], 747 | volume_points[i + 1][j][0], 748 | volume_points[i + 1][j + 1][0], 749 | volume_points[i][j + 1][0]], 750 | color)) 751 | 752 | if "front" in self.visible_faces["cape"]["front"]: 753 | for i in range(0, 10 * hd_ratio): 754 | for j in range(0, 16 * hd_ratio): 755 | color = im_cape.getpixel((12 * hd_ratio + i, 1 * hd_ratio + j)) 756 | if color[3] != 0: 757 | self.polygons["cape"]["front"].append(Polygon([ 758 | volume_points[i][j][1 * hd_ratio], 759 | volume_points[i + 1][j][1 * hd_ratio], 760 | volume_points[i + 1][j + 1][1 * hd_ratio], 761 | volume_points[i][j + 1][1 * hd_ratio]], 762 | color)) 763 | 764 | if "right" in self.visible_faces["cape"]["front"]: 765 | for j in range(0, 16 * hd_ratio): 766 | color = im_cape.getpixel((12 * hd_ratio, 1 * hd_ratio + j)) 767 | if color[3] != 0: 768 | self.polygons["cape"]["right"].append(Polygon([ 769 | volume_points[0][j][0], 770 | volume_points[0][j][1], 771 | volume_points[0][j + 1][1], 772 | volume_points[0][j + 1][0]], 773 | color)) 774 | 775 | if "left" in self.visible_faces["cape"]["front"]: 776 | for j in range(0, 16 * hd_ratio): 777 | color = im_cape.getpixel((1 * hd_ratio, 1 * hd_ratio + j)) 778 | if color[3] != 0: 779 | self.polygons["cape"]["left"].append(Polygon([ 780 | volume_points[10 * hd_ratio][j][0], 781 | volume_points[10 * hd_ratio][j][1], 782 | volume_points[10 * hd_ratio][j + 1][1], 783 | volume_points[10 * hd_ratio][j + 1][0]], 784 | color)) 785 | 786 | if "top" in self.visible_faces["cape"]["front"]: 787 | for i in range(0, 10 * hd_ratio): 788 | color = im_cape.getpixel((1 + i, 0)) 789 | if color[3] != 0: 790 | self.polygons["cape"]["top"].append(Polygon([ 791 | volume_points[i][0][0], 792 | volume_points[i + 1][0][0], 793 | volume_points[i + 1][0][1], 794 | volume_points[i][0][1]], 795 | color)) 796 | 797 | if "bottom" in self.visible_faces["cape"]["front"]: 798 | for i in range(0, 10 * hd_ratio): 799 | color = im_cape.getpixel((11 * hd_ratio + i, 0)) 800 | if color[3] != 0: 801 | self.polygons["cape"]["bottom"].append(Polygon([ 802 | volume_points[i][16 * hd_ratio][0], 803 | volume_points[i + 1][16 * hd_ratio][0], 804 | volume_points[i + 1][16 * hd_ratio][1], 805 | volume_points[i][16 * hd_ratio][1]], 806 | color)) 807 | 808 | start = 1 if self.player.is_slim else 0 809 | """Right arm""" 810 | volume_points = {} 811 | for i in range(start, 5 * hd_ratio): 812 | for j in range(0, 13 * hd_ratio): 813 | volume_points = append_dict(volume_points, i, j, 0, 814 | Point(self, np.array([i - 4 * hd_ratio, j + 8 * hd_ratio, 0]))) 815 | volume_points = append_dict(volume_points, i, j, 4 * hd_ratio, 816 | Point(self, np.array([i - 4 * hd_ratio, j + 8 * hd_ratio, 4 * hd_ratio]))) 817 | 818 | for j in range(0, 13 * hd_ratio): 819 | for k in range(0, 5 * hd_ratio): 820 | volume_points = append_dict(volume_points, start, j, k, 821 | Point(self, np.array([-4 * hd_ratio + start, j + 8 * hd_ratio, k]))) 822 | volume_points = append_dict(volume_points, 4 * hd_ratio, j, k, 823 | Point(self, np.array([0, j + 8 * hd_ratio, k]))) 824 | 825 | for i in range(start, 5 * hd_ratio): 826 | for k in range(0, 5 * hd_ratio): 827 | volume_points = append_dict(volume_points, i, 0, k, 828 | Point(self, np.array([i - 4 * hd_ratio, 8 * hd_ratio, k]))) 829 | volume_points = append_dict(volume_points, i, 12 * hd_ratio, k, 830 | Point(self, np.array([i - 4 * hd_ratio, 20 * hd_ratio, k]))) 831 | 832 | if "back" in self.visible_faces["r_arm"]["front"]: 833 | for i in range(start, 4 * hd_ratio): 834 | for j in range(0, 12 * hd_ratio): 835 | color = skin.getpixel((((56 - start) * hd_ratio - 1) - i, 20 * hd_ratio + j)) 836 | if color[3] != 0: 837 | self.polygons["r_arm"]["back"].append(Polygon([ 838 | volume_points[i][j][0], 839 | volume_points[i + 1][j][0], 840 | volume_points[i + 1][j + 1][0], 841 | volume_points[i][j + 1][0]], 842 | color)) 843 | 844 | if "front" in self.visible_faces["r_arm"]["front"]: 845 | for i in range(start, 4 * hd_ratio): 846 | for j in range(0, 12 * hd_ratio): 847 | color = skin.getpixel(((44 - start) * hd_ratio + i, 20 * hd_ratio + j)) 848 | if color[3] != 0: 849 | self.polygons["r_arm"]["front"].append(Polygon([ 850 | volume_points[i][j][4 * hd_ratio], 851 | volume_points[i + 1][j][4 * hd_ratio], 852 | volume_points[i + 1][j + 1][4 * hd_ratio], 853 | volume_points[i][j + 1][4 * hd_ratio]], 854 | color)) 855 | 856 | if "right" in self.visible_faces["r_arm"]["front"]: 857 | for j in range(0, 12 * hd_ratio): 858 | for k in range(0, 4 * hd_ratio): 859 | color = skin.getpixel((40 * hd_ratio + k, 20 * hd_ratio + j)) 860 | if color[3] != 0: 861 | self.polygons["r_arm"]["right"].append(Polygon([ 862 | volume_points[start][j][k], 863 | volume_points[start][j][k + 1], 864 | volume_points[start][j + 1][k + 1], 865 | volume_points[start][j + 1][k]], 866 | color)) 867 | 868 | if "left" in self.visible_faces["r_arm"]["front"]: 869 | for j in range(0, 12 * hd_ratio): 870 | for k in range(0, 4 * hd_ratio): 871 | color = skin.getpixel((((52 - start) * hd_ratio - 1) - k, 20 * hd_ratio + j)) 872 | if color[3] != 0: 873 | self.polygons["r_arm"]["left"].append(Polygon([ 874 | volume_points[4 * hd_ratio][j][k], 875 | volume_points[4 * hd_ratio][j][k + 1], 876 | volume_points[4 * hd_ratio][j + 1][k + 1], 877 | volume_points[4 * hd_ratio][j + 1][k]], 878 | color)) 879 | 880 | if "top" in self.visible_faces["r_arm"]["front"]: 881 | for i in range(start, 4 * hd_ratio): 882 | for k in range(0, 4 * hd_ratio): 883 | color = skin.getpixel(((44 - start) * hd_ratio + i, 16 * hd_ratio + k)) 884 | if color[3] != 0: 885 | self.polygons["r_arm"]["top"].append(Polygon([ 886 | volume_points[i][0][k], 887 | volume_points[i + 1][0][k], 888 | volume_points[i + 1][0][k + 1], 889 | volume_points[i][0][k + 1]], 890 | color)) 891 | 892 | if "bottom" in self.visible_faces["r_arm"]["front"]: 893 | for i in range(start, 4 * hd_ratio): 894 | for k in range(0, 4 * hd_ratio): 895 | color = skin.getpixel(((48 - start * 2) * hd_ratio + i, 16 * hd_ratio + k)) 896 | if color[3] != 0: 897 | self.polygons["r_arm"]["bottom"].append(Polygon([ 898 | volume_points[i][12 * hd_ratio][k], 899 | volume_points[i + 1][12 * hd_ratio][k], 900 | volume_points[i + 1][12 * hd_ratio][k + 1], 901 | volume_points[i][12 * hd_ratio][k + 1]], 902 | color)) 903 | 904 | """Right arm 2nd layer""" 905 | if self.layers: 906 | volume_points = {} 907 | for i in range(start, 5 * hd_ratio): 908 | for j in range(0, 13 * hd_ratio): 909 | volume_points = append_dict(volume_points, i, j, 0, 910 | Point(self, np.array([(i * 4.25 / 4 - 0.125 * hd_ratio) - 4 * hd_ratio, 911 | (j * 12.25 / 12 - 0.125 * hd_ratio) + 8 * hd_ratio, 912 | -0.125 * hd_ratio]))) 913 | volume_points = append_dict(volume_points, i, j, 4 * hd_ratio, 914 | Point(self, np.array([(i * 4.25 / 4 - 0.125 * hd_ratio) - 4 * hd_ratio, 915 | (j * 12.25 / 12 - 0.125 * hd_ratio) + 8 * hd_ratio, 916 | 4.125 * hd_ratio]))) 917 | 918 | for j in range(0, 13 * hd_ratio): 919 | for k in range(0, 5 * hd_ratio): 920 | volume_points = append_dict(volume_points, start, j, k, 921 | Point(self, np.array([(-4.125 + start) * hd_ratio, 922 | (j * 12.25 / 12 - 0.125 * hd_ratio) + 8 * hd_ratio, 923 | k * 4.25 / 4 - 0.125 * hd_ratio]))) 924 | volume_points = append_dict(volume_points, 4 * hd_ratio, j, k, 925 | Point(self, np.array([0.125 * hd_ratio, 926 | (j * 12.25 / 12 - 0.125 * hd_ratio) + 8 * hd_ratio, 927 | k * 4.25 / 4 - 0.125 * hd_ratio]))) 928 | 929 | for i in range(start, 5 * hd_ratio): 930 | for k in range(0, 5 * hd_ratio): 931 | volume_points = append_dict(volume_points, i, 0, k, 932 | Point(self, np.array([(i * 4.25 / 4 - 0.125 * hd_ratio) - 4 * hd_ratio, 933 | 7.875 * hd_ratio, 934 | k * 4.25 / 4 - 0.125 * hd_ratio]))) 935 | volume_points = append_dict(volume_points, i, 12 * hd_ratio, k, 936 | Point(self, np.array([(i * 4.25 / 4 - 0.125 * hd_ratio) - 4 * hd_ratio, 937 | 20.125 * hd_ratio, 938 | k * 4.25 / 4 - 0.125 * hd_ratio]))) 939 | 940 | if "back" in self.visible_faces["r_arm_layer"]["front"]: 941 | for i in range(start, 4 * hd_ratio): 942 | for j in range(0, 12 * hd_ratio): 943 | color = skin.getpixel((((56 - start * 2) * hd_ratio - 1) - i, 20 * hd_ratio + j + 16)) 944 | if color[3] != 0: 945 | self.polygons["r_arm_layer"]["back"].append(Polygon([ 946 | volume_points[i][j][0], 947 | volume_points[i + 1][j][0], 948 | volume_points[i + 1][j + 1][0], 949 | volume_points[i][j + 1][0]], 950 | color)) 951 | 952 | if "front" in self.visible_faces["r_arm_layer"]["front"]: 953 | for i in range(start, 4 * hd_ratio): 954 | for j in range(0, 12 * hd_ratio): 955 | color = skin.getpixel(((44 - start) * hd_ratio + i, 20 * hd_ratio + j + 16)) 956 | if color[3] != 0: 957 | self.polygons["r_arm_layer"]["front"].append(Polygon([ 958 | volume_points[i][j][4 * hd_ratio], 959 | volume_points[i + 1][j][4 * hd_ratio], 960 | volume_points[i + 1][j + 1][4 * hd_ratio], 961 | volume_points[i][j + 1][4 * hd_ratio]], 962 | color)) 963 | 964 | if "right" in self.visible_faces["r_arm_layer"]["front"]: 965 | for j in range(0, 12 * hd_ratio): 966 | for k in range(0, 4 * hd_ratio): 967 | color = skin.getpixel((40 * hd_ratio + k, 20 * hd_ratio + j + 16)) 968 | if color[3] != 0: 969 | self.polygons["r_arm_layer"]["right"].append(Polygon([ 970 | volume_points[start][j][k], 971 | volume_points[start][j][k + 1], 972 | volume_points[start][j + 1][k + 1], 973 | volume_points[start][j + 1][k]], 974 | color)) 975 | 976 | if "left" in self.visible_faces["r_arm_layer"]["front"]: 977 | for j in range(0, 12 * hd_ratio): 978 | for k in range(0, 4 * hd_ratio): 979 | color = skin.getpixel((((52 - start) * hd_ratio - 1) - k, 20 * hd_ratio + j + 16)) 980 | if color[3] != 0: 981 | self.polygons["r_arm_layer"]["left"].append(Polygon([ 982 | volume_points[4 * hd_ratio][j][k], 983 | volume_points[4 * hd_ratio][j][k + 1], 984 | volume_points[4 * hd_ratio][j + 1][k + 1], 985 | volume_points[4 * hd_ratio][j + 1][k]], 986 | color)) 987 | 988 | if "top" in self.visible_faces["r_arm_layer"]["front"]: 989 | for i in range(start, 4 * hd_ratio): 990 | for k in range(0, 4 * hd_ratio): 991 | color = skin.getpixel(((44 - start) * hd_ratio + i, 16 * hd_ratio + k + 16)) 992 | if color[3] != 0: 993 | self.polygons["r_arm_layer"]["top"].append(Polygon([ 994 | volume_points[i][0][k], 995 | volume_points[i + 1][0][k], 996 | volume_points[i + 1][0][k + 1], 997 | volume_points[i][0][k + 1]], 998 | color)) 999 | 1000 | if "bottom" in self.visible_faces["r_arm_layer"]["front"]: 1001 | for i in range(start, 4 * hd_ratio): 1002 | for k in range(0, 4 * hd_ratio): 1003 | color = skin.getpixel(((48 - start * 2) * hd_ratio + i, 16 * hd_ratio + k + 16)) 1004 | if color[3] != 0: 1005 | self.polygons["r_arm_layer"]["bottom"].append(Polygon([ 1006 | volume_points[i][12 * hd_ratio][k], 1007 | volume_points[i + 1][12 * hd_ratio][k], 1008 | volume_points[i + 1][12 * hd_ratio][k + 1], 1009 | volume_points[i][12 * hd_ratio][k + 1]], 1010 | color)) 1011 | 1012 | """Left arm""" 1013 | volume_points = {} 1014 | for i in range(0, (5 - start) * hd_ratio): 1015 | for j in range(0, 13 * hd_ratio): 1016 | volume_points = append_dict(volume_points, i, j, 0, 1017 | Point(self, np.array([i + 8 * hd_ratio, j + 8 * hd_ratio, 0]))) 1018 | volume_points = append_dict(volume_points, i, j, 4 * hd_ratio, 1019 | Point(self, np.array([i + 8 * hd_ratio, j + 8 * hd_ratio, 4 * hd_ratio]))) 1020 | 1021 | for j in range(0, 13 * hd_ratio): 1022 | for k in range(0, 5 * hd_ratio): 1023 | volume_points = append_dict(volume_points, 0, j, k, 1024 | Point(self, np.array([8 * hd_ratio, j + 8 * hd_ratio, k]))) 1025 | volume_points = append_dict(volume_points, (4 - start) * hd_ratio, j, k, 1026 | Point(self, np.array([(12 - start) * hd_ratio, j + 8 * hd_ratio, k]))) 1027 | 1028 | for i in range(0, (5 - start) * hd_ratio): 1029 | for k in range(0, 5 * hd_ratio): 1030 | volume_points = append_dict(volume_points, i, 0, k, 1031 | Point(self, np.array([i + 8 * hd_ratio, 8 * hd_ratio, k]))) 1032 | volume_points = append_dict(volume_points, i, 12 * hd_ratio, k, 1033 | Point(self, np.array([i + 8 * hd_ratio, 20 * hd_ratio, k]))) 1034 | 1035 | if "back" in self.visible_faces["l_arm"]["front"]: 1036 | for i in range(0, (4 - start) * hd_ratio): 1037 | for j in range(0, 12 * hd_ratio): 1038 | color = skin.getpixel(((48 - start * 2) * hd_ratio - 1 - i, 52 * hd_ratio + j)) 1039 | if color[3] != 0: 1040 | self.polygons["l_arm"]["back"].append(Polygon([ 1041 | volume_points[i][j][0], 1042 | volume_points[i + 1][j][0], 1043 | volume_points[i + 1][j + 1][0], 1044 | volume_points[i][j + 1][0]], 1045 | color)) 1046 | 1047 | if "front" in self.visible_faces["l_arm"]["front"]: 1048 | for i in range(0, (4 - start) * hd_ratio): 1049 | for j in range(0, 12 * hd_ratio): 1050 | color = skin.getpixel((36 * hd_ratio + i, 52 * hd_ratio + j)) 1051 | if color[3] != 0: 1052 | self.polygons["l_arm"]["front"].append(Polygon([ 1053 | volume_points[i][j][4 * hd_ratio], 1054 | volume_points[i + 1][j][4 * hd_ratio], 1055 | volume_points[i + 1][j + 1][4 * hd_ratio], 1056 | volume_points[i][j + 1][4 * hd_ratio]], 1057 | color)) 1058 | 1059 | if "right" in self.visible_faces["l_arm"]["front"]: 1060 | for j in range(0, 12 * hd_ratio): 1061 | for k in range(0, 4 * hd_ratio): 1062 | color = skin.getpixel((32 * hd_ratio + k, 52 * hd_ratio + j)) 1063 | if color[3] != 0: 1064 | self.polygons["l_arm"]["right"].append(Polygon([ 1065 | volume_points[0][j][k], 1066 | volume_points[0][j][k + 1], 1067 | volume_points[0][j + 1][k + 1], 1068 | volume_points[0][j + 1][k]], 1069 | color)) 1070 | 1071 | if "left" in self.visible_faces["l_arm"]["front"]: 1072 | for j in range(0, 12 * hd_ratio): 1073 | for k in range(0, 4 * hd_ratio): 1074 | color = skin.getpixel(((44 - start) * hd_ratio - 1 - k, 52 * hd_ratio + j)) 1075 | if color[3] != 0: 1076 | self.polygons["l_arm"]["left"].append(Polygon([ 1077 | volume_points[(4 - start) * hd_ratio][j][k], 1078 | volume_points[(4 - start) * hd_ratio][j][k + 1], 1079 | volume_points[(4 - start) * hd_ratio][j + 1][k + 1], 1080 | volume_points[(4 - start) * hd_ratio][j + 1][k]], 1081 | color)) 1082 | 1083 | if "top" in self.visible_faces["l_arm"]["front"]: 1084 | for i in range(0, (4 - start) * hd_ratio): 1085 | for k in range(0, 4 * hd_ratio): 1086 | color = skin.getpixel((36 * hd_ratio + i, 48 * hd_ratio + k)) 1087 | if color[3] != 0: 1088 | self.polygons["l_arm"]["top"].append(Polygon([ 1089 | volume_points[i][0][k], 1090 | volume_points[i + 1][0][k], 1091 | volume_points[i + 1][0][k + 1], 1092 | volume_points[i][0][k + 1]], 1093 | color)) 1094 | 1095 | if "bottom" in self.visible_faces["l_arm"]["front"]: 1096 | for i in range(0, (4 - start) * hd_ratio): 1097 | for k in range(0, 4 * hd_ratio): 1098 | color = skin.getpixel(((40 - start) * hd_ratio + i, 48 * hd_ratio + k)) 1099 | if color[3] != 0: 1100 | self.polygons["l_arm"]["bottom"].append(Polygon([ 1101 | volume_points[i][12 * hd_ratio][k], 1102 | volume_points[i + 1][12 * hd_ratio][k], 1103 | volume_points[i + 1][12 * hd_ratio][k + 1], 1104 | volume_points[i][12 * hd_ratio][k + 1]], 1105 | color)) 1106 | 1107 | """Left arm 2nd layer""" 1108 | if self.layers: 1109 | volume_points = {} 1110 | for i in range(0, (5 - start) * hd_ratio): 1111 | for j in range(0, 13 * hd_ratio): 1112 | volume_points = append_dict(volume_points, i, j, 0, 1113 | Point(self, np.array([(i * 4.25 / 4 - 0.125 * hd_ratio) + 8 * hd_ratio, 1114 | (j * 12.25 / 12 - 0.125 * hd_ratio) + 8 * hd_ratio, 1115 | -0.125 * hd_ratio]))) 1116 | volume_points = append_dict(volume_points, i, j, 4 * hd_ratio, 1117 | Point(self, np.array([(i * 4.25 / 4 - 0.125 * hd_ratio) + 8 * hd_ratio, 1118 | (j * 12.25 / 12 - 0.125 * hd_ratio) + 8 * hd_ratio, 1119 | 4.125 * hd_ratio]))) 1120 | 1121 | for j in range(0, 13 * hd_ratio): 1122 | for k in range(0, 5 * hd_ratio): 1123 | volume_points = append_dict(volume_points, 0, j, k, 1124 | Point(self, np.array([7.875 * hd_ratio, 1125 | (j * 12.25 / 12 - 0.125 * hd_ratio) + 8 * hd_ratio, 1126 | k * 4.25 / 4 - 0.125 * hd_ratio]))) 1127 | volume_points = append_dict(volume_points, (4 - start) * hd_ratio, j, k, 1128 | Point(self, np.array([(12.125 - start) * hd_ratio, 1129 | (j * 12.25 / 12 - 0.125 * hd_ratio) + 8 * hd_ratio, 1130 | k * 4.25 / 4 - 0.125 * hd_ratio]))) 1131 | 1132 | for i in range(0, (5 - start) * hd_ratio): 1133 | for k in range(0, 5 * hd_ratio): 1134 | volume_points = append_dict(volume_points, i, 0, k, 1135 | Point(self, np.array([(i * 4.25 / 4 - 0.125 * hd_ratio) + 8 * hd_ratio, 1136 | 7.875 * hd_ratio, 1137 | k * 4.25 / 4 - 0.125 * hd_ratio]))) 1138 | volume_points = append_dict(volume_points, i, 12 * hd_ratio, k, 1139 | Point(self, np.array([(i * 4.25 / 4 - 0.125 * hd_ratio) + 8 * hd_ratio, 1140 | 20.125 * hd_ratio, 1141 | k * 4.25 / 4 - 0.125 * hd_ratio]))) 1142 | 1143 | if "back" in self.visible_faces["l_arm_layer"]["front"]: 1144 | for i in range(0, (4 - start) * hd_ratio): 1145 | for j in range(0, 12 * hd_ratio): 1146 | color = skin.getpixel(((64 - start * 2) * hd_ratio - 1 - i, 52 * hd_ratio + j)) 1147 | if color[3] != 0: 1148 | self.polygons["l_arm_layer"]["back"].append(Polygon([ 1149 | volume_points[i][j][0], 1150 | volume_points[i + 1][j][0], 1151 | volume_points[i + 1][j + 1][0], 1152 | volume_points[i][j + 1][0]], 1153 | color)) 1154 | 1155 | if "front" in self.visible_faces["l_arm_layer"]["front"]: 1156 | for i in range(0, (4 - start) * hd_ratio): 1157 | for j in range(0, 12 * hd_ratio): 1158 | color = skin.getpixel((52 * hd_ratio + i, 52 * hd_ratio + j)) 1159 | if color[3] != 0: 1160 | self.polygons["l_arm_layer"]["front"].append(Polygon([ 1161 | volume_points[i][j][4 * hd_ratio], 1162 | volume_points[i + 1][j][4 * hd_ratio], 1163 | volume_points[i + 1][j + 1][4 * hd_ratio], 1164 | volume_points[i][j + 1][4 * hd_ratio]], 1165 | color)) 1166 | 1167 | if "right" in self.visible_faces["l_arm_layer"]["front"]: 1168 | for j in range(0, 12 * hd_ratio): 1169 | for k in range(0, 4 * hd_ratio): 1170 | color = skin.getpixel((48 * hd_ratio + k, 52 * hd_ratio + j)) 1171 | if color[3] != 0: 1172 | self.polygons["l_arm_layer"]["right"].append(Polygon([ 1173 | volume_points[0][j][k], 1174 | volume_points[0][j][k + 1], 1175 | volume_points[0][j + 1][k + 1], 1176 | volume_points[0][j + 1][k]], 1177 | color)) 1178 | 1179 | if "left" in self.visible_faces["l_arm_layer"]["front"]: 1180 | for j in range(0, 12 * hd_ratio): 1181 | for k in range(0, 4 * hd_ratio): 1182 | color = skin.getpixel(((60 - start) * hd_ratio - 1 - k, 52 * hd_ratio + j)) 1183 | if color[3] != 0: 1184 | self.polygons["l_arm_layer"]["left"].append(Polygon([ 1185 | volume_points[(4 - start) * hd_ratio][j][k], 1186 | volume_points[(4 - start) * hd_ratio][j][k + 1], 1187 | volume_points[(4 - start) * hd_ratio][j + 1][k + 1], 1188 | volume_points[(4 - start) * hd_ratio][j + 1][k]], 1189 | color)) 1190 | 1191 | if "top" in self.visible_faces["l_arm_layer"]["front"]: 1192 | for i in range(0, (4 - start) * hd_ratio): 1193 | for k in range(0, 4 * hd_ratio): 1194 | color = skin.getpixel((52 * hd_ratio + i, 48 * hd_ratio + k)) 1195 | if color[3] != 0: 1196 | self.polygons["l_arm_layer"]["top"].append(Polygon([ 1197 | volume_points[i][0][k], 1198 | volume_points[i + 1][0][k], 1199 | volume_points[i + 1][0][k + 1], 1200 | volume_points[i][0][k + 1]], 1201 | color)) 1202 | 1203 | if "bottom" in self.visible_faces["l_arm_layer"]["front"]: 1204 | for i in range(0, (4 - start) * hd_ratio): 1205 | for k in range(0, 4 * hd_ratio): 1206 | color = skin.getpixel(((56 - start) * hd_ratio + i, 48 * hd_ratio + k)) 1207 | if color[3] != 0: 1208 | self.polygons["l_arm_layer"]["bottom"].append(Polygon([ 1209 | volume_points[i][12 * hd_ratio][k], 1210 | volume_points[i + 1][12 * hd_ratio][k], 1211 | volume_points[i + 1][12 * hd_ratio][k + 1], 1212 | volume_points[i][12 * hd_ratio][k + 1]], 1213 | color)) 1214 | 1215 | """Right leg""" 1216 | volume_points = {} 1217 | for i in range(0, 5 * hd_ratio): 1218 | for j in range(0, 13 * hd_ratio): 1219 | volume_points = append_dict(volume_points, i, j, 0, 1220 | Point(self, np.array([i, j + 20 * hd_ratio, 0]))) 1221 | volume_points = append_dict(volume_points, i, j, 4 * hd_ratio, 1222 | Point(self, np.array([i, j + 20 * hd_ratio, 4 * hd_ratio]))) 1223 | 1224 | for j in range(0, 13 * hd_ratio): 1225 | for k in range(0, 5 * hd_ratio): 1226 | volume_points = append_dict(volume_points, 0, j, k, 1227 | Point(self, np.array([0, j + 20 * hd_ratio, k]))) 1228 | volume_points = append_dict(volume_points, 4 * hd_ratio, j, k, 1229 | Point(self, np.array([4 * hd_ratio, j + 20 * hd_ratio, k]))) 1230 | 1231 | for i in range(0, 5 * hd_ratio): 1232 | for k in range(0, 5 * hd_ratio): 1233 | volume_points = append_dict(volume_points, i, 0, k, 1234 | Point(self, np.array([i, 20 * hd_ratio, k]))) 1235 | volume_points = append_dict(volume_points, i, 12 * hd_ratio, k, 1236 | Point(self, np.array([i, 32 * hd_ratio, k]))) 1237 | 1238 | if "back" in self.visible_faces["r_leg"]["front"]: 1239 | for i in range(0, 4 * hd_ratio): 1240 | for j in range(0, 12 * hd_ratio): 1241 | color = skin.getpixel(((16 * hd_ratio - 1) - i, 20 * hd_ratio + j)) 1242 | if color[3] != 0: 1243 | self.polygons["r_leg"]["back"].append(Polygon([ 1244 | volume_points[i][j][0], 1245 | volume_points[i + 1][j][0], 1246 | volume_points[i + 1][j + 1][0], 1247 | volume_points[i][j + 1][0]], 1248 | color)) 1249 | 1250 | if "front" in self.visible_faces["r_leg"]["front"]: 1251 | for i in range(0, 4 * hd_ratio): 1252 | for j in range(0, 12 * hd_ratio): 1253 | color = skin.getpixel((4 * hd_ratio + i, 20 * hd_ratio + j)) 1254 | if color[3] != 0: 1255 | self.polygons["r_leg"]["front"].append(Polygon([ 1256 | volume_points[i][j][4 * hd_ratio], 1257 | volume_points[i + 1][j][4 * hd_ratio], 1258 | volume_points[i + 1][j + 1][4 * hd_ratio], 1259 | volume_points[i][j + 1][4 * hd_ratio]], 1260 | color)) 1261 | 1262 | if "right" in self.visible_faces["r_leg"]["front"]: 1263 | for j in range(0, 12 * hd_ratio): 1264 | for k in range(0, 4 * hd_ratio): 1265 | color = skin.getpixel((k, 20 * hd_ratio + j)) 1266 | if color[3] != 0: 1267 | self.polygons["r_leg"]["right"].append(Polygon([ 1268 | volume_points[0][j][k], 1269 | volume_points[0][j][k + 1], 1270 | volume_points[0][j + 1][k + 1], 1271 | volume_points[0][j + 1][k]], 1272 | color)) 1273 | 1274 | if "left" in self.visible_faces["r_leg"]["front"]: 1275 | for j in range(0, 12 * hd_ratio): 1276 | for k in range(0, 4 * hd_ratio): 1277 | color = skin.getpixel(((12 * hd_ratio - 1) - k, 20 * hd_ratio + j)) 1278 | if color[3] != 0: 1279 | self.polygons["r_leg"]["left"].append(Polygon([ 1280 | volume_points[4 * hd_ratio][j][k], 1281 | volume_points[4 * hd_ratio][j][k + 1], 1282 | volume_points[4 * hd_ratio][j + 1][k + 1], 1283 | volume_points[4 * hd_ratio][j + 1][k]], 1284 | color)) 1285 | 1286 | if "top" in self.visible_faces["r_leg"]["front"]: 1287 | for i in range(0, 4 * hd_ratio): 1288 | for k in range(0, 4 * hd_ratio): 1289 | color = skin.getpixel((4 * hd_ratio + i, 16 * hd_ratio + k)) 1290 | if color[3] != 0: 1291 | self.polygons["r_leg"]["top"].append(Polygon([ 1292 | volume_points[i][0][k], 1293 | volume_points[i + 1][0][k], 1294 | volume_points[i + 1][0][k + 1], 1295 | volume_points[i][0][k + 1]], 1296 | color)) 1297 | 1298 | if "bottom" in self.visible_faces["r_leg"]["front"]: 1299 | for i in range(0, 4 * hd_ratio): 1300 | for k in range(0, 4 * hd_ratio): 1301 | color = skin.getpixel((8 * hd_ratio + i, 16 * hd_ratio + k)) 1302 | if color[3] != 0: 1303 | self.polygons["r_leg"]["bottom"].append(Polygon([ 1304 | volume_points[i][12 * hd_ratio][k], 1305 | volume_points[i + 1][12 * hd_ratio][k], 1306 | volume_points[i + 1][12 * hd_ratio][k + 1], 1307 | volume_points[i][12 * hd_ratio][k + 1]], 1308 | color)) 1309 | 1310 | """Right leg 2nd layer""" 1311 | if self.layers: 1312 | volume_points = {} 1313 | for i in range(0, 5 * hd_ratio): 1314 | for j in range(0, 13 * hd_ratio): 1315 | volume_points = append_dict(volume_points, i, j, 0, 1316 | Point(self, np.array([i * 4.25 / 4 - 0.125 * hd_ratio, 1317 | (j * 12.25 / 12 - 0.125 * hd_ratio) + 20 * hd_ratio, 1318 | -0.125 * hd_ratio]))) 1319 | volume_points = append_dict(volume_points, i, j, 4 * hd_ratio, 1320 | Point(self, np.array([i * 4.25 / 4 - 0.125 * hd_ratio, 1321 | (j * 12.25 / 12 - 0.125 * hd_ratio) + 20 * hd_ratio, 1322 | 4.125 * hd_ratio]))) 1323 | 1324 | for j in range(0, 13 * hd_ratio): 1325 | for k in range(0, 5 * hd_ratio): 1326 | volume_points = append_dict(volume_points, 0, j, k, 1327 | Point(self, np.array([-0.125 * hd_ratio, 1328 | (j * 12.25 / 12 - 0.125 * hd_ratio) + 20 * hd_ratio, 1329 | k * 4.25 / 4 - 0.125 * hd_ratio]))) 1330 | volume_points = append_dict(volume_points, 4 * hd_ratio, j, k, 1331 | Point(self, np.array([4.125 * hd_ratio, 1332 | (j * 12.25 / 12 - 0.125 * hd_ratio) + 20 * hd_ratio, 1333 | k * 4.25 / 4 - 0.125 * hd_ratio]))) 1334 | 1335 | for i in range(0, 5 * hd_ratio): 1336 | for k in range(0, 5 * hd_ratio): 1337 | volume_points = append_dict(volume_points, i, 0, k, 1338 | Point(self, np.array([i * 4.25 / 4 - 0.125 * hd_ratio, 1339 | 19.875 * hd_ratio, 1340 | k * 4.25 / 4 - 0.125 * hd_ratio]))) 1341 | volume_points = append_dict(volume_points, i, 12 * hd_ratio, k, 1342 | Point(self, np.array([i * 4.25 / 4 - 0.125 * hd_ratio, 1343 | 32.125 * hd_ratio, 1344 | k * 4.25 / 4 - 0.125 * hd_ratio]))) 1345 | 1346 | if "back" in self.visible_faces["r_leg_layer"]["front"]: 1347 | for i in range(0, 4 * hd_ratio): 1348 | for j in range(0, 12 * hd_ratio): 1349 | color = skin.getpixel((16 * hd_ratio - 1 - i, 36 * hd_ratio + j)) 1350 | if color[3] != 0: 1351 | self.polygons["r_leg_layer"]["back"].append(Polygon([ 1352 | volume_points[i][j][0], 1353 | volume_points[i + 1][j][0], 1354 | volume_points[i + 1][j + 1][0], 1355 | volume_points[i][j + 1][0]], 1356 | color)) 1357 | 1358 | if "front" in self.visible_faces["r_leg_layer"]["front"]: 1359 | for i in range(0, 4 * hd_ratio): 1360 | for j in range(0, 12 * hd_ratio): 1361 | color = skin.getpixel((4 * hd_ratio + i, 36 * hd_ratio + j)) 1362 | if color[3] != 0: 1363 | self.polygons["r_leg_layer"]["front"].append(Polygon([ 1364 | volume_points[i][j][4 * hd_ratio], 1365 | volume_points[i + 1][j][4 * hd_ratio], 1366 | volume_points[i + 1][j + 1][4 * hd_ratio], 1367 | volume_points[i][j + 1][4 * hd_ratio]], 1368 | color)) 1369 | 1370 | if "right" in self.visible_faces["r_leg_layer"]["front"]: 1371 | for j in range(0, 12 * hd_ratio): 1372 | for k in range(0, 4 * hd_ratio): 1373 | color = skin.getpixel((k, 36 * hd_ratio + j)) 1374 | if color[3] != 0: 1375 | self.polygons["r_leg_layer"]["right"].append(Polygon([ 1376 | volume_points[0][j][k], 1377 | volume_points[0][j][k + 1], 1378 | volume_points[0][j + 1][k + 1], 1379 | volume_points[0][j + 1][k]], 1380 | color)) 1381 | 1382 | if "left" in self.visible_faces["r_leg_layer"]["front"]: 1383 | for j in range(0, 12 * hd_ratio): 1384 | for k in range(0, 4 * hd_ratio): 1385 | color = skin.getpixel((12 * hd_ratio - 1 - k, 36 * hd_ratio + j)) 1386 | if color[3] != 0: 1387 | self.polygons["r_leg_layer"]["left"].append(Polygon([ 1388 | volume_points[4 * hd_ratio][j][k], 1389 | volume_points[4 * hd_ratio][j][k + 1], 1390 | volume_points[4 * hd_ratio][j + 1][k + 1], 1391 | volume_points[4 * hd_ratio][j + 1][k]], 1392 | color)) 1393 | 1394 | if "top" in self.visible_faces["r_leg_layer"]["front"]: 1395 | for i in range(0, 4 * hd_ratio): 1396 | for k in range(0, 4 * hd_ratio): 1397 | color = skin.getpixel((4 * hd_ratio + i, 32 * hd_ratio + k)) 1398 | if color[3] != 0: 1399 | self.polygons["r_leg_layer"]["top"].append(Polygon([ 1400 | volume_points[i][0][k], 1401 | volume_points[i + 1][0][k], 1402 | volume_points[i + 1][0][k + 1], 1403 | volume_points[i][0][k + 1]], 1404 | color)) 1405 | 1406 | if "bottom" in self.visible_faces["r_leg_layer"]["front"]: 1407 | for i in range(0, 4 * hd_ratio): 1408 | for k in range(0, 4 * hd_ratio): 1409 | color = skin.getpixel((8 * hd_ratio + i, 32 * hd_ratio + k)) 1410 | if color[3] != 0: 1411 | self.polygons["r_leg_layer"]["bottom"].append(Polygon([ 1412 | volume_points[i][12 * hd_ratio][k], 1413 | volume_points[i + 1][12 * hd_ratio][k], 1414 | volume_points[i + 1][12 * hd_ratio][k + 1], 1415 | volume_points[i][12 * hd_ratio][k + 1]], 1416 | color)) 1417 | 1418 | """Left leg""" 1419 | volume_points = {} 1420 | for i in range(0, 9 * hd_ratio): 1421 | for j in range(0, 13 * hd_ratio): 1422 | volume_points = append_dict(volume_points, i, j, 0, 1423 | Point(self, np.array([i + 4 * hd_ratio, j + 20 * hd_ratio, 0]))) 1424 | volume_points = append_dict(volume_points, i, j, 4 * hd_ratio, 1425 | Point(self, np.array([i + 4 * hd_ratio, j + 20 * hd_ratio, 4 * hd_ratio]))) 1426 | 1427 | for j in range(0, 13 * hd_ratio): 1428 | for k in range(0, 5 * hd_ratio): 1429 | volume_points = append_dict(volume_points, 0, j, k, 1430 | Point(self, np.array([4 * hd_ratio, j + 20 * hd_ratio, k]))) 1431 | volume_points = append_dict(volume_points, 4 * hd_ratio, j, k, 1432 | Point(self, np.array([8 * hd_ratio, j + 20 * hd_ratio, k]))) 1433 | 1434 | for i in range(0, 9 * hd_ratio): 1435 | for k in range(0, 5 * hd_ratio): 1436 | volume_points = append_dict(volume_points, i, 0, k, 1437 | Point(self, np.array([i + 4 * hd_ratio, 20 * hd_ratio, k]))) 1438 | volume_points = append_dict(volume_points, i, 12 * hd_ratio, k, 1439 | Point(self, np.array([i + 4 * hd_ratio, 32 * hd_ratio, k]))) 1440 | 1441 | if "back" in self.visible_faces["l_leg"]["front"]: 1442 | for i in range(0, 4 * hd_ratio): 1443 | for j in range(0, 12 * hd_ratio): 1444 | color = skin.getpixel((32 * hd_ratio - 1 - i, 52 * hd_ratio + j)) 1445 | if color[3] != 0: 1446 | self.polygons["l_leg"]["back"].append(Polygon([ 1447 | volume_points[i][j][0], 1448 | volume_points[i + 1][j][0], 1449 | volume_points[i + 1][j + 1][0], 1450 | volume_points[i][j + 1][0]], 1451 | color)) 1452 | 1453 | if "front" in self.visible_faces["l_leg"]["front"]: 1454 | for i in range(0, 4 * hd_ratio): 1455 | for j in range(0, 12 * hd_ratio): 1456 | color = skin.getpixel((20 * hd_ratio + i, 52 * hd_ratio + j)) 1457 | if color[3] != 0: 1458 | self.polygons["l_leg"]["front"].append(Polygon([ 1459 | volume_points[i][j][4 * hd_ratio], 1460 | volume_points[i + 1][j][4 * hd_ratio], 1461 | volume_points[i + 1][j + 1][4 * hd_ratio], 1462 | volume_points[i][j + 1][4 * hd_ratio]], 1463 | color)) 1464 | 1465 | if "right" in self.visible_faces["l_leg"]["front"]: 1466 | for j in range(0, 12 * hd_ratio): 1467 | for k in range(0, 4 * hd_ratio): 1468 | color = skin.getpixel((16 * hd_ratio + k, 52 * hd_ratio + j)) 1469 | if color[3] != 0: 1470 | self.polygons["l_leg"]["right"].append(Polygon([ 1471 | volume_points[0][j][k], 1472 | volume_points[0][j][k + 1], 1473 | volume_points[0][j + 1][k + 1], 1474 | volume_points[0][j + 1][k]], 1475 | color)) 1476 | 1477 | if "left" in self.visible_faces["l_leg"]["front"]: 1478 | for j in range(0, 12 * hd_ratio): 1479 | for k in range(0, 4 * hd_ratio): 1480 | color = skin.getpixel((28 * hd_ratio - 1 - k, 52 * hd_ratio + j)) 1481 | if color[3] != 0: 1482 | self.polygons["l_leg"]["left"].append(Polygon([ 1483 | volume_points[4 * hd_ratio][j][k], 1484 | volume_points[4 * hd_ratio][j][k + 1], 1485 | volume_points[4 * hd_ratio][j + 1][k + 1], 1486 | volume_points[4 * hd_ratio][j + 1][k]], 1487 | color)) 1488 | 1489 | if "top" in self.visible_faces["l_leg"]["front"]: 1490 | for i in range(0, 4 * hd_ratio): 1491 | for k in range(0, 4 * hd_ratio): 1492 | color = skin.getpixel((20 * hd_ratio + i, 48 * hd_ratio + k)) 1493 | if color[3] != 0: 1494 | self.polygons["l_leg"]["top"].append(Polygon([ 1495 | volume_points[i][0][k], 1496 | volume_points[i + 1][0][k], 1497 | volume_points[i + 1][0][k + 1], 1498 | volume_points[i][0][k + 1]], 1499 | color)) 1500 | 1501 | if "bottom" in self.visible_faces["l_leg"]["front"]: 1502 | for i in range(0, 4 * hd_ratio): 1503 | for k in range(0, 4 * hd_ratio): 1504 | color = skin.getpixel((24 * hd_ratio + i, 48 * hd_ratio + k)) 1505 | if color[3] != 0: 1506 | self.polygons["l_leg"]["bottom"].append(Polygon([ 1507 | volume_points[i][12 * hd_ratio][k], 1508 | volume_points[i + 1][12 * hd_ratio][k], 1509 | volume_points[i + 1][12 * hd_ratio][k + 1], 1510 | volume_points[i][12 * hd_ratio][k + 1]], 1511 | color)) 1512 | 1513 | """Left leg 2nd layer""" 1514 | if self.layers: 1515 | volume_points = {} 1516 | for i in range(0, 5 * hd_ratio): 1517 | for j in range(0, 13 * hd_ratio): 1518 | volume_points = append_dict(volume_points, i, j, 0, 1519 | Point(self, np.array([(i * 4.25 / 4 - 0.125 * hd_ratio) + 4 * hd_ratio, 1520 | (j * 12.25 / 12 - 0.125 * hd_ratio) + 20 * hd_ratio, 1521 | -0.125 * hd_ratio]))) 1522 | volume_points = append_dict(volume_points, i, j, 4 * hd_ratio, 1523 | Point(self, 1524 | np.array([(i * 4.25 / 4 - 0.125 * hd_ratio) + 4 * hd_ratio, 1525 | (j * 12.25 / 12 - 0.125 * hd_ratio) + 20 * hd_ratio, 1526 | 4.125 * hd_ratio]))) 1527 | 1528 | for j in range(0, 13 * hd_ratio): 1529 | for k in range(0, 5 * hd_ratio): 1530 | volume_points = append_dict(volume_points, 0, j, k, 1531 | Point(self, np.array([3.875 * hd_ratio, 1532 | (j * 12.25 / 12 - 0.125 * hd_ratio) + 20 * hd_ratio, 1533 | k * 4.25 / 4 - 0.125 * hd_ratio]))) 1534 | volume_points = append_dict(volume_points, 4 * hd_ratio, j, k, 1535 | Point(self, np.array([8.125 * hd_ratio, 1536 | (j * 12.25 / 12 - 0.125 * hd_ratio) + 20 * hd_ratio, 1537 | k * 4.25 / 4 - 0.125 * hd_ratio]))) 1538 | 1539 | for i in range(0, 5 * hd_ratio): 1540 | for k in range(0, 5 * hd_ratio): 1541 | volume_points = append_dict(volume_points, i, 0, k, 1542 | Point(self, np.array([(i * 4.25 / 4 - 0.125 * hd_ratio) + 4 * hd_ratio, 1543 | 19.875 * hd_ratio, 1544 | k * 4.25 / 4 - 0.125 * hd_ratio]))) 1545 | volume_points = append_dict(volume_points, i, 12 * hd_ratio, k, 1546 | Point(self, 1547 | np.array([(i * 4.25 / 4 - 0.125 * hd_ratio) + 4 * hd_ratio, 1548 | 32.125 * hd_ratio, 1549 | k * 4.25 / 4 - 0.125 * hd_ratio]))) 1550 | 1551 | if "back" in self.visible_faces["l_leg_layer"]["front"]: 1552 | for i in range(0, 4 * hd_ratio): 1553 | for j in range(0, 12 * hd_ratio): 1554 | color1 = skin.getpixel((16 * hd_ratio - 1 - i, 52 * hd_ratio + j)) 1555 | if color1[3] != 0: 1556 | self.polygons["l_leg_layer"]["back"].append(Polygon([ 1557 | volume_points[i][j][0], 1558 | volume_points[i + 1][j][0], 1559 | volume_points[i + 1][j + 1][0], 1560 | volume_points[i][j + 1][0]], 1561 | color1)) 1562 | 1563 | if "front" in self.visible_faces["l_leg_layer"]["front"]: 1564 | for i in range(0, 4 * hd_ratio): 1565 | for j in range(0, 12 * hd_ratio): 1566 | color2 = skin.getpixel((4 * hd_ratio + i, 52 * hd_ratio + j)) 1567 | if color2[3] != 0: 1568 | self.polygons["l_leg_layer"]["front"].append(Polygon([ 1569 | volume_points[i][j][4 * hd_ratio], 1570 | volume_points[i + 1][j][4 * hd_ratio], 1571 | volume_points[i + 1][j + 1][4 * hd_ratio], 1572 | volume_points[i][j + 1][4 * hd_ratio]], 1573 | color2)) 1574 | 1575 | if "right" in self.visible_faces["l_leg_layer"]["front"]: 1576 | for j in range(0, 12 * hd_ratio): 1577 | for k in range(0, 4 * hd_ratio): 1578 | color1 = skin.getpixel((k, 52 * hd_ratio + j)) 1579 | if color1[3] != 0: 1580 | self.polygons["l_leg_layer"]["right"].append(Polygon([ 1581 | volume_points[0][j][k], 1582 | volume_points[0][j][k + 1], 1583 | volume_points[0][j + 1][k + 1], 1584 | volume_points[0][j + 1][k]], 1585 | color1)) 1586 | 1587 | if "left" in self.visible_faces["l_leg_layer"]["front"]: 1588 | for j in range(0, 12 * hd_ratio): 1589 | for k in range(0, 4 * hd_ratio): 1590 | color2 = skin.getpixel((12 * hd_ratio - 1 - k, 52 * hd_ratio + j)) 1591 | if color2[3] != 0: 1592 | self.polygons["l_leg_layer"]["left"].append(Polygon([ 1593 | volume_points[4 * hd_ratio][j][k], 1594 | volume_points[4 * hd_ratio][j][k + 1], 1595 | volume_points[4 * hd_ratio][j + 1][k + 1], 1596 | volume_points[4 * hd_ratio][j + 1][k]], 1597 | color2)) 1598 | 1599 | if "top" in self.visible_faces["l_leg_layer"]["front"]: 1600 | for i in range(0, 4 * hd_ratio): 1601 | for k in range(0, 4 * hd_ratio): 1602 | color1 = skin.getpixel((4 * hd_ratio + i, 48 * hd_ratio + k)) 1603 | if color1[3] != 0: 1604 | self.polygons["l_leg_layer"]["top"].append(Polygon([ 1605 | volume_points[i][0][k], 1606 | volume_points[i + 1][0][k], 1607 | volume_points[i + 1][0][k + 1], 1608 | volume_points[i][0][k + 1]], 1609 | color1)) 1610 | 1611 | if "bottom" in self.visible_faces["l_leg_layer"]["front"]: 1612 | for i in range(0, 4 * hd_ratio): 1613 | for k in range(0, 4 * hd_ratio): 1614 | color2 = skin.getpixel((8 * hd_ratio + i, 48 * hd_ratio + k)) 1615 | if color2[3] != 0: 1616 | self.polygons["l_leg_layer"]["bottom"].append(Polygon([ 1617 | volume_points[i][12 * hd_ratio][k], 1618 | volume_points[i + 1][12 * hd_ratio][k], 1619 | volume_points[i + 1][12 * hd_ratio][k + 1], 1620 | volume_points[i][12 * hd_ratio][k + 1]], 1621 | color2)) 1622 | 1623 | def member_rotation(self, hd_ratio): 1624 | for face in self.polygons["head"]: 1625 | for poly in self.polygons["head"][face]: 1626 | poly.project( 1627 | np.array([4 * hd_ratio, 8 * hd_ratio, 2 * hd_ratio]), 1628 | self.body_angles["head"] 1629 | ) 1630 | 1631 | if self.display_hair: 1632 | for face in self.polygons["helmet"]: 1633 | for poly in self.polygons["helmet"][face]: 1634 | poly.project( 1635 | np.array([4 * hd_ratio, 8 * hd_ratio, 2 * hd_ratio]), 1636 | self.body_angles["head"] 1637 | ) 1638 | 1639 | if not self.head_only: 1640 | for face in self.polygons["cape"]: 1641 | for poly in self.polygons["cape"][face]: 1642 | poly.project( 1643 | np.array([4 * hd_ratio, 8 * hd_ratio, 0]), 1644 | self.body_angles["cape"] 1645 | ) 1646 | 1647 | for face in self.polygons["r_arm"]: 1648 | for poly in self.polygons["r_arm"][face]: 1649 | poly.project( 1650 | np.array([-2 * hd_ratio, 10 * hd_ratio, 2 * hd_ratio]), 1651 | self.body_angles["r_arm"] 1652 | ) 1653 | 1654 | for face in self.polygons["r_arm_layer"]: 1655 | for poly in self.polygons["r_arm_layer"][face]: 1656 | poly.project( 1657 | np.array([-2 * hd_ratio, 10 * hd_ratio, 2 * hd_ratio]), 1658 | self.body_angles["r_arm_layer"] 1659 | ) 1660 | 1661 | for face in self.polygons["l_arm"]: 1662 | for poly in self.polygons["l_arm"][face]: 1663 | poly.project( 1664 | np.array([10 * hd_ratio, 10 * hd_ratio, 2 * hd_ratio]), 1665 | self.body_angles["l_arm"] 1666 | ) 1667 | 1668 | for face in self.polygons["l_arm_layer"]: 1669 | for poly in self.polygons["l_arm_layer"][face]: 1670 | poly.project( 1671 | np.array([10 * hd_ratio, 10 * hd_ratio, 2 * hd_ratio]), 1672 | self.body_angles["l_arm_layer"] 1673 | ) 1674 | 1675 | for face in self.polygons["r_leg"]: 1676 | for poly in self.polygons["r_leg"][face]: 1677 | poly.project( 1678 | np.array([2 * hd_ratio, 22 * hd_ratio, 2 * hd_ratio]), 1679 | self.body_angles["r_leg"] 1680 | ) 1681 | 1682 | for face in self.polygons["r_leg_layer"]: 1683 | for poly in self.polygons["r_leg_layer"][face]: 1684 | poly.project( 1685 | np.array([2 * hd_ratio, 22 * hd_ratio, 2 * hd_ratio]), 1686 | self.body_angles["r_leg_layer"] 1687 | ) 1688 | 1689 | for face in self.polygons["l_leg"]: 1690 | for poly in self.polygons["l_leg"][face]: 1691 | poly.project( 1692 | np.array([6 * hd_ratio, 22 * hd_ratio, 2 * hd_ratio]), 1693 | self.body_angles["l_leg"] 1694 | ) 1695 | 1696 | for face in self.polygons["l_leg_layer"]: 1697 | for poly in self.polygons["l_leg_layer"][face]: 1698 | poly.project( 1699 | np.array([6 * hd_ratio, 22 * hd_ratio, 2 * hd_ratio]), 1700 | self.body_angles["l_leg_layer"] 1701 | ) 1702 | 1703 | for body_part in ["torso", "torso_layer"]: 1704 | for face in self.polygons[body_part]: 1705 | for poly in self.polygons[body_part][face]: 1706 | poly.project( 1707 | np.array([0, 0, 0]), 1708 | self.body_angles[body_part] 1709 | ) 1710 | 1711 | def display_image(self): 1712 | width = self.max_x - self.min_x 1713 | height = self.max_y - self.min_y 1714 | ratio = self.ratio 1715 | if ratio < 2: 1716 | ratio = 2 1717 | 1718 | if self.aa: 1719 | ratio *= 2 1720 | 1721 | src_width = ratio * width + 1 1722 | src_height = ratio * height + 1 1723 | real_width = src_width / 2 1724 | real_height = src_height / 2 1725 | 1726 | image = Image.new('RGBA', (int(src_width), int(src_height))) 1727 | 1728 | display_order = self.get_display_order() 1729 | draw = ImageDraw.Draw(image) 1730 | for pieces in display_order: 1731 | for piece, faces in pieces.items(): 1732 | for face in faces: 1733 | for poly in self.polygons[piece][face]: 1734 | poly.add_png_polygon(draw, self.min_x, self.min_y, ratio) 1735 | 1736 | if self.aa: 1737 | image = image.resize((int(real_width), int(real_height)), resample=Image.LANCZOS) 1738 | return image 1739 | 1740 | def get_display_order(self): 1741 | display_order = [] 1742 | if "front" in self.front_faces: 1743 | display_order.append({"cape": self.visible_faces["cape"]["front"]}) 1744 | 1745 | if "top" in self.front_faces: 1746 | if "right" in self.front_faces: 1747 | display_order.append({"l_leg_layer": self.back_faces}) 1748 | display_order.append({"l_leg": self.visible_faces["l_leg"]["front"]}) 1749 | display_order.append({"l_leg_layer": self.visible_faces["l_leg"]["front"]}) 1750 | 1751 | display_order.append({"r_leg_layer": self.back_faces}) 1752 | display_order.append({"r_leg": self.visible_faces["r_leg"]["front"]}) 1753 | display_order.append({"r_leg_layer": self.visible_faces["r_leg"]["front"]}) 1754 | 1755 | display_order.append({"l_arm_layer": self.back_faces}) 1756 | display_order.append({"l_arm": self.visible_faces["l_arm"]["front"]}) 1757 | display_order.append({"l_arm_layer": self.visible_faces["l_arm"]["front"]}) 1758 | 1759 | display_order.append({"torso_layer": self.back_faces}) 1760 | display_order.append({"torso": self.visible_faces["torso"]["front"]}) 1761 | display_order.append({"torso_layer": self.visible_faces["torso"]["front"]}) 1762 | 1763 | display_order.append({"helmet": self.back_faces}) 1764 | display_order.append({"head": self.visible_faces["head"]["front"]}) 1765 | display_order.append({"helmet": self.visible_faces["head"]["front"]}) 1766 | 1767 | display_order.append({"r_arm_layer": self.back_faces}) 1768 | display_order.append({"r_arm": self.visible_faces["r_arm"]["front"]}) 1769 | display_order.append({"r_arm_layer": self.visible_faces["r_arm"]["front"]}) 1770 | else: 1771 | display_order.append({"r_leg_layer": self.back_faces}) 1772 | display_order.append({"r_leg": self.visible_faces["r_leg"]["front"]}) 1773 | display_order.append({"r_leg_layer": self.visible_faces["r_leg"]["front"]}) 1774 | 1775 | display_order.append({"l_leg_layer": self.back_faces}) 1776 | display_order.append({"l_leg": self.visible_faces["l_leg"]["front"]}) 1777 | display_order.append({"l_leg_layer": self.visible_faces["l_leg"]["front"]}) 1778 | 1779 | display_order.append({"r_arm_layer": self.back_faces}) 1780 | display_order.append({"r_arm": self.visible_faces["r_arm"]["front"]}) 1781 | display_order.append({"r_arm_layer": self.visible_faces["r_arm"]["front"]}) 1782 | 1783 | display_order.append({"torso_layer": self.back_faces}) 1784 | display_order.append({"torso": self.visible_faces["torso"]["front"]}) 1785 | display_order.append({"torso_layer": self.visible_faces["torso"]["front"]}) 1786 | 1787 | display_order.append({"helmet": self.back_faces}) 1788 | display_order.append({"head": self.visible_faces["head"]["front"]}) 1789 | display_order.append({"helmet": self.visible_faces["head"]["front"]}) 1790 | 1791 | display_order.append({"l_arm_layer": self.back_faces}) 1792 | display_order.append({"l_arm": self.visible_faces["l_arm"]["front"]}) 1793 | display_order.append({"l_arm_layer": self.visible_faces["l_arm"]["front"]}) 1794 | 1795 | if "back" in self.front_faces: 1796 | display_order.append({"cape": self.visible_faces["cape"]["front"]}) 1797 | 1798 | else: 1799 | if "right" in self.front_faces: 1800 | display_order.append({"l_arm_layer": self.back_faces}) 1801 | display_order.append({"l_arm": self.visible_faces["l_arm"]["front"]}) 1802 | display_order.append({"l_arm_layer": self.visible_faces["l_arm"]["front"]}) 1803 | 1804 | display_order.append({"helmet": self.back_faces}) 1805 | display_order.append({"head": self.visible_faces["head"]["front"]}) 1806 | display_order.append({"helmet": self.visible_faces["head"]["front"]}) 1807 | 1808 | display_order.append({"torso_layer": self.back_faces}) 1809 | display_order.append({"torso": self.visible_faces["torso"]["front"]}) 1810 | display_order.append({"torso_layer": self.visible_faces["torso"]["front"]}) 1811 | 1812 | display_order.append({"r_arm_layer": self.back_faces}) 1813 | display_order.append({"r_arm": self.visible_faces["r_arm"]["front"]}) 1814 | display_order.append({"r_arm_layer": self.visible_faces["r_arm"]["front"]}) 1815 | 1816 | display_order.append({"l_leg_layer": self.back_faces}) 1817 | display_order.append({"l_leg": self.visible_faces["l_leg"]["front"]}) 1818 | display_order.append({"l_leg_layer": self.visible_faces["l_leg"]["front"]}) 1819 | 1820 | display_order.append({"r_leg_layer": self.back_faces}) 1821 | display_order.append({"r_leg": self.visible_faces["r_leg"]["front"]}) 1822 | display_order.append({"r_leg_layer": self.visible_faces["r_leg"]["front"]}) 1823 | else: 1824 | display_order.append({"r_arm_layer": self.back_faces}) 1825 | display_order.append({"r_arm": self.visible_faces["r_arm"]["front"]}) 1826 | display_order.append({"r_arm_layer": self.visible_faces["r_arm"]["front"]}) 1827 | 1828 | display_order.append({"helmet": self.back_faces}) 1829 | display_order.append({"head": self.visible_faces["head"]["front"]}) 1830 | display_order.append({"helmet": self.visible_faces["head"]["front"]}) 1831 | 1832 | display_order.append({"torso_layer": self.back_faces}) 1833 | display_order.append({"torso": self.visible_faces["torso"]["front"]}) 1834 | display_order.append({"torso_layer": self.visible_faces["torso"]["front"]}) 1835 | 1836 | display_order.append({"l_arm_layer": self.back_faces}) 1837 | display_order.append({"l_arm": self.visible_faces["l_arm"]["front"]}) 1838 | display_order.append({"l_arm_layer": self.visible_faces["l_arm"]["front"]}) 1839 | 1840 | display_order.append({"r_leg_layer": self.back_faces}) 1841 | display_order.append({"r_leg": self.visible_faces["r_leg"]["front"]}) 1842 | display_order.append({"r_leg_layer": self.visible_faces["r_leg"]["front"]}) 1843 | 1844 | display_order.append({"l_leg_layer": self.back_faces}) 1845 | display_order.append({"l_leg": self.visible_faces["l_leg"]["front"]}) 1846 | display_order.append({"l_leg_layer": self.visible_faces["l_leg"]["front"]}) 1847 | 1848 | if "back" in self.front_faces: 1849 | display_order.append({"cape": self.visible_faces["cape"]["front"]}) 1850 | 1851 | return display_order 1852 | 1853 | 1854 | class Point: 1855 | def __init__(self, super_cls, origin_coords: np.array): 1856 | self.is_projected: bool = False 1857 | self.super = super_cls 1858 | self.origin_coords: np.array = origin_coords 1859 | self.dest_coords: Optional[np.array] = None 1860 | 1861 | @property 1862 | def depth(self): 1863 | return self.dest_coords[2] 1864 | 1865 | def project(self, offset: np.array, rotation_matrix: np.array): 1866 | self.dest_coords = np.dot( 1867 | np.dot(self.origin_coords - offset, rotation_matrix) + offset, self.super.body_angles["general"] 1868 | ) 1869 | 1870 | self.super.min_x = min(self.super.min_x, self.dest_coords[0]) 1871 | self.super.max_x = max(self.super.max_x, self.dest_coords[0]) 1872 | self.super.min_y = min(self.super.min_y, self.dest_coords[1]) 1873 | self.super.max_y = max(self.super.max_y, self.dest_coords[1]) 1874 | self.is_projected = True 1875 | 1876 | 1877 | class Polygon: 1878 | def __init__(self, dots: List[Point], color, face="w", face_depth=0): 1879 | self.face = face 1880 | self.is_projected = False 1881 | self.face_depth = face_depth 1882 | self.dots = dots 1883 | self.color = color 1884 | coord_0 = dots[0].origin_coords 1885 | coord_1 = dots[1].origin_coords 1886 | coord_2 = dots[2].origin_coords 1887 | 1888 | if (coord_0[0] == coord_1[0]) and (coord_1[0] == coord_2[0]): 1889 | self.face = "x" 1890 | self.face_depth = coord_0[0] 1891 | elif (coord_0[1] == coord_1[1]) and (coord_1[1] == coord_2[1]): 1892 | self.face = "y" 1893 | self.face_depth = coord_0[1] 1894 | elif (coord_0[2] == coord_1[2]) and (coord_1[2] == coord_2[2]): 1895 | self.face = "z" 1896 | self.face_depth = coord_0[2] 1897 | 1898 | def add_png_polygon(self, draw, min_x, min_y, ratio): 1899 | points_2d = [] 1900 | 1901 | same_plan_x = True 1902 | same_plan_y = True 1903 | coord_x = None 1904 | coord_y = None 1905 | 1906 | for dot in self.dots: 1907 | coord = dot.dest_coords 1908 | if coord_x is None: 1909 | coord_x = coord[0] 1910 | if coord_y is None: 1911 | coord_y = coord[1] 1912 | if coord_x != coord[0]: 1913 | same_plan_x = False 1914 | if coord_y != coord[1]: 1915 | same_plan_y = False 1916 | points_2d.append(((coord[0] - min_x) * ratio, (coord[1] - min_y) * ratio)) 1917 | 1918 | if not (same_plan_x or same_plan_y): 1919 | draw.polygon(points_2d, fill=self.color, outline=self.color) 1920 | 1921 | def project(self, offset: np.array, rotation_matrix: np.array): 1922 | for dot in self.dots: 1923 | if not dot.is_projected: 1924 | dot.project(offset, rotation_matrix) 1925 | self.is_projected = True 1926 | 1927 | --------------------------------------------------------------------------------