├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── asset-sources ├── LICENSE.txt ├── control-icon-bg.png ├── control-icon-border.png ├── control-icon-corner.png ├── icons │ └── svg │ │ ├── acid.png │ │ ├── anchor.png │ │ ├── angel.png │ │ ├── aura.png │ │ ├── barrel.png │ │ ├── biohazard.png │ │ ├── blind.png │ │ ├── blood.png │ │ ├── bones.png │ │ ├── book.png │ │ ├── bridge.png │ │ ├── cancel.png │ │ ├── card-hand.png │ │ ├── card-joker.png │ │ ├── castle.png │ │ ├── cave.png │ │ ├── chest.png │ │ ├── circle.png │ │ ├── city.png │ │ ├── clockwork.png │ │ ├── coins.png │ │ ├── combat.png │ │ ├── cowled.png │ │ ├── d10-grey.png │ │ ├── d12-grey.png │ │ ├── d20-black.png │ │ ├── d20-grey.png │ │ ├── d20-highlight.png │ │ ├── d20.png │ │ ├── d4-grey.png │ │ ├── d6-grey.png │ │ ├── d8-grey.png │ │ ├── daze.png │ │ ├── deaf.png │ │ ├── degen.png │ │ ├── dice-target.png │ │ ├── direction.png │ │ ├── door-closed-outline.png │ │ ├── door-closed.png │ │ ├── door-exit.png │ │ ├── door-locked-outline.png │ │ ├── door-open-outline.png │ │ ├── door-secret-outline.png │ │ ├── door-steel.png │ │ ├── down.png │ │ ├── downgrade.png │ │ ├── explosion.png │ │ ├── eye.png │ │ ├── falling.png │ │ ├── fire-shield.png │ │ ├── fire.png │ │ ├── frozen.png │ │ ├── hanging-sign.png │ │ ├── hazard.png │ │ ├── heal.png │ │ ├── holy-shield.png │ │ ├── house.png │ │ ├── ice-aura.png │ │ ├── ice-shield.png │ │ ├── invisible.png │ │ ├── item-bag.png │ │ ├── lever.png │ │ ├── light-off.png │ │ ├── light.png │ │ ├── lightning.png │ │ ├── mage-shield.png │ │ ├── mole.png │ │ ├── mountain.png │ │ ├── mystery-man-black.png │ │ ├── mystery-man.png │ │ ├── net.png │ │ ├── oak.png │ │ ├── obelisk.png │ │ ├── padlock.png │ │ ├── paralysis.png │ │ ├── pawprint.png │ │ ├── pill.png │ │ ├── poison.png │ │ ├── radiation.png │ │ ├── regen.png │ │ ├── ruins.png │ │ ├── shield.png │ │ ├── silenced.png │ │ ├── skull.png │ │ ├── sleep.png │ │ ├── sound-off.png │ │ ├── sound.png │ │ ├── statue.png │ │ ├── stone-path.png │ │ ├── stoned.png │ │ ├── sun.png │ │ ├── sword.png │ │ ├── tankard.png │ │ ├── target.png │ │ ├── temple.png │ │ ├── terror.png │ │ ├── thrust.png │ │ ├── tower-flag.png │ │ ├── tower.png │ │ ├── trap.png │ │ ├── unconscious.png │ │ ├── up.png │ │ ├── upgrade.png │ │ ├── video.png │ │ ├── village.png │ │ ├── wall-direction.png │ │ ├── waterfall.png │ │ ├── windmill.png │ │ ├── wing.png │ │ └── wingfoot.png ├── noise │ ├── fbm2_3.png │ ├── fbm4.png │ ├── fbmHQ3.png │ └── noise.ptex └── spritesheets │ ├── base-icons-0.png │ └── base-icons-1.png ├── assets ├── control-icon-bg.png ├── control-icon-border.png ├── control-icon-corner.png ├── noise │ ├── fbm2_3.avif │ ├── fbm4.avif │ └── fbmHQ3.basis └── spritesheets │ ├── base-icons-0.basis │ ├── base-icons-0.json │ ├── base-icons-1.basis │ └── base-icons-1.json ├── eslint.config.js ├── img ├── after.png ├── before.png ├── default-foundry │ ├── 03-after-darkness.webp │ ├── 04-lighting.webp │ ├── 05-after-lighting.webp │ ├── 06-grid.webp │ ├── 07-first-token-ui.webp │ ├── 08-more-token-ui.webp │ ├── 09-erase.webp │ ├── 10-dragon-token-ui.webp │ ├── 12-methit-erase-and-ui.webp │ ├── 13-complete-ui.webp │ └── 14-done.webp └── short-version │ ├── 01-goblin-ui.webp │ ├── 02-erase-dragon-image.webp │ └── 03-final-comparison.webp ├── lang ├── de.json ├── en.json ├── fr.json └── pl.json ├── module.json ├── package-lock.json ├── package.json ├── src ├── DynamicSpriteSheet.ts ├── apps │ └── CustomSpritesheetConfig.ts ├── constants.ts ├── global.d.ts ├── hacks │ ├── effectsCaching.ts │ ├── index.ts │ ├── precomputedNoiseTextures.ts │ ├── spritesheetSubstitution.ts │ ├── tokenBarsCaching.ts │ ├── tokenRingSpritesheetSupport.ts │ └── useOooTokenRendering.ts ├── index.js ├── index.ts ├── init.ts ├── settings │ ├── constants.ts │ └── settings.ts └── utils │ ├── foundryShim.ts │ ├── getBitmapCacheResolution.ts │ ├── registerWrapper.ts │ └── wrapFunction.ts ├── templates └── custom-spritesheets-config.hbs ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | 3 | [*] 4 | charset=utf-8 5 | end_of_line=lf 6 | indent_style=tab 7 | indent_size=4 8 | trim_trailing_whitespace=true 9 | insert_final_newline=true 10 | max_line_length=115 11 | 12 | [*.{json,md}] 13 | # Hack to make Prettier not collapse arrays 14 | max_line_length=1 15 | 16 | [*.yml] 17 | indent_size = 4 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | packs/** binary 2 | *.lockb binary diff=lockb 3 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Release Creation 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | # build package 14 | - name: Build 15 | run: | 16 | npm ci 17 | npm run build 18 | 19 | # get part of the tag after the `v` 20 | - name: Extract tag version number 21 | id: get_version 22 | uses: battila7/get-version-action@v2 23 | 24 | # Substitute the Manifest and Download URLs in the module.json 25 | - name: Substitute Manifest and Download Links For Versioned Ones 26 | id: sub_manifest_link_version 27 | uses: microsoft/variable-substitution@v1 28 | with: 29 | files: 'module.json' 30 | env: 31 | version: ${{steps.get_version.outputs.version-without-v}} 32 | url: https://github.com/${{github.repository}} 33 | manifest: https://github.com/${{github.repository}}/releases/latest/download/module.json 34 | download: https://github.com/${{github.repository}}/releases/download/${{github.event.release.tag_name}}/module.zip 35 | 36 | # Create a zip file with all files required by the module to add to the release 37 | - run: zip -r ./module.zip module.json CHANGELOG.md README.md LICENSE lang/ dist/ 38 | 39 | # Create a release for this specific version 40 | - name: Update Release with Files 41 | id: create_version_release 42 | uses: ncipollo/release-action@v1 43 | with: 44 | allowUpdates: true # Set this to false if you want to prevent updating existing releases 45 | name: ${{ github.event.release.name }} 46 | draft: ${{ github.event.release.unpublished }} 47 | prerelease: ${{ github.event.release.prerelease }} 48 | token: ${{ secrets.GITHUB_TOKEN }} 49 | artifacts: './module.json, ./module.zip' 50 | tag: ${{ github.event.release.tag_name }} 51 | body: ${{ github.event.release.body }} 52 | 53 | - name: Publish to FoundryVTT 54 | uses: cs96and/FoundryVTT-release-package@v1.0.2 55 | if: ${{ !github.event.release.prerelease && env.PACKAGE_TOKEN }} 56 | env: 57 | PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }} 58 | with: 59 | package-token: ${{ env.PACKAGE_TOKEN }} 60 | manifest-url: https://github.com/${{ github.repository }}/releases/download/${{ github.event.release.tag_name }}/module.json 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | fvtt-perf-optim.* 3 | *timestamp* 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | pnpm-debug.log* 12 | lerna-debug.log* 13 | 14 | node_modules 15 | dist 16 | dist-ssr 17 | *.local 18 | .vite-cache 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | .DS_Store 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v22.12.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": true, 5 | "singleQuote": true, 6 | "semi": true, 7 | "overrides": [ 8 | { 9 | "files": ["*.scss", "*.css"], 10 | "options": { 11 | "requirePragma": false, 12 | "parser": "scss" 13 | } 14 | }, 15 | { 16 | "files": ["*.yml"], 17 | "options": { 18 | "tabWidth": 2 19 | } 20 | }, 21 | { 22 | "files": "*.html", 23 | "options": { 24 | "requirePragma": false, 25 | "parser": "html", 26 | "htmlWhitespaceSensitivity": "ignore" 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.9.2 2 | 3 | - Update polish translations (thanks @Lioheart) 4 | - Improve noise textures fidelity and size 5 | - Add optimized light shaders for black hole, roiling darkness, and light dome animations 6 | 7 | ## 0.9.1 8 | 9 | - Fix Foundry v12 compatibility 10 | 11 | ## 0.9.0 12 | 13 | - Add animated lights shader optimizations. The following animations have been optimized: Bewitching Wave, Fairy Light, Ghostly Light, Smoke Patch, Swirling Fog, Vortex 14 | 15 | ## 0.8.3 16 | 17 | - Update polish translations (thanks @Lioheart) 18 | 19 | ## 0.8.2 20 | 21 | - Re-Enable spritesheet based texture replacement 22 | - Improve render batching of ambient sounds and light overlays 23 | - Add settings menu to register custom spritesheets 24 | - Patch dynamic ring rendering code to support spritesheet token subject images 25 | 26 | ## 0.8.1 27 | 28 | - Revert 0.8.0 for now 29 | 30 | ## 0.8.0 31 | 32 | - Add spritesheet based icon replacement 33 | 34 | ## 0.7.0 35 | 36 | - Confirmed working in Foundry VTT v13 37 | 38 | ## 0.6.7 39 | 40 | - Increase texture caching size further for very small grid sizes 41 | 42 | ## 0.6.6 43 | 44 | - Increase resolution of cached textures for lowest performance setting 45 | 46 | ## 0.6.5 47 | 48 | - Fix issues with bar brawl module 49 | - Fix issues with effect hider module 50 | 51 | ## 0.6.4 52 | 53 | - Update polish translations (thanks @Lioheart) 54 | 55 | ## 0.6.3 56 | 57 | - Really Fix vision/light/etc calculation not working for certain walls (created bottom-to-top or right-to-left) 58 | 59 | ## 0.6.2 60 | 61 | - Fix vision/light/etc calculation not working for certain walls (created bottom-to-top or right-to-left) 62 | 63 | ## 0.6.1 64 | 65 | - Fix vision/light/etc calculation not working in when no bounding enclosing walls are in range 66 | 67 | ## 0.6.0 68 | 69 | - Improve performance of collision checks in foundry. This greatly improves occlusion checks for the levels 70 | module and calculation of aura templates for the pf2e system. 71 | 72 | ## 0.5.6 73 | 74 | - Fix bounds calculation for tokens. 75 | This should fix cases where interface elements were not covered correctly by token images 76 | 77 | ## 0.5.5 78 | 79 | - Fix token effects caching issues with modules that prevent refreshEffects render flag from being set 80 | 81 | ## 0.5.4 82 | 83 | - Fix token effects caching issues with Token Variant Art and potentially other modules 84 | 85 | ## 0.5.3 86 | 87 | - Fix token effect duplication in case of async \_drawEffect base implementation 88 | 89 | ## 0.5.2 90 | 91 | - Add support for modules overriding the token effects ui 92 | 93 | ## 0.5.1 94 | 95 | - Add support for barbrawl module 96 | - This module now requires libwrapper to be installed 97 | 98 | ## 0.4.0 99 | 100 | - Batch tokens with dynamic token ring together to minimize draw calls in void mesh 101 | drawing phase 102 | - Fix typos in readme and english translation 103 | 104 | ## 0.3.7 105 | 106 | - Add french translation. Thank you @Dolgrenn! 107 | 108 | ## 0.3.6 109 | 110 | - Add polish translation. Thank you @Lioheart! 111 | 112 | ## 0.3.5 113 | 114 | - Update module compatibility fields 115 | 116 | ## 0.3.4 117 | 118 | - Rename Module 119 | 120 | ## 0.3.3 121 | 122 | - Fix token hp bar resolution not scaling based on performance setting 123 | 124 | ## 0.3.2 125 | 126 | - Fix Token effect caching being enabled even for dorako UX radial token hud, leading to lower than expected fidelity. 127 | - Make texture cache resolution depend on performance setting. Increases fidelity on medium and maximum settings. 128 | 129 | ## 0.3.1 130 | 131 | - Enable culling when rendering children in batches 132 | 133 | ## 0.3.0 134 | 135 | - More intelligent batching by grouping tokens in sets of token that don't overlap with eachother 136 | 137 | ## 0.2.2 138 | 139 | - Fix blank canvas after deleting a token 140 | 141 | ## 0.2.1 142 | 143 | - Fix token ui elements being drawn when token itself should be invisible 144 | 145 | ## 0.2.0 146 | 147 | - New setting! Optimize Token UI Render Batching. Has the same impact as the old 148 | Optimize Interface Clipping option, but is more generall applicable, 149 | performant and does not require as many hacks and workarounds. 150 | - New setting! Cache Token Effects. This enables texture caching for token effect 151 | icons and improves the batch rendering. 152 | - Remove Cache Token Nameplate setting. It turns out text rendering can be efficiently 153 | batched on its own. 154 | - Remove Optimize Interface Clipping setting as the new Token UI Render Batchinging setting 155 | is a much cleaner solution and just as fast and with fewer problems. 156 | - Add German localization. 157 | - This module should now work for all systems and was for PF2e and D&D 5e specifically. 158 | 159 | ## 0.1.2 160 | 161 | - More fixes for errors that might result in a black canvas. 162 | 163 | ## 0.1.1 164 | 165 | - Fix token dragging sometimes rersulting in a black canvas. 166 | 167 | ## 0.1.0 168 | 169 | Initial Release! 170 | 171 | - Provide settings to activate or disable certain performance hacks in foundry. 172 | - All settings are enabled by default 173 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Codas 2 | 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: 3 | 4 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 5 | 6 | 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. 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Foundry VTT Prime Performance 2 | 3 | Buy Me a Coffee at ko-fi.com 4 | 5 | Increases your Foundry VTT Performance by your favorite prime number![^1][^2] Without impact on the visual fidelity[^3] 6 | 7 | Going from\ 8 | ![28 fps](img/before.png)\ 9 | to\ 10 | ![120 fps](img/after.png) 11 | 12 | In certain scenes with many tokens with resource bars, status effects etc visible. 13 | 14 | [^1]: If you favorite prime number is one of 2, 3, 5 or somewhere inbetween 15 | 16 | [^2]: Greatest speedup in scenes with many tokens, resource bars and status effects 17 | 18 | [^3]: HP bars and status effects might look a bit blurry on lowest foundry performance settings. 19 | 20 | ## DISCLAIMER 21 | 22 | This should be save and has been tested to the best of my ability. BUT foundry might just switch around stuff in future updates that makes these hacks obsolete (which would be the best case) or just breaks the module. 23 | 24 | Please use this module on your own risk and if you notice visual glitches (names, resource bars not working or other token-related stuff) please let me know so I can fix the issues :) 25 | 26 | This module does not persist any data except for settings, so a simple reload with the module disabled should revert everything back to normal. 27 | 28 | ## Expected performance gains 29 | 30 | The exact value or increase in performance is very difficult to estimate! All it really does is greatly reduce or even elimate the overhead each token has on the performance in a scene. With >20 tokens, each wich certain effect, nameplates visible etc, the improvement might very well be a doubling in framerate. In other very complex scenes with only 4-5 tokens and without token UI elements visible, the performance improvement might not even be noticable. 31 | 32 | My observation so far was: Performance improved (soometimes greatly) where needed and was good enough anyway otherwise. 33 | 34 | ## Settings 35 | 36 | This module provides some individual settings to activate certain hacks that aim to improve performance by optimizing the token drawing pipeline in Foundry. 37 | 38 | --- 39 | 40 | **Optimize Token UI Render Batching**\ 41 | Optimizes how token UI elements are erased by overlapping token images. 42 | Gives the highest performance gains in most scenarios when only few token names or hp bars are shown. 43 | 44 | **Cache token nameplates**\ 45 | Caches token nameplates to textures, allowing for better batching of the UI elements. 46 | 47 | **Cache token resource bars**\ 48 | Caches token resource bars to textures, allowing for better batching of the UI elements. This can result in slightly blockier hp bars in very high zoom levels. 49 | 50 | **Texture Replacement with Spritesheets**\ 51 | Replaces build in SVG icon rendering with a sprite atlas. Greatly reduces the amount of textures needed to render scenes, especially those with large amount of notes, different door states etc. Also introduces a sprite based replacement for control icon backgrounds to allow for more batching when rendering the interface layer. 52 | 53 | **Custom Spritesheets**\ 54 | Allows for the replacement of arbitrary textures referenced in foundry with optimized spritesheets. To create your own spritesheets to replace build-in textures, simply name the spritesheet frames like the asset path referenced in foundry, prefixed by a `#` sign. For example, if a tile in your scene references a texture in `assets/tiles/my-fancy-tile.webp`, the spritesheet frame should be named `#assets/tiles/my-fancy-tile.webp`. 55 | 56 | **Optimize animated lights**\ 57 | Patches the shader of certain animated lights that use fbm (fractal brownian motion) noise to use precomputed noise textures where possible. This requires each client to load a few additional textures with associated memory overhead, but in my testing this is absolytely worth it.\ 58 | Only of of the textures is actually saved in a GPU compressible format as it caused extremely blocky animations otherweise. If anyone as some ideas as to why this is, please create an issue or contact me on discord. :) 59 | 60 | If you create custom spritesheets, it is strongly recommended to use GPU compressible textures. Supported formats are basis_universal files (`.basis`) with ETC1 texture compression and zstandard supercompression for Foundry VTT v12 and `.ktx2` files with either ETC1 or UASTC texture format and zstandard supercompression for Foundry VTT v13. 61 | [TexturePacker](https://www.codeandweb.com/texturepacker) can be used to quickly create spritesheets and has built-in support for `.basis` files with ETC1 compression. 62 | 63 | The best use case for this currently is to create spritesheet textures for party actors. If you are also using dynamic tokens, don't forget to also enable **Dynamic Token Spritesheet Support**. Regular token graphics _should_ use premultiplied alpha (and setting the `'alpha_mode'` property in the spritesheet meta data object to `'pma'`). For token subject images premultiplied alpha is **required**. 64 | 65 | Future version of Prime Performance will likely contain a feature to create these spritesheets automatically and on a per-scene basis to optimize rendering in complex scenes with many tiles. 66 | 67 | **Dynamic Token Spritesheet Support**\ 68 | Patches the dynamic token ring rendering engine to support spritesheets for the token subjects. This should only be used if you also use **Custom Spritesheets** to replace token images. 69 | 70 | --- 71 | 72 | All Settings are considered save and activated by default. If only very few names or resource bars are shown by default (only on hover for example for every token), the caching of token nameplates and resource bars can be disabled. 73 | 74 | This module is currently expected to be used with Dorako UX's "adjust token effect hud" setting which includes some performance optimizations. A dedicated setting for the default foundry effect textures might come in the future. 75 | 76 | ## Did you notice any other performance issues in foundry? 77 | 78 | Please let me know when you experience performance issues. This whole project started because someone noticed performance issues with the "Adjust token effect HUD" setting of Dorako UX. 79 | 80 | For anyone who wants to learn more about the technical details of how foundry renders token and how these measures improve the performance, feel free to read on. 81 | 82 | ## Observed Performance Issues in Foundry VTT 83 | 84 | Foundry VTT has grown quite complex over the years! 85 | Its elevation handling, lighting, the new dynamic token rings etc. 86 | are great and I am still in awe at what Foundry VTT can handle. 87 | Especially the recent V12 update has brought with it numerous 88 | improvements to general canvas performance, elevation and token order handling, 89 | interface layer occlusion and so on. 90 | 91 | However, I also think that the performance in scenes, especially with multiple tokens 92 | has somewhat regressed, mainly because of how token occlusion is now handled. 93 | 94 | ### How Tokens are rendered in Foundry 95 | 96 | This is the frame we want to draw: 97 | ![alt text](img/default-foundry/14-done.webp) 98 | _Battlemap by [Lone Mapper](https://www.patreon.com/lonemapper)_ 99 | 100 | All in all, the background and lighting takes about 36 "Draw calls" to complete, while the token UI alone takes 45 more. There is not much we can do about the background drawing, but 45 calls for the UI layer seems exessive. And worse, each token adds 3-5 extra calls! That makes large scenes and battles especially demanding to draw. I'll skip over the nerdy details on how a frame is drawn, but needless to say, there is something we can do to improve it. 101 | 102 |
103 | Wait a minute, show me the nerdy details! 104 | To do something as simple as rendering a token, Foundry uses WebGL and there are quite a few layers to it. Lets focus on the important parts: 105 | 106 | First, the background, token icons, darkness layer and more is Drawn. This is actually done very efficiently and only takes 11 draw calls in webgl. Draw calls are essentially the point in a frame where the CPU transfers data to the GPU along with instruction on how to draw it. Then the CPU waits for the GPU to finish and after the frame has been drawn, the programm continues. Each draw call has a fixed overhead and is one of the more expensive operations in drawing in WebGL. 107 | 108 | After the initial layers have been drawn, the frame looks like this: 109 | ![alt text](img/default-foundry/03-after-darkness.webp) 110 | 111 | Almost done it seems! Next, lighting layer and effects are drawn and composited with the previous image. This takes another 26 draw calls. Considering 15 animated lights and 57 walls, this is not too bad. 112 | After lighting, the frame looks like this (note the lighting/bloom effects in the lava): 113 | ![alt text](img/default-foundry/05-after-lighting.webp) 114 | 115 | Only the grid and token interface UI (names, hp bars and status effects left)! 116 | Considering every token and the background layer was initially drawn to the canvas in just one call, how long can this take? 117 | It turns out, 118 | 119 | 8 Calls for the grid, which I don't exactly know why, but then its on to the tokens. And each token in foundry takes about 5 draw calls to complete if nameplates, hp bars and status icons are shown! All in all, just the token UI takes about 45 calls to complete. Lets count the steps for one token: 120 | 121 | Preparation: Create a new transparent image to draw everything, then lets look at the poor Kobold in the top left that is behind the dragons wing: 122 | 123 | 1. Clear the canvas below the token image. Why will become clear very soon. Since switching from painting to clearing breaks batching, this takes one call 124 | 1. Paint HP Bars 125 | 1. Paint Token Status Effect Background and borders 126 | 1. Paint Status Effect Icons 127 | 1. Paint Nameplates 128 | Depending on the situation, some but not all of these calls can be combined, but The clear + 2-3 more calls are common. 129 | 130 | Done it looks like this: 131 | ![alt text](img/default-foundry/08-more-token-ui.webp) 132 | 133 | Why the canvas is erased becomes clear when the next token, the dragon is drawn: We want to clear the ui behind the dragons wing! 134 | ![alt text](img/default-foundry/10-dragon-token-ui.webp) 135 | 136 | and the complete UI, which is then "just" set atop the background layer. 137 | ![alt text](img/default-foundry/13-complete-ui.webp) 138 | 139 |
140 | 141 | ### The problem 142 | 143 | It turns out, the main bottleneck is an erase step in which after a token UI is drawn, parts of it can be erased by an overlapping token. For example, the kobold UI in the top left is almost completely covered by the dragon wing. For this to be possible, Foundry VTT clears the region behind a tokens graphic before its interface is drawn. So the process is: 144 | **For every token:** 145 | 146 | 1. Draw token selection border if selected 147 | 2. erase everything that has previously been drawn that is behind the current tokens image 148 | 3. draw other UI elements. 149 | 150 | The result after the goblin and dragon have been drawn then looks something like this: 151 | 152 | Drawing Goblin UI 153 | 154 | Next, UI is cleared behind the dragon token. Final image is overlayed to make the clearing more visible: 155 | 156 | Erase UI behind dragon wing 157 | 158 | Switching from drawing to erasing sadly always breaks batching, meaning that a new call to the GPU to erase part of the image has to be made. In a perfect world, drawing all token UI elements for all tokens would just be 1-2 calls to the GPU in total, whereas it is curerntly 2-5 GPU calls per token. this is a major bottleneck in rendering and incurs additional load to both CPU and GPU. 159 | 160 | ### The solution 161 | 162 | Instead of this loop of drawing UI elements, clearing something behind the token, drawing more ui elements, ... we can in certain cases reorder the operations and make use of batching! 163 | 164 | #### Batching draw operations 165 | 166 | The calls to erase and draw ui elements normally are done in sequence, per token and looks roughly like this: 167 | 168 | 1. Erase behind Token 1 (new call) 169 | 1. Draw Token 1 UI (new call) 170 | 1. Erase behind Token 2 (new call) 171 | 1. Draw Token 2 UI (new call) 172 | 1. Erase behind Token 3 (new call) 173 | 1. Draw Token 3 UI (new call) 174 | 1. etc ... 175 | 176 | In caess where token UI elements are not covered by other tokens however, we can simply do this: 177 | 178 | 1. Erase behind Token 1 (new call) 179 | 1. Erase behind Token 2 180 | 1. Erase behind Token 3 181 | 1. etc ... 182 | 1. Draw Token 1 UI (new call) 183 | 1. Draw Token 2 UI 184 | 1. Draw Token 3 UI 185 | 1. etc ... 186 | 187 | Lets say drawing the token UI takes just two calls in total for: erase behind token, UI. This reordering changes the amount of total GPU calls needed from the Number of tokens \* 2 to simply 2, as no more switching between erasing and painting per token is needed. 188 | 189 | However, the world is not perfect and sometimes tokens overlap. If we just batch all these calls, token UI elements of tokens that are covered by other tokens would suddenly appear over each other again. No good! 190 | To remidy this, break the batch at the point in the token rendering order where we detect overlap and start another batch. 191 | 192 | This means that in the case that every token overlaps every other token, we have gained nothing. But in the common case where we have a few overlapping tokens and many that are completely separated, we could arrive at 4-6 GPU calls for 20+ tokens instead of 40-60! 193 | 194 | But now we face another problem: Drawing the hp bars and token effect isons also break the pipeline since we switch from drawing simple complex graphics to simple textures to graphics again. To make matters worse, every token effect icon has its own, separate texture which can someteimes be too much to batch in one go. 195 | 196 | #### Simple bitmap caching for token effects and resource bars 197 | 198 | Luckily, enabling caching for resource bars and effect icons is as simple as telling the graphics library foundry uses to enable caching. This just means that whenever one of the resources (HP, legendary actions, ...) of a token changes, we redraw the UI element once and then write this result to a texture. In later draw operations, no complicated graphics are drawn again. Instead, the texture cache is used to almost instantly (and batchable!) show the same image again. 199 | 200 | The same strategy can be used to cache the status efect icon rows, which most of the time change even less frequently. 201 | 202 | #### the result 203 | 204 | After all these steps, what have we gained? With all these optimizations in place and in a scene with only static light sources with many tokens and effects (typcal late-stage encounter), we go from 85 draw calls, 1092 individual webgl commands and 205 | 55 max FPS to 36 draw calls, 440 WebGL commands and 100+ FPS! Which means theses optimizations save about 8ms per frame! 206 | 207 | In more complex scenes the absolute gains in FPS might be lower and also depend very much on the hardware. 208 | -------------------------------------------------------------------------------- /asset-sources/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Prime performance uses a set of icons provided by Foundry VTT and sourced from the awesome https://game-icons.net where many creators and contributors release their icons for free under a Creative Commons by Attribution license. All icons compiled in the base-icons.png or base-icons.ktx2 spritesheet under the path "icons/svg" are provided by game-icons.net under a CC-BY 3.0 license(https://creativecommons.org/licenses/by/3.0/) and were created by numerous authors. The full list of authors responsible for all game-icons.net content can be found at: https://game-icons.net/about.html#authors 2 | -------------------------------------------------------------------------------- /asset-sources/control-icon-bg.png: -------------------------------------------------------------------------------- 1 | ../assets/control-icon-bg.png -------------------------------------------------------------------------------- /asset-sources/control-icon-border.png: -------------------------------------------------------------------------------- 1 | ../assets/control-icon-border.png -------------------------------------------------------------------------------- /asset-sources/control-icon-corner.png: -------------------------------------------------------------------------------- 1 | ../assets/control-icon-corner.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/acid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/acid.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/anchor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/anchor.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/angel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/angel.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/aura.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/aura.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/barrel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/barrel.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/biohazard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/biohazard.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/blind.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/blind.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/blood.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/blood.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/bones.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/bones.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/book.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/bridge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/bridge.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/cancel.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/card-hand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/card-hand.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/card-joker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/card-joker.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/castle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/castle.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/cave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/cave.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/chest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/chest.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/circle.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/city.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/city.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/clockwork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/clockwork.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/coins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/coins.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/combat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/combat.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/cowled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/cowled.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/d10-grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/d10-grey.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/d12-grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/d12-grey.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/d20-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/d20-black.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/d20-grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/d20-grey.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/d20-highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/d20-highlight.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/d20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/d20.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/d4-grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/d4-grey.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/d6-grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/d6-grey.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/d8-grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/d8-grey.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/daze.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/daze.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/deaf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/deaf.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/degen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/degen.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/dice-target.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/dice-target.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/direction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/direction.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/door-closed-outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/door-closed-outline.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/door-closed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/door-closed.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/door-exit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/door-exit.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/door-locked-outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/door-locked-outline.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/door-open-outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/door-open-outline.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/door-secret-outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/door-secret-outline.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/door-steel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/door-steel.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/down.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/downgrade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/downgrade.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/explosion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/explosion.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/eye.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/falling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/falling.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/fire-shield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/fire-shield.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/fire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/fire.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/frozen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/frozen.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/hanging-sign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/hanging-sign.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/hazard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/hazard.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/heal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/heal.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/holy-shield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/holy-shield.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/house.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/house.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/ice-aura.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/ice-aura.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/ice-shield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/ice-shield.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/invisible.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/invisible.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/item-bag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/item-bag.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/lever.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/lever.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/light-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/light-off.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/light.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/lightning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/lightning.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/mage-shield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/mage-shield.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/mole.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/mole.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/mountain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/mountain.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/mystery-man-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/mystery-man-black.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/mystery-man.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/mystery-man.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/net.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/net.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/oak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/oak.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/obelisk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/obelisk.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/padlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/padlock.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/paralysis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/paralysis.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/pawprint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/pawprint.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/pill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/pill.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/poison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/poison.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/radiation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/radiation.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/regen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/regen.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/ruins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/ruins.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/shield.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/shield.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/silenced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/silenced.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/skull.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/skull.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/sleep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/sleep.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/sound-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/sound-off.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/sound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/sound.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/statue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/statue.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/stone-path.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/stone-path.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/stoned.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/stoned.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/sun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/sun.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/sword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/sword.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/tankard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/tankard.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/target.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/target.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/temple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/temple.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/terror.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/terror.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/thrust.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/thrust.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/tower-flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/tower-flag.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/tower.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/tower.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/trap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/trap.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/unconscious.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/unconscious.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/up.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/upgrade.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/upgrade.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/video.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/village.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/village.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/wall-direction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/wall-direction.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/waterfall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/waterfall.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/windmill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/windmill.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/wing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/wing.png -------------------------------------------------------------------------------- /asset-sources/icons/svg/wingfoot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/icons/svg/wingfoot.png -------------------------------------------------------------------------------- /asset-sources/noise/fbm2_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/noise/fbm2_3.png -------------------------------------------------------------------------------- /asset-sources/noise/fbm4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/noise/fbm4.png -------------------------------------------------------------------------------- /asset-sources/noise/fbmHQ3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/noise/fbmHQ3.png -------------------------------------------------------------------------------- /asset-sources/noise/noise.ptex: -------------------------------------------------------------------------------- 1 | { 2 | "connections": [ 3 | { 4 | "from": "combine_2", 5 | "from_port": 0, 6 | "to": "tones_map_2", 7 | "to_port": 0 8 | }, 9 | { 10 | "from": "fbm3_3", 11 | "from_port": 0, 12 | "to": "combine_2", 13 | "to_port": 0 14 | }, 15 | { 16 | "from": "fbm3_4", 17 | "from_port": 0, 18 | "to": "combine_2", 19 | "to_port": 1 20 | }, 21 | { 22 | "from": "fbm3_2", 23 | "from_port": 0, 24 | "to": "tones_map", 25 | "to_port": 0 26 | }, 27 | { 28 | "from": "fbm3", 29 | "from_port": 0, 30 | "to": "tones_map_3", 31 | "to_port": 0 32 | }, 33 | { 34 | "from": "tones_map_3", 35 | "from_port": 0, 36 | "to": "combine", 37 | "to_port": 1 38 | }, 39 | { 40 | "from": "tones_map", 41 | "from_port": 0, 42 | "to": "combine", 43 | "to_port": 2 44 | }, 45 | { 46 | "from": "fbm3_6", 47 | "from_port": 0, 48 | "to": "tones_map_4", 49 | "to_port": 0 50 | }, 51 | { 52 | "from": "tones_map_4", 53 | "from_port": 0, 54 | "to": "combine", 55 | "to_port": 0 56 | }, 57 | { 58 | "from": "fbm3_5", 59 | "from_port": 0, 60 | "to": "combine_3", 61 | "to_port": 0 62 | }, 63 | { 64 | "from": "fbm3_7", 65 | "from_port": 0, 66 | "to": "combine_3", 67 | "to_port": 1 68 | }, 69 | { 70 | "from": "combine_3", 71 | "from_port": 0, 72 | "to": "tones_map_5", 73 | "to_port": 0 74 | } 75 | ], 76 | "label": "Graph", 77 | "longdesc": "", 78 | "name": "_Node_705", 79 | "node_position": { 80 | "x": 0.0, 81 | "y": 0.0 82 | }, 83 | "nodes": [ 84 | { 85 | "export_paths": { 86 | 87 | }, 88 | "name": "Material", 89 | "node_position": { 90 | "x": 974.87060546875, 91 | "y": 159.030883789062 92 | }, 93 | "parameters": { 94 | "albedo_color": { 95 | "a": 1.0, 96 | "b": 1.0, 97 | "g": 1.0, 98 | "r": 1.0, 99 | "type": "Color" 100 | }, 101 | "ao": 1.0, 102 | "depth_scale": 0.5, 103 | "emission_energy": 1.0, 104 | "flags_transparent": false, 105 | "metallic": 0.0, 106 | "normal": 1.0, 107 | "roughness": 1.0, 108 | "size": 12, 109 | "sss": 1.0 110 | }, 111 | "seed_int": 0, 112 | "type": "material" 113 | }, 114 | { 115 | "name": "combine", 116 | "node_position": { 117 | "x": 497.009613037109, 118 | "y": 344.505645751953 119 | }, 120 | "parameters": { 121 | 122 | }, 123 | "seed_int": 0, 124 | "type": "combine" 125 | }, 126 | { 127 | "name": "fbm3", 128 | "node_position": { 129 | "x": -121.244773864746, 130 | "y": 226.879898071289 131 | }, 132 | "parameters": { 133 | "folds": 0.0, 134 | "iterations": 3.0, 135 | "noise": 1, 136 | "offset": 0.0, 137 | "persistence": 0.5, 138 | "scale_x": 3.0, 139 | "scale_y": 3.0 140 | }, 141 | "seed_int": 3545316096, 142 | "seed_locked": true, 143 | "type": "fbm3" 144 | }, 145 | { 146 | "name": "fbm3_2", 147 | "node_position": { 148 | "x": -124.49690246582, 149 | "y": 499.532562255859 150 | }, 151 | "parameters": { 152 | "folds": 0.0, 153 | "iterations": 3.0, 154 | "noise": 1.0, 155 | "offset": 0.0, 156 | "persistence": 0.5, 157 | "scale_x": 15.0, 158 | "scale_y": 15.0 159 | }, 160 | "seed_int": 261850864, 161 | "type": "fbm3" 162 | }, 163 | { 164 | "generic_size": 1, 165 | "name": "tones_map", 166 | "node_position": { 167 | "x": 205.1796875, 168 | "y": 491.002868652344 169 | }, 170 | "parameters": { 171 | "in_max": 0.68, 172 | "in_min": 0.0, 173 | "out_max": 1.0, 174 | "out_min": -0.35 175 | }, 176 | "preview": 1, 177 | "seed_int": 0, 178 | "type": "tones_map" 179 | }, 180 | { 181 | "name": "fbm3_3", 182 | "node_position": { 183 | "x": -126.74137878418, 184 | "y": 764.778442382812 185 | }, 186 | "parameters": { 187 | "folds": 0.0, 188 | "iterations": 4.0, 189 | "noise": 1.0, 190 | "offset": 0.0, 191 | "persistence": 0.5, 192 | "scale_x": 3.0, 193 | "scale_y": 3.0 194 | }, 195 | "seed_int": 3545316096, 196 | "type": "fbm3" 197 | }, 198 | { 199 | "name": "fbm3_4", 200 | "node_position": { 201 | "x": -130.548919677734, 202 | "y": 1011.86840820312 203 | }, 204 | "parameters": { 205 | "folds": 0.0, 206 | "iterations": 4.0, 207 | "noise": 1.0, 208 | "offset": 0.0, 209 | "persistence": 0.5, 210 | "scale_x": 24.0, 211 | "scale_y": 24.0 212 | }, 213 | "seed_int": 3535832576, 214 | "type": "fbm3" 215 | }, 216 | { 217 | "name": "combine_2", 218 | "node_position": { 219 | "x": 334.624267578125, 220 | "y": 707.365905761719 221 | }, 222 | "parameters": { 223 | 224 | }, 225 | "seed_int": 0, 226 | "type": "combine" 227 | }, 228 | { 229 | "generic_size": 1, 230 | "name": "tones_map_2", 231 | "node_position": { 232 | "x": 542.456604003906, 233 | "y": 689.529724121094 234 | }, 235 | "parameters": { 236 | "in_max": 0.7, 237 | "in_min": 0.0, 238 | "out_max": 1.0, 239 | "out_min": -0.4 240 | }, 241 | "seed_int": 0, 242 | "type": "tones_map" 243 | }, 244 | { 245 | "name": "fbm3_6", 246 | "node_position": { 247 | "x": -108.140281677246, 248 | "y": -44.0681076049805 249 | }, 250 | "parameters": { 251 | "folds": 0.0, 252 | "iterations": 2.0, 253 | "noise": 1.0, 254 | "offset": 0.0, 255 | "persistence": 0.5, 256 | "scale_x": 5.0, 257 | "scale_y": 5.0 258 | }, 259 | "seed_int": 3545316096, 260 | "seed_locked": true, 261 | "type": "fbm3" 262 | }, 263 | { 264 | "generic_size": 1, 265 | "name": "tones_map_3", 266 | "node_position": { 267 | "x": 226.425018310547, 268 | "y": 286.419860839844 269 | }, 270 | "parameters": { 271 | "in_max": 0.68, 272 | "in_min": 0.0, 273 | "out_max": 1.0, 274 | "out_min": -0.35 275 | }, 276 | "preview": 1, 277 | "seed_int": 0, 278 | "type": "tones_map" 279 | }, 280 | { 281 | "generic_size": 1, 282 | "name": "tones_map_4", 283 | "node_position": { 284 | "x": 219.58251953125, 285 | "y": 2.45681238174438 286 | }, 287 | "parameters": { 288 | "in_max": 0.66, 289 | "in_min": 0.17, 290 | "out_max": 0.9, 291 | "out_min": 0.0 292 | }, 293 | "preview": 1, 294 | "seed_int": 0, 295 | "type": "tones_map" 296 | }, 297 | { 298 | "name": "fbm3_5", 299 | "node_position": { 300 | "x": -127.709785461426, 301 | "y": 1292.95288085938 302 | }, 303 | "parameters": { 304 | "folds": 0.0, 305 | "iterations": 4.0, 306 | "noise": 1, 307 | "offset": 0.0, 308 | "persistence": 0.5, 309 | "scale_x": 3.0, 310 | "scale_y": 3.0 311 | }, 312 | "seed_int": 3535832576, 313 | "type": "fbm3" 314 | }, 315 | { 316 | "name": "combine_3", 317 | "node_position": { 318 | "x": 350.179473876953, 319 | "y": 1312.78881835938 320 | }, 321 | "parameters": { 322 | 323 | }, 324 | "seed_int": 0, 325 | "type": "combine" 326 | }, 327 | { 328 | "name": "fbm3_7", 329 | "node_position": { 330 | "x": -125.520217895508, 331 | "y": 1543.70727539062 332 | }, 333 | "parameters": { 334 | "folds": 0.0, 335 | "iterations": 4.0, 336 | "noise": 1, 337 | "offset": 0.0, 338 | "persistence": 0.5, 339 | "scale_x": 20.0, 340 | "scale_y": 20.0 341 | }, 342 | "seed_int": 3535832576, 343 | "type": "fbm3" 344 | }, 345 | { 346 | "generic_size": 1, 347 | "name": "tones_map_5", 348 | "node_position": { 349 | "x": 559.882019042969, 350 | "y": 1312.7431640625 351 | }, 352 | "parameters": { 353 | "in_max": 1.0, 354 | "in_min": 0.0, 355 | "out_max": 1.0, 356 | "out_min": -0.1 357 | }, 358 | "seed_int": 0, 359 | "type": "tones_map" 360 | } 361 | ], 362 | "parameters": { 363 | 364 | }, 365 | "seed_int": 0, 366 | "shortdesc": "", 367 | "type": "graph" 368 | } -------------------------------------------------------------------------------- /asset-sources/spritesheets/base-icons-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/spritesheets/base-icons-0.png -------------------------------------------------------------------------------- /asset-sources/spritesheets/base-icons-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/asset-sources/spritesheets/base-icons-1.png -------------------------------------------------------------------------------- /assets/control-icon-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/assets/control-icon-bg.png -------------------------------------------------------------------------------- /assets/control-icon-border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/assets/control-icon-border.png -------------------------------------------------------------------------------- /assets/control-icon-corner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/assets/control-icon-corner.png -------------------------------------------------------------------------------- /assets/noise/fbm2_3.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/assets/noise/fbm2_3.avif -------------------------------------------------------------------------------- /assets/noise/fbm4.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/assets/noise/fbm4.avif -------------------------------------------------------------------------------- /assets/noise/fbmHQ3.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/assets/noise/fbmHQ3.basis -------------------------------------------------------------------------------- /assets/spritesheets/base-icons-0.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/assets/spritesheets/base-icons-0.basis -------------------------------------------------------------------------------- /assets/spritesheets/base-icons-0.json: -------------------------------------------------------------------------------- 1 | { 2 | "frames": { 3 | "#control-icon-bg": { 4 | "frame": { "x": 962, "y": 1350, "w": 4, "h": 4 }, 5 | "rotated": false, 6 | "trimmed": false, 7 | "spriteSourceSize": { "x": 0, "y": 0, "w": 4, "h": 4 }, 8 | "sourceSize": { "w": 4, "h": 4 } 9 | }, 10 | "#control-icon-border": { 11 | "frame": { "x": 3386, "y": 486, "w": 4, "h": 24 }, 12 | "rotated": false, 13 | "trimmed": false, 14 | "spriteSourceSize": { "x": 0, "y": 0, "w": 4, "h": 24 }, 15 | "sourceSize": { "w": 4, "h": 24 } 16 | }, 17 | "#control-icon-corner": { 18 | "frame": { "x": 1414, "y": 4070, "w": 24, "h": 24 }, 19 | "rotated": false, 20 | "trimmed": false, 21 | "spriteSourceSize": { "x": 0, "y": 0, "w": 24, "h": 24 }, 22 | "sourceSize": { "w": 24, "h": 24 } 23 | }, 24 | "#icons/svg/acid.svg": { 25 | "frame": { "x": 2, "y": 2, "w": 480, "h": 484 }, 26 | "rotated": false, 27 | "trimmed": true, 28 | "spriteSourceSize": { "x": 16, "y": 12, "w": 480, "h": 484 }, 29 | "sourceSize": { "w": 512, "h": 512 } 30 | }, 31 | "#icons/svg/anchor.svg": { 32 | "frame": { "x": 2906, "y": 486, "w": 476, "h": 476 }, 33 | "rotated": false, 34 | "trimmed": true, 35 | "spriteSourceSize": { "x": 16, "y": 20, "w": 476, "h": 476 }, 36 | "sourceSize": { "w": 512, "h": 512 } 37 | }, 38 | "#icons/svg/angel.svg": { 39 | "frame": { "x": 2906, "y": 966, "w": 476, "h": 464 }, 40 | "rotated": false, 41 | "trimmed": true, 42 | "spriteSourceSize": { "x": 20, "y": 28, "w": 476, "h": 464 }, 43 | "sourceSize": { "w": 512, "h": 512 } 44 | }, 45 | "#icons/svg/aura.svg": { 46 | "frame": { "x": 2, "y": 978, "w": 476, "h": 484 }, 47 | "rotated": false, 48 | "trimmed": true, 49 | "spriteSourceSize": { "x": 20, "y": 12, "w": 476, "h": 484 }, 50 | "sourceSize": { "w": 512, "h": 512 } 51 | }, 52 | "#icons/svg/barrel.svg": { 53 | "frame": { "x": 1874, "y": 3726, "w": 368, "h": 436 }, 54 | "rotated": true, 55 | "trimmed": true, 56 | "spriteSourceSize": { "x": 72, "y": 40, "w": 368, "h": 436 }, 57 | "sourceSize": { "w": 512, "h": 512 } 58 | }, 59 | "#icons/svg/biohazard.svg": { 60 | "frame": { "x": 482, "y": 2790, "w": 476, "h": 416 }, 61 | "rotated": false, 62 | "trimmed": true, 63 | "spriteSourceSize": { "x": 20, "y": 40, "w": 476, "h": 416 }, 64 | "sourceSize": { "w": 512, "h": 512 } 65 | }, 66 | "#icons/svg/blind.svg": { 67 | "frame": { "x": 2398, "y": 3222, "w": 464, "h": 360 }, 68 | "rotated": false, 69 | "trimmed": true, 70 | "spriteSourceSize": { "x": 24, "y": 76, "w": 464, "h": 360 }, 71 | "sourceSize": { "w": 512, "h": 512 } 72 | }, 73 | "#icons/svg/blood.svg": { 74 | "frame": { "x": 2, "y": 2438, "w": 476, "h": 476 }, 75 | "rotated": false, 76 | "trimmed": true, 77 | "spriteSourceSize": { "x": 16, "y": 16, "w": 476, "h": 476 }, 78 | "sourceSize": { "w": 512, "h": 512 } 79 | }, 80 | "#icons/svg/bones.svg": { 81 | "frame": { "x": 1454, "y": 970, "w": 476, "h": 472 }, 82 | "rotated": false, 83 | "trimmed": true, 84 | "spriteSourceSize": { "x": 20, "y": 20, "w": 476, "h": 472 }, 85 | "sourceSize": { "w": 512, "h": 512 } 86 | }, 87 | "#icons/svg/book.svg": { 88 | "frame": { "x": 1446, "y": 2398, "w": 452, "h": 468 }, 89 | "rotated": true, 90 | "trimmed": true, 91 | "spriteSourceSize": { "x": 32, "y": 24, "w": 452, "h": 468 }, 92 | "sourceSize": { "w": 512, "h": 512 } 93 | }, 94 | "#icons/svg/bridge.svg": { 95 | "frame": { "x": 2, "y": 3886, "w": 464, "h": 208 }, 96 | "rotated": false, 97 | "trimmed": true, 98 | "spriteSourceSize": { "x": 24, "y": 264, "w": 464, "h": 208 }, 99 | "sourceSize": { "w": 512, "h": 512 } 100 | }, 101 | "#icons/svg/cancel.svg": { 102 | "frame": { "x": 486, "y": 486, "w": 480, "h": 480 }, 103 | "rotated": false, 104 | "trimmed": true, 105 | "spriteSourceSize": { "x": 16, "y": 16, "w": 480, "h": 480 }, 106 | "sourceSize": { "w": 512, "h": 512 } 107 | }, 108 | "#icons/svg/card-hand.svg": { 109 | "frame": { "x": 2866, "y": 1930, "w": 460, "h": 348 }, 110 | "rotated": true, 111 | "trimmed": true, 112 | "spriteSourceSize": { "x": 24, "y": 80, "w": 460, "h": 348 }, 113 | "sourceSize": { "w": 512, "h": 512 } 114 | }, 115 | "#icons/svg/castle.svg": { 116 | "frame": { "x": 1918, "y": 2394, "w": 464, "h": 464 }, 117 | "rotated": false, 118 | "trimmed": true, 119 | "spriteSourceSize": { "x": 24, "y": 24, "w": 464, "h": 464 }, 120 | "sourceSize": { "w": 512, "h": 512 } 121 | }, 122 | "#icons/svg/cave.svg": { 123 | "frame": { "x": 2, "y": 2918, "w": 480, "h": 472 }, 124 | "rotated": true, 125 | "trimmed": true, 126 | "spriteSourceSize": { "x": 16, "y": 24, "w": 480, "h": 472 }, 127 | "sourceSize": { "w": 512, "h": 512 } 128 | }, 129 | "#icons/svg/chest.svg": { 130 | "frame": { "x": 3662, "y": 2014, "w": 432, "h": 336 }, 131 | "rotated": false, 132 | "trimmed": true, 133 | "spriteSourceSize": { "x": 40, "y": 88, "w": 432, "h": 336 }, 134 | "sourceSize": { "w": 512, "h": 512 } 135 | }, 136 | "#icons/svg/circle.svg": { 137 | "frame": { "x": 2414, "y": 970, "w": 472, "h": 472 }, 138 | "rotated": false, 139 | "trimmed": true, 140 | "spriteSourceSize": { "x": 20, "y": 20, "w": 472, "h": 472 }, 141 | "sourceSize": { "w": 512, "h": 512 } 142 | }, 143 | "#icons/svg/city.svg": { 144 | "frame": { "x": 1442, "y": 1446, "w": 464, "h": 472 }, 145 | "rotated": false, 146 | "trimmed": true, 147 | "spriteSourceSize": { "x": 24, "y": 20, "w": 464, "h": 472 }, 148 | "sourceSize": { "w": 512, "h": 512 } 149 | }, 150 | "#icons/svg/clockwork.svg": { 151 | "frame": { "x": 2, "y": 490, "w": 480, "h": 484 }, 152 | "rotated": false, 153 | "trimmed": true, 154 | "spriteSourceSize": { "x": 16, "y": 16, "w": 480, "h": 484 }, 155 | "sourceSize": { "w": 512, "h": 512 } 156 | }, 157 | "#icons/svg/combat.svg": { 158 | "frame": { "x": 3386, "y": 1118, "w": 424, "h": 480 }, 159 | "rotated": true, 160 | "trimmed": true, 161 | "spriteSourceSize": { "x": 48, "y": 16, "w": 424, "h": 480 }, 162 | "sourceSize": { "w": 512, "h": 512 } 163 | }, 164 | "#icons/svg/cowled.svg": { 165 | "frame": { "x": 486, "y": 970, "w": 376, "h": 480 }, 166 | "rotated": true, 167 | "trimmed": true, 168 | "spriteSourceSize": { "x": 64, "y": 16, "w": 376, "h": 480 }, 169 | "sourceSize": { "w": 512, "h": 512 } 170 | }, 171 | "#icons/svg/d4-grey.svg": { 172 | "frame": { "x": 3698, "y": 3218, "w": 64, "h": 56 }, 173 | "rotated": false, 174 | "trimmed": true, 175 | "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 56 }, 176 | "sourceSize": { "w": 64, "h": 64 } 177 | }, 178 | "#icons/svg/d6-grey.svg": { 179 | "frame": { "x": 3538, "y": 3542, "w": 56, "h": 56 }, 180 | "rotated": false, 181 | "trimmed": true, 182 | "spriteSourceSize": { "x": 4, "y": 4, "w": 56, "h": 56 }, 183 | "sourceSize": { "w": 64, "h": 64 } 184 | }, 185 | "#icons/svg/d8-grey.svg": { 186 | "frame": { "x": 3598, "y": 2006, "w": 56, "h": 64 }, 187 | "rotated": false, 188 | "trimmed": true, 189 | "spriteSourceSize": { "x": 4, "y": 0, "w": 56, "h": 64 }, 190 | "sourceSize": { "w": 64, "h": 64 } 191 | }, 192 | "#icons/svg/d10-grey.svg": { 193 | "frame": { "x": 3342, "y": 3542, "w": 64, "h": 64 }, 194 | "rotated": false, 195 | "trimmed": false, 196 | "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 }, 197 | "sourceSize": { "w": 64, "h": 64 } 198 | }, 199 | "#icons/svg/d12-grey.svg": { 200 | "frame": { "x": 3410, "y": 3542, "w": 64, "h": 64 }, 201 | "rotated": false, 202 | "trimmed": false, 203 | "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 }, 204 | "sourceSize": { "w": 64, "h": 64 } 205 | }, 206 | "#icons/svg/d20-black.svg": { 207 | "frame": { "x": 3478, "y": 3542, "w": 56, "h": 64 }, 208 | "rotated": false, 209 | "trimmed": true, 210 | "spriteSourceSize": { "x": 4, "y": 0, "w": 56, "h": 64 }, 211 | "sourceSize": { "w": 64, "h": 64 } 212 | }, 213 | "#icons/svg/d20-grey.svg": { 214 | "frame": { "x": 3598, "y": 2074, "w": 56, "h": 64 }, 215 | "rotated": false, 216 | "trimmed": true, 217 | "spriteSourceSize": { "x": 4, "y": 0, "w": 56, "h": 64 }, 218 | "sourceSize": { "w": 64, "h": 64 } 219 | }, 220 | "#icons/svg/d20-highlight.svg": { 221 | "frame": { "x": 2854, "y": 2394, "w": 408, "h": 472 }, 222 | "rotated": false, 223 | "trimmed": true, 224 | "spriteSourceSize": { "x": 52, "y": 20, "w": 408, "h": 472 }, 225 | "sourceSize": { "w": 512, "h": 512 } 226 | }, 227 | "#icons/svg/d20.svg": { 228 | "frame": { "x": 2866, "y": 3218, "w": 408, "h": 472 }, 229 | "rotated": true, 230 | "trimmed": true, 231 | "spriteSourceSize": { "x": 52, "y": 20, "w": 408, "h": 472 }, 232 | "sourceSize": { "w": 512, "h": 512 } 233 | }, 234 | "#icons/svg/daze.svg": { 235 | "frame": { "x": 970, "y": 970, "w": 480, "h": 464 }, 236 | "rotated": false, 237 | "trimmed": true, 238 | "spriteSourceSize": { "x": 16, "y": 28, "w": 480, "h": 464 }, 239 | "sourceSize": { "w": 512, "h": 512 } 240 | }, 241 | "#icons/svg/deaf.svg": { 242 | "frame": { "x": 1446, "y": 2854, "w": 448, "h": 464 }, 243 | "rotated": true, 244 | "trimmed": true, 245 | "spriteSourceSize": { "x": 32, "y": 24, "w": 448, "h": 464 }, 246 | "sourceSize": { "w": 512, "h": 512 } 247 | }, 248 | "#icons/svg/degen.svg": { 249 | "frame": { "x": 974, "y": 2, "w": 480, "h": 480 }, 250 | "rotated": false, 251 | "trimmed": true, 252 | "spriteSourceSize": { "x": 16, "y": 16, "w": 480, "h": 480 }, 253 | "sourceSize": { "w": 512, "h": 512 } 254 | }, 255 | "#icons/svg/dice-target.svg": { 256 | "frame": { "x": 970, "y": 486, "w": 480, "h": 480 }, 257 | "rotated": false, 258 | "trimmed": true, 259 | "spriteSourceSize": { "x": 16, "y": 16, "w": 480, "h": 480 }, 260 | "sourceSize": { "w": 512, "h": 512 } 261 | }, 262 | "#icons/svg/direction.svg": { 263 | "frame": { "x": 3890, "y": 2, "w": 184, "h": 480 }, 264 | "rotated": false, 265 | "trimmed": true, 266 | "spriteSourceSize": { "x": 164, "y": 16, "w": 184, "h": 480 }, 267 | "sourceSize": { "w": 512, "h": 512 } 268 | }, 269 | "#icons/svg/door-closed.svg": { 270 | "frame": { "x": 2314, "y": 3726, "w": 368, "h": 432 }, 271 | "rotated": true, 272 | "trimmed": true, 273 | "spriteSourceSize": { "x": 72, "y": 56, "w": 368, "h": 432 }, 274 | "sourceSize": { "w": 512, "h": 512 } 275 | }, 276 | "#icons/svg/door-exit.svg": { 277 | "frame": { "x": 3210, "y": 3630, "w": 448, "h": 464 }, 278 | "rotated": false, 279 | "trimmed": true, 280 | "spriteSourceSize": { "x": 32, "y": 28, "w": 448, "h": 464 }, 281 | "sourceSize": { "w": 512, "h": 512 } 282 | }, 283 | "#icons/svg/door-locked-outline.svg": { 284 | "frame": { "x": 3386, "y": 722, "w": 392, "h": 484 }, 285 | "rotated": true, 286 | "trimmed": true, 287 | "spriteSourceSize": { "x": 60, "y": 12, "w": 392, "h": 484 }, 288 | "sourceSize": { "w": 512, "h": 512 } 289 | }, 290 | "#icons/svg/door-open-outline.svg": { 291 | "frame": { "x": 478, "y": 3210, "w": 456, "h": 468 }, 292 | "rotated": false, 293 | "trimmed": true, 294 | "spriteSourceSize": { "x": 28, "y": 24, "w": 456, "h": 468 }, 295 | "sourceSize": { "w": 512, "h": 512 } 296 | }, 297 | "#icons/svg/down.svg": { 298 | "frame": { "x": 3890, "y": 2, "w": 184, "h": 480 }, 299 | "rotated": false, 300 | "trimmed": true, 301 | "spriteSourceSize": { "x": 164, "y": 16, "w": 184, "h": 480 }, 302 | "sourceSize": { "w": 512, "h": 512 } 303 | }, 304 | "#icons/svg/downgrade.svg": { 305 | "frame": { "x": 2386, "y": 2286, "w": 464, "h": 456 }, 306 | "rotated": false, 307 | "trimmed": true, 308 | "spriteSourceSize": { "x": 24, "y": 28, "w": 464, "h": 456 }, 309 | "sourceSize": { "w": 512, "h": 512 } 310 | }, 311 | "#icons/svg/explosion.svg": { 312 | "frame": { "x": 486, "y": 2, "w": 484, "h": 480 }, 313 | "rotated": false, 314 | "trimmed": true, 315 | "spriteSourceSize": { "x": 16, "y": 16, "w": 484, "h": 480 }, 316 | "sourceSize": { "w": 512, "h": 512 } 317 | }, 318 | "#icons/svg/falling.svg": { 319 | "frame": { "x": 1458, "y": 2, "w": 480, "h": 480 }, 320 | "rotated": false, 321 | "trimmed": true, 322 | "spriteSourceSize": { "x": 16, "y": 16, "w": 480, "h": 480 }, 323 | "sourceSize": { "w": 512, "h": 512 } 324 | }, 325 | "#icons/svg/fire.svg": { 326 | "frame": { "x": 482, "y": 1350, "w": 476, "h": 476 }, 327 | "rotated": false, 328 | "trimmed": true, 329 | "spriteSourceSize": { "x": 20, "y": 20, "w": 476, "h": 476 }, 330 | "sourceSize": { "w": 512, "h": 512 } 331 | }, 332 | "#icons/svg/frozen.svg": { 333 | "frame": { "x": 1454, "y": 486, "w": 480, "h": 480 }, 334 | "rotated": false, 335 | "trimmed": true, 336 | "spriteSourceSize": { "x": 16, "y": 12, "w": 480, "h": 480 }, 337 | "sourceSize": { "w": 512, "h": 512 } 338 | }, 339 | "#icons/svg/hanging-sign.svg": { 340 | "frame": { "x": 962, "y": 2398, "w": 480, "h": 452 }, 341 | "rotated": false, 342 | "trimmed": true, 343 | "spriteSourceSize": { "x": 16, "y": 36, "w": 480, "h": 452 }, 344 | "sourceSize": { "w": 512, "h": 512 } 345 | }, 346 | "#icons/svg/hazard.svg": { 347 | "frame": { "x": 3606, "y": 2354, "w": 476, "h": 436 }, 348 | "rotated": false, 349 | "trimmed": true, 350 | "spriteSourceSize": { "x": 16, "y": 32, "w": 476, "h": 436 }, 351 | "sourceSize": { "w": 512, "h": 512 } 352 | }, 353 | "#icons/svg/heal.svg": { 354 | "frame": { "x": 1910, "y": 1918, "w": 472, "h": 464 }, 355 | "rotated": true, 356 | "trimmed": true, 357 | "spriteSourceSize": { "x": 20, "y": 32, "w": 472, "h": 464 }, 358 | "sourceSize": { "w": 512, "h": 512 } 359 | }, 360 | "#icons/svg/house.svg": { 361 | "frame": { "x": 2, "y": 3402, "w": 480, "h": 472 }, 362 | "rotated": true, 363 | "trimmed": true, 364 | "spriteSourceSize": { "x": 16, "y": 16, "w": 480, "h": 472 }, 365 | "sourceSize": { "w": 512, "h": 512 } 366 | }, 367 | "#icons/svg/ice-aura.svg": { 368 | "frame": { "x": 1942, "y": 2, "w": 480, "h": 480 }, 369 | "rotated": false, 370 | "trimmed": true, 371 | "spriteSourceSize": { "x": 16, "y": 16, "w": 480, "h": 480 }, 372 | "sourceSize": { "w": 512, "h": 512 } 373 | }, 374 | "#icons/svg/invisible.svg": { 375 | "frame": { "x": 3394, "y": 470, "w": 248, "h": 488 }, 376 | "rotated": true, 377 | "trimmed": true, 378 | "spriteSourceSize": { "x": 132, "y": 12, "w": 248, "h": 488 }, 379 | "sourceSize": { "w": 512, "h": 512 } 380 | }, 381 | "#icons/svg/item-bag.svg": { 382 | "frame": { "x": 3698, "y": 3278, "w": 364, "h": 476 }, 383 | "rotated": false, 384 | "trimmed": true, 385 | "spriteSourceSize": { "x": 68, "y": 20, "w": 364, "h": 476 }, 386 | "sourceSize": { "w": 512, "h": 512 } 387 | }, 388 | "#icons/svg/lever.svg": { 389 | "frame": { "x": 1910, "y": 1446, "w": 468, "h": 468 }, 390 | "rotated": false, 391 | "trimmed": true, 392 | "spriteSourceSize": { "x": 16, "y": 28, "w": 468, "h": 468 }, 393 | "sourceSize": { "w": 512, "h": 512 } 394 | }, 395 | "#icons/svg/lightning.svg": { 396 | "frame": { "x": 2, "y": 1954, "w": 476, "h": 480 }, 397 | "rotated": false, 398 | "trimmed": true, 399 | "spriteSourceSize": { "x": 20, "y": 16, "w": 476, "h": 480 }, 400 | "sourceSize": { "w": 512, "h": 512 } 401 | }, 402 | "#icons/svg/mage-shield.svg": { 403 | "frame": { "x": 478, "y": 3682, "w": 412, "h": 456 }, 404 | "rotated": true, 405 | "trimmed": true, 406 | "spriteSourceSize": { "x": 52, "y": 20, "w": 412, "h": 456 }, 407 | "sourceSize": { "w": 512, "h": 512 } 408 | }, 409 | "#icons/svg/mole.svg": { 410 | "frame": { "x": 962, "y": 2854, "w": 480, "h": 364 }, 411 | "rotated": false, 412 | "trimmed": true, 413 | "spriteSourceSize": { "x": 16, "y": 104, "w": 480, "h": 364 }, 414 | "sourceSize": { "w": 512, "h": 512 } 415 | }, 416 | "#icons/svg/mountain.svg": { 417 | "frame": { "x": 1914, "y": 2862, "w": 480, "h": 440 }, 418 | "rotated": false, 419 | "trimmed": true, 420 | "spriteSourceSize": { "x": 16, "y": 16, "w": 480, "h": 440 }, 421 | "sourceSize": { "w": 512, "h": 512 } 422 | }, 423 | "#icons/svg/mystery-man-black.svg": { 424 | "frame": { "x": 2382, "y": 1906, "w": 376, "h": 480 }, 425 | "rotated": true, 426 | "trimmed": true, 427 | "spriteSourceSize": { "x": 64, "y": 16, "w": 376, "h": 480 }, 428 | "sourceSize": { "w": 512, "h": 512 } 429 | }, 430 | "#icons/svg/mystery-man.svg": { 431 | "frame": { "x": 486, "y": 970, "w": 376, "h": 480 }, 432 | "rotated": true, 433 | "trimmed": true, 434 | "spriteSourceSize": { "x": 64, "y": 16, "w": 376, "h": 480 }, 435 | "sourceSize": { "w": 512, "h": 512 } 436 | }, 437 | "#icons/svg/net.svg": { 438 | "frame": { "x": 2, "y": 1466, "w": 484, "h": 476 }, 439 | "rotated": true, 440 | "trimmed": true, 441 | "spriteSourceSize": { "x": 12, "y": 16, "w": 484, "h": 476 }, 442 | "sourceSize": { "w": 512, "h": 512 } 443 | }, 444 | "#icons/svg/oak.svg": { 445 | "frame": { "x": 3394, "y": 2, "w": 464, "h": 492 }, 446 | "rotated": true, 447 | "trimmed": true, 448 | "spriteSourceSize": { "x": 24, "y": 12, "w": 464, "h": 492 }, 449 | "sourceSize": { "w": 512, "h": 512 } 450 | }, 451 | "#icons/svg/padlock.svg": { 452 | "frame": { "x": 1390, "y": 3306, "w": 388, "h": 480 }, 453 | "rotated": true, 454 | "trimmed": true, 455 | "spriteSourceSize": { "x": 60, "y": 16, "w": 388, "h": 480 }, 456 | "sourceSize": { "w": 512, "h": 512 } 457 | }, 458 | "#icons/svg/paralysis.svg": { 459 | "frame": { "x": 482, "y": 1830, "w": 476, "h": 476 }, 460 | "rotated": false, 461 | "trimmed": true, 462 | "spriteSourceSize": { "x": 20, "y": 20, "w": 476, "h": 476 }, 463 | "sourceSize": { "w": 512, "h": 512 } 464 | }, 465 | "#icons/svg/poison.svg": { 466 | "frame": { "x": 1938, "y": 486, "w": 480, "h": 480 }, 467 | "rotated": false, 468 | "trimmed": true, 469 | "spriteSourceSize": { "x": 16, "y": 16, "w": 480, "h": 480 }, 470 | "sourceSize": { "w": 512, "h": 512 } 471 | }, 472 | "#icons/svg/radiation.svg": { 473 | "frame": { "x": 482, "y": 2310, "w": 476, "h": 476 }, 474 | "rotated": false, 475 | "trimmed": true, 476 | "spriteSourceSize": { "x": 16, "y": 16, "w": 476, "h": 476 }, 477 | "sourceSize": { "w": 512, "h": 512 } 478 | }, 479 | "#icons/svg/regen.svg": { 480 | "frame": { "x": 2426, "y": 2, "w": 480, "h": 480 }, 481 | "rotated": false, 482 | "trimmed": true, 483 | "spriteSourceSize": { "x": 16, "y": 16, "w": 480, "h": 480 }, 484 | "sourceSize": { "w": 512, "h": 512 } 485 | }, 486 | "#icons/svg/ruins.svg": { 487 | "frame": { "x": 962, "y": 1438, "w": 476, "h": 476 }, 488 | "rotated": false, 489 | "trimmed": true, 490 | "spriteSourceSize": { "x": 20, "y": 16, "w": 476, "h": 476 }, 491 | "sourceSize": { "w": 512, "h": 512 } 492 | }, 493 | "#icons/svg/shield.svg": { 494 | "frame": { "x": 2866, "y": 1446, "w": 304, "h": 480 }, 495 | "rotated": false, 496 | "trimmed": true, 497 | "spriteSourceSize": { "x": 104, "y": 16, "w": 304, "h": 480 }, 498 | "sourceSize": { "w": 512, "h": 512 } 499 | }, 500 | "#icons/svg/silenced.svg": { 501 | "frame": { "x": 3266, "y": 2330, "w": 336, "h": 480 }, 502 | "rotated": false, 503 | "trimmed": true, 504 | "spriteSourceSize": { "x": 88, "y": 16, "w": 336, "h": 480 }, 505 | "sourceSize": { "w": 512, "h": 512 } 506 | }, 507 | "#icons/svg/skull.svg": { 508 | "frame": { "x": 2422, "y": 486, "w": 480, "h": 480 }, 509 | "rotated": false, 510 | "trimmed": true, 511 | "spriteSourceSize": { "x": 16, "y": 16, "w": 480, "h": 480 }, 512 | "sourceSize": { "w": 512, "h": 512 } 513 | }, 514 | "#icons/svg/sleep.svg": { 515 | "frame": { "x": 3218, "y": 1546, "w": 440, "h": 456 }, 516 | "rotated": false, 517 | "trimmed": true, 518 | "spriteSourceSize": { "x": 32, "y": 24, "w": 440, "h": 456 }, 519 | "sourceSize": { "w": 512, "h": 512 } 520 | }, 521 | "#icons/svg/sound-off.svg": { 522 | "frame": { "x": 3218, "y": 2006, "w": 376, "h": 320 }, 523 | "rotated": false, 524 | "trimmed": true, 525 | "spriteSourceSize": { "x": 80, "y": 96, "w": 376, "h": 320 }, 526 | "sourceSize": { "w": 512, "h": 512 } 527 | }, 528 | "#icons/svg/sound.svg": { 529 | "frame": { "x": 3342, "y": 3218, "w": 352, "h": 320 }, 530 | "rotated": false, 531 | "trimmed": true, 532 | "spriteSourceSize": { "x": 80, "y": 96, "w": 352, "h": 320 }, 533 | "sourceSize": { "w": 512, "h": 512 } 534 | }, 535 | "#icons/svg/stoned.svg": { 536 | "frame": { "x": 1874, "y": 3306, "w": 416, "h": 480 }, 537 | "rotated": true, 538 | "trimmed": true, 539 | "spriteSourceSize": { "x": 36, "y": 16, "w": 416, "h": 480 }, 540 | "sourceSize": { "w": 512, "h": 512 } 541 | }, 542 | "#icons/svg/sun.svg": { 543 | "frame": { "x": 962, "y": 1918, "w": 476, "h": 476 }, 544 | "rotated": false, 545 | "trimmed": true, 546 | "spriteSourceSize": { "x": 20, "y": 16, "w": 476, "h": 476 }, 547 | "sourceSize": { "w": 512, "h": 512 } 548 | }, 549 | "#icons/svg/tankard.svg": { 550 | "frame": { "x": 938, "y": 3222, "w": 448, "h": 476 }, 551 | "rotated": false, 552 | "trimmed": true, 553 | "spriteSourceSize": { "x": 28, "y": 16, "w": 448, "h": 476 }, 554 | "sourceSize": { "w": 512, "h": 512 } 555 | }, 556 | "#icons/svg/target.svg": { 557 | "frame": { "x": 2910, "y": 2, "w": 480, "h": 480 }, 558 | "rotated": false, 559 | "trimmed": true, 560 | "spriteSourceSize": { "x": 16, "y": 16, "w": 480, "h": 480 }, 561 | "sourceSize": { "w": 512, "h": 512 } 562 | }, 563 | "#icons/svg/temple.svg": { 564 | "frame": { "x": 3662, "y": 1546, "w": 432, "h": 464 }, 565 | "rotated": false, 566 | "trimmed": true, 567 | "spriteSourceSize": { "x": 40, "y": 24, "w": 432, "h": 464 }, 568 | "sourceSize": { "w": 512, "h": 512 } 569 | }, 570 | "#icons/svg/thrust.svg": { 571 | "frame": { "x": 3322, "y": 2814, "w": 400, "h": 456 }, 572 | "rotated": true, 573 | "trimmed": true, 574 | "spriteSourceSize": { "x": 56, "y": 28, "w": 400, "h": 456 }, 575 | "sourceSize": { "w": 512, "h": 512 } 576 | }, 577 | "#icons/svg/tower-flag.svg": { 578 | "frame": { "x": 1414, "y": 3698, "w": 368, "h": 456 }, 579 | "rotated": true, 580 | "trimmed": true, 581 | "spriteSourceSize": { "x": 72, "y": 32, "w": 368, "h": 456 }, 582 | "sourceSize": { "w": 512, "h": 512 } 583 | }, 584 | "#icons/svg/tower.svg": { 585 | "frame": { "x": 938, "y": 3702, "w": 392, "h": 472 }, 586 | "rotated": true, 587 | "trimmed": true, 588 | "spriteSourceSize": { "x": 64, "y": 20, "w": 392, "h": 472 }, 589 | "sourceSize": { "w": 512, "h": 512 } 590 | }, 591 | "#icons/svg/trap.svg": { 592 | "frame": { "x": 3782, "y": 2794, "w": 480, "h": 312 }, 593 | "rotated": true, 594 | "trimmed": true, 595 | "spriteSourceSize": { "x": 16, "y": 112, "w": 480, "h": 312 }, 596 | "sourceSize": { "w": 512, "h": 512 } 597 | }, 598 | "#icons/svg/upgrade.svg": { 599 | "frame": { "x": 2750, "y": 3630, "w": 464, "h": 456 }, 600 | "rotated": true, 601 | "trimmed": true, 602 | "spriteSourceSize": { "x": 24, "y": 28, "w": 464, "h": 456 }, 603 | "sourceSize": { "w": 512, "h": 512 } 604 | }, 605 | "#icons/svg/wall-direction.svg": { 606 | "frame": { "x": 1442, "y": 1922, "w": 464, "h": 472 }, 607 | "rotated": false, 608 | "trimmed": true, 609 | "spriteSourceSize": { "x": 24, "y": 20, "w": 464, "h": 472 }, 610 | "sourceSize": { "w": 512, "h": 512 } 611 | }, 612 | "#icons/svg/waterfall.svg": { 613 | "frame": { "x": 2382, "y": 1446, "w": 480, "h": 456 }, 614 | "rotated": false, 615 | "trimmed": true, 616 | "spriteSourceSize": { "x": 16, "y": 28, "w": 480, "h": 456 }, 617 | "sourceSize": { "w": 512, "h": 512 } 618 | }, 619 | "#icons/svg/windmill.svg": { 620 | "frame": { "x": 2850, "y": 2870, "w": 344, "h": 468 }, 621 | "rotated": true, 622 | "trimmed": true, 623 | "spriteSourceSize": { "x": 84, "y": 20, "w": 344, "h": 468 }, 624 | "sourceSize": { "w": 512, "h": 512 } 625 | }, 626 | "#icons/svg/wing.svg": { 627 | "frame": { "x": 2398, "y": 2746, "w": 472, "h": 448 }, 628 | "rotated": true, 629 | "trimmed": true, 630 | "spriteSourceSize": { "x": 20, "y": 28, "w": 472, "h": 448 }, 631 | "sourceSize": { "w": 512, "h": 512 } 632 | }, 633 | "#icons/svg/wingfoot.svg": { 634 | "frame": { "x": 1934, "y": 970, "w": 476, "h": 472 }, 635 | "rotated": false, 636 | "trimmed": true, 637 | "spriteSourceSize": { "x": 20, "y": 20, "w": 476, "h": 472 }, 638 | "sourceSize": { "w": 512, "h": 512 } 639 | } 640 | }, 641 | "meta": { 642 | "app": "https://www.codeandweb.com/texturepacker", 643 | "version": "1.1", 644 | "image": "base-icons-0.basis", 645 | "format": "BASISU_ETC1S", 646 | "size": { "w": 4096, "h": 4096 }, 647 | "scale": "1", 648 | "related_multi_packs": ["base-icons-1.json"], 649 | "smartupdate": "$TexturePacker:SmartUpdate:df8c088bb10db08b7b97a5f43e7562cd:de60e47eb4799a3caa3a5885689c70a9:c758e3a44cea65e12ab0c0c458a7add7$" 650 | } 651 | } 652 | -------------------------------------------------------------------------------- /assets/spritesheets/base-icons-1.basis: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/assets/spritesheets/base-icons-1.basis -------------------------------------------------------------------------------- /assets/spritesheets/base-icons-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "frames": { 3 | "#icons/svg/card-joker.svg": { 4 | "frame": { "x": 1690, "y": 462, "w": 340, "h": 448 }, 5 | "rotated": false, 6 | "trimmed": true, 7 | "spriteSourceSize": { "x": 88, "y": 32, "w": 340, "h": 448 }, 8 | "sourceSize": { "w": 512, "h": 512 } 9 | }, 10 | "#icons/svg/coins.svg": { 11 | "frame": { "x": 346, "y": 1450, "w": 472, "h": 328 }, 12 | "rotated": true, 13 | "trimmed": true, 14 | "spriteSourceSize": { "x": 20, "y": 92, "w": 472, "h": 328 }, 15 | "sourceSize": { "w": 512, "h": 512 } 16 | }, 17 | "#icons/svg/door-closed-outline.svg": { 18 | "frame": { "x": 1038, "y": 2, "w": 328, "h": 464 }, 19 | "rotated": false, 20 | "trimmed": true, 21 | "spriteSourceSize": { "x": 92, "y": 24, "w": 328, "h": 464 }, 22 | "sourceSize": { "w": 512, "h": 512 } 23 | }, 24 | "#icons/svg/door-secret-outline.svg": { 25 | "frame": { "x": 1382, "y": 462, "w": 280, "h": 448 }, 26 | "rotated": false, 27 | "trimmed": true, 28 | "spriteSourceSize": { "x": 116, "y": 32, "w": 280, "h": 448 }, 29 | "sourceSize": { "w": 512, "h": 512 } 30 | }, 31 | "#icons/svg/door-steel.svg": { 32 | "frame": { "x": 1366, "y": 946, "w": 320, "h": 456 }, 33 | "rotated": false, 34 | "trimmed": true, 35 | "spriteSourceSize": { "x": 96, "y": 28, "w": 320, "h": 456 }, 36 | "sourceSize": { "w": 512, "h": 512 } 37 | }, 38 | "#icons/svg/eye.svg": { 39 | "frame": { "x": 2, "y": 1454, "w": 476, "h": 340 }, 40 | "rotated": true, 41 | "trimmed": true, 42 | "spriteSourceSize": { "x": 20, "y": 88, "w": 476, "h": 340 }, 43 | "sourceSize": { "w": 512, "h": 512 } 44 | }, 45 | "#icons/svg/fire-shield.svg": { 46 | "frame": { "x": 942, "y": 950, "w": 420, "h": 464 }, 47 | "rotated": false, 48 | "trimmed": true, 49 | "spriteSourceSize": { "x": 48, "y": 24, "w": 420, "h": 464 }, 50 | "sourceSize": { "w": 512, "h": 512 } 51 | }, 52 | "#icons/svg/holy-shield.svg": { 53 | "frame": { "x": 342, "y": 2, "w": 408, "h": 472 }, 54 | "rotated": false, 55 | "trimmed": true, 56 | "spriteSourceSize": { "x": 52, "y": 20, "w": 408, "h": 472 }, 57 | "sourceSize": { "w": 512, "h": 512 } 58 | }, 59 | "#icons/svg/ice-shield.svg": { 60 | "frame": { "x": 326, "y": 486, "w": 424, "h": 472 }, 61 | "rotated": false, 62 | "trimmed": true, 63 | "spriteSourceSize": { "x": 48, "y": 20, "w": 424, "h": 472 }, 64 | "sourceSize": { "w": 512, "h": 512 } 65 | }, 66 | "#icons/svg/light-off.svg": { 67 | "frame": { "x": 494, "y": 962, "w": 296, "h": 472 }, 68 | "rotated": false, 69 | "trimmed": true, 70 | "spriteSourceSize": { "x": 100, "y": 16, "w": 296, "h": 472 }, 71 | "sourceSize": { "w": 512, "h": 512 } 72 | }, 73 | "#icons/svg/light.svg": { 74 | "frame": { "x": 678, "y": 1438, "w": 296, "h": 472 }, 75 | "rotated": false, 76 | "trimmed": true, 77 | "spriteSourceSize": { "x": 100, "y": 16, "w": 296, "h": 472 }, 78 | "sourceSize": { "w": 512, "h": 512 } 79 | }, 80 | "#icons/svg/obelisk.svg": { 81 | "frame": { "x": 794, "y": 950, "w": 144, "h": 468 }, 82 | "rotated": false, 83 | "trimmed": true, 84 | "spriteSourceSize": { "x": 184, "y": 20, "w": 144, "h": 468 }, 85 | "sourceSize": { "w": 512, "h": 512 } 86 | }, 87 | "#icons/svg/pawprint.svg": { 88 | "frame": { "x": 1430, "y": 1406, "w": 360, "h": 452 }, 89 | "rotated": true, 90 | "trimmed": true, 91 | "spriteSourceSize": { "x": 84, "y": 16, "w": 360, "h": 452 }, 92 | "sourceSize": { "w": 512, "h": 512 } 93 | }, 94 | "#icons/svg/pill.svg": { 95 | "frame": { "x": 1022, "y": 478, "w": 464, "h": 356 }, 96 | "rotated": true, 97 | "trimmed": true, 98 | "spriteSourceSize": { "x": 24, "y": 68, "w": 464, "h": 356 }, 99 | "sourceSize": { "w": 512, "h": 512 } 100 | }, 101 | "#icons/svg/statue.svg": { 102 | "frame": { "x": 754, "y": 478, "w": 264, "h": 468 }, 103 | "rotated": false, 104 | "trimmed": true, 105 | "spriteSourceSize": { "x": 124, "y": 24, "w": 264, "h": 468 }, 106 | "sourceSize": { "w": 512, "h": 512 } 107 | }, 108 | "#icons/svg/stone-path.svg": { 109 | "frame": { "x": 2, "y": 2, "w": 336, "h": 480 }, 110 | "rotated": false, 111 | "trimmed": true, 112 | "spriteSourceSize": { "x": 88, "y": 16, "w": 336, "h": 480 }, 113 | "sourceSize": { "w": 512, "h": 512 } 114 | }, 115 | "#icons/svg/sword.svg": { 116 | "frame": { "x": 1370, "y": 2, "w": 456, "h": 456 }, 117 | "rotated": false, 118 | "trimmed": true, 119 | "spriteSourceSize": { "x": 28, "y": 28, "w": 456, "h": 456 }, 120 | "sourceSize": { "w": 512, "h": 512 } 121 | }, 122 | "#icons/svg/terror.svg": { 123 | "frame": { "x": 190, "y": 970, "w": 300, "h": 476 }, 124 | "rotated": false, 125 | "trimmed": true, 126 | "spriteSourceSize": { "x": 104, "y": 20, "w": 300, "h": 476 }, 127 | "sourceSize": { "w": 512, "h": 512 } 128 | }, 129 | "#icons/svg/unconscious.svg": { 130 | "frame": { "x": 2, "y": 486, "w": 320, "h": 480 }, 131 | "rotated": false, 132 | "trimmed": true, 133 | "spriteSourceSize": { "x": 88, "y": 16, "w": 320, "h": 480 }, 134 | "sourceSize": { "w": 512, "h": 512 } 135 | }, 136 | "#icons/svg/up.svg": { 137 | "frame": { "x": 2, "y": 970, "w": 184, "h": 480 }, 138 | "rotated": false, 139 | "trimmed": true, 140 | "spriteSourceSize": { "x": 164, "y": 16, "w": 184, "h": 480 }, 141 | "sourceSize": { "w": 512, "h": 512 } 142 | }, 143 | "#icons/svg/video.svg": { 144 | "frame": { "x": 754, "y": 2, "w": 472, "h": 280 }, 145 | "rotated": true, 146 | "trimmed": true, 147 | "spriteSourceSize": { "x": 20, "y": 116, "w": 472, "h": 280 }, 148 | "sourceSize": { "w": 512, "h": 512 } 149 | }, 150 | "#icons/svg/village.svg": { 151 | "frame": { "x": 978, "y": 1418, "w": 456, "h": 448 }, 152 | "rotated": true, 153 | "trimmed": true, 154 | "spriteSourceSize": { "x": 28, "y": 32, "w": 456, "h": 448 }, 155 | "sourceSize": { "w": 512, "h": 512 } 156 | } 157 | }, 158 | "meta": { 159 | "app": "https://www.codeandweb.com/texturepacker", 160 | "version": "1.1", 161 | "image": "base-icons-1.basis", 162 | "format": "BASISU_ETC1S", 163 | "size": { "w": 2048, "h": 2048 }, 164 | "scale": "1", 165 | "related_multi_packs": ["base-icons-0.json"], 166 | "smartupdate": "$TexturePacker:SmartUpdate:df8c088bb10db08b7b97a5f43e7562cd:de60e47eb4799a3caa3a5885689c70a9:c758e3a44cea65e12ab0c0c458a7add7$" 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config'; 2 | 3 | export default antfu({ 4 | formatters: false, 5 | 6 | stylistic: { 7 | indent: 'tab', 8 | quotes: 'single', 9 | semi: true, 10 | }, 11 | 12 | rules: { 13 | 'antfu/consistent-list-newline': 'warn', 14 | 'antfu/if-newline': 'off', 15 | 'import/no-mutable-exports': 'off', 16 | 'style/brace-style': ['error', '1tbs', { allowSingleLine: true }], 17 | 'unused-imports/no-unused-vars': 'warn', 18 | }, 19 | 20 | ignores: [], 21 | }); 22 | -------------------------------------------------------------------------------- /img/after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/img/after.png -------------------------------------------------------------------------------- /img/before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/img/before.png -------------------------------------------------------------------------------- /img/default-foundry/03-after-darkness.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/img/default-foundry/03-after-darkness.webp -------------------------------------------------------------------------------- /img/default-foundry/04-lighting.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/img/default-foundry/04-lighting.webp -------------------------------------------------------------------------------- /img/default-foundry/05-after-lighting.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/img/default-foundry/05-after-lighting.webp -------------------------------------------------------------------------------- /img/default-foundry/06-grid.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/img/default-foundry/06-grid.webp -------------------------------------------------------------------------------- /img/default-foundry/07-first-token-ui.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/img/default-foundry/07-first-token-ui.webp -------------------------------------------------------------------------------- /img/default-foundry/08-more-token-ui.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/img/default-foundry/08-more-token-ui.webp -------------------------------------------------------------------------------- /img/default-foundry/09-erase.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/img/default-foundry/09-erase.webp -------------------------------------------------------------------------------- /img/default-foundry/10-dragon-token-ui.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/img/default-foundry/10-dragon-token-ui.webp -------------------------------------------------------------------------------- /img/default-foundry/12-methit-erase-and-ui.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/img/default-foundry/12-methit-erase-and-ui.webp -------------------------------------------------------------------------------- /img/default-foundry/13-complete-ui.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/img/default-foundry/13-complete-ui.webp -------------------------------------------------------------------------------- /img/default-foundry/14-done.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/img/default-foundry/14-done.webp -------------------------------------------------------------------------------- /img/short-version/01-goblin-ui.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/img/short-version/01-goblin-ui.webp -------------------------------------------------------------------------------- /img/short-version/02-erase-dragon-image.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/img/short-version/02-erase-dragon-image.webp -------------------------------------------------------------------------------- /img/short-version/03-final-comparison.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/4fd1e0c9a291791cf40c789ece8cec64ab00d3d1/img/short-version/03-final-comparison.webp -------------------------------------------------------------------------------- /lang/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "fvtt-perf-optim": { 3 | "settings": { 4 | "optimize-interface-layer-clipping": { 5 | "name": "Optimiere Render-Reihenfolge von Token UI-Elementen", 6 | "hint": "Optimiert, in welcher Reihenfolge die Token UI Elemente gerendert werden für verbessertes Draw Call Batching. Keine optimische Beeinflussung der Tokens zu erwarten. Wird benötigt, damit die anderen Einstellunge einen merkbaren Einfluss haben." 7 | }, 8 | "token-bars-caching": { 9 | "name": "Token-Ressourceleisten cachen", 10 | "hint": "Cacht die Token-Ressourcenleisten Draw Batching zu verbessern. Kann auf hohen Zoomstufen geringe negative Auswirkung auf die Qualität der haben." 11 | }, 12 | "token-effects-caching": { 13 | "name": "Token-Efekte cachen", 14 | "hint": "Cacht die Token-Effektleiste. Kann auf hohen Zoomstufen geringe negative Auswirkung auf die Qualität haben." 15 | }, 16 | "spritesheet-substitution": { 17 | "name": "Austausch von Texturen mit Spritesheets", 18 | "hint": "Tauscht gewisse Icons und andere Grafikelemente von Foundry gegen Spritesheets aus. Dies kann die Leistung und den Resourcenverbrauch bei der Anzeige von vielen UI Icons für Journals, Lichter, Türen etc verbessern." 19 | }, 20 | "spritesheet-substitution-custom": { 21 | "name": "Eigene Spritesheets", 22 | "label": "Verwalte eigene Spritesheets", 23 | "hint": "Eigene Spritesheets können verwendet werden, um Grafiken von Effekten, Tiles, Tokens, Szenen und anderen Objekten auszutauschen.", 24 | "menu": { 25 | "add": "Eigenes Spritesheet hinzufügen", 26 | "spritesheet": "Wähle Spritesheet aus", 27 | "delete": "Lösche Spritesheet", 28 | "title": "Verwalte eigene Spritesheets", 29 | "warn-no-src": "Du musst einen Spritesheet-Pfad auswählen, um ihn hinzuzufügen", 30 | "warn-no-json": "Die ausgewählte Datei muss eine gültige pixi.js Spritesheet JSON-Datei sein", 31 | "warn-inalid-asset": "Die ausgewählte Datei enthält keine gültigen pixi.js Spritesheet-Daten", 32 | "warn-init-failed": "Prime Performance: Fehler beim laden der Standard-Spritesheets. Optimierung durch Spritesheets wird deaktiviert. Bitte prüfe die Konsole auf weitere Details.", 33 | "warn-custom-spritesheets-failed": "Prime Performance: Fehler beim laden von einen oder mehreren eigenen Spritesheets. Bitte prüfe die Konsole auf weitere Details." 34 | } 35 | }, 36 | "token-ring-spritesheet-support": { 37 | "name": "Unterstützung für Spritesheets im dynamischen Token-Ring", 38 | "hint": "Erlaubt es, Token-Grafiken mit dynamischen Ringen durch Spritesheet-Texturen zu ersetzen. Dies ist nur nötig, wenn du dynamische Token-Ringe und den Austausch von Spritesheets verwendest, um Token-Grafiken (subject textures) auszutauschen." 39 | }, 40 | "precomputed-noise-textures": { 41 | "name": "Optimierte animierte Lichter", 42 | "hint": "Optimiert den Shader von animierten Lichtern (Bewitching Wave, Fairy Light, Ghostly Light, Smoke Patch, Swirling Fog, Vortex) indem vorberechnete Texturen für das FBM Rauschen verwendet werden." 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "fvtt-perf-optim": { 3 | "settings": { 4 | "optimize-interface-layer-clipping": { 5 | "name": "Optimize Token UI Render Batching", 6 | "hint": "Optimizes render batching of Token UI elements. No visual degredation expected. Needed in order for the other settings to have a noticable impact." 7 | }, 8 | "token-bars-caching": { 9 | "name": "Cache Token Resource Bars", 10 | "hint": "Caches token resource bars to textures. Very minor visual degradataion on high zoom levels expected." 11 | }, 12 | "token-effects-caching": { 13 | "name": "Cache Token Effects", 14 | "hint": "Caches the token effects bar. Very minor visual degradataion on high zoom levels expected." 15 | }, 16 | "spritesheet-substitution": { 17 | "name": "Texture Replacement with Spritesheets", 18 | "hint": "Substitutes core SVG icons and other graphic elements with optimized spriteshseets. This can improve performance and resource usage when displaying many UI icons for journals, lights, doors, etc." 19 | }, 20 | "spritesheet-substitution-custom": { 21 | "name": "Custom Spritesheets", 22 | "label": "Manage Custom Spritesheets", 23 | "hint": "Custom spritesheets can be used to replace more core or custom Icons and textures, for example custom condition marker, token art or tiles for improved performance", 24 | "menu": { 25 | "add": "Add Custom Spritesheet", 26 | "spritesheet": "Select Custom Spritesheet", 27 | "delete": "Delete Custom Spritesheet", 28 | "title": "Manage Custom Spritesheets", 29 | "warn-no-src": "You must select a spritesheet source to add it", 30 | "warn-no-json": "The file you select must be a valid pixi.js spritesheet JSON file", 31 | "warn-inalid-asset": "The file you select does not contain valid pixi.js spritesheet data", 32 | "warn-init-failed": "Prime Performance: Failed to load default icon substitution spritesheets. Spritesheet optimizations will be disabled. Please check the console for more details.", 33 | "warn-custom-spritesheets-failed": "Prime Performance: Failed to load one or more custom spritesheet. Please check the console for more details." 34 | } 35 | }, 36 | "token-ring-spritesheet-support": { 37 | "name": "Dynamic Token Spritesheet Support", 38 | "hint": "Allows tokens with dynamic rings to be replaced with spritesheet textures. This is only needed if you use dynamic token rings and the spritesheet substitution feature to replace token textures." 39 | }, 40 | "precomputed-noise-textures": { 41 | "name": "Optimize animated lights", 42 | "hint": "Optimizes the shader of certain animated lights (Bewitching Wave, Fairy Light, Ghostly Light, Smoke Patch, Swirling Fog, Vortex) by utilizing precomputed FBM noise textures." 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lang/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "fvtt-perf-optim": { 3 | "settings": { 4 | "optimize-interface-layer-clipping": { 5 | "name": "Optimise le rendu de l'IU des tokens", 6 | "hint": "Optimise le rendu des éléments de l'IU des jetons. Aucune dégradation visuelle n'est attendue. Nécessaire pour que les autres paramètres aient un impact notable." 7 | }, 8 | "token-bars-caching": { 9 | "name": "Mise en cache des barres de ressources des tokens", 10 | "hint": "Met en cache les barres de ressources des jetons dans les textures. Une très légère dégradation visuelle est attendue à des niveaux de zoom élevés." 11 | }, 12 | "token-effects-caching": { 13 | "name": "mise en cache des effets de token", 14 | "hint": "Mise en cache de la barre d'effets de token. Une dégradation visuelle très mineure est attendue pour les niveaux de zoom élevés." 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lang/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "fvtt-perf-optim": { 3 | "settings": { 4 | "optimize-interface-layer-clipping": { 5 | "name": "Optymalizacja wsadowego renderowania Token UI", 6 | "hint": "Optymalizuje renderowanie elementów interfejsu użytkownika Tokena. Nie przewiduje się pogorszenia wyglądu. Wymagane, aby inne ustawienia miały zauważalny wpływ." 7 | }, 8 | "token-bars-caching": { 9 | "name": "Paski zasobów tokenów w pamięci podręcznej", 10 | "hint": "Paski zasobów tokenów w pamięci podręcznej do tekstur. Oczekiwana bardzo niewielka degradacja wizualna na wysokich poziomach powiększenia." 11 | }, 12 | "token-effects-caching": { 13 | "name": "Efekty tokenów w pamięci podręcznej", 14 | "hint": "Przechowuje w pamięci podręcznej pasek efektów tokenów. Oczekiwana bardzo niewielka degradacja wizualna na wysokich poziomach powiększenia." 15 | }, 16 | "spritesheet-substitution": { 17 | "name": "Zastąpienie tekstur szablonami sprite'ów", 18 | "hint": "Zastępuje podstawowe ikony SVG i inne elementy graficzne zoptymalizowanymi zestawami sprite'ów. Może to poprawić wydajność i wykorzystanie zasobów podczas wyświetlania wielu ikon interfejsu użytkownika dla dzienników, świateł, drzwi itp." 19 | }, 20 | "spritesheet-substitution-custom": { 21 | "name": "Niestandardowe zestawy sprite'ów", 22 | "label": "Zarządzaj niestandardowymi zestawami sprite'ów", 23 | "hint": "Niestandardowe zestawy sprite'ów mogą być używane do zastąpienia bardziej podstawowych lub niestandardowych ikon i tekstur, na przykład niestandardowych znaczników stanu, grafiki tokenów lub kafelków, aby poprawić wydajność.", 24 | "menu": { 25 | "add": "Dodaj niestandardowy zestaw sprite'ów", 26 | "spritesheet": "Wybierz niestandardowy zestaw sprite'ów", 27 | "delete": "Usuń niestandardowy zestaw sprite'ów", 28 | "title": "Zarządzaj niestandardowymi zestawami sprite'ów", 29 | "warn-no-src": "Musisz wybrać źródło zestawu sprite'ów, aby go dodać.", 30 | "warn-no-json": "Wybrany plik musi być prawidłowym plikiem JSON zawierającym zestaw sprite'ów pixi.js.", 31 | "warn-inalid-asset": "Wybrany plik nie zawiera prawidłowych danych zestawu sprite'ów pixi.js.", 32 | "warn-init-failed": "Prime Performance: Nie udało się załadować domyślnych zestawów sprite'ów zastępujących ikony. Optymalizacja zestawów sprite'ów zostanie wyłączona. Więcej szczegółów znajdziesz w konsoli.", 33 | "warn-custom-spritesheets-failed": "Prime Performance: Nie udało się załadować jednego lub więcej niestandardowych zestawów sprite'ów. Sprawdź konsolę, aby uzyskać więcej informacji." 34 | } 35 | }, 36 | "token-ring-spritesheet-support": { 37 | "name": "Obsługa zestawów sprite'ów tokenów dynamicznych", 38 | "hint": "Umożliwia zastąpienie tokenów z dynamicznymi pierścieniami teksturami zestawu sprite'ów. Jest to konieczne tylko w przypadku używania dynamicznych pierścieni tokenów i funkcji zastępowania tokenów teksturami zestawu sprite'ów." 39 | }, 40 | "precomputed-noise-textures": { 41 | "name": "Optymalizacja animowanych świateł", 42 | "hint": "Optymalizuje shader niektórych animowanych świateł (Czarująca fala, Wróżkowe światło, Widmowe światło, Zadymiona ścieżka, Wirująca mgła, Wir) poprzez wykorzystanie wstępnie obliczonych tekstur szumu FBM." 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /module.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "fvtt-perf-optim", 3 | "title": "Prime Performance", 4 | "version": "0.9.2", 5 | "description": "Provides a collection of unofficial performance optimizations and hacks for foundry vtt.", 6 | "esmodules": ["dist/fvtt-perf-optim.js"], 7 | "languages": [ 8 | { 9 | "lang": "en", 10 | "name": "English", 11 | "path": "lang/en.json", 12 | "flags": {} 13 | }, 14 | { 15 | "lang": "de", 16 | "name": "Deutsch", 17 | "path": "lang/de.json", 18 | "flags": {} 19 | }, 20 | { 21 | "lang": "pl", 22 | "name": "Polski", 23 | "path": "lang/pl.json", 24 | "flags": {} 25 | }, 26 | { 27 | "lang": "fr", 28 | "name": "French", 29 | "path": "lang/fr.json", 30 | "flags": {} 31 | } 32 | ], 33 | "compatibility": { 34 | "minimum": "12.324", 35 | "verified": "13.341", 36 | "maximum": "13" 37 | }, 38 | "relationships": { 39 | "requires": [ 40 | { 41 | "id": "lib-wrapper", 42 | "type": "module", 43 | "compatibility": { 44 | "minimum": "1.0.0.0", 45 | "verified": "1.12.14.0" 46 | } 47 | } 48 | ], 49 | "systems": [] 50 | }, 51 | "authors": [ 52 | { 53 | "name": "Codas", 54 | "discord": "Codas" 55 | } 56 | ], 57 | "url": "https://github.com/Codas/foundryvtt-performance-hacks", 58 | "bugs": "https://github.com/Codas/foundryvtt-performance-hacks/issues", 59 | "readme": "https://github.com/Codas/foundryvtt-performance-hacks/blob/main/README.md", 60 | "changelog": "https://github.com/Codas/foundryvtt-performance-hacks/releases", 61 | "manifest": "https://raw.githubusercontent.com/Codas/foundryvtt-performance-hacks/main/module.json", 62 | "download": "https://github.com/Codas/foundryvtt-performance-hacks/aaprchive/refs/heads/main.zip" 63 | } 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fvtt-perf-optim", 3 | "type": "module", 4 | "version": "0.1.0", 5 | "private": true, 6 | "scripts": { 7 | "foundry": "npx node ~/Development/rpgs/FoundryVTT/resources/app/main.js --noupnp --noupdate --noipdiscovery", 8 | "foundry11": "npx node ~/Development/rpgs/FoundryVTT-11/resources/app/main.js --noupnp --noupdate --noipdiscovery ---port=30001 --dataPath=\"$HOME/Library/Application Support/FoundryVTT-11\"", 9 | "dev": "vite", 10 | "build": "vite build", 11 | "lint": "eslint .", 12 | "lint:fix": "eslint . --fix" 13 | }, 14 | "devDependencies": { 15 | "@antfu/eslint-config": "^2.21.2", 16 | "@rollup/plugin-node-resolve": "^15.2.3", 17 | "eslint": "^8.57.0", 18 | "eslint-plugin-format": "^0.1.2", 19 | "foundry-pf2e-types": "github:reonZ/foundry-pf2e-types", 20 | "vite-plugin-checker": "^0.7.0", 21 | "vite-tsconfig-paths": "^4.3.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/DynamicSpriteSheet.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Looks like this class is not needed anymore. 3 | */ 4 | 5 | interface Size { 6 | width: number; 7 | height: number; 8 | } 9 | 10 | interface Pos { 11 | x: number; 12 | y: number; 13 | } 14 | 15 | interface Rect extends Size, Pos {} 16 | 17 | function fitsIn(inner: Size, outer: Size) { 18 | return outer.width >= inner.width && outer.height >= inner.height; 19 | } 20 | function isSameSize(self: Size, other: Size) { 21 | return self.width == other.width && self.height == other.height; 22 | } 23 | 24 | class AvailableSpace { 25 | #left?: AvailableSpace; 26 | #right?: AvailableSpace; 27 | #rect: Rect; 28 | #filled = false; 29 | #lastFailedWidth?: number; 30 | #lastFailedHeight?: number; 31 | #parent?: AvailableSpace; 32 | 33 | constructor(rect: Rect, parent?: AvailableSpace) { 34 | this.#rect = rect; 35 | this.#parent = parent; 36 | } 37 | 38 | get rect(): Rect { 39 | return this.#rect; 40 | } 41 | 42 | #markInsertFailed(size: Size) { 43 | this.#lastFailedWidth = size.width; 44 | this.#lastFailedHeight = size.height; 45 | } 46 | 47 | #isKnownFailedInsert(rect: Size) { 48 | return ( 49 | this.#lastFailedWidth != null && 50 | this.#lastFailedHeight != null && 51 | this.#lastFailedWidth >= rect.width && 52 | this.#lastFailedHeight >= rect.height 53 | ); 54 | } 55 | 56 | resetFailed() { 57 | this.#lastFailedWidth = undefined; 58 | this.#lastFailedHeight = undefined; 59 | this.#parent?.resetFailed(); 60 | } 61 | 62 | clear(): void { 63 | this.#filled = false; 64 | this.resetFailed(); 65 | } 66 | 67 | insertRect(rect: Size): AvailableSpace | null { 68 | if (this.#isKnownFailedInsert(rect)) { 69 | return null; 70 | } 71 | 72 | if (this.#left != null && this.#right != null) { 73 | const res = this.#left.insertRect(rect) || this.#right.insertRect(rect); 74 | if (!res) { 75 | this.#markInsertFailed(rect); 76 | } 77 | return res; 78 | } 79 | 80 | if (this.#filled) return null; 81 | 82 | if (isSameSize(rect, this.#rect)) { 83 | this.#filled = true; 84 | return this; 85 | } 86 | if (!fitsIn(rect, this.#rect)) return null; 87 | 88 | const widthDiff = this.#rect.width - rect.width; 89 | const heightDiff = this.#rect.height - rect.height; 90 | 91 | const me = this.#rect; 92 | 93 | if (widthDiff > heightDiff) { 94 | // split literally into left and right, putting the rect on the left. 95 | this.#left = new AvailableSpace({ ...me, width: rect.width, height: me.height }, this); 96 | this.#right = new AvailableSpace({ ...me, x: me.x + rect.width, width: me.width - rect.width }, this); 97 | } else { 98 | // split into top and bottom, putting rect on top. 99 | this.#left = new AvailableSpace({ ...me, height: rect.height }, this); 100 | this.#right = new AvailableSpace({ ...me, y: me.y + rect.height, height: me.height - rect.height }, this); 101 | } 102 | 103 | return this.#left.insertRect(rect); 104 | } 105 | } 106 | 107 | export class DynamicSpriteSheet { 108 | static #baseTextureSize = 4096; 109 | static get baseTextureSize() { 110 | return this.#baseTextureSize; 111 | } 112 | 113 | // only cache textures that fill up less than 50% of one cache entry 114 | static #maxCachableArea = this.#baseTextureSize ** 2 * 0.5; 115 | static get maxCachableArea(): number { 116 | return this.#maxCachableArea; 117 | } 118 | 119 | #baseTextures: [PIXI.BaseRenderTexture, AvailableSpace][] = []; 120 | #textureCache = new Map(); 121 | #debugSprite = new PIXI.Sprite(); 122 | 123 | get debugSprite() { 124 | return this.#debugSprite; 125 | } 126 | 127 | #getCacheKey(ns: string, key: string): string { 128 | return `${ns}-${key}`; 129 | } 130 | 131 | #createBaseRenderTexture() { 132 | const baseTextureSize = DynamicSpriteSheet.baseTextureSize; 133 | return new PIXI.BaseRenderTexture({ 134 | width: baseTextureSize, 135 | height: baseTextureSize, 136 | }); 137 | } 138 | 139 | #createRenderTexture( 140 | baseRenderTexture: PIXI.BaseRenderTexture, 141 | space: AvailableSpace, 142 | size: Size, 143 | padding: number, 144 | ): [PIXI.RenderTexture, AvailableSpace] | undefined { 145 | const result = space.insertRect({ width: size.width + padding, height: size.height + padding }); 146 | if (!result) { 147 | return undefined; 148 | } 149 | const rect = result.rect; 150 | const frame = new PIXI.Rectangle( 151 | rect.x + padding, 152 | rect.y + padding, 153 | rect.width - padding * 2, 154 | rect.height - padding * 2, 155 | ); 156 | const renderTexture = new PIXI.RenderTexture(baseRenderTexture, frame); 157 | return [renderTexture, result]; 158 | } 159 | 160 | #getNextRenderTexture(renderable: PIXI.Container, padding: number): [PIXI.RenderTexture, AvailableSpace] | undefined { 161 | const size = { width: renderable.width, height: renderable.height }; 162 | for (const [baseTexture, space] of this.#baseTextures) { 163 | const result = this.#createRenderTexture(baseTexture, space, size, padding); 164 | if (result) { 165 | return result; 166 | } 167 | } 168 | // no existing base texture can accomodate this renderable, create new one! 169 | const baseRenderTexture = this.#createBaseRenderTexture(); 170 | const space = new AvailableSpace({ 171 | x: 0, 172 | y: 0, 173 | width: DynamicSpriteSheet.baseTextureSize, 174 | height: DynamicSpriteSheet.baseTextureSize, 175 | }); 176 | this.#debugSprite.texture = new PIXI.RenderTexture(baseRenderTexture); 177 | this.#baseTextures.push([baseRenderTexture, space]); 178 | return this.#createRenderTexture(baseRenderTexture, space, size, padding); 179 | } 180 | 181 | clear(): void { 182 | this.#baseTextures.forEach(([texture]) => texture.destroy()); 183 | this.#baseTextures = []; 184 | this.#debugSprite.texture = PIXI.Texture.EMPTY; 185 | this.#textureCache.clear(); 186 | } 187 | 188 | addToCache(ns: string, key: string, renderable: PIXI.Container, padding = 1): PIXI.RenderTexture | undefined { 189 | const cacheKey = this.#getCacheKey(ns, key); 190 | const existingTexture = this.#textureCache.get(cacheKey); 191 | 192 | // separate check if it exists, because it could also just be that the result was undefined 193 | // b/c the object is not cacheable. 194 | if (this.#textureCache.has(cacheKey)) { 195 | return existingTexture?.[0]; 196 | } 197 | if ( 198 | renderable.width * renderable.height > DynamicSpriteSheet.maxCachableArea || 199 | renderable.width > DynamicSpriteSheet.baseTextureSize || 200 | renderable.height > DynamicSpriteSheet.baseTextureSize || 201 | renderable.width === 0 || 202 | renderable.height === 0 203 | ) { 204 | return undefined; 205 | } 206 | // load base render texture or create new if it does not exist 207 | const result = this.#getNextRenderTexture(renderable, padding); 208 | canvas.app.renderer.render(renderable, { renderTexture: result?.[0] }); 209 | this.#textureCache.set(cacheKey, result); 210 | return result?.[0]; 211 | } 212 | 213 | addOrUpdateCache(ns: string, key: string, renderable: PIXI.Container, padding = 1): PIXI.RenderTexture | undefined { 214 | const cacheKey = this.#getCacheKey(ns, key); 215 | const entry = this.#textureCache.get(cacheKey); 216 | if (!entry || !fitsIn(renderable, entry[1].rect)) { 217 | this.remove(ns, key); 218 | return this.addToCache(ns, key, renderable, padding); 219 | } 220 | canvas.app.renderer.render(renderable, { renderTexture: entry[0] }); 221 | return entry[0]; 222 | } 223 | 224 | loadTexture(ns: string, key: string): PIXI.RenderTexture | undefined { 225 | return this.#textureCache.get(this.#getCacheKey(ns, key))?.[0]; 226 | } 227 | 228 | remove(ns: string, key: string): void { 229 | const cacheKey = this.#getCacheKey(ns, key); 230 | const entry = this.#textureCache.get(cacheKey); 231 | if (!entry) { 232 | return; 233 | } 234 | canvas.app.renderer.render(new PIXI.Container(), { renderTexture: entry[0], clear: true }); 235 | entry[1].clear(); 236 | entry[0].destroy(false); 237 | this.#textureCache.delete(cacheKey); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/apps/CustomSpritesheetConfig.ts: -------------------------------------------------------------------------------- 1 | import { NAMESPACE } from 'src/constants.ts'; 2 | import { SETTINGS } from 'src/settings/constants.ts'; 3 | import { FOUNDRY_API } from 'src/utils/foundryShim.ts'; 4 | 5 | export class CustomSpritesheetConfig extends foundry.applications.api.HandlebarsApplicationMixin( 6 | foundry.applications.api.ApplicationV2, 7 | ) { 8 | static override DEFAULT_OPTIONS = { 9 | id: 'spritesheet-config', 10 | tag: 'form', 11 | window: { 12 | contentClasses: ['standard-form'], 13 | title: `${NAMESPACE}.settings.${SETTINGS.CustomSpritesheets}.menu.title`, 14 | icon: 'fa-solid fa-images', 15 | }, 16 | position: { 17 | width: 600, 18 | }, 19 | form: { 20 | closeOnSubmit: true, 21 | }, 22 | }; 23 | 24 | static override PARTS = { 25 | body: { 26 | template: `modules/${NAMESPACE}/templates/custom-spritesheets-config.hbs`, 27 | scrollable: [''], 28 | }, 29 | footer: { 30 | template: 'templates/generic/form-footer.hbs', 31 | }, 32 | }; 33 | 34 | #initialData: string[] = game.settings.get(NAMESPACE, SETTINGS.CustomSpritesheets) ?? []; 35 | 36 | override async _prepareContext() { 37 | const spritesheets = game.settings.get(NAMESPACE, SETTINGS.CustomSpritesheets) ?? []; 38 | return { 39 | spritesheets, 40 | buttons: [ 41 | { 42 | type: 'button', 43 | label: `${NAMESPACE}.settings.${SETTINGS.CustomSpritesheets}.menu.add`, 44 | icon: 'fa-solid fa-plus', 45 | action: 'add', 46 | }, 47 | ], 48 | }; 49 | } 50 | 51 | async #onAddSpritesheet() { 52 | const fd = new FormDataExtended(this.parts.body); 53 | const src = fd.get('src') || ''; 54 | if (!src) { 55 | ui.notifications.warn( 56 | game.i18n.localize(`${NAMESPACE}.settings.${SETTINGS.CustomSpritesheets}.menu.warn-no-src`), 57 | ); 58 | return; 59 | } 60 | 61 | if (typeof src !== 'string') { 62 | return; 63 | } 64 | 65 | if (!src.endsWith('.json')) { 66 | ui.notifications.warn( 67 | game.i18n.localize(`${NAMESPACE}.settings.${SETTINGS.CustomSpritesheets}.menu.warn-no-json`), 68 | ); 69 | return; 70 | } 71 | 72 | const result = await this.#testSpritesheet(src); 73 | if (!result) { 74 | return; 75 | } 76 | 77 | const spritesheets = game.settings.get(NAMESPACE, SETTINGS.CustomSpritesheets) ?? []; 78 | 79 | if (spritesheets.includes(src)) { 80 | return; 81 | } 82 | spritesheets.push(src); 83 | await game.settings.set(NAMESPACE, SETTINGS.CustomSpritesheets, spritesheets); 84 | 85 | this.render(true); 86 | } 87 | 88 | async #testSpritesheet(src: string) { 89 | try { 90 | const result = await PIXI.Assets.load(src); 91 | if (!(result instanceof PIXI.Spritesheet)) { 92 | ui.notifications.warn( 93 | game.i18n.localize(`${NAMESPACE}.settings.${SETTINGS.CustomSpritesheets}.menu.warn-file-invalid-asset`), 94 | ); 95 | 96 | return false; 97 | } 98 | } catch (error) { 99 | ui.notifications.warn( 100 | game.i18n.localize(`${NAMESPACE}.settings.${SETTINGS.CustomSpritesheets}.menu.warn-file-not-loaded`), 101 | ); 102 | console.error('error loading spritesheet', error); 103 | 104 | return false; 105 | } 106 | 107 | return true; 108 | } 109 | 110 | async #onDeleteSpritesheet(event: Event) { 111 | event.preventDefault(); 112 | const btn = event.target?.closest('[data-index]'); 113 | if (!btn) { 114 | return; 115 | } 116 | 117 | const { index } = btn.dataset; 118 | if (index === undefined) { 119 | return; 120 | } 121 | 122 | const spritesheets = game.settings.get(NAMESPACE, SETTINGS.CustomSpritesheets) ?? []; 123 | 124 | spritesheets.splice(Number(index), 1); 125 | await game.settings.set(NAMESPACE, SETTINGS.CustomSpritesheets, spritesheets); 126 | 127 | this.render(true); 128 | } 129 | 130 | override _onClickAction(event: Event, htmlElement: HTMLElement) { 131 | const action = htmlElement.dataset.action; 132 | switch (action) { 133 | case 'add': 134 | event.preventDefault(); 135 | return this.#onAddSpritesheet(); 136 | case 'delete': 137 | return this.#onDeleteSpritesheet(event); 138 | } 139 | } 140 | 141 | override async close(options = {}) { 142 | await super.close(options); 143 | 144 | const spritesheets = game.settings.get(NAMESPACE, SETTINGS.CustomSpritesheets) ?? []; 145 | const dataModified = !new Set(this.#initialData).equals(new Set(spritesheets)); 146 | 147 | const Config = FOUNDRY_API.generation < 13 ? SettingsConfig : foundry.applications.settings.SettingsConfig; 148 | if (dataModified) { 149 | await Config.reloadConfirm({ world: true }); 150 | } 151 | 152 | return this; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const NAMESPACE = 'fvtt-perf-optim'; 2 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | import type { DynamicSpriteSheet } from './DynamicSpriteSheet.ts'; 2 | 3 | interface FvttPerfHacks { 4 | autoSpritesheetCache: DynamicSpriteSheet; 5 | } 6 | 7 | declare global { 8 | interface Window { 9 | fvttPerfHacks: FvttPerfHacks; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/hacks/effectsCaching.ts: -------------------------------------------------------------------------------- 1 | import { NAMESPACE } from 'src/constants.ts'; 2 | import { SETTINGS } from 'src/settings/constants.ts'; 3 | import { getSetting } from 'src/settings/settings.ts'; 4 | import { getBitmapCacheResolution } from 'src/utils/getBitmapCacheResolution.ts'; 5 | 6 | function refreshEffectCache(object: PIXI.DisplayObject) { 7 | object.cacheAsBitmap = false; 8 | object.cacheAsBitmapResolution = getBitmapCacheResolution(); 9 | object.cacheAsBitmap = true; 10 | } 11 | 12 | function shouldCacheIndividualEffects() { 13 | const effectHider = game.modules.get('effect-hider'); 14 | if (effectHider && effectHider.active) { 15 | return true; 16 | } 17 | return false; 18 | } 19 | 20 | async function cacheEffects(this: Token, wrapper: Function, ...args: any[]) { 21 | const wrappedResult = wrapper(...args); 22 | if (wrappedResult instanceof Promise) { 23 | await wrappedResult; 24 | } 25 | 26 | const [flags] = args; 27 | if (flags?.redrawEffects || flags?.refreshEffects) { 28 | if (shouldCacheIndividualEffects()) { 29 | this.effects.children.forEach((effect) => { 30 | refreshEffectCache(effect); 31 | }); 32 | } else { 33 | refreshEffectCache(this.effects); 34 | } 35 | } 36 | } 37 | 38 | function isTokenEffectsCachingAvailable() { 39 | // Caching is not needed for dorako UX since radial hud already uses prerendered textures 40 | // for the status effect icons for performance 41 | const hasDorakoRadialHud = 42 | game.settings.settings.has('pf2e-dorako-ux.moving.adjust-token-effects-hud') && 43 | game.settings.get('pf2e-dorako-ux', 'moving.adjust-token-effects-hud'); 44 | if (hasDorakoRadialHud) { 45 | return false; 46 | } 47 | 48 | if (game.modules.has('pf2e-effects-halo') && game.modules.get('pf2e-effects-halo')?.active) { 49 | return false; 50 | } 51 | 52 | return true; 53 | } 54 | 55 | let enableEffectsCaching = () => { 56 | if (!isTokenEffectsCachingAvailable()) { 57 | return; 58 | } 59 | 60 | const enabled = getSetting(SETTINGS.TokenBarsCaching); 61 | 62 | if (!enabled) { 63 | return; 64 | } 65 | 66 | libWrapper.register(NAMESPACE, 'CONFIG.Token.objectClass.prototype._applyRenderFlags', cacheEffects, 'WRAPPER'); 67 | }; 68 | export { enableEffectsCaching }; 69 | 70 | if (import.meta.hot) { 71 | import.meta.hot.accept((newModule) => { 72 | enableEffectsCaching = newModule?.foo; 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /src/hacks/index.ts: -------------------------------------------------------------------------------- 1 | import { enableEffectsCaching } from './effectsCaching.ts'; 2 | import { enablePrecomputedNoiseTextures } from './precomputedNoiseTextures.ts'; 3 | import { enableSpritesheetSubstitution } from './spritesheetSubstitution.ts'; 4 | import { enableTokenBarsCaching } from './tokenBarsCaching.ts'; 5 | import { enableTokenRingSpritesheetSupport } from './tokenRingSpritesheetSupport.ts'; 6 | import { useOooTokenRendering } from './useOooTokenRendering.ts'; 7 | 8 | Hooks.once('setup', () => { 9 | useOooTokenRendering(); 10 | enableEffectsCaching(); 11 | enableTokenBarsCaching(); 12 | enableTokenRingSpritesheetSupport(); 13 | enableSpritesheetSubstitution(); 14 | enablePrecomputedNoiseTextures(); 15 | }); 16 | -------------------------------------------------------------------------------- /src/hacks/precomputedNoiseTextures.ts: -------------------------------------------------------------------------------- 1 | import type BaseLightSource from 'foundry-pf2e-types/foundry/client-esm/canvas/sources/base-light-source.js'; 2 | import { NAMESPACE } from 'src/constants.ts'; 3 | import { SETTINGS } from 'src/settings/constants.ts'; 4 | import { getSetting } from 'src/settings/settings.ts'; 5 | import { FOUNDRY_API } from 'src/utils/foundryShim.ts'; 6 | import { registerWrapperForVersion } from 'src/utils/registerWrapper.ts'; 7 | 8 | interface NoiseTextureData { 9 | path: string; 10 | format: PIXI.FORMATS; 11 | } 12 | 13 | const FBM_TEXTURE_DATA = { 14 | 'fbm2+3': { 15 | path: `modules/${NAMESPACE}/dist/noise/fbm2_3.avif`, 16 | format: PIXI.FORMATS.RGB, 17 | }, 18 | fbm4: { 19 | path: `modules/${NAMESPACE}/dist/noise/fbm4.avif`, 20 | format: PIXI.FORMATS.RG, 21 | }, 22 | fbmHQ3: { 23 | path: `modules/${NAMESPACE}/dist/noise/fbmHQ3.basis`, 24 | format: PIXI.FORMATS.RG, 25 | }, 26 | }; 27 | 28 | // TODO remove me, this is just for debugging! 29 | for (const [key, value] of Object.entries(FBM_TEXTURE_DATA)) { 30 | FBM_TEXTURE_DATA[key].path = `${value.path}?v=${Math.random()}`; 31 | } 32 | 33 | const NOISE_TEXTURE_MAP: Record = { 34 | ghost: FBM_TEXTURE_DATA['fbm2+3'], 35 | fairy: FBM_TEXTURE_DATA['fbm2+3'], 36 | witchwave: FBM_TEXTURE_DATA.fbm4, 37 | fog: FBM_TEXTURE_DATA.fbm4, 38 | vortex: FBM_TEXTURE_DATA.fbm4, 39 | smokepatch: FBM_TEXTURE_DATA.fbmHQ3, 40 | hole: FBM_TEXTURE_DATA.fbmHQ3, 41 | dome: FBM_TEXTURE_DATA['fbm2+3'], 42 | roiling: FBM_TEXTURE_DATA['fbm2+3'], 43 | }; 44 | 45 | const NOISE_TEXTURE_URLS = Array.from(new Set(Object.values(NOISE_TEXTURE_MAP))); 46 | 47 | function AdaptiveLightingShader__updateCommonUniforms( 48 | this: BaseLightSource, 49 | wrapped: (...args: any) => void, 50 | shader: any, 51 | ) { 52 | wrapped(shader); 53 | const u = shader.uniforms; 54 | const animationType: string | undefined = this.animation?.type; 55 | if (!animationType) { 56 | return; 57 | } 58 | const noiseTextureData = NOISE_TEXTURE_MAP[animationType]; 59 | if (!noiseTextureData) { 60 | return; 61 | } 62 | u.fbmTexture = FOUNDRY_API.getTexture(noiseTextureData.path); 63 | } 64 | 65 | function AdaptiveLightingShader__updateDarknessUniforms(this: BaseLightSource, wrapped: (...args: any) => void) { 66 | wrapped(); 67 | const u = this.layers.darkness?.shader?.uniforms; 68 | const animationType: string | undefined = this.animation?.type; 69 | if (!animationType) { 70 | return; 71 | } 72 | const noiseTextureData = NOISE_TEXTURE_MAP[animationType]; 73 | if (!noiseTextureData) { 74 | return; 75 | } 76 | u.fbmTexture = FOUNDRY_API.getTexture(noiseTextureData.path); 77 | } 78 | 79 | function optimizedFBMOld() { 80 | return ` 81 | 82 | uniform sampler2D fbmTexture; 83 | float fbmLQ(in float factor, in float channel, in vec2 uv) { 84 | vec4 fbmColor = texture2D(fbmTexture, uv * 0.1 / factor ); 85 | if (channel < 1.5) { 86 | return fbmColor.r; 87 | } else if (channel < 2.5) { 88 | return fbmColor.g; 89 | } else { 90 | return fbmColor.b; 91 | } 92 | } 93 | float fbmLQ(in float factor, in float channel, in vec2 uv, in float ignored_smoothness) { 94 | return fbmLQ(factor, channel, uv) * 1.5; 95 | } 96 | `; 97 | } 98 | 99 | interface FBMData { 100 | scale: number; 101 | channel: 'r' | 'g' | 'b' | 'a'; 102 | boost?: number; 103 | name?: string; 104 | } 105 | function optimizedFBM(...fbmData: FBMData[]) { 106 | const functions = fbmData 107 | .map(({ channel, scale, name, boost = 1 }) => { 108 | const boostCalculation = boost === 1 ? '' : ` * ${boost}`; 109 | const scaleRec = 1 / 5 / scale; 110 | return ` 111 | float fbm${name}(in vec2 uv) { 112 | vec4 color = texture2D(fbmTexture, uv * ${scaleRec.toPrecision(4)}); 113 | return color.${channel}${boostCalculation}; 114 | } 115 | float fbm${name}(in vec2 uv, in float ignoredSmoothness) { 116 | return fbm${name}(uv); 117 | } 118 | `; 119 | }) 120 | .join('\n'); 121 | return `uniform sampler2D fbmTexture;\n${functions}`; 122 | } 123 | 124 | function patchShaderFBM() { 125 | const getShaderByName = (name: string) => { 126 | return FOUNDRY_API.generation < 13 ? eval(name) : foundry.canvas.rendering.shaders[name]; 127 | }; 128 | const fbm2 = getShaderByName('GhostLightColorationShader').FBM(2, 1); 129 | const fbm3 = getShaderByName('GhostLightColorationShader').FBM(3, 1); 130 | const fbm4 = getShaderByName('GhostLightColorationShader').FBM(4, 1); 131 | const fbmHQ3 = getShaderByName('GhostLightColorationShader').FBMHQ(3); 132 | 133 | const optimizedFBM2 = optimizedFBM({ name: '2', channel: 'r', scale: 1.6 }); 134 | const optimizedFBM3 = optimizedFBM({ name: '3_1', channel: 'g', scale: 1 }, { name: '3_5', channel: 'b', scale: 5 }); 135 | const optimizedFBM4 = optimizedFBM({ name: '4_1', channel: 'r', scale: 1 }, { name: '4_8', channel: 'g', scale: 8 }); 136 | const optimizedFBMHQ3 = optimizedFBM( 137 | { name: 'HQ3_1', channel: 'r', scale: 1 }, 138 | { name: 'HQ3_10', channel: 'g', scale: 5 }, 139 | ); 140 | 141 | const shaders = [ 142 | { 143 | shader: 'GhostLightIlluminationShader', 144 | replacements: [ 145 | [fbm3, fbm3 + optimizedFBM3], 146 | [ 147 | // wrap-line 148 | 'float distortion1 = fbm(vec2(', 149 | 'float distortion1 = fbm3_1(vec2(', 150 | ], 151 | [ 152 | // wrap-line 153 | 'fbm(vUvs * 5.0 - time * 0.50),', 154 | 'fbm3_5(vUvs * 5.0 - time * 0.50),', 155 | ], 156 | [ 157 | // wrap-line 158 | 'fbm((-vUvs - vec2(0.01)) * 5.0 + time * INVTHREE)));', 159 | 'fbm3_5((-vUvs - vec2(0.01)) * 5.0 + time * INVTHREE)));', 160 | ], 161 | [ 162 | // wrap-line 163 | 'float distortion2 = fbm(vec2(', 164 | 'float distortion2 = fbm3_1(vec2(', 165 | ], 166 | [ 167 | // wrap-line 168 | 'fbm(-vUvs * 5.0 - time * 0.50),', 169 | 'fbm3_5(-vUvs * 5.0 - time * 0.50),', 170 | ], 171 | [ 172 | // wrap-line 173 | 'fbm((-vUvs + vec2(0.01)) * 5.0 + time * INVTHREE)));', 174 | 'fbm3_5((-vUvs + vec2(0.01)) * 5.0 + time * INVTHREE)));', 175 | ], 176 | ], 177 | }, 178 | { 179 | shader: 'GhostLightColorationShader', 180 | replacements: [ 181 | [fbm3, fbm3 + optimizedFBM3], 182 | [ 183 | // wrap-line 184 | 'float distortion1 = fbm(vec2(', 185 | 'float distortion1 = fbm3_1(vec2(', 186 | ], 187 | [ 188 | // wrap-line 189 | 'fbm(vUvs * 3.0 + time * 0.50),', 190 | 'fbm3_5(vUvs * 3.0 + time * 0.50),', 191 | ], 192 | [ 193 | // wrap-line 194 | 'fbm((-vUvs + vec2(1.)) * 5.0 + time * INVTHREE)));', 195 | 'fbm3_5((-vUvs + vec2(1.)) * 5.0 + time * INVTHREE)));', 196 | ], 197 | [ 198 | // wrap-line 199 | 'float distortion2 = fbm(vec2(', 200 | 'float distortion2 = fbm3_1(vec2(', 201 | ], 202 | [ 203 | // wrap-line 204 | 'fbm(-vUvs * 3.0 + time * 0.50),', 205 | 'fbm3_5(-vUvs * 3.0 + time * 0.50),', 206 | ], 207 | [ 208 | // wrap-line 209 | 'fbm((-vUvs + vec2(1.)) * 5.0 - time * INVTHREE)));', 210 | 'fbm3_5((-vUvs + vec2(1.)) * 5.0 - time * INVTHREE)));', 211 | ], 212 | [ 213 | // wrap-line 214 | 'uv *= fbm(vec2(time + distortion1, time + distortion2));', 215 | 'uv *= fbm3_1(vec2(time + distortion1, time + distortion2));', 216 | ], 217 | ], 218 | }, 219 | 220 | { 221 | shader: 'FairyLightIlluminationShader', 222 | replacements: [ 223 | [fbm3, fbm3 + optimizedFBM3], 224 | // distortion 1 225 | [ 226 | // wrap-line 227 | 'float distortion1 = fbm(vec2(', 228 | 'float distortion1 = fbm3_1(vec2(', 229 | ], 230 | [ 231 | // wrap-line 232 | 'fbm(vUvs * 3.0 - time * 0.50), ', 233 | 'fbm3_5(vUvs * 3.0 - time * 0.50),', 234 | ], 235 | ['fbm((-vUvs + vec2(1.)) * 5.0 + time * INVTHREE)));', 'fbm3_5((-vUvs + vec2(1.)) * 5.0 + time * INVTHREE)));'], 236 | // distortion 2 237 | [ 238 | // wrap-line 239 | 'float distortion2 = fbm(vec2(', 240 | 'float distortion2 = fbm3_1(vec2(', 241 | ], 242 | [ 243 | // wrap-line 244 | 'fbm(-vUvs * 3.0 - time * 0.50),', 245 | 'fbm3_5(-vUvs * 3.0 - time * 0.50),', 246 | ], 247 | [ 248 | // wrap-line 249 | 'fbm((-vUvs + vec2(1.)) * 5.0 - time * INVTHREE)));', 250 | 'fbm3_5((-vUvs + vec2(1.)) * 5.0 - time * INVTHREE)));', 251 | ], 252 | ], 253 | }, 254 | { 255 | shader: 'FairyLightColorationShader', 256 | replacements: [ 257 | [fbm3, fbm3 + optimizedFBM3], 258 | // distortion 1 259 | [ 260 | // wrap-line 261 | 'float distortion1 = fbm(vec2(', 262 | 'float distortion1 = fbm3_1(vec2(', 263 | ], 264 | [ 265 | // wrap-line 266 | 'fbm(vUvs * 3.0 + time * 0.50),', 267 | 'fbm3_5(vUvs * 3.0 + time * 0.50),', 268 | ], 269 | ['fbm((-vUvs + vec2(1.)) * 5.0 + time * INVTHREE)));', 'fbm3_5((-vUvs + vec2(1.)) * 5.0 + time * INVTHREE)));'], 270 | // distortion 2 271 | [ 272 | // wrap-line 273 | 'float distortion2 = fbm(vec2(', 274 | 'float distortion2 = fbm3_1(vec2(', 275 | ], 276 | [ 277 | // wrap-line 278 | 'fbm(-vUvs * 3.0 + time * 0.50),', 279 | 'fbm3_5(-vUvs * 3.0 + time * 0.50),', 280 | ], 281 | [ 282 | // wrap-line 283 | 'fbm((-vUvs + vec2(1.)) * 5.0 - time * INVTHREE)));', 284 | 'fbm3_5((-vUvs + vec2(1.)) * 5.0 - time * INVTHREE)));', 285 | ], 286 | // final FBM 287 | [ 288 | // wrap-line 289 | 'uv *= fbm(vec2(time + distortion1, time + distortion2));', 290 | 'uv *= fbm3_1(vec2(time + distortion1, time + distortion2));', 291 | ], 292 | ], 293 | }, 294 | 295 | { 296 | shader: 'BewitchingWaveIlluminationShader', 297 | replacements: [ 298 | [fbm4, fbm4 + optimizedFBM4], 299 | [ 300 | // wrap-line 301 | 'float motion = fbm(uv + time * 0.25);', 302 | 'float motion = fbm4_1(uv + time * 0.25);', 303 | ], 304 | ], 305 | }, 306 | { 307 | shader: 'BewitchingWaveColorationShader', 308 | replacements: [ 309 | [fbm4, fbm4 + optimizedFBM4], 310 | [ 311 | // wrap-line 312 | 'float motion = fbm(uv + time * 0.25);', 313 | 'float motion = fbm4_1(uv + time * 0.25);', 314 | ], 315 | ], 316 | }, 317 | 318 | { 319 | shader: 'FogColorationShader', 320 | replacements: [ 321 | [fbm4, fbm4 + optimizedFBM4], 322 | [ 323 | // wrap-line 324 | 'float q = fbm(p - time * 0.1);', 325 | 'float q = fbm4_8(p - time * 0.1);', 326 | ], 327 | [ 328 | // wrap-line 329 | 'vec2 r = vec2(fbm(p + q - time * 0.5 - p.x - p.y),', 330 | 'vec2 r = vec2(fbm4_8(p + q - time * 0.5 - p.x - p.y),', 331 | ], 332 | [ 333 | // wrap-line 334 | 'fbm(p + q - time * 0.3));', 335 | 'fbm4_8(p + q - time * 0.3));', 336 | ], 337 | [ 338 | // wrap-line 339 | 'fbm(p + r)) + mix(c3, c4, r.x)', 340 | 'fbm4_8(p + r)) + mix(c3, c4, r.x)', 341 | ], 342 | ], 343 | }, 344 | 345 | { 346 | shader: 'VortexIlluminationShader', 347 | replacements: [ 348 | [fbm4, fbm4 + optimizedFBM4], 349 | [ 350 | // wrap-line 351 | 'float q = fbm(p + time);', 352 | 'float q = fbm4_8(p + time);', 353 | ], 354 | [ 355 | // wrap-line 356 | 'vec2 r = vec2(fbm(p + q + time * 0.9 - p.x - p.y), fbm(p + q + time * 0.6));', 357 | 'vec2 r = vec2(fbm4_8(p + q + time * 0.9 - p.x - p.y), fbm4_8(p + q + time * 0.6));', 358 | ], 359 | [ 360 | // wrap-line 361 | 'return mix(c1, c2, fbm(p + r)) + mix(c3, c4, r.x) - mix(c5, c6, r.y);', 362 | 'return mix(c1, c2, fbm4_8(p + r)) + mix(c3, c4, r.x) - mix(c5, c6, r.y);', 363 | ], 364 | ], 365 | }, 366 | { 367 | shader: 'VortexColorationShader', 368 | replacements: [ 369 | [fbm4, fbm4 + optimizedFBM4], 370 | [ 371 | // wrap-line 372 | 'float q = fbm(p + time);', 373 | 'float q = fbm4_8(p + time);', 374 | ], 375 | [ 376 | // wrap-line 377 | 'vec2 r = vec2(fbm(p + q + time * 0.9 - p.x - p.y), ', 378 | 'vec2 r = vec2(fbm4_1(p + q + time * 0.9 - p.x - p.y), ', 379 | ], 380 | [ 381 | // wrap-line 382 | 'fbm(p + q + time * 0.6));', 383 | 'fbm4_8(p + q + time * 0.6));', 384 | ], 385 | [ 386 | // wrap-line 387 | 'fbm(p + r)) + mix(c3, c4, r.x) ', 388 | 'fbm4_8(p + r)) + mix(c3, c4, r.x) ', 389 | ], 390 | ], 391 | }, 392 | 393 | { 394 | shader: 'SmokePatchIlluminationShader', 395 | replacements: [ 396 | [fbmHQ3, fbmHQ3 + optimizedFBMHQ3], 397 | [ 398 | // wrap-line 399 | 'max(fbm(uv + t, 1.0),', 400 | 'max(fbmHQ3_1(uv + t, 1.0),', 401 | ], 402 | [ 403 | // wrap-line 404 | 'fbm(uv - t, 1.0)),', 405 | 'fbmHQ3_1(uv - t, 1.0)),', 406 | ], 407 | ], 408 | }, 409 | { 410 | shader: 'SmokePatchColorationShader', 411 | replacements: [ 412 | [fbmHQ3, fbmHQ3 + optimizedFBMHQ3], 413 | [ 414 | // wrap-line 415 | 'max(fbm(uv + t, 1.0),', 416 | 'max(fbmHQ3_1(uv + t, 1.0),', 417 | ], 418 | [ 419 | // wrap-line 420 | 'fbm(uv - t, 1.0)),', 421 | 'fbmHQ3_1(uv - t, 1.0)),', 422 | ], 423 | ], 424 | }, 425 | 426 | { 427 | shader: 'BlackHoleDarknessShader', 428 | replacements: [ 429 | [fbmHQ3, fbmHQ3 + optimizedFBMHQ3], 430 | [ 431 | // wrap-line 432 | 'float beams = fract(angle + sin(dist * 30.0 * (intensity * 0.2) - time + fbm(uv * 10.0 + time * 0.25, 1.0) * dad));', 433 | 'float beams = fract(angle + sin(dist * 30.0 * (intensity * 0.2) - time + fbmHQ3_10(uv * 10.0 + time * 0.25, 1.0) * dad));', 434 | ], 435 | ], 436 | }, 437 | 438 | { 439 | shader: 'LightDomeColorationShader', 440 | replacements: [ 441 | [fbm2, fbm2 + optimizedFBM2], 442 | [ 443 | // wrap-line 444 | 'float q = 2.0 * fbm(p + time * 0.2);', 445 | 'float q = 2.0 * fbm2(p + time * 0.2);', 446 | ], 447 | [ 448 | // wrap-line 449 | 'vec2 r = vec2(fbm(p + q + ( time ) - p.x - p.y), fbm(p * 2.0 + ( time )));', 450 | 'vec2 r = vec2(fbm2(p + q + ( time ) - p.x - p.y), fbm2(p * 2.0 + ( time )));', 451 | ], 452 | [ 453 | // wrap-line 454 | 'return clamp( mix( c1, c2, abs(fbm(p + r)) ) + mix( c3, c4, abs(r.x * r.x * r.x) ) - mix(', 455 | 'return clamp( mix( c1, c2, fbm2(p + r) ) + mix( c3, c4, abs(r.x * r.x * r.x) ) - mix(', 456 | ], 457 | ], 458 | }, 459 | 460 | { 461 | shader: 'RoilingDarknessShader', 462 | replacements: [ 463 | [fbm3, fbm3 + optimizedFBM3], 464 | [ 465 | // wrap-line 466 | `float distortion1 = fbm( vec2(`, 467 | `float distortion1 = fbm3_1(vec2(`, 468 | ], 469 | [ 470 | // wrap-line 471 | `fbm( vUvs * 2.5 + time * 0.5),`, 472 | `fbm3_5(vUvs * 2.5 + time * 0.5),`, 473 | ], 474 | [ 475 | // wrap-line 476 | `fbm( (-vUvs - vec2(0.01)) * 5.0 + time * INVTHREE)));`, 477 | `fbm3_5((-vUvs - vec2(0.01)) * 5.0 + time * INVTHREE)));`, 478 | ], 479 | 480 | [ 481 | // wrap-line 482 | `float distortion2 = fbm( vec2(`, 483 | `float distortion2 = fbm3_1(vec2(`, 484 | ], 485 | [ 486 | // wrap-line 487 | `fbm( -vUvs * 5.0 + time * 0.5),`, 488 | `fbm3_5(-vUvs * 5.0 + time * 0.5),`, 489 | ], 490 | [ 491 | // wrap-line 492 | `fbm( (vUvs + vec2(0.01)) * 2.5 + time * INVTHREE)));`, 493 | `fbm3_5((vUvs + vec2(0.01)) * 2.5 + time * INVTHREE)));`, 494 | ], 495 | ], 496 | }, 497 | ]; 498 | for (const { shader, replacements } of shaders) { 499 | const ShaderClass = getShaderByName(shader); 500 | for (const [search, replace] of replacements) { 501 | ShaderClass.fragmentShader = ShaderClass.fragmentShader.replace(search, replace); 502 | } 503 | } 504 | } 505 | 506 | async function enablePrecomputedNoiseTextures() { 507 | const enabled = getSetting(SETTINGS.PrecomputedNoiseTextures); 508 | 509 | if (!enabled) { 510 | return; 511 | } 512 | 513 | registerWrapperForVersion(AdaptiveLightingShader__updateCommonUniforms, 'WRAPPER', { 514 | v12: 'foundry.canvas.sources.BaseLightSource.prototype._updateCommonUniforms', 515 | v13: 'foundry.canvas.sources.BaseLightSource.prototype._updateCommonUniforms', 516 | }); 517 | registerWrapperForVersion(AdaptiveLightingShader__updateDarknessUniforms, 'WRAPPER', { 518 | v12: 'foundry.canvas.sources.PointDarknessSource.prototype._updateDarknessUniforms', 519 | v13: 'foundry.canvas.sources.PointDarknessSource.prototype._updateDarknessUniforms', 520 | }); 521 | 522 | patchShaderFBM(); 523 | 524 | Hooks.on('canvasInit', () => { 525 | for (const noiseTextureData of NOISE_TEXTURE_URLS) { 526 | canvas.sceneTextures[noiseTextureData.path] = noiseTextureData; 527 | } 528 | }); 529 | 530 | await Promise.all( 531 | NOISE_TEXTURE_URLS.map(async ({ path, format }) => { 532 | const texture = await PIXI.Assets.load(path); 533 | if (!(texture instanceof PIXI.Texture)) { 534 | return; 535 | } 536 | texture.baseTexture.wrapMode = PIXI.WRAP_MODES.REPEAT; 537 | texture.baseTexture.format = format; 538 | texture.baseTexture.scaleMode = PIXI.SCALE_MODES.LINEAR; 539 | texture.baseTexture.alphaMode = PIXI.ALPHA_MODES.NPM; 540 | texture.baseTexture.mipmap = PIXI.MIPMAP_MODES.OFF; 541 | }), 542 | ); 543 | } 544 | 545 | export { enablePrecomputedNoiseTextures }; 546 | -------------------------------------------------------------------------------- /src/hacks/spritesheetSubstitution.ts: -------------------------------------------------------------------------------- 1 | import type { PrimarySpriteMesh } from 'foundry-pf2e-types/foundry/client/pixi/placeables/primary-canvas-objects'; 2 | import { NAMESPACE } from 'src/constants.ts'; 3 | import { SETTINGS } from 'src/settings/constants.ts'; 4 | import { getSetting } from 'src/settings/settings.ts'; 5 | import { FOUNDRY_API } from 'src/utils/foundryShim.ts'; 6 | import { registerWrapperForVersion } from 'src/utils/registerWrapper.ts'; 7 | 8 | /** 9 | * For spritesheets to work with dynamic token rings, the texture coords need to be changed slightly. 10 | * Essentially: They have to be mulitplied by the relative base texture size. 11 | * For a texture that is 3x2 image tiles, using the texture at the bottom left corner, this would be: 12 | * vertex shader main code: 13 | * vOrigTextureCoord = aTextureCoord * vec2(3, 2) - vec2(0, 1) 14 | * 15 | * This fixes the color band being displayed relative to the base texture size. 16 | * 17 | * Gradient color rings still need to be fixed, as that uses the textureCoord + smoothstep to 18 | * create a gradient, which again shifts the gradient to be over the whole base texture are 19 | * as apposed to just the token cutout. Best way to fix this would probably be to pass another 20 | * vTextureCoordClipped or somthing to the shader, that does the same calculation as for the 21 | * textureCoord ((aTextureCoord - 0.5) * aTextureScaleCorrection + 0.5), but also applies the 22 | * correction for the vOrigTextureCoord as above: 23 | * vTextureCoordClipped = (vOrigTextureCoord - 0.5) * aTextureScaleCorrection + 0.5; 24 | * 25 | * then use vTextureCoordClipped in the gradient calculation instead of vTextureCoord 26 | */ 27 | 28 | const CONTROL_ICON_SRC = { 29 | background: 'control-icon-bg', 30 | border: 'control-icon-border', 31 | corner: 'control-icon-corner', 32 | }; 33 | 34 | const { resolve: resolveInitializationPromise, promise: initializationPromise } = Promise.withResolvers(); 35 | const spritesheetSubstitutions: Record = {}; 36 | 37 | function getSpritesheetSubstitution(path: string): PIXI.Texture | undefined { 38 | // If the spritesheet substitution is already loaded, return it 39 | path = `#${path}`; 40 | if (spritesheetSubstitutions[path]) { 41 | return spritesheetSubstitutions[path]; 42 | } 43 | 44 | // If the spritesheet substitution is not loaded, return undefined 45 | return undefined; 46 | } 47 | 48 | async function loadTextureWaitForInitialization( 49 | wrapped: (src: string) => Promise, 50 | src: string, 51 | ): Promise { 52 | // wait for initialization to complete 53 | await initializationPromise; 54 | 55 | return wrapped(src); 56 | } 57 | 58 | function getTextureWithSubstitution( 59 | wrapped: (src: string) => PIXI.Texture | PIXI.Spritesheet, 60 | src: string, 61 | ): PIXI.Texture | PIXI.Spritesheet { 62 | // wait for initialization to complete 63 | const texture = getSpritesheetSubstitution(src); 64 | 65 | return texture ?? wrapped(src); 66 | } 67 | 68 | async function loadTextureWithSubstitution( 69 | wrapped: (src: string) => PIXI.Texture | PIXI.Spritesheet, 70 | src: string, 71 | ): Promise { 72 | // wait for initialization to complete 73 | const texture = getSpritesheetSubstitution(src); 74 | 75 | return texture ?? wrapped(src); 76 | } 77 | 78 | /** 79 | * Fix mipmap transcoding for KTX2 textures in v13+. 80 | * This is needed because the default transcoding does not support mipmaps, 81 | * instead multiple textures are created and registered to the cache 82 | * for each mipmap level, leading to spritesheet initiazliation code 83 | * receiving an array of textures each with a single mip level instead 84 | * of one texture with multiple mip levels. 85 | */ 86 | async function transcodeAsyncWithMipmaps( 87 | wrapped: (...args: any[]) => Promise, 88 | ...args: any[] 89 | ) { 90 | // Call original implementation to transcode the texture 91 | // Each resource result is a single mip level texture 92 | const resources = await wrapped(...args); 93 | 94 | // If it's just one texture, no mip maps are encoded in the texture and we 95 | // can return the resource as is. 96 | if (!Array.isArray(resources) || resources.length <= 1) { 97 | return resources; 98 | } 99 | 100 | // If there are multiple resources, we need to create a single texture 101 | // that contains all mip levels. This is done by creating a new 102 | // PIXI.CompressedTextureResource with the levelBuffers set to the 103 | // individual mip level buffers. 104 | resources.splice( 105 | 0, 106 | resources.length, 107 | new PIXI.CompressedTextureResource(null, { 108 | format: resources[0].format, 109 | width: resources[0].width, 110 | height: resources[0].height, 111 | levels: resources.length, 112 | levelBuffers: resources.map((res, idx) => { 113 | let levelWidth = res._levelBuffers[0].levelWidth; 114 | let levelHeight = res._levelBuffers[0].levelHeight; 115 | if (idx === resources.length - 1) { 116 | levelWidth = 1; 117 | levelHeight = 1; 118 | } else if (idx === resources.length - 2) { 119 | levelWidth = 2; 120 | levelHeight = 2; 121 | } 122 | return { ...res._levelBuffers[0], levelWidth, levelHeight, levelID: idx }; 123 | }), 124 | }), 125 | ); 126 | return resources; 127 | } 128 | 129 | function ControlIcon_draw(this: ControlIcon, wrapped: (...args: any[]) => void, ...args: any[]) { 130 | this.texture ??= FOUNDRY_API.getTexture(this.iconSrc); 131 | 132 | if (!(this.bg instanceof PIXI.Graphics)) { 133 | return wrapped(...args); 134 | } 135 | 136 | this.removeChild(this.bg); 137 | 138 | const [offset, _, width] = this.rect; 139 | 140 | const cornerTopLeft = FOUNDRY_API.createSpriteMesh(FOUNDRY_API.getTexture(CONTROL_ICON_SRC.corner)); 141 | const borderTop = FOUNDRY_API.createSpriteMesh(FOUNDRY_API.getTexture(CONTROL_ICON_SRC.border)); 142 | 143 | const cornerTopRight = FOUNDRY_API.createSpriteMesh(FOUNDRY_API.getTexture(CONTROL_ICON_SRC.corner)); 144 | const borderRight = FOUNDRY_API.createSpriteMesh(FOUNDRY_API.getTexture(CONTROL_ICON_SRC.border)); 145 | 146 | const cornerBottomRight = FOUNDRY_API.createSpriteMesh(FOUNDRY_API.getTexture(CONTROL_ICON_SRC.corner)); 147 | const borderBottom = FOUNDRY_API.createSpriteMesh(FOUNDRY_API.getTexture(CONTROL_ICON_SRC.border)); 148 | 149 | const cornerBottomLeft = FOUNDRY_API.createSpriteMesh(FOUNDRY_API.getTexture(CONTROL_ICON_SRC.corner)); 150 | const borderLeft = FOUNDRY_API.createSpriteMesh(FOUNDRY_API.getTexture(CONTROL_ICON_SRC.border)); 151 | 152 | const background = FOUNDRY_API.createSpriteMesh(FOUNDRY_API.getTexture(CONTROL_ICON_SRC.background)); 153 | const cornerSize = 4 * (canvas.dimensions.uiScale ?? 1); 154 | background.width = width - cornerSize * 2; 155 | background.height = width - cornerSize * 2; 156 | background.anchor.set(0.5, 0.5); 157 | background.position.set(width / 2, width / 2); 158 | 159 | const backgroundContainer = new PIXI.Container(); 160 | const borderLength = width - cornerSize * 2; 161 | const borders = [ 162 | { 163 | element: borderTop, 164 | left: cornerSize, 165 | top: 0, 166 | rotation: 0, 167 | }, 168 | { 169 | element: borderRight, 170 | left: width, 171 | top: cornerSize, 172 | rotation: Math.PI * 0.5, 173 | }, 174 | { 175 | element: borderBottom, 176 | left: width - cornerSize, 177 | top: width, 178 | rotation: Math.PI * 1, 179 | }, 180 | { 181 | element: borderLeft, 182 | left: 0, 183 | top: width - cornerSize, 184 | rotation: Math.PI * 1.5, 185 | }, 186 | ]; 187 | borders.forEach(({ element, top, left, rotation }) => { 188 | element.rotation = rotation; 189 | element.position.set(left, top); 190 | element.width = borderLength; 191 | element.height = cornerSize; 192 | backgroundContainer.addChild(element); 193 | }); 194 | 195 | const corners = [ 196 | { element: cornerTopLeft, left: 0, top: 0, rotation: 0 }, 197 | { element: cornerTopRight, left: 1, top: 0, rotation: Math.PI * 0.5 }, 198 | { element: cornerBottomRight, left: 1, top: 1, rotation: Math.PI }, 199 | { element: cornerBottomLeft, left: 0, top: 1, rotation: Math.PI * 1.5 }, 200 | ]; 201 | 202 | corners.forEach(({ element, top, left, rotation }) => { 203 | element.rotation = rotation; 204 | element.position.set(left * width, top * width); 205 | element.width = cornerSize; 206 | element.height = cornerSize; 207 | backgroundContainer.addChild(element); 208 | }); 209 | 210 | backgroundContainer.addChild(background); 211 | backgroundContainer.position.set(offset, offset); 212 | 213 | this.addChildAt(backgroundContainer, 0); 214 | this.bg = backgroundContainer; 215 | 216 | return wrapped(...args); 217 | } 218 | 219 | function TokenRing_configure(this: any, wrapped: (...args: any[]) => void, mesh: PrimarySpriteMesh | undefined) { 220 | mesh ??= this.token.mesh; 221 | wrapped(mesh); 222 | 223 | const src = this.token.document.ring.subject.texture; 224 | const subjectTexture = getSpritesheetSubstitution(src); 225 | if (subjectTexture?.valid) { 226 | mesh.texture = subjectTexture; 227 | } 228 | } 229 | 230 | // Override for AmbientLight refreshControl to correctly set control icon texture 231 | // to the new spritesheet textures. Rest is copied from the original method. 232 | function AmbientLight_refreshControl(this: AmbientLight) { 233 | const isHidden = this.id && this.document.hidden; 234 | this.controlIcon.texture = FOUNDRY_API.getTexture( 235 | this.isVisible ? CONFIG.controlIcons.light : CONFIG.controlIcons.lightOff, 236 | ); 237 | this.controlIcon.tintColor = isHidden ? 0xff3300 : 0xffffff; 238 | this.controlIcon.borderColor = isHidden ? 0xff3300 : 0xff5500; 239 | this.controlIcon.elevation = this.document.elevation; 240 | this.controlIcon.refresh({ visible: this.layer.active, borderVisible: this.hover || this.layer.highlightObjects }); 241 | this.controlIcon.draw(); 242 | } 243 | 244 | // Override for AmbientSound refreshControl to correctly set control icon texture 245 | // to the new spritesheet textures. Rest is copied from the original method. 246 | function AmbientSound_refreshControl(this: AmbientLight) { 247 | const isHidden = this.id && (this.document.hidden || !this.document.path); 248 | this.controlIcon.tintColor = isHidden ? 0xff3300 : 0xffffff; 249 | this.controlIcon.borderColor = isHidden ? 0xff3300 : 0xff5500; 250 | this.controlIcon.texture = FOUNDRY_API.getTexture( 251 | this.isAudible ? CONFIG.controlIcons.sound : CONFIG.controlIcons.soundOff, 252 | ); 253 | this.controlIcon.elevation = this.document.elevation; 254 | this.controlIcon.refresh({ visible: this.layer.active, borderVisible: this.hover || this.layer.highlightObjects }); 255 | this.controlIcon.draw(); 256 | } 257 | 258 | // Wrapper for ambient light rendering. 259 | // Reorders rendering of ambient lights to first render the aura circles, then 260 | // the control icon to improve render batching 261 | function LightingOrSoundLayer_render( 262 | this: AmbientLight | AmbientSound, 263 | wrapped: (...args: any[]) => void, 264 | ...args: any[] 265 | ) { 266 | // disable everything except the objects (ambient light placeables) container 267 | disableRenderingFor(this.children, (child) => child !== this.objects); 268 | 269 | // render ambient light objects, but do it in two phases: Aura circles (light or sound fields) first, then control icons 270 | 271 | // render aura field circles: 272 | this.objects.children.forEach((object: AmbientLight | AmbientSound) => { 273 | disableRenderingFor(object.children, (child) => child !== object.field); 274 | }); 275 | wrapped(...args); 276 | 277 | // render control icons: 278 | this.objects.children.forEach((object: AmbientLight | AmbientSound) => { 279 | restoreRenderableOriginal(object.children); 280 | disableRenderingFor(object.children, (child) => child === object.field); 281 | }); 282 | wrapped(...args); 283 | 284 | // restore renderable state for ambient light objects 285 | this.objects.children.forEach((object: AmbientLight | AmbientSound) => { 286 | restoreRenderableOriginal(object.children); 287 | }); 288 | 289 | // render everything else except the objects container 290 | restoreRenderableOriginal(this.children); 291 | disableRenderingFor(this.children, (child) => child === this.objects); 292 | wrapped(...args); 293 | 294 | // restore renderable state 295 | restoreRenderableOriginal(this.children); 296 | } 297 | 298 | function restoreRenderableOriginal(children: PIXI.DisplayObject[]) { 299 | for (const child of children) { 300 | if (Object.hasOwnProperty.call(child, 'renderableOriginal')) { 301 | child.renderable = child.renderableOriginal; 302 | delete child.renderableOriginal; 303 | } 304 | } 305 | } 306 | 307 | function disableRenderingFor(children: T[], test: (child: T) => boolean) { 308 | for (const child of children) { 309 | if (test(child)) { 310 | child.renderableOriginal = child.renderable; 311 | child.renderable = false; 312 | } 313 | } 314 | } 315 | 316 | // Override the DoorControl _getTexture method to use the new spritesheet textures 317 | // This should be a 1:1 copy except for the texture loading call itself. 318 | function DoorControl__getTexture(this: DoorControl) { 319 | // Determine displayed door state 320 | const ds = CONST.WALL_DOOR_STATES; 321 | let s = this.wall.document.ds; 322 | if (!FOUNDRY_API.game.user.isGM && s === ds.LOCKED) s = ds.CLOSED; 323 | 324 | // Determine texture path 325 | const icons = CONFIG.controlIcons; 326 | let path = 327 | { 328 | [ds.LOCKED]: icons.doorLocked, 329 | [ds.CLOSED]: icons.doorClosed, 330 | [ds.OPEN]: icons.doorOpen, 331 | }[s] || icons.doorClosed; 332 | if (s === ds.CLOSED && this.wall.document.door === CONST.WALL_DOOR_TYPES.SECRET) path = icons.doorSecret; 333 | 334 | // Obtain the icon texture 335 | return FOUNDRY_API.getTexture(path); 336 | } 337 | 338 | async function loadAndRegisterSpritesheets(spritesheetUrls: string[]) { 339 | const spritesheets: PIXI.Spritesheet[] = await Promise.all(spritesheetUrls.map((url) => PIXI.Assets.load(url))); 340 | for (const spritesheet of spritesheets) { 341 | if (!(spritesheet instanceof PIXI.Spritesheet)) { 342 | continue; 343 | } 344 | 345 | if (spritesheet.data.meta.alphaMode === 'pma') { 346 | spritesheet.baseTexture.alphaMode = PIXI.ALPHA_MODES.PMA; 347 | } 348 | 349 | const allSheets = [spritesheet, ...(spritesheet.linkedSheets ?? [])]; 350 | for (const sheet of allSheets) { 351 | for (const [name, texture] of Object.entries(sheet.textures)) { 352 | if (texture instanceof PIXI.Texture) { 353 | spritesheetSubstitutions[name] = texture; 354 | } 355 | } 356 | } 357 | } 358 | } 359 | 360 | async function loadBaseSpritesheets(): Promise { 361 | const baseSpritesheet = `modules/${NAMESPACE}/dist/spritesheets/base-icons-0.json`; 362 | 363 | // track loading state. Should something go wrong during spritesheet loading, we 364 | // simply cancel the initialization and log an error 365 | let loadingSuccessful = true; 366 | 367 | // load the base spritesheets and extract the textures for substitution 368 | try { 369 | await loadAndRegisterSpritesheets([baseSpritesheet]); 370 | } catch (error) { 371 | ui.notifications.error( 372 | game.i18n.localize(`${NAMESPACE}.settings.${SETTINGS.CustomSpritesheets}.menu.warn-init-failed`), 373 | ); 374 | console.error('Error loading base spritesheets for substitution'); 375 | console.error(error); 376 | loadingSuccessful = false; 377 | } 378 | 379 | try { 380 | const customSpritesheets: string[] = game.settings.get(NAMESPACE, SETTINGS.CustomSpritesheets) ?? []; 381 | await loadAndRegisterSpritesheets(customSpritesheets); 382 | } catch (error) { 383 | ui.notifications.warn( 384 | game.i18n.localize(`${NAMESPACE}.settings.${SETTINGS.CustomSpritesheets}.menu.warn-custom-spritesheets-failed`), 385 | ); 386 | console.error('Error loading custom spritesheets for substitution', error); 387 | // this will not mark initialization as failed, because the base spritesheets 388 | // are still loaded and the substitution will work for those 389 | } 390 | 391 | // Resolve the initialization promise to indicate that spritesheet substitution is ready 392 | // This is behind a timeout to ensure the setup / wrapper code needed for further 393 | // spritesheet substitution setup is executed before the promise resolves 394 | // and loading of textures continues 395 | setTimeout(() => { 396 | resolveInitializationPromise(loadingSuccessful); 397 | }, 0); 398 | 399 | return loadingSuccessful; 400 | } 401 | 402 | async function enableSpritesheetSubstitution() { 403 | const enabled = getSetting(SETTINGS.SpritesheetSubstitution); 404 | 405 | if (!enabled) { 406 | return; 407 | } 408 | 409 | // make sure basis transcoder is enabled (it is by default in v13+) 410 | if (FOUNDRY_API.game.release.generation < 13) { 411 | CONFIG.Canvas.transcoders.basis = true; 412 | } 413 | 414 | // register wrapper for texture loader to wait for spritesheet initialization when loading textures 415 | registerWrapperForVersion(loadTextureWaitForInitialization, 'WRAPPER', { 416 | v12: 'TextureLoader.prototype.loadTexture', 417 | v13: 'foundry.canvas.TextureLoader.prototype.loadTexture', 418 | }); 419 | 420 | // fix transcoding of KTX2 textures to support mipmaps (only needed for v13+) 421 | registerWrapperForVersion(transcodeAsyncWithMipmaps, 'WRAPPER', { v13: 'PixiBasisKTX2.KTX2Parser.transcodeAsync' }); 422 | 423 | // load spritesheets for substitution 424 | const loadingSuccessful = await loadBaseSpritesheets(); 425 | 426 | // cancel further setup of spritesheet substitution if loading failed 427 | if (!loadingSuccessful) { 428 | return; 429 | } 430 | 431 | // Override publically accessible getTexture and loadTexture API methods. 432 | // This takes care of all modules, system and some core code that uses these methods 433 | // to load textures (async) or get cached textures (sync) 434 | if (FOUNDRY_API.game.release.generation < 13) { 435 | const originalLoadTexture = loadTexture; 436 | loadTexture = (src: string) => loadTextureWithSubstitution(originalLoadTexture, src); 437 | 438 | const originalGetTexture = getTexture; 439 | getTexture = (src: string) => getTextureWithSubstitution(originalGetTexture, src); 440 | } else { 441 | const originalLoadTexture = foundry.canvas.loadTexture; 442 | foundry.canvas = Object.freeze({ 443 | ...foundry.canvas, 444 | loadTexture: (src: string) => loadTextureWithSubstitution(originalLoadTexture, src), 445 | }); 446 | 447 | const originalGetTexture = foundry.canvas.getTexture; 448 | foundry.canvas = Object.freeze({ 449 | ...foundry.canvas, 450 | getTexture: (src: string) => getTextureWithSubstitution(originalGetTexture, src), 451 | }); 452 | } 453 | 454 | // Override the ControlIcon draw method to use the new spritesheet textures and 455 | // create an optimized texture based background and border for the control icon 456 | registerWrapperForVersion(ControlIcon_draw, 'WRAPPER', { 457 | v12: 'ControlIcon.prototype.draw', 458 | v13: 'foundry.canvas.containers.ControlIcon.prototype.draw', 459 | }); 460 | 461 | // Override the AmbientLight refreshControl because it sets the control icon texture manually 462 | registerWrapperForVersion(AmbientLight_refreshControl, 'OVERRIDE', { 463 | v12: 'AmbientLight.prototype.refreshControl', 464 | v13: 'foundry.canvas.placeables.AmbientLight.prototype.refreshControl', 465 | }); 466 | 467 | // Override the AmbientLight refreshControl because it sets the control icon texture manually 468 | registerWrapperForVersion(AmbientSound_refreshControl, 'OVERRIDE', { 469 | v12: 'AmbientSound.prototype.refreshControl', 470 | v13: 'foundry.canvas.placeables.AmbientSound.prototype.refreshControl', 471 | }); 472 | 473 | // Override the DoorControl _getTexture method to use the new spritesheet textures 474 | registerWrapperForVersion(DoorControl__getTexture, 'OVERRIDE', { 475 | v12: 'DoorControl.prototype._getTexture', 476 | v13: 'foundry.canvas.containers.DoorControl.prototype._getTexture', 477 | }); 478 | 479 | // Override LightingLayer render method to improve render batching for AmbientLight objects 480 | registerWrapperForVersion(LightingOrSoundLayer_render, 'WRAPPER', { 481 | v12: 'LightingLayer.prototype.render', 482 | v13: 'foundry.canvas.layers.LightingLayer.prototype.render', 483 | }); 484 | 485 | // Override SoundsLayer render method to improve render batching for AmbientLight objects 486 | registerWrapperForVersion(LightingOrSoundLayer_render, 'WRAPPER', { 487 | v12: 'SoundsLayer.prototype.render', 488 | v13: 'foundry.canvas.layers.SoundsLayer.prototype.render', 489 | }); 490 | 491 | // Override TokenRing configure method to fix texture loading 492 | registerWrapperForVersion(TokenRing_configure, 'WRAPPER', { 493 | v12: 'foundry.canvas.tokens.TokenRing.prototype.configure', 494 | v13: 'foundry.canvas.placeables.tokens.TokenRing.prototype.configure', 495 | }); 496 | } 497 | 498 | export { enableSpritesheetSubstitution }; 499 | -------------------------------------------------------------------------------- /src/hacks/tokenBarsCaching.ts: -------------------------------------------------------------------------------- 1 | import { NAMESPACE } from 'src/constants.ts'; 2 | import { SETTINGS } from 'src/settings/constants.ts'; 3 | import { getSetting } from 'src/settings/settings.ts'; 4 | import { getBitmapCacheResolution } from 'src/utils/getBitmapCacheResolution.ts'; 5 | 6 | async function cacheResourceBars(this: Token, wrapped: Function, ...args: any[]) { 7 | const wrappedResult = wrapped(...args); 8 | if (wrappedResult instanceof Promise) { 9 | await wrappedResult; 10 | } 11 | const cacheAsBitmapResolution = getBitmapCacheResolution(); 12 | const cacheGraphics = (bars: PIXI.Graphics | PIXI.Container | unknown) => { 13 | if (bars instanceof PIXI.Graphics) { 14 | bars.cacheAsBitmap = false; 15 | bars.cacheAsBitmapResolution = cacheAsBitmapResolution; 16 | bars.cacheAsBitmap = true; 17 | } else if (bars instanceof PIXI.Container) { 18 | bars.children.forEach((child) => cacheGraphics(child)); 19 | } 20 | }; 21 | if (game.modules.get('barbrawl')?.active) { 22 | // barbrawl refreshes the bars asynchronously... 23 | // this is just a dirty hack to wait for the bars to be refreshed and actually available 24 | setTimeout(() => cacheGraphics(this.bars), 50); 25 | } else { 26 | cacheGraphics(this.bars); 27 | } 28 | } 29 | 30 | function isTokenBarCachingAvailable() { 31 | // disable for newer bar brawl versions as that modules comes with its own caching 32 | // and is broken by our caching attempts 33 | const barBrawl = game.modules.get('barbrawl'); 34 | if (barBrawl && barBrawl.active && foundry.utils.isNewerVersion(barBrawl.version, '1.8.8')) { 35 | return false; 36 | } 37 | 38 | return true; 39 | } 40 | 41 | let enableTokenBarsCaching = () => { 42 | if (!isTokenBarCachingAvailable()) { 43 | return; 44 | } 45 | 46 | const enabled = getSetting(SETTINGS.TokenBarsCaching); 47 | if (!enabled) { 48 | return; 49 | } 50 | 51 | libWrapper.register(NAMESPACE, 'CONFIG.Token.objectClass.prototype.drawBars', cacheResourceBars, 'WRAPPER'); 52 | }; 53 | export { enableTokenBarsCaching }; 54 | 55 | if (import.meta.hot) { 56 | import.meta.hot.accept((newModule) => { 57 | enableTokenBarsCaching = newModule?.foo; 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /src/hacks/tokenRingSpritesheetSupport.ts: -------------------------------------------------------------------------------- 1 | // --------------------------------------------- 2 | // Ring shader patching for spritesheet support 3 | 4 | import { SETTINGS } from 'src/settings/constants.ts'; 5 | import { getSetting } from 'src/settings/settings.ts'; 6 | import { FOUNDRY_API } from 'src/utils/foundryShim.ts'; 7 | import { registerWrapperForVersion } from 'src/utils/registerWrapper.ts'; 8 | 9 | function patchTokenRingShader() { 10 | const ShaderClass = 11 | FOUNDRY_API.generation < 13 ? TokenRingSamplerShader : foundry.canvas.rendering.shaders.TokenRingSamplerShader; 12 | ShaderClass.batchGeometry.push({ 13 | id: 'aTextureScaleOffset', 14 | size: 4, 15 | normalized: false, 16 | type: PIXI.TYPES.FLOAT, 17 | }); 18 | ShaderClass.spritesheetSupportVertexSize = 4; 19 | ShaderClass.batchVertexSize += ShaderClass.spritesheetSupportVertexSize; 20 | 21 | // add the new inputs to the batch vertex shader 22 | ShaderClass._batchVertexShader = ShaderClass._batchVertexShader.replace( 23 | 'in vec2 aRingTextureCoord;', 24 | `in vec2 aRingTextureCoord;\n in vec4 aTextureScaleOffset;`, 25 | ); 26 | 27 | // add new vertex outputs 28 | ShaderClass._batchVertexShader = ShaderClass._batchVertexShader.replace( 29 | 'out vec2 vOrigTextureCoord;', 30 | `out vec2 vOrigTextureCoord;\n out vec2 vTextureCoordClipped;`, 31 | ); 32 | 33 | // // set vertex outputs 34 | ShaderClass._batchVertexShader = ShaderClass._batchVertexShader.replace( 35 | 'vOrigTextureCoord = aTextureCoord;', 36 | `vOrigTextureCoord = aTextureCoord * aTextureScaleOffset.xy - aTextureScaleOffset.zw; 37 | vTextureCoordClipped = (vOrigTextureCoord - 0.5) * aTextureScaleCorrection + 0.5; 38 | `, 39 | ); 40 | 41 | // add fragment shader input 42 | ShaderClass._batchFragmentShader = ShaderClass._batchFragmentShader.replace( 43 | 'float gradientMix = smoothstep(0.0, 1.0, dot(rotation(vTextureCoord, time), vec2(0.5)));', 44 | 'float gradientMix = smoothstep(0.0, 1.0, dot(rotation(vTextureCoordClipped, time), vec2(0.5)));', 45 | ); 46 | 47 | // // fix the gradient mix calculation to use the clipped texture coordinates 48 | ShaderClass._batchFragmentShader = ShaderClass._batchFragmentShader.replace( 49 | 'in vec2 vOrigTextureCoord;', 50 | `in vec2 vOrigTextureCoord;\n in vec2 vTextureCoordClipped;`, 51 | ); 52 | } 53 | 54 | function TokenRingSamplerShader__packInterleavedGeometry( 55 | this: BaseSamplerShader, 56 | wrapped: (...args: any[]) => void, 57 | element: any, 58 | attributeBuffer: any, 59 | indexBuffer: any, 60 | aIndex: number, 61 | iIndex: number, 62 | ) { 63 | wrapped(element, attributeBuffer, indexBuffer, aIndex, iIndex); 64 | 65 | const TokenRingSamplerShaderClass = 66 | FOUNDRY_API.generation < 13 ? TokenRingSamplerShader : foundry.canvas.rendering.shaders.TokenRingSamplerShader; 67 | 68 | // Destructure the properties from the element for faster access 69 | const { vertexData } = element; 70 | const object = element.object.object || {}; 71 | 72 | // Retrieve ring properties with default values 73 | const { 74 | baseTexture: { width: baseTextureWidth, height: baseTextureHeight }, 75 | width: textureWidth, 76 | height: textureHeight, 77 | frame: textureFrame, 78 | } = object.mesh.texture || {}; 79 | 80 | // Calculate colors using the PIXI.Color class 81 | 82 | // Access Float32 and Uint32 views of the attribute buffer 83 | const { float32View } = attributeBuffer; 84 | 85 | const vertexSize = this.vertexSize; 86 | const offset = aIndex + vertexSize - TokenRingSamplerShaderClass.spritesheetSupportVertexSize; 87 | 88 | // Loop through the vertex data to fill attribute buffers 89 | for (let i = 0, j = 0; i < vertexData.length; i += 2, j += vertexSize) { 90 | let k = offset + j; 91 | 92 | float32View[k++] = baseTextureWidth / textureWidth; 93 | float32View[k++] = baseTextureHeight / textureHeight; 94 | float32View[k++] = textureFrame.x / textureWidth; 95 | float32View[k++] = textureFrame.y / textureHeight; 96 | } 97 | } 98 | 99 | async function enableTokenRingSpritesheetSupport() { 100 | const enabled = getSetting(SETTINGS.TokenRingSpritesheetSupport); 101 | 102 | if (!enabled) { 103 | return; 104 | } 105 | 106 | // Token Ring shader patching 107 | patchTokenRingShader(); 108 | registerWrapperForVersion(TokenRingSamplerShader__packInterleavedGeometry, 'WRAPPER', { 109 | v12: 'TokenRingSamplerShader._packInterleavedGeometry', 110 | v13: 'foundry.canvas.rendering.shaders.TokenRingSamplerShader._packInterleavedGeometry', 111 | }); 112 | } 113 | 114 | export { enableTokenRingSpritesheetSupport }; 115 | -------------------------------------------------------------------------------- /src/hacks/useOooTokenRendering.ts: -------------------------------------------------------------------------------- 1 | import { SETTINGS } from 'src/settings/constants.ts'; 2 | import { getSetting } from 'src/settings/settings.ts'; 3 | import { wrapFunction } from 'src/utils/wrapFunction.ts'; 4 | 5 | /** 6 | * Principles behind out of order token rendering 7 | * 8 | * tokens consist of many different ui element types and void meshes 9 | * (erase blend mode) that break batch rendering of multiple tokens. 10 | * Batching can be greatly improved by not rendering one token at a time 11 | * but rendering the token container children out of order, so that 12 | * first every token border is drawn in one batch, then every void mesh, 13 | * all nameplates, ... etc. This greatly improves batching potential but 14 | * is challenging! This is only save to do for tokens that do not have 15 | * masks or filters (in this case rendering in order is probably faster) 16 | * and that do not overlap each other either in their void meshes or UI 17 | * elements. 18 | * 19 | * Fortunately, testing for overlap with quadtrees is quite fast! 20 | * 21 | * To actually build a list of tokens that can be batched out of order, we 22 | * first divide the token layer children in segments divided only by elements 23 | * that are not tokens at all. This should not happen, but modules are known 24 | * to mess with the layer objects in certain instances... 25 | * 26 | * This means a token layer looking like this: 27 | * [Token, Token, Other, Token, Token, Other] will be sectioned like this: 28 | * => [[Token, Token], [Other], [Token, Token], [Other]] 29 | * 30 | * withing each section we now split the tokens into layers of tokens 31 | * that do not interact with eachother. 32 | * This is done by testing each token for overlap and moving those 33 | * that do not overlap anything or only tokens from a previous batch into 34 | * a new render batch. This is done until there are on tokens left. 35 | * 36 | * Now that we have our batches of non-interfering tokens we are free to 37 | * reorder rendering them as we whish! 38 | * The first thing to do is moving all those tokens that have masks or 39 | * filter defined into a separate group and rendering them in the end. 40 | * All other tokens (should be the vast majority) are now also rendered, 41 | * but not in order, token by token, but sliced by their child elements, 42 | * meaning for Token1, Token2, Token3 we render 43 | * Child1(T1), Child1(T2), Child1(T3) 44 | * Child2(T1), Child2(T2), Child2(T3) and so on, which means 45 | * that instead of switching between painting graphics, erasing with a 46 | * different blend mode, drawing text etc we erase everything in one go 47 | * (no new draw call because drawing mode does not change), drawing 48 | * nameplates in batches etc. 49 | * 50 | * This can effectively reduce draw calls from 40+ for 10 tokens 51 | * that each show hp bars, nameplate etc to as little as 2-3 if all 52 | * individual child painting operations are batchable! 53 | * 54 | */ 55 | 56 | interface InterfaceQuadtreeBounds { 57 | r: PIXI.Rectangle; 58 | t: Token; 59 | n: Set; 60 | } 61 | 62 | class TokenRenderBatch { 63 | #tokens: Token[]; 64 | 65 | #maxChildCount = 0; 66 | #sliceable: Token[] = []; 67 | #unslicable: Token[] = []; 68 | constructor(tokens: Token[]) { 69 | this.#tokens = tokens; 70 | tokens.forEach((token) => { 71 | if (token.mask || (token.filters && token.filters.length)) { 72 | this.#unslicable.push(token); 73 | } else { 74 | this.#sliceable.push(token); 75 | this.#maxChildCount = Math.max(this.#maxChildCount, token.children.length); 76 | } 77 | }); 78 | // sort sliceables so that those with dynamic token ring are rendered together. 79 | // This minimizes breaking the void mesh drawing batch because dynamic token rings 80 | // currently break batching because of their custom shader 81 | this.#sliceable.sort((a, b) => +a.hasDynamicRing - +b.hasDynamicRing); 82 | } 83 | 84 | get tokens(): Token[] { 85 | return this.#tokens; 86 | } 87 | 88 | render(renderer: PIXI.Renderer) { 89 | // render slicables in slices 90 | 91 | for (let i = 0; i < this.#maxChildCount; i++) { 92 | this.#sliceable.forEach((token) => { 93 | // lets render the container without any children first so that custom 94 | // render implementations get called... This is not the way 95 | if (i === 0) { 96 | const renderFns: ((renderer: PIXI.Renderer) => void)[] = []; 97 | token.children.forEach((child, i) => { 98 | renderFns[i] = child.render; 99 | child.render = () => {}; 100 | }); 101 | token.render(renderer); 102 | token.children.forEach((child, i) => { 103 | child.render = renderFns[i]; 104 | }); 105 | } 106 | 107 | const child = token.children[i]; 108 | if (child) { 109 | const cullable = child.cullable; 110 | child.cullable = cullable ?? token.cullable; 111 | child.render(renderer); 112 | child.cullable = cullable; 113 | } 114 | }); 115 | } 116 | // render everything else just as normal 117 | this.#unslicable.forEach((token) => { 118 | token.render(renderer); 119 | }); 120 | } 121 | } 122 | 123 | abstract class RenderSegment { 124 | abstract render(renderer: PIXI.Renderer): void; 125 | } 126 | 127 | function isTokenBelow(a: Token, b: Token) { 128 | return (a.document.elevation - b.document.elevation || a.document.sort - b.document.sort || a.zIndex - b.zIndex) < 0; 129 | } 130 | 131 | class TokenRenderSegment extends RenderSegment { 132 | #primaryQuadTree = canvas.primary.quadtree; 133 | #interfaceQuadTree: CanvasQuadtree; 134 | #tokens: InterfaceQuadtreeBounds[] = []; 135 | #renderBatches: TokenRenderBatch[] = []; 136 | 137 | constructor(interfaceQuadTree: CanvasQuadtree) { 138 | super(); 139 | this.#interfaceQuadTree = interfaceQuadTree; 140 | } 141 | 142 | add(token: InterfaceQuadtreeBounds) { 143 | this.#tokens.push(token); 144 | } 145 | 146 | render(renderer: PIXI.Renderer) { 147 | // build batches of non-overlapping token UIs 148 | let openSet = this.#tokens.slice(); 149 | const processed = new Set(); 150 | const segmentTokens = new Set(this.#tokens.map((t) => t.t)); 151 | while (openSet.length) { 152 | const batchTokens: Token[] = []; 153 | const overlapTokens: InterfaceQuadtreeBounds[] = []; 154 | for (const token of openSet) { 155 | if (this.#checkTokenOverlap(token, processed, segmentTokens)) { 156 | overlapTokens.push(token); 157 | } else { 158 | batchTokens.push(token.t); 159 | } 160 | } 161 | batchTokens.forEach((t) => processed.add(t)); 162 | if (overlapTokens.length === openSet.length) { 163 | console.error('Overlap testing endless loop! Aborting!'); 164 | break; 165 | } 166 | openSet = overlapTokens; 167 | this.#renderBatches.push(new TokenRenderBatch(batchTokens)); 168 | } 169 | this.#renderBatches.forEach((batch) => batch.render(renderer)); 170 | } 171 | 172 | #checkTokenOverlap( 173 | bounds: InterfaceQuadtreeBounds, 174 | previousBatch: Set, 175 | segmentTokens: Set, 176 | ): boolean { 177 | const interfaceQuadTree = this.#interfaceQuadTree; 178 | const primaryToken = bounds.t; 179 | 180 | const filterOverlap = (collidingToken: PlaceableObject) => 181 | // tokens always collide with each other, remove those 182 | collidingToken !== primaryToken && 183 | // don't include tokens that collide with previous batches 184 | !previousBatch.has(collidingToken) && 185 | // only include those in our current segment 186 | segmentTokens.has(collidingToken) && 187 | // only include colliding tokens that are below the primary token 188 | isTokenBelow(collidingToken as any, primaryToken); 189 | 190 | const interfaceOverlap = interfaceQuadTree.getObjects(bounds.r, { 191 | collisionTest: ({ t: collidingToken }) => filterOverlap(collidingToken), 192 | }); 193 | if (interfaceOverlap.size > 0) { 194 | return true; 195 | } 196 | 197 | const primaryQuadTree = this.#primaryQuadTree; 198 | const primaryMeshOverlap = primaryQuadTree.getObjects(bounds.r, { 199 | collisionTest: ({ t }) => { 200 | const collidingToken = (t as any).object; 201 | if (!(collidingToken instanceof CONFIG.Token.objectClass)) { 202 | return false; 203 | } 204 | return filterOverlap(collidingToken); 205 | }, 206 | }); 207 | return primaryMeshOverlap.size > 0; 208 | } 209 | } 210 | class OtherRenderSegment extends RenderSegment { 211 | #other: PIXI.DisplayObject; 212 | constructor(other: PIXI.DisplayObject) { 213 | super(); 214 | this.#other = other; 215 | } 216 | 217 | render(renderer: PIXI.Renderer) { 218 | this.#other.render(renderer); 219 | } 220 | } 221 | const tempMatrix = new PIXI.Matrix(); 222 | 223 | function getInterfaceBounds(object: PlaceableObject) { 224 | const b = object.bounds; 225 | const lb = object.getLocalBounds(); 226 | const localRect = new PIXI.Rectangle(b.x + lb.x, b.y + lb.y, lb.width, lb.height); 227 | const PrimarySpriteMeshClass = foundry?.canvas?.primary?.PrimarySpriteMesh ?? PrimarySpriteMesh; 228 | const TokenClass = foundry?.canvas?.placeables?.Token ?? Token; 229 | 230 | if (!(object instanceof TokenClass) || !(object.mesh instanceof PrimarySpriteMeshClass)) { 231 | return localRect; 232 | } 233 | 234 | const meshBounds = (object.mesh as any)._canvasBounds as any; 235 | const meshRect = new PIXI.Rectangle( 236 | meshBounds.minX, 237 | meshBounds.minY, 238 | meshBounds.maxX - meshBounds.minX, 239 | meshBounds.maxY - meshBounds.minY, 240 | ); 241 | const left = Math.min(localRect.left, meshRect.left); 242 | const right = Math.max(localRect.right, meshRect.right); 243 | const top = Math.min(localRect.top, meshRect.top); 244 | const bottom = Math.max(localRect.bottom, meshRect.bottom); 245 | return new PIXI.Rectangle(left, top, right - left, bottom - top); 246 | } 247 | 248 | function buildAndRenderTokenBatches(renderer: PIXI.Renderer, container: PIXI.Container) { 249 | // build initial batches 250 | const Quadtree = foundry?.canvas?.geometry?.CanvasQuadtree ?? CanvasQuadtree; 251 | // @ts-expect-error types are wrong for CanvasQuadtree. Does not need params 252 | const interfaceQuadtree = new Quadtree(); 253 | const segments: RenderSegment[] = []; 254 | const TokenClass = foundry?.canvas?.placeables?.Token ?? Token; 255 | 256 | // populate quadtree 257 | container.children.forEach((child, i) => { 258 | if (!child.visible || child.worldAlpha <= 0 || !child.renderable) { 259 | return; 260 | } 261 | if (!(child instanceof TokenClass)) { 262 | segments.push(new OtherRenderSegment(child)); 263 | return; 264 | } 265 | 266 | const bounds = { r: getInterfaceBounds(child), t: child, n: new Set>() }; 267 | interfaceQuadtree.insert(bounds); 268 | 269 | const lastBatch = segments.at(-1); 270 | if (lastBatch && lastBatch instanceof TokenRenderSegment) { 271 | lastBatch.add(bounds); 272 | } else { 273 | const newBatch = new TokenRenderSegment(interfaceQuadtree); 274 | newBatch.add(bounds); 275 | segments.push(newBatch); 276 | } 277 | }); 278 | segments.forEach((segment) => segment.render(renderer)); 279 | } 280 | 281 | function renderTokensOoo(this: PIXI.Container, renderer: PIXI.Renderer) { 282 | // everything to the very end is just copied from the PIXI.Container implementation 283 | const sourceFrame = renderer.renderTexture.sourceFrame; 284 | 285 | if (!(sourceFrame.width > 0 && sourceFrame.height > 0)) { 286 | return; 287 | } 288 | let bounds: PIXI.Rectangle | undefined; 289 | let transform: PIXI.Matrix | undefined; 290 | if (this.cullArea) { 291 | bounds = this.cullArea; 292 | transform = this.worldTransform; 293 | } else if (this._render !== PIXI.Container.prototype._render) { 294 | bounds = this.getBounds(true); 295 | } 296 | const projectionTransform = renderer.projection.transform; 297 | if (projectionTransform) { 298 | if (transform) { 299 | transform = tempMatrix.copyFrom(transform); 300 | transform.prepend(projectionTransform); 301 | } else { 302 | transform = projectionTransform; 303 | } 304 | } 305 | if (bounds && sourceFrame.intersects(bounds, transform)) { 306 | this._render(renderer); 307 | } else if (this.cullArea) { 308 | return; 309 | } 310 | 311 | // custom code 312 | buildAndRenderTokenBatches(renderer, this); 313 | } 314 | 315 | function enableOooTokenRendering() { 316 | const enabled = getSetting(SETTINGS.OptimizeTokenUiBatching); 317 | if (!enabled) { 318 | return; 319 | } 320 | 321 | /** 322 | * on draw, update the 'objects' container (the one containing all the tokens) 323 | * to use a specialized rendering function that renders the children (potentially) 324 | * out of order to increase batching 325 | */ 326 | wrapFunction(TokenLayer.prototype, '_draw', function (this: TokenLayer) { 327 | if (this.objects) { 328 | this.objects.render = renderTokensOoo; 329 | } 330 | }); 331 | } 332 | export { enableOooTokenRendering as useOooTokenRendering }; 333 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // Required because Vite does not support .ts files as entry points, I guess?? 2 | import './index.ts'; 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import './hacks/index.ts'; 2 | import './settings/settings.ts'; 3 | -------------------------------------------------------------------------------- /src/init.ts: -------------------------------------------------------------------------------- 1 | import { DynamicSpriteSheet } from './DynamicSpriteSheet.ts'; 2 | 3 | window.fvttPerfHacks = { 4 | autoSpritesheetCache: new DynamicSpriteSheet(), 5 | }; 6 | 7 | Hooks.on('canvasTearDown', () => { 8 | window.fvttPerfHacks.autoSpritesheetCache.clear(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/settings/constants.ts: -------------------------------------------------------------------------------- 1 | export enum SETTINGS { 2 | OptimizeTokenUiBatching = 'optimize-interface-layer-clipping', 3 | EffectsCaching = 'token-effects-caching', 4 | TokenBarsCaching = 'token-bars-caching', 5 | SpritesheetSubstitution = 'spritesheet-substitution', 6 | PrecomputedNoiseTextures = 'precomputed-noise-textures', 7 | TokenRingSpritesheetSupport = 'token-ring-spritesheet-support', 8 | CustomSpritesheets = 'spritesheet-substitution-custom', 9 | } 10 | -------------------------------------------------------------------------------- /src/settings/settings.ts: -------------------------------------------------------------------------------- 1 | import { NAMESPACE } from 'src/constants.ts'; 2 | import { CustomSpritesheetConfig } from '../apps/CustomSpritesheetConfig.ts'; 3 | import { SETTINGS } from './constants.ts'; 4 | 5 | export function getSetting(settings: SETTINGS): unknown { 6 | return game.settings.get(NAMESPACE, settings); 7 | } 8 | 9 | Hooks.on('init', () => { 10 | game.settings.register(NAMESPACE, SETTINGS.OptimizeTokenUiBatching, { 11 | name: `${NAMESPACE}.settings.${SETTINGS.OptimizeTokenUiBatching}.name`, 12 | hint: `${NAMESPACE}.settings.${SETTINGS.OptimizeTokenUiBatching}.hint`, 13 | scope: 'client', 14 | config: true, 15 | requiresReload: true, 16 | type: Boolean, 17 | default: true, 18 | }); 19 | game.settings.register(NAMESPACE, SETTINGS.EffectsCaching, { 20 | name: `${NAMESPACE}.settings.${SETTINGS.EffectsCaching}.name`, 21 | hint: `${NAMESPACE}.settings.${SETTINGS.EffectsCaching}.hint`, 22 | scope: 'client', 23 | config: true, 24 | requiresReload: true, 25 | type: Boolean, 26 | default: true, 27 | }); 28 | game.settings.register(NAMESPACE, SETTINGS.TokenBarsCaching, { 29 | name: `${NAMESPACE}.settings.${SETTINGS.TokenBarsCaching}.name`, 30 | hint: `${NAMESPACE}.settings.${SETTINGS.TokenBarsCaching}.hint`, 31 | scope: 'client', 32 | config: true, 33 | requiresReload: true, 34 | type: Boolean, 35 | default: true, 36 | }); 37 | game.settings.register(NAMESPACE, SETTINGS.SpritesheetSubstitution, { 38 | name: `${NAMESPACE}.settings.${SETTINGS.SpritesheetSubstitution}.name`, 39 | hint: `${NAMESPACE}.settings.${SETTINGS.SpritesheetSubstitution}.hint`, 40 | scope: 'client', 41 | config: true, 42 | requiresReload: true, 43 | type: Boolean, 44 | default: true, 45 | }); 46 | 47 | game.settings.register(NAMESPACE, SETTINGS.CustomSpritesheets, { 48 | name: `${NAMESPACE}.settings.${SETTINGS.CustomSpritesheets}.name`, 49 | hint: `${NAMESPACE}.settings.${SETTINGS.CustomSpritesheets}.hint`, 50 | config: false, 51 | type: Array, 52 | default: [], 53 | }); 54 | 55 | game.settings.registerMenu(NAMESPACE, SETTINGS.CustomSpritesheets, { 56 | name: `${NAMESPACE}.settings.${SETTINGS.CustomSpritesheets}.name`, 57 | hint: `${NAMESPACE}.settings.${SETTINGS.CustomSpritesheets}.hint`, 58 | label: `${NAMESPACE}.settings.${SETTINGS.CustomSpritesheets}.label`, 59 | icon: 'fa-solid fa-images', 60 | type: CustomSpritesheetConfig, 61 | restricted: true, 62 | }); 63 | 64 | game.settings.register(NAMESPACE, SETTINGS.TokenRingSpritesheetSupport, { 65 | name: `${NAMESPACE}.settings.${SETTINGS.TokenRingSpritesheetSupport}.name`, 66 | hint: `${NAMESPACE}.settings.${SETTINGS.TokenRingSpritesheetSupport}.hint`, 67 | scope: 'world', 68 | config: true, 69 | requiresReload: true, 70 | type: Boolean, 71 | default: false, 72 | }); 73 | 74 | game.settings.register(NAMESPACE, SETTINGS.PrecomputedNoiseTextures, { 75 | name: `${NAMESPACE}.settings.${SETTINGS.PrecomputedNoiseTextures}.name`, 76 | hint: `${NAMESPACE}.settings.${SETTINGS.PrecomputedNoiseTextures}.hint`, 77 | scope: 'client', 78 | config: true, 79 | requiresReload: true, 80 | type: Boolean, 81 | default: true, 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /src/utils/foundryShim.ts: -------------------------------------------------------------------------------- 1 | export const FOUNDRY_API = { 2 | loadTexture: (src: string) => (game.release.generation < 13 ? loadTexture(src) : foundry.canvas.loadTexture(src)), 3 | getTexture: (src: string) => (game.release.generation < 13 ? getTexture(src) : foundry.canvas.getTexture(src)), 4 | createSpriteMesh: (texture: PIXI.Texture): SpriteMesh => 5 | game.release.generation < 13 ? new SpriteMesh(texture) : new foundry.canvas.containers.SpriteMesh(texture), 6 | get game() { 7 | return foundry.game ?? game; 8 | }, 9 | get generation(): number { 10 | return game.release.generation; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/getBitmapCacheResolution.ts: -------------------------------------------------------------------------------- 1 | export function getBitmapCacheResolution() { 2 | let scaleFactor = canvas.performance.mode === CONST.CANVAS_PERFORMANCE_MODES.MAX ? 2 : 1.5; 3 | if (canvas.scene.grid.size < 100 && canvas.scene.grid.size > 50) { 4 | scaleFactor *= 1.5; 5 | } else if (canvas.scene.grid.size <= 50) { 6 | scaleFactor *= 2; 7 | } 8 | return canvas.app.renderer.resolution * scaleFactor; 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/registerWrapper.ts: -------------------------------------------------------------------------------- 1 | import { NAMESPACE } from 'src/constants.ts'; 2 | import { FOUNDRY_API } from './foundryShim.ts'; 3 | 4 | export function registerWrapperForVersion( 5 | fn: (...args: any) => unknown, 6 | type: 'OVERRIDE' | 'WRAPPER' | 'MIXED', 7 | { v12, v13 }: { v12?: string; v13?: string }, 8 | ) { 9 | const generation = FOUNDRY_API.game.release.generation; 10 | if (v12 && generation < 13) { 11 | libWrapper.register(NAMESPACE, v12, fn, type); 12 | } else if (v13 && generation >= 13) { 13 | libWrapper.register(NAMESPACE, v13, fn, type); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/wrapFunction.ts: -------------------------------------------------------------------------------- 1 | export function wrapFunction(object: any, path: string, callback: (this: THIS, ...args: any[]) => void) { 2 | const origFn = object[path]; 3 | object[path] = function (this: THIS, ...args: any[]) { 4 | const res = origFn.apply(this, args); 5 | if (res instanceof Promise) { 6 | return res.then(() => callback.apply(this, args)); 7 | } else { 8 | return callback.apply(this, args); 9 | } 10 | }; 11 | } 12 | 13 | export function wrapFunctionManual(object: any, path: string, callback: (this: THIS, ...args: any[]) => void) { 14 | const origFn = object[path]; 15 | object[path] = function (this: THIS, ...args: any[]) { 16 | return callback.apply(this, [origFn, ...args]); 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /templates/custom-spritesheets-config.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#each spritesheets}} 3 |
4 | {{{this}}} 5 | 10 | 11 | 12 |
13 | {{/each}} 14 | 15 |
16 | 17 |
18 | 19 |
20 |
21 |
-------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "baseUrl": ".", 6 | "rootDir": ".", 7 | "module": "NodeNext", 8 | "moduleResolution": "nodenext", 9 | "resolveJsonModule": true, 10 | "types": ["vite/client", "foundry-pf2e-types"], 11 | "allowImportingTsExtensions": true, 12 | "allowJs": true, 13 | "checkJs": true, 14 | "strict": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitOverride": true, 17 | "noImplicitReturns": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | // Best practices 21 | "noEmit": true, 22 | "sourceMap": true, 23 | "esModuleInterop": true, 24 | "forceConsistentCasingInFileNames": true, 25 | "isolatedModules": true, 26 | "skipLibCheck": true 27 | }, 28 | "references": [ 29 | { 30 | "path": "./tsconfig.node.json" 31 | } 32 | ], 33 | "include": ["src/**/*.ts", "src/**/*.js"] 34 | } 35 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "strict": true, 7 | "skipLibCheck": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | import resolve from '@rollup/plugin-node-resolve'; // This resolves NPM modules from node_modules. 3 | import { type Connect, defineConfig } from 'vite'; 4 | import tsconfigPaths from 'vite-tsconfig-paths'; 5 | import moduleJSON from './module.json' with { type: 'json' }; 6 | 7 | const packagePath = `modules/${moduleJSON.id}`; 8 | 9 | const FOUNDRY_PORT = 29999; 10 | 11 | export default defineConfig(({ command: _buildOrServe }) => ({ 12 | root: 'src', 13 | base: `/${packagePath}/dist`, 14 | cacheDir: '../.vite-cache', 15 | publicDir: '../assets', 16 | 17 | esbuild: { 18 | target: ['es2022'], 19 | }, 20 | 21 | resolve: { conditions: ['import', 'browser'] }, 22 | 23 | server: { 24 | open: '/join', 25 | port: 30001, 26 | proxy: { 27 | // Serves static files from main Foundry server. 28 | [`^(/${packagePath}/(assets|lang|packs}))`]: `http://localhost:${FOUNDRY_PORT}`, 29 | 30 | // All other paths besides package ID path are served from main Foundry server. 31 | [`^(?!/${packagePath}/)`]: `http://localhost:${FOUNDRY_PORT}`, 32 | 33 | // Enable socket.io from main Foundry server. 34 | '/socket.io': { target: `ws://localhost:${FOUNDRY_PORT}`, ws: true }, 35 | }, 36 | }, 37 | 38 | build: { 39 | outDir: '../dist', 40 | emptyOutDir: true, 41 | sourcemap: true, 42 | minify: true, 43 | lib: { 44 | entry: 'index.js', 45 | formats: ['es'], 46 | fileName: moduleJSON.id, 47 | }, 48 | }, 49 | 50 | optimizeDeps: { 51 | esbuildOptions: { 52 | target: 'es2022', 53 | }, 54 | }, 55 | 56 | plugins: [ 57 | tsconfigPaths(), 58 | resolve({ 59 | browser: true, 60 | }), 61 | { 62 | name: 'change-names', 63 | configureServer(server) { 64 | server.middlewares.use((req: Connect.IncomingMessage & { url?: string }, res, next) => { 65 | if (req.originalUrl === `/${packagePath}/dist/${moduleJSON.id}.js`) { 66 | req.url = `/${packagePath}/dist/index.js`; 67 | } 68 | next(); 69 | }); 70 | }, 71 | }, 72 | ], 73 | })); 74 | --------------------------------------------------------------------------------