├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src └── main ├── java └── com │ └── unascribed │ └── blockrenderer │ ├── AsyncRenderer.java │ ├── BlockRenderer.java │ ├── ClientRenderHandler.java │ ├── EntityRenderTask.java │ ├── GuiConfigureRender.java │ ├── ItemRenderTask.java │ ├── RenderProgressGui.java │ └── RenderTask.java └── resources ├── META-INF └── mods.toml ├── assets └── blockrenderer │ └── lang │ └── en_us.json └── pack.mcmeta /.gitignore: -------------------------------------------------------------------------------- 1 | private.properties 2 | bin/ 3 | *.launch 4 | build/ 5 | .classpath 6 | .project 7 | .settings 8 | .metadata 9 | .gradle 10 | eclipse/ 11 | minecraft/ 12 | .pmd 13 | .DS_Store 14 | run 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2020 Una Thompson (unascribed) and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BlockRenderer 2 | 3 | A mod to render blocks and items. Useful for wikis. 4 | 5 | Press \` when hovering over an item to render it to an image, or press Ctrl + \` to configure rendering and render entire mods at a time. 6 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | import java.time.Instant 2 | 3 | buildscript { 4 | repositories { 5 | maven { url = 'https://files.minecraftforge.net/maven' } 6 | jcenter() 7 | mavenCentral() 8 | } 9 | dependencies { 10 | classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '3.+', changing: true 11 | } 12 | } 13 | 14 | plugins { 15 | id 'java' 16 | id 'eclipse' 17 | id 'idea' 18 | } 19 | 20 | apply plugin: 'net.minecraftforge.gradle' 21 | 22 | idea { 23 | module { 24 | //Exclude directories from being managed 25 | for (String excludeDirName in ["run", "out", "logs", "gradle"]) { 26 | File excludeDir = new File(projectDir, excludeDirName) 27 | excludeDirs.add(excludeDir) 28 | } 29 | } 30 | } 31 | 32 | version = "${mod_version}" 33 | group = 'com.unascribed' 34 | archivesBaseName = 'BlockRenderer' 35 | sourceCompatibility = targetCompatibility = compileJava.sourceCompatibility = compileJava.targetCompatibility = '1.8' // Need this here so eclipse task generates correctly. 36 | 37 | minecraft { 38 | mappings channel: 'snapshot', version: "${mappings_version}" 39 | 40 | runs { 41 | client { 42 | workingDirectory file('run') 43 | 44 | //The below if statements are to add args to your gradle.properties file in user home 45 | // (DO NOT add them directly to the gradle.properties file for this project) 46 | // Setting the below properties allows use of your normal Minecraft account in the 47 | // dev environment including having your skin load. Each property also has a comment 48 | // explaining what information to set the value to/format it expects 49 | // One thing to note is because of the caching that goes on, after changing these 50 | // variables, you need to refresh the project and rerun genIntellijRuns/genEclipseRuns 51 | if (project.hasProperty('mc_uuid')) { 52 | //Your uuid without any dashes in the middle 53 | args '--uuid', project.getProperty('mc_uuid') 54 | } 55 | if (project.hasProperty('mc_username')) { 56 | //Your username/display name, this is the name that shows up in chat 57 | // Note: This is not your email, even if you have a Mojang account 58 | args '--username', project.getProperty('mc_username') 59 | } 60 | if (project.hasProperty('mc_accessToken')) { 61 | //Your access token, you can find it in your '.minecraft/launcher_profiles.json' file 62 | args '--accessToken', project.getProperty('mc_accessToken') 63 | } 64 | if (project.hasProperty('forge_force_ansi')) { 65 | //Force ansi if declared as a gradle variable, as the auto detection doesn't detect IntelliJ properly 66 | // or eclipse's plugin that adds support for ansi escape in console 67 | jvmArg('-Dterminal.ansi=' + project.getProperty('forge_force_ansi')) 68 | } 69 | 70 | mods { 71 | blockrenderer.source((SourceSet) sourceSets.main) 72 | } 73 | } 74 | 75 | server { 76 | workingDirectory file("run") 77 | if (project.hasProperty('forge_force_ansi')) { 78 | //Force ansi if declared as a gradle variable, as the auto detection doesn't detect IntelliJ properly 79 | // or eclipse's plugin that adds support for ansi escape in console 80 | jvmArg('-Dterminal.ansi=' + project.getProperty('forge_force_ansi')) 81 | } 82 | 83 | mods { 84 | blockrenderer.source((SourceSet) sourceSets.main) 85 | } 86 | } 87 | } 88 | } 89 | 90 | dependencies { 91 | minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}" 92 | } 93 | 94 | jar.manifest.attributes([ 95 | 'Specification-Title' : rootProject.name, 96 | 'Specification-Vendor' : project.group, 97 | 'Specification-Version' : "${project.mod_version}", 98 | 'Implementation-Title' : rootProject.name, 99 | 'Implementation-Version' : "${project.mod_version}", 100 | 'Implementation-Vendor' : project.group, 101 | 'Implementation-Timestamp': Instant.now() 102 | ]) 103 | 104 | task replaceResources(type: Copy) { 105 | outputs.upToDateWhen { false } 106 | from(sourceSets.main.resources) { 107 | include "META-INF/mods.toml" 108 | expand "version": mod_version, "mc_version": minecraft_version_range, "forge_version": forge_version_range, "loader_version": loader_version_range 109 | } 110 | into "$buildDir/resources/main/" 111 | } 112 | 113 | processResources { 114 | //Exclude the mods.toml file as we manually handle that and don't want it to invalidate our cache 115 | exclude 'META-INF/mods.toml' 116 | finalizedBy replaceResources 117 | } 118 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx3G 2 | org.gradle.daemon=false 3 | 4 | minecraft_version=1.16.4 5 | mappings_version=20201028-1.16.3 6 | loader_version_range=[34,) 7 | forge_version=35.1.7 8 | mod_version=2.0.0 9 | #This determines the minimum version of forge required to use 10 | # Only bump it whenever we need access to a feature in forge that is not available in earlier versions 11 | forge_version_range=[34.1.40,) 12 | minecraft_version_range=[1.16.3, 1.16.4] 13 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unascribed/BlockRenderer/914264f1415cf7f1b13f90e82234cceec918e3df/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStorePath=wrapper/dists 5 | zipStoreBase=GRADLE_USER_HOME 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/main/java/com/unascribed/blockrenderer/AsyncRenderer.java: -------------------------------------------------------------------------------- 1 | package com.unascribed.blockrenderer; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashSet; 5 | import java.util.List; 6 | import java.util.Set; 7 | import java.util.concurrent.CompletableFuture; 8 | import java.util.concurrent.atomic.AtomicInteger; 9 | 10 | import javax.annotation.Nonnull; 11 | 12 | import net.minecraft.client.Minecraft; 13 | import net.minecraft.resources.IAsyncReloader; 14 | import net.minecraft.util.Unit; 15 | import net.minecraft.util.Util; 16 | 17 | /** 18 | * Heavily modified version of AsyncReloader 19 | */ 20 | public class AsyncRenderer implements IAsyncReloader { 21 | 22 | private final CompletableFuture allAsyncCompleted = new CompletableFuture<>(); 23 | private final CompletableFuture> resultListFuture; 24 | private final Set> taskSet; 25 | private final List> sourceFurtures; 26 | private final int taskCount; 27 | private final AtomicInteger asyncScheduled = new AtomicInteger(); 28 | private final AtomicInteger asyncCompleted = new AtomicInteger(); 29 | 30 | public AsyncRenderer(List> futures) { 31 | sourceFurtures = futures; 32 | taskCount = futures.size(); 33 | taskSet = new HashSet<>(futures); 34 | asyncScheduled.incrementAndGet(); 35 | CompletableFuture alsoWaitedFor = CompletableFuture.completedFuture(Unit.INSTANCE); 36 | alsoWaitedFor.thenRun(asyncCompleted::incrementAndGet); 37 | List> list = new ArrayList<>(); 38 | CompletableFuture waitFor = alsoWaitedFor; 39 | for (CompletableFuture future : futures) { 40 | final CompletableFuture finalWaitFor = waitFor; 41 | CompletableFuture stateFuture = future.thenCompose(backgroundResult -> { 42 | Minecraft.getInstance().execute(() -> { 43 | AsyncRenderer.this.taskSet.remove(future); 44 | if (AsyncRenderer.this.taskSet.isEmpty()) { 45 | AsyncRenderer.this.allAsyncCompleted.complete(Unit.INSTANCE); 46 | } 47 | }); 48 | return AsyncRenderer.this.allAsyncCompleted.thenCombine(finalWaitFor, (unit, instance) -> null); 49 | }); 50 | list.add(stateFuture); 51 | waitFor = stateFuture; 52 | } 53 | resultListFuture = Util.gather(list); 54 | } 55 | 56 | public void cancel() { 57 | for (CompletableFuture future : sourceFurtures) { 58 | if (!future.isDone()) { 59 | future.cancel(false); 60 | } 61 | } 62 | resultListFuture.cancel(false); 63 | } 64 | 65 | @Nonnull 66 | @Override 67 | public CompletableFuture onceDone() { 68 | return resultListFuture.thenApply(result -> Unit.INSTANCE); 69 | } 70 | 71 | @Override 72 | public float estimateExecutionSpeed() { 73 | int remaining = taskCount - taskSet.size(); 74 | float completed = 2 * asyncCompleted.get() + remaining; 75 | float total = 2 * asyncScheduled.get() + taskCount; 76 | return completed / total; 77 | } 78 | 79 | @Override 80 | public boolean asyncPartDone() { 81 | return allAsyncCompleted.isDone(); 82 | } 83 | 84 | @Override 85 | public boolean fullyDone() { 86 | return resultListFuture.isDone(); 87 | } 88 | 89 | @Override 90 | public void join() { 91 | if (resultListFuture.isCompletedExceptionally()) { 92 | resultListFuture.join(); 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /src/main/java/com/unascribed/blockrenderer/BlockRenderer.java: -------------------------------------------------------------------------------- 1 | package com.unascribed.blockrenderer; 2 | 3 | import org.apache.commons.lang3.tuple.Pair; 4 | import org.apache.logging.log4j.LogManager; 5 | import org.apache.logging.log4j.Logger; 6 | 7 | import net.minecraftforge.api.distmarker.Dist; 8 | import net.minecraftforge.fml.DistExecutor; 9 | import net.minecraftforge.fml.ExtensionPoint; 10 | import net.minecraftforge.fml.ModLoadingContext; 11 | import net.minecraftforge.fml.common.Mod; 12 | 13 | @Mod(BlockRenderer.MODID) 14 | public class BlockRenderer { 15 | 16 | public static final Logger log = LogManager.getLogger("BlockRenderer"); 17 | public static final String MODID = "blockrenderer"; 18 | 19 | public static ClientRenderHandler renderHandler; 20 | 21 | public BlockRenderer() { 22 | DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> renderHandler = new ClientRenderHandler()); 23 | ModLoadingContext.get().registerExtensionPoint(ExtensionPoint.DISPLAYTEST, () -> Pair.of(() -> "ignored", (remote, isServer) -> true)); 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/java/com/unascribed/blockrenderer/ClientRenderHandler.java: -------------------------------------------------------------------------------- 1 | package com.unascribed.blockrenderer; 2 | 3 | import java.awt.image.BufferedImage; 4 | import java.io.File; 5 | import java.nio.ByteBuffer; 6 | import java.text.DateFormat; 7 | import java.text.SimpleDateFormat; 8 | import java.util.ArrayList; 9 | import java.util.Date; 10 | import java.util.List; 11 | import java.util.Map.Entry; 12 | import java.util.Set; 13 | import java.util.concurrent.CompletableFuture; 14 | import java.util.concurrent.Executor; 15 | import java.util.concurrent.ScheduledExecutorService; 16 | import java.util.concurrent.ScheduledThreadPoolExecutor; 17 | import java.util.concurrent.TimeUnit; 18 | 19 | import javax.imageio.ImageIO; 20 | 21 | import org.lwjgl.BufferUtils; 22 | import org.lwjgl.glfw.GLFW; 23 | import org.lwjgl.opengl.GL11; 24 | import org.lwjgl.opengl.GL12; 25 | 26 | import com.mojang.blaze3d.platform.GlStateManager.DestFactor; 27 | import com.mojang.blaze3d.platform.GlStateManager.SourceFactor; 28 | import com.mojang.blaze3d.systems.RenderSystem; 29 | import com.mojang.datafixers.util.Pair; 30 | 31 | import com.google.common.base.Joiner; 32 | import com.google.common.collect.Lists; 33 | import com.google.common.collect.Sets; 34 | import com.google.common.io.Files; 35 | 36 | import net.minecraft.client.MainWindow; 37 | import net.minecraft.client.Minecraft; 38 | import net.minecraft.client.gui.LoadingGui; 39 | import net.minecraft.client.gui.screen.ChatScreen; 40 | import net.minecraft.client.gui.screen.IngameMenuScreen; 41 | import net.minecraft.client.gui.screen.Screen; 42 | import net.minecraft.client.gui.screen.inventory.ContainerScreen; 43 | import net.minecraft.client.renderer.RenderHelper; 44 | import net.minecraft.client.settings.KeyBinding; 45 | import net.minecraft.client.util.InputMappings; 46 | import net.minecraft.entity.Entity; 47 | import net.minecraft.entity.EntityType; 48 | import net.minecraft.inventory.container.Slot; 49 | import net.minecraft.item.EnchantedBookItem; 50 | import net.minecraft.item.Item; 51 | import net.minecraft.item.ItemGroup; 52 | import net.minecraft.item.ItemStack; 53 | import net.minecraft.util.NonNullList; 54 | import net.minecraft.util.RegistryKey; 55 | import net.minecraft.util.Util; 56 | import net.minecraft.util.text.ITextComponent; 57 | import net.minecraft.util.text.TranslationTextComponent; 58 | import net.minecraftforge.common.MinecraftForge; 59 | import net.minecraftforge.event.TickEvent.Phase; 60 | import net.minecraftforge.event.TickEvent.RenderTickEvent; 61 | import net.minecraftforge.eventbus.api.EventPriority; 62 | import net.minecraftforge.fml.client.registry.ClientRegistry; 63 | import net.minecraftforge.registries.ForgeRegistries; 64 | 65 | public class ClientRenderHandler { 66 | 67 | private static final ScheduledExecutorService SCHEDULER = new ScheduledThreadPoolExecutor(0); 68 | private static final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss"); 69 | 70 | protected KeyBinding bind; 71 | protected boolean down = false; 72 | protected String pendingBulkRender; 73 | protected int pendingBulkRenderSize; 74 | protected boolean pendingBulkItems; 75 | protected boolean pendingBulkEntities; 76 | protected boolean pendingBulkStructures; 77 | private float oldZLevel; 78 | 79 | public ClientRenderHandler() { 80 | bind = new KeyBinding("key.blockrenderer.render", GLFW.GLFW_KEY_GRAVE_ACCENT, "key.blockrenderer.category"); 81 | ClientRegistry.registerKeyBinding(bind); 82 | MinecraftForge.EVENT_BUS.addListener(EventPriority.HIGHEST, this::onFrameStart); 83 | } 84 | 85 | public void onFrameStart(RenderTickEvent e) { 86 | //Quick primer: OpenGL is double-buffered. This means, where we draw to is 87 | // /not/ on the screen. As such, we are free to do whatever we like before 88 | // Minecraft renders, as long as we put everything back the way it was. 89 | if (e.phase == Phase.START) { 90 | Minecraft mc = Minecraft.getInstance(); 91 | LoadingGui loadingGui = mc.getLoadingGui(); 92 | if (loadingGui instanceof RenderProgressGui && InputMappings.isKeyDown(Minecraft.getInstance().getMainWindow().getHandle(), GLFW.GLFW_KEY_ESCAPE)) { 93 | //If we are currently rendering our progress bar and the user hit escapes, cancel the bulk rendering 94 | ((RenderProgressGui) loadingGui).cancel(); 95 | return; 96 | } 97 | if (pendingBulkRender != null) { 98 | //We *must* call render code in pre-render. If we don't, it won't work right. 99 | bulkRender(pendingBulkRender, pendingBulkRenderSize, pendingBulkItems, pendingBulkEntities, pendingBulkStructures); 100 | pendingBulkRender = null; 101 | } 102 | if (isKeyDown(bind)) { 103 | if (!down) { 104 | down = true; 105 | if (mc.world == null) { 106 | mc.ingameGUI.getChatGUI().printChatMessage(new TranslationTextComponent("msg.blockrenderer.no_world")); 107 | return; 108 | } 109 | Slot hovered = null; 110 | Screen currentScreen = mc.currentScreen; 111 | if (currentScreen instanceof ChatScreen) return; 112 | if (currentScreen instanceof ContainerScreen) { 113 | hovered = ((ContainerScreen) currentScreen).getSlotUnderMouse(); 114 | } 115 | 116 | if (Screen.hasControlDown()) { 117 | String modId = null; 118 | if (hovered != null && hovered.getHasStack()) { 119 | modId = hovered.getStack().getItem().getRegistryName().getNamespace(); 120 | } 121 | mc.displayGuiScreen(new GuiConfigureRender(mc.currentScreen, modId)); 122 | } else if (currentScreen instanceof ContainerScreen) { 123 | if (hovered == null) { 124 | mc.ingameGUI.getChatGUI().printChatMessage(new TranslationTextComponent("msg.blockrenderer.slot.absent")); 125 | } else { 126 | ItemStack stack = hovered.getStack(); 127 | if (stack.isEmpty()) { 128 | mc.ingameGUI.getChatGUI().printChatMessage(new TranslationTextComponent("msg.blockrenderer.slot.empty")); 129 | } else { 130 | int size = 512; 131 | if (Screen.hasShiftDown()) { 132 | size = (int) (16 * mc.getMainWindow().getGuiScaleFactor()); 133 | } 134 | mc.ingameGUI.getChatGUI().printChatMessage(render(mc, new ItemRenderTask(stack), size, new File("renders/items"), true)); 135 | } 136 | } 137 | } else { 138 | mc.ingameGUI.getChatGUI().printChatMessage(new TranslationTextComponent("msg.blockrenderer.not_container")); 139 | } 140 | } 141 | } else { 142 | down = false; 143 | } 144 | } 145 | } 146 | 147 | private void bulkRender(String modidSpec, int size, boolean items, boolean entities, boolean structures) { 148 | Minecraft mc = Minecraft.getInstance(); 149 | mc.displayGuiScreen(new IngameMenuScreen(true)); 150 | Set modIds = Sets.newHashSet(); 151 | for (String str : modidSpec.split(",")) { 152 | modIds.add(str.trim()); 153 | } 154 | List toRender = new ArrayList<>(); 155 | NonNullList li = NonNullList.create(); 156 | boolean wildcard = modIds.contains("*"); 157 | if (items) { 158 | for (Entry, Item> entry : ForgeRegistries.ITEMS.getEntries()) { 159 | if (wildcard || modIds.contains(entry.getKey().getLocation().getNamespace())) { 160 | li.clear(); 161 | Item item = entry.getValue(); 162 | ItemGroup group = item.getGroup(); 163 | if (group == null && item instanceof EnchantedBookItem) { 164 | //Vanilla has special handing for filling the enchanted book item's group, so just grab a single enchanted book 165 | li.add(new ItemStack(item)); 166 | } else { 167 | try { 168 | item.fillItemGroup(group, li); 169 | } catch (Throwable t) { 170 | BlockRenderer.log.warn("Failed to get renderable items for {} and group {}", item.getRegistryName(), group, t); 171 | } 172 | } 173 | for (ItemStack is : li) { 174 | toRender.add(new ItemRenderTask(is)); 175 | } 176 | } 177 | } 178 | } 179 | if (entities) { 180 | for (Entry>, EntityType> entry : ForgeRegistries.ENTITIES.getEntries()) { 181 | if (wildcard || modIds.contains(entry.getKey().getLocation().getNamespace())) { 182 | li.clear(); 183 | EntityType entityType = entry.getValue(); 184 | try { 185 | Entity e = entityType.create(mc.world); 186 | if (e == null) continue; 187 | toRender.add(new EntityRenderTask(e)); 188 | } catch (Throwable t) { 189 | BlockRenderer.log.warn("Failed to get renderable entity for {}", entry.getKey().getRegistryName()); 190 | } 191 | } 192 | } 193 | } 194 | File folder = new File("renders/" + dateFormat.format(new Date()) + "_" + sanitize(modidSpec) + "/"); 195 | RenderProgressGui progressBar = new RenderProgressGui(mc, toRender.size(), Joiner.on(", ").join(modIds)); 196 | List> futures = new ArrayList<>(); 197 | //Create futures for generating and saving the images for each item 198 | // we split our items to render into batches, and then delay each batch 199 | // by batchIndex + 1. This allows us to have our progress bar properly 200 | // render instead of us freezing the game trying to render all the items 201 | // during a single tick 202 | List> batchedLists = Lists.partition(toRender, 10); 203 | for (int batchIndex = 0, batchedCount = batchedLists.size(); batchIndex < batchedCount; batchIndex++) { 204 | futures.add(createFuture(batchedLists.get(batchIndex), size, folder, false, batchIndex + 1, progressBar)); 205 | } 206 | progressBar.setFutures(futures); 207 | mc.setLoadingGui(progressBar); 208 | } 209 | 210 | private void setUpRenderState(Minecraft mc, int desiredSize) { 211 | RenderSystem.pushMatrix(); 212 | //As we render to the back-buffer, we need to cap our render size 213 | // to be within the window's bounds. If we didn't do this, the results 214 | // of our readPixels up ahead would be undefined. And nobody likes 215 | // undefined behavior. 216 | MainWindow window = mc.getMainWindow(); 217 | int size = Math.min(Math.min(window.getHeight(), window.getWidth()), desiredSize); 218 | 219 | //Switches from 3D to 2D 220 | RenderSystem.clear(GL11.GL_DEPTH_BUFFER_BIT, Minecraft.IS_RUNNING_ON_MAC); 221 | RenderSystem.matrixMode(GL11.GL_PROJECTION); 222 | RenderSystem.loadIdentity(); 223 | RenderSystem.ortho(0, window.getWidth(), window.getHeight(), 0, 1000, 3000); 224 | RenderSystem.matrixMode(GL11.GL_MODELVIEW); 225 | RenderSystem.loadIdentity(); 226 | RenderSystem.translatef(0.0F, 0.0F, -2000.0F); 227 | RenderHelper.enableStandardItemLighting(); 228 | double scale = size / (16D); 229 | RenderSystem.translated(0, 0, -(scale * 100)); 230 | 231 | RenderSystem.scaled(scale, scale, scale); 232 | 233 | oldZLevel = mc.getItemRenderer().zLevel; 234 | mc.getItemRenderer().zLevel = -50; 235 | 236 | RenderSystem.enableRescaleNormal(); 237 | RenderSystem.enableColorMaterial(); 238 | RenderSystem.enableDepthTest(); 239 | RenderSystem.enableBlend(); 240 | RenderSystem.blendFuncSeparate(SourceFactor.SRC_ALPHA, DestFactor.ONE_MINUS_SRC_ALPHA, SourceFactor.SRC_ALPHA, DestFactor.ONE); 241 | RenderSystem.blendFunc(SourceFactor.SRC_ALPHA, DestFactor.ONE_MINUS_SRC_ALPHA); 242 | RenderSystem.disableAlphaTest(); 243 | } 244 | 245 | private void tearDownRenderState() { 246 | RenderSystem.disableLighting(); 247 | RenderSystem.disableColorMaterial(); 248 | RenderSystem.disableDepthTest(); 249 | RenderSystem.disableBlend(); 250 | 251 | Minecraft.getInstance().getItemRenderer().zLevel = oldZLevel; 252 | RenderSystem.popMatrix(); 253 | } 254 | 255 | private ITextComponent render(Minecraft mc, RenderTask task, int size, File folder, boolean includeDateInFilename) { 256 | setUpRenderState(mc, size); 257 | RenderSystem.clearColor(0, 0, 0, 0); 258 | RenderSystem.clear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT, Minecraft.IS_RUNNING_ON_MAC); 259 | try { 260 | task.render(size); 261 | } catch (Throwable t) { 262 | BlockRenderer.log.warn("Failed to render "+task.getId(), t); 263 | return new TranslationTextComponent("msg.blockrenderer.render.fail"); 264 | } 265 | BufferedImage image = readPixels(size, size); 266 | tearDownRenderState(); 267 | // This code would need to be refactored to perform this save off-thread and not block the 268 | // main thread. This is a problem for later. 269 | File file = saveImage(image, task, folder, includeDateInFilename); 270 | return new TranslationTextComponent("msg.blockrenderer.render.success", file.getPath()); 271 | } 272 | 273 | private CompletableFuture createFuture(List tasks, int size, File folder, boolean includeDateInFilename, int tickDelay, RenderProgressGui progressBar) { 274 | Executor gameExecutor; 275 | if (tickDelay == 0) { 276 | gameExecutor = Minecraft.getInstance(); 277 | } else { 278 | //Note: We delay our executors by the given number of ticks so that we 279 | // can allow the progress screen to properly render instead of clogging 280 | // up the main game thread on rendering all the items 281 | gameExecutor = r -> SCHEDULER.schedule(() -> Minecraft.getInstance().execute(r), tickDelay * 50, TimeUnit.MILLISECONDS); 282 | } 283 | return CompletableFuture.supplyAsync(() -> { 284 | //Setup the render state for our batch on the main thread, 285 | // render the entire batch gathering the images for it 286 | // and revert the render state 287 | setUpRenderState(Minecraft.getInstance(), size); 288 | List> images = new ArrayList<>(); 289 | for (RenderTask task : tasks) { 290 | RenderSystem.clearColor(0, 0, 0, 0); 291 | RenderSystem.clear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT, Minecraft.IS_RUNNING_ON_MAC); 292 | task.render(size); 293 | images.add(Pair.of(task, readPixels(size, size))); 294 | //Update the progress bar 295 | progressBar.update(task); 296 | } 297 | tearDownRenderState(); 298 | return images; 299 | }, gameExecutor).thenAcceptAsync(images -> { 300 | //Save images off thread 301 | int types = 0; 302 | if (pendingBulkItems) types++; 303 | if (pendingBulkEntities) types++; 304 | if (pendingBulkStructures) types++; 305 | for (Pair image : images) { 306 | saveImage(image.getSecond(), image.getFirst(), types > 1 ? new File(folder, image.getFirst().getCategory()) : folder, includeDateInFilename); 307 | } 308 | }, Util.getServerExecutor()); 309 | } 310 | 311 | private static File saveImage(BufferedImage image, RenderTask task, File folder, boolean includeDateInFilename) { 312 | try { 313 | String fileName = (includeDateInFilename ? dateFormat.format(new Date()) + "_" : "") + sanitize(task.getDisplayName()); 314 | File f = new File(folder, fileName + ".png"); 315 | int i = 2; 316 | while (f.exists()) { 317 | f = new File(folder, fileName + "_" + i + ".png"); 318 | i++; 319 | } 320 | Files.createParentDirs(f); 321 | f.createNewFile(); 322 | ImageIO.write(image, "PNG", f); 323 | return f; 324 | } catch (Exception e) { 325 | e.printStackTrace(); 326 | return null; 327 | } 328 | } 329 | 330 | private static String sanitize(String str) { 331 | return str.replaceAll("[^A-Za-z0-9-_ ]", "_"); 332 | } 333 | 334 | private static BufferedImage readPixels(int width, int height) { 335 | ByteBuffer buf = BufferUtils.createByteBuffer(width * height * 4); 336 | RenderSystem.readPixels(0, Minecraft.getInstance().getMainWindow().getHeight() - height, width, height, GL12.GL_BGRA, GL11.GL_UNSIGNED_BYTE, buf); 337 | BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 338 | int[] pixels = new int[width * height]; 339 | buf.asIntBuffer().get(pixels); 340 | // Load the pixels into the BufferedImage, flipped vertically as OpenGL and 341 | // Minecraft disagree about which way is up 342 | for (int y = 0; y < height; y++) { 343 | img.setRGB(0, (height-1)-y, width, 1, pixels, y*width, width); 344 | } 345 | return img; 346 | } 347 | 348 | private static boolean isKeyDown(KeyBinding keyBinding) { 349 | InputMappings.Input key = keyBinding.getKey(); 350 | int keyCode = key.getKeyCode(); 351 | if (keyCode != InputMappings.INPUT_INVALID.getKeyCode()) { 352 | long windowHandle = Minecraft.getInstance().getMainWindow().getHandle(); 353 | try { 354 | if (key.getType() == InputMappings.Type.KEYSYM) { 355 | return InputMappings.isKeyDown(windowHandle, keyCode); 356 | } else if (key.getType() == InputMappings.Type.MOUSE) { 357 | return GLFW.glfwGetMouseButton(windowHandle, keyCode) == GLFW.GLFW_PRESS; 358 | } 359 | } catch (Exception ignored) { 360 | } 361 | } 362 | return false; 363 | } 364 | 365 | } -------------------------------------------------------------------------------- /src/main/java/com/unascribed/blockrenderer/EntityRenderTask.java: -------------------------------------------------------------------------------- 1 | package com.unascribed.blockrenderer; 2 | 3 | import com.mojang.blaze3d.matrix.MatrixStack; 4 | import com.mojang.blaze3d.systems.RenderSystem; 5 | 6 | import com.google.common.primitives.Doubles; 7 | 8 | import net.minecraft.client.Minecraft; 9 | import net.minecraft.client.renderer.IRenderTypeBuffer; 10 | import net.minecraft.client.renderer.entity.EntityRendererManager; 11 | import net.minecraft.client.settings.GraphicsFanciness; 12 | import net.minecraft.entity.Entity; 13 | import net.minecraft.entity.LivingEntity; 14 | import net.minecraft.util.ResourceLocation; 15 | import net.minecraft.util.math.AxisAlignedBB; 16 | import net.minecraft.util.math.vector.Quaternion; 17 | import net.minecraft.util.math.vector.Vector3f; 18 | import net.minecraft.util.text.ITextComponent; 19 | 20 | public class EntityRenderTask extends RenderTask { 21 | 22 | public final Entity entity; 23 | 24 | public EntityRenderTask(Entity entity) { 25 | this.entity = entity; 26 | } 27 | 28 | @Override 29 | public String getCategory() { 30 | return "entities"; 31 | } 32 | 33 | @Override 34 | public ITextComponent getPreviewDisplayName() { 35 | return entity.getDisplayName(); 36 | } 37 | 38 | @Override 39 | public String getDisplayName() { 40 | return entity.getDisplayName().getString(); 41 | } 42 | 43 | @Override 44 | public ResourceLocation getId() { 45 | return entity.getType().getRegistryName(); 46 | } 47 | 48 | @Override 49 | public void renderPreview(MatrixStack matrices, int x, int y) { 50 | RenderSystem.pushMatrix(); 51 | try { 52 | RenderSystem.multMatrix(matrices.getLast().getMatrix()); 53 | RenderSystem.translatef(x+8, y+8, 0); 54 | AxisAlignedBB rbb = entity.getRenderBoundingBox(); 55 | drawEntity(entity, 16/(float)Doubles.max(rbb.getXSize(), rbb.getYSize(), rbb.getZSize())); 56 | } finally { 57 | RenderSystem.popMatrix(); 58 | } 59 | } 60 | 61 | @Override 62 | public void render(int renderSize) { 63 | RenderSystem.pushMatrix(); 64 | try { 65 | RenderSystem.translatef(0.5f, 0.5f, 0); 66 | AxisAlignedBB rbb = entity.getRenderBoundingBox(); 67 | drawEntity(entity, 1/(float)Doubles.max(rbb.getXSize(), rbb.getYSize(), rbb.getZSize())); 68 | } finally { 69 | RenderSystem.popMatrix(); 70 | } 71 | } 72 | 73 | public static void drawEntity(Entity entity, float scale) { 74 | if (entity == null) return; 75 | float yaw = -45; 76 | float pitch = 0; 77 | RenderSystem.pushMatrix(); 78 | MatrixStack matrices = new MatrixStack(); 79 | matrices.scale(scale, scale, scale); 80 | Quaternion rot = Vector3f.ZP.rotationDegrees(180f); 81 | Quaternion xRot = Vector3f.XP.rotationDegrees(20f); 82 | rot.multiply(xRot); 83 | matrices.rotate(rot); 84 | float oldYaw = entity.rotationYaw; 85 | float oldPitch = entity.rotationPitch; 86 | LivingEntity lentity = entity instanceof LivingEntity ? (LivingEntity)entity : null; 87 | Float oldYawOfs = lentity != null ? lentity.renderYawOffset : null; 88 | Float oldPrevYawHead = lentity != null ? lentity.prevRotationYawHead : null; 89 | Float oldYawHead = lentity != null ? lentity.rotationYawHead : null; 90 | entity.rotationYaw = yaw; 91 | entity.rotationPitch = -pitch; 92 | if (lentity != null) { 93 | lentity.renderYawOffset = yaw; 94 | lentity.rotationYawHead = entity.rotationYaw; 95 | lentity.prevRotationYawHead = entity.rotationYaw; 96 | } 97 | EntityRendererManager erm = Minecraft.getInstance().getRenderManager(); 98 | xRot.conjugate(); 99 | erm.setCameraOrientation(xRot); 100 | erm.setRenderShadow(false); 101 | GraphicsFanciness oldFanciness = Minecraft.getInstance().gameSettings.graphicFanciness; 102 | Minecraft.getInstance().gameSettings.graphicFanciness = GraphicsFanciness.FANCY; 103 | try { 104 | IRenderTypeBuffer.Impl buf = Minecraft.getInstance().getRenderTypeBuffers().getBufferSource(); 105 | erm.renderEntityStatic(entity, 0, 0, 0, 0, 1, matrices, buf, 0xF000F0); 106 | buf.finish(); 107 | } finally { 108 | Minecraft.getInstance().gameSettings.graphicFanciness = oldFanciness; 109 | erm.setRenderShadow(true); 110 | entity.rotationYaw = oldYaw; 111 | entity.rotationPitch = oldPitch; 112 | if (lentity != null) { 113 | lentity.renderYawOffset = oldYawOfs; 114 | lentity.prevRotationYawHead = oldPrevYawHead; 115 | lentity.rotationYawHead = oldYawHead; 116 | } 117 | RenderSystem.popMatrix(); 118 | } 119 | } 120 | 121 | } -------------------------------------------------------------------------------- /src/main/java/com/unascribed/blockrenderer/GuiConfigureRender.java: -------------------------------------------------------------------------------- 1 | package com.unascribed.blockrenderer; 2 | 3 | import javax.annotation.Nonnull; 4 | 5 | import org.lwjgl.glfw.GLFW; 6 | 7 | import com.mojang.blaze3d.matrix.MatrixStack; 8 | import com.google.common.base.Strings; 9 | 10 | import net.minecraft.client.Minecraft; 11 | import net.minecraft.client.gui.screen.Screen; 12 | import net.minecraft.client.gui.widget.AbstractSlider; 13 | import net.minecraft.client.gui.widget.TextFieldWidget; 14 | import net.minecraft.client.gui.widget.button.Button; 15 | import net.minecraft.client.resources.I18n; 16 | import net.minecraft.util.math.MathHelper; 17 | import net.minecraft.util.text.ITextComponent; 18 | import net.minecraft.util.text.StringTextComponent; 19 | import net.minecraft.util.text.TranslationTextComponent; 20 | 21 | public class GuiConfigureRender extends Screen { 22 | 23 | private final String prefill; 24 | private final Screen old; 25 | private TextFieldWidget text; 26 | private Slider slider; 27 | private double sliderValue; 28 | private boolean fixSliderMax; 29 | 30 | public GuiConfigureRender(Screen old, String prefill) { 31 | super(StringTextComponent.EMPTY); 32 | this.old = old; 33 | this.prefill = Strings.nullToEmpty(prefill); 34 | this.sliderValue = 512; 35 | } 36 | 37 | @Override 38 | public void resize(@Nonnull Minecraft minecraft, int width, int height) { 39 | String oldText = text.getText(); 40 | this.init(minecraft, width, height); 41 | text.setText(oldText); 42 | fixSliderMax = true; 43 | } 44 | 45 | @Override 46 | public void init() { 47 | minecraft.keyboardListener.enableRepeatEvents(true); 48 | text = new TextFieldWidget(font, width / 2 - 100, height / 6 + 50, 200, 20, StringTextComponent.EMPTY); 49 | text.setMaxStringLength(4096); 50 | text.setText(prefill); 51 | 52 | addButton(new Button(width / 2 - 100, height / 6 + 120, 98, 20, new TranslationTextComponent("gui.blockrenderer.cancel"), 53 | button -> minecraft.displayGuiScreen(old))); 54 | 55 | addButton(new Button(width / 2 + 2, height / 6 + 120, 98, 20, new TranslationTextComponent("gui.blockrenderer.render"), button -> render())); 56 | slider = addButton(new Slider(width / 2 - 100, height / 6 + 80, 200, 20, new TranslationTextComponent("gui.blockrenderer.render_size"), 57 | sliderValue, 16, getSliderMax())); 58 | 59 | text.setFocused2(true); 60 | text.setCanLoseFocus(false); 61 | } 62 | 63 | private int getSliderMax() { 64 | return Math.min(2048, Math.min(minecraft.getMainWindow().getWidth(), minecraft.getMainWindow().getHeight())); 65 | } 66 | 67 | private int round(double value) { 68 | int val = (int) value; 69 | // There's a more efficient method in MathHelper, but it rounds up. We want the nearest. 70 | int nearestPowerOfTwo = (int) Math.pow(2, Math.ceil(Math.log(val) / Math.log(2))); 71 | int minSize = Math.min(minecraft.getMainWindow().getHeight(), minecraft.getMainWindow().getWidth()); 72 | if (nearestPowerOfTwo < minSize && Math.abs(val - nearestPowerOfTwo) < 32) { 73 | val = nearestPowerOfTwo; 74 | } 75 | return Math.min(val, minSize); 76 | } 77 | 78 | @Override 79 | public void render(@Nonnull MatrixStack matrix, int mouseX, int mouseY, float partialTicks) { 80 | renderBackground(matrix); 81 | if (text.getText().isEmpty()) { 82 | text.setSuggestion(I18n.format("gui.blockrenderer.namespace")); 83 | } else { 84 | text.setSuggestion(""); 85 | } 86 | super.render(matrix, mouseX, mouseY, partialTicks); 87 | drawCenteredString(matrix, minecraft.fontRenderer, new TranslationTextComponent("gui.blockrenderer.configure"), width / 2, height / 6, -1); 88 | int displayWidth = minecraft.getMainWindow().getWidth(); 89 | int displayHeight = minecraft.getMainWindow().getHeight(); 90 | boolean widthCap = (displayWidth < 2048); 91 | boolean heightCap = (displayHeight < 2048); 92 | String translationKey = null; 93 | if (widthCap && heightCap) { 94 | if (displayWidth > displayHeight) { 95 | translationKey = "gui.blockrenderer.capped_height"; 96 | } else if (displayWidth == displayHeight) { 97 | translationKey = "gui.blockrenderer.capped_both"; 98 | } else { //displayHeight > displayWidth 99 | translationKey = "gui.blockrenderer.capped_width"; 100 | } 101 | } else if (widthCap) { 102 | translationKey = "gui.blockrenderer.capped_width"; 103 | } else if (heightCap) { 104 | translationKey = "gui.blockrenderer.capped_height"; 105 | } 106 | if (translationKey != null) { 107 | drawCenteredString(matrix, minecraft.fontRenderer, new TranslationTextComponent(translationKey, Math.min(displayHeight, displayWidth)), 108 | width / 2, height / 6 + 104, 0xFFFFFF); 109 | } 110 | text.renderButton(matrix, mouseX, mouseY, partialTicks); 111 | } 112 | 113 | private void render() { 114 | if (minecraft.world != null) { 115 | BlockRenderer.renderHandler.pendingBulkRender = text.getText(); 116 | BlockRenderer.renderHandler.pendingBulkRenderSize = round(sliderValue); 117 | BlockRenderer.renderHandler.pendingBulkItems = true; 118 | BlockRenderer.renderHandler.pendingBulkEntities = false; 119 | BlockRenderer.renderHandler.pendingBulkStructures = false; 120 | } 121 | minecraft.displayGuiScreen(old); 122 | } 123 | 124 | @Override 125 | public void tick() { 126 | super.tick(); 127 | text.tick(); 128 | if (fixSliderMax) {//Ugly "hack" because the window's size isn't actually updated yet during init after a resize 129 | fixSliderMax = false; 130 | slider.updateSliderMax(getSliderMax()); 131 | } 132 | } 133 | 134 | @Override 135 | public boolean keyPressed(int keyCode, int scanCode, int modifiers) { 136 | if (text.canWrite() && keyCode != GLFW.GLFW_KEY_ESCAPE) {//Manually handle hitting escape to make the whole interface go away 137 | if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) { 138 | //Handle processing both the enter key and the numpad enter key 139 | render(); 140 | return true; 141 | } 142 | return text.keyPressed(keyCode, scanCode, modifiers); 143 | } 144 | return super.keyPressed(keyCode, scanCode, modifiers); 145 | } 146 | 147 | @Override 148 | public boolean charTyped(char c, int keyCode) { 149 | if (text.canWrite()) { 150 | return text.charTyped(c, keyCode); 151 | } 152 | return super.charTyped(c, keyCode); 153 | } 154 | 155 | @Override 156 | public boolean mouseClicked(double mouseX, double mouseY, int mouseButton) { 157 | if (text.isMouseOver(mouseX, mouseY)) { 158 | return text.mouseClicked(mouseX, mouseY, mouseButton); 159 | } 160 | return super.mouseClicked(mouseX, mouseY, mouseButton); 161 | } 162 | 163 | @Override 164 | public void onClose() { 165 | super.onClose(); 166 | minecraft.keyboardListener.enableRepeatEvents(false); 167 | } 168 | 169 | private static double normalizeValue(double value, double min, double max) { 170 | return MathHelper.clamp((MathHelper.clamp(value, min, max) - min) / (max - min), 0.0D, 1.0D); 171 | } 172 | 173 | private class Slider extends AbstractSlider { 174 | 175 | private final double minValue; 176 | private double maxValue; 177 | 178 | public Slider(int x, int y, int width, int height, ITextComponent message, double defaultValue, double minValue, double maxValue) { 179 | super(x, y, width, height, message, normalizeValue(defaultValue, minValue, maxValue)); 180 | this.minValue = minValue; 181 | this.maxValue = maxValue; 182 | func_230979_b_(); 183 | } 184 | 185 | @Override 186 | protected void func_230979_b_() { 187 | setMessage(new TranslationTextComponent("gui.blockrenderer.selected_dimensions", round(GuiConfigureRender.this.sliderValue))); 188 | } 189 | 190 | private double denormalizeValue() { 191 | return MathHelper.clamp(MathHelper.lerp(MathHelper.clamp(sliderValue, 0.0D, 1.0D), minValue, maxValue), minValue, maxValue); 192 | } 193 | 194 | @Override 195 | protected void func_230972_a_() { 196 | GuiConfigureRender.this.sliderValue = denormalizeValue(); 197 | } 198 | 199 | protected void updateSliderMax(double maxValue) { 200 | double value = denormalizeValue(); 201 | this.maxValue = maxValue; 202 | sliderValue = normalizeValue(value, minValue, this.maxValue); 203 | func_230972_a_(); 204 | func_230979_b_(); 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/main/java/com/unascribed/blockrenderer/ItemRenderTask.java: -------------------------------------------------------------------------------- 1 | package com.unascribed.blockrenderer; 2 | 3 | import com.mojang.blaze3d.matrix.MatrixStack; 4 | import com.mojang.blaze3d.systems.RenderSystem; 5 | 6 | import net.minecraft.client.Minecraft; 7 | import net.minecraft.item.ItemStack; 8 | import net.minecraft.util.ResourceLocation; 9 | import net.minecraft.util.text.ITextComponent; 10 | 11 | public class ItemRenderTask extends RenderTask { 12 | 13 | public final ItemStack stack; 14 | 15 | public ItemRenderTask(ItemStack stack) { 16 | this.stack = stack; 17 | } 18 | 19 | @Override 20 | public String getCategory() { 21 | return "items"; 22 | } 23 | 24 | @Override 25 | public ITextComponent getPreviewDisplayName() { 26 | return stack.getDisplayName(); 27 | } 28 | 29 | @Override 30 | public String getDisplayName() { 31 | return stack.getDisplayName().getString(); 32 | } 33 | 34 | @Override 35 | public ResourceLocation getId() { 36 | return stack.getItem().getRegistryName(); 37 | } 38 | 39 | @Override 40 | public void renderPreview(MatrixStack matrices, int x, int y) { 41 | RenderSystem.pushMatrix(); 42 | Minecraft.getInstance().getItemRenderer().renderItemAndEffectIntoGUI(stack, x, y); 43 | RenderSystem.popMatrix(); 44 | } 45 | 46 | @Override 47 | public void render(int renderSize) { 48 | Minecraft.getInstance().getItemRenderer().renderItemAndEffectIntoGUI(stack, 0, 0); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/unascribed/blockrenderer/RenderProgressGui.java: -------------------------------------------------------------------------------- 1 | package com.unascribed.blockrenderer; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | import javax.annotation.Nonnull; 9 | 10 | import com.mojang.blaze3d.matrix.MatrixStack; 11 | import net.minecraft.client.Minecraft; 12 | import net.minecraft.client.gui.LoadingGui; 13 | import net.minecraft.util.ColorHelper; 14 | import net.minecraft.util.Util; 15 | import net.minecraft.util.math.MathHelper; 16 | import net.minecraft.util.text.ITextComponent; 17 | import net.minecraft.util.text.TranslationTextComponent; 18 | 19 | public class RenderProgressGui extends LoadingGui { 20 | 21 | private static final int BG = 0xFF263238; 22 | private static final int BG_NOALPHA = BG & 0xFFFFFF; 23 | 24 | private final Minecraft mc; 25 | private final int total; 26 | private final String joined; 27 | private ITextComponent title; 28 | private List subTitles = Collections.emptyList(); 29 | private AsyncRenderer renderer; 30 | private float progress; 31 | private long fadeOutStart = -1; 32 | private long fadeInStart = -1; 33 | private long start = -1; 34 | private int rendered; 35 | private RenderTask task = null; 36 | private boolean canceled; 37 | 38 | public RenderProgressGui(Minecraft mc, int total, String joined) { 39 | this.mc = mc; 40 | this.total = total; 41 | this.joined = joined; 42 | this.title = new TranslationTextComponent("gui.blockrenderer.rendering", this.total, this.joined); 43 | } 44 | 45 | public void setFutures(List> futures) { 46 | this.renderer = new AsyncRenderer(futures); 47 | } 48 | 49 | public void cancel() { 50 | if (!canceled) { 51 | canceled = true; 52 | title = new TranslationTextComponent("gui.blockrenderer.render_cancelled", rendered, total, total - rendered); 53 | subTitles = Collections.emptyList(); 54 | task = null; 55 | renderer.cancel(); 56 | } 57 | } 58 | 59 | public void update(RenderTask task) { 60 | rendered++; 61 | int remaining = total - rendered; 62 | if (remaining > 0) { 63 | if (canceled) { 64 | //If canceled just update how much actually has been rendered so far 65 | title = new TranslationTextComponent("gui.blockrenderer.render_cancelled", rendered, total, remaining); 66 | } else { 67 | //Otherwise update the subtitles and the like 68 | subTitles = new ArrayList<>(); 69 | subTitles.add(new TranslationTextComponent("gui.blockrenderer.progress", rendered, total, remaining)); 70 | subTitles.add(task.getPreviewDisplayName()); 71 | this.task = task; 72 | } 73 | } else { 74 | title = new TranslationTextComponent("gui.blockrenderer.rendered", total, joined); 75 | subTitles = Collections.emptyList(); 76 | this.task = null; 77 | } 78 | } 79 | 80 | @Override 81 | public void render(@Nonnull MatrixStack matrix, int mouseX, int mouseY, float partialTicks) { 82 | int scaledWidth = mc.getMainWindow().getScaledWidth(); 83 | int scaledHeight = mc.getMainWindow().getScaledHeight(); 84 | long now = Util.milliTime(); 85 | if (start == -1) { 86 | start = now; 87 | } 88 | if ((renderer.asyncPartDone() || mc.currentScreen != null) && fadeInStart == -1) { 89 | fadeInStart = now; 90 | } 91 | float fadeOutTime = fadeOutStart > -1 ? (now - fadeOutStart) / 1000f : -1; 92 | float fadeInTime = fadeInStart > -1 ? (now - fadeInStart) / 500f : -1; 93 | int a; 94 | if (fadeOutTime >= 1) { 95 | if (mc.currentScreen != null) { 96 | mc.currentScreen.render(matrix, 0, 0, partialTicks); 97 | } 98 | a = MathHelper.ceil((1 - MathHelper.clamp(fadeOutTime - 1, 0, 1)) * 255); 99 | fill(matrix, 0, 0, scaledWidth, scaledHeight, BG_NOALPHA | a << 24); 100 | } else { 101 | if (mc.currentScreen != null && fadeInTime < 1) { 102 | mc.currentScreen.render(matrix, mouseX, mouseY, partialTicks); 103 | } 104 | a = MathHelper.ceil(MathHelper.clamp(fadeInTime, 0.15, 1) * 255); 105 | fill(matrix, 0, 0, scaledWidth, scaledHeight, BG_NOALPHA | a << 24); 106 | } 107 | 108 | int barSize = (int)(((Math.min(scaledWidth * 0.75, scaledHeight) * 0.25)*4)/2); 109 | int barPosition = (int)(scaledHeight * 0.8325); 110 | progress = MathHelper.clamp(progress * 0.95f + renderer.estimateExecutionSpeed() * 0.05f, 0, 1); 111 | renderProgressText(matrix, scaledWidth, scaledHeight, a << 24); 112 | if (fadeOutTime < 1) { 113 | drawProgressBar(matrix, scaledWidth / 2 - barSize, barPosition - 5, scaledWidth / 2 + barSize, barPosition + 5, 1 - MathHelper.clamp(fadeOutTime, 0, 1)); 114 | } else if (fadeOutTime >= 2) { 115 | mc.setLoadingGui(null); 116 | } 117 | if (fadeOutStart == -1 && renderer.fullyDone() && fadeInTime >= 2) { 118 | fadeOutStart = Util.milliTime(); 119 | try { 120 | renderer.join(); 121 | mc.ingameGUI.getChatGUI().printChatMessage(new TranslationTextComponent("msg.blockrenderer.rendered", total, joined, now - start)); 122 | } catch (Throwable t) { 123 | BlockRenderer.log.warn("Error joining renderer", t); 124 | } 125 | if (mc.currentScreen != null) { 126 | mc.currentScreen.init(mc, scaledWidth, scaledHeight); 127 | } 128 | } 129 | } 130 | 131 | private void drawProgressBar(MatrixStack matrix, int xStart, int yStart, int xEnd, int yEnd, float alpha) { 132 | int filled = MathHelper.ceil((xEnd - xStart - 2) * this.progress); 133 | int a = Math.round(alpha * 255); 134 | int packed = ColorHelper.PackedColor.packColor(a, 255, 255, 255); 135 | fill(matrix, xStart + 1, yStart, xEnd - 1, yStart + 1, packed); 136 | fill(matrix, xStart + 1, yEnd, xEnd - 1, yEnd - 1, packed); 137 | fill(matrix, xStart, yStart, xStart + 1, yEnd, packed); 138 | fill(matrix, xEnd, yStart, xEnd - 1, yEnd, packed); 139 | fill(matrix, xStart + 2, yStart + 2, xStart + filled, yEnd - 2, packed); 140 | } 141 | 142 | private void renderProgressText(MatrixStack matrix, int scaledWidth, int scaledHeight, int a) { 143 | matrix.push(); 144 | matrix.scale(2, 2, 0); 145 | if (a != 0) { 146 | drawCenteredString(matrix, mc.fontRenderer, title, scaledWidth / 4, scaledHeight / 4 - 24, 0xFFFFFF|a); 147 | } 148 | int subTitleCount = subTitles.size(); 149 | if (subTitleCount > 0) { 150 | matrix.scale(0.5F, 0.5F, 0); 151 | int subTitleX = scaledWidth / 2; 152 | int subTitleY = scaledHeight / 2; 153 | if (a != 0) { 154 | for (int i = 0; i < subTitleCount; i++) { 155 | drawCenteredString(matrix, mc.fontRenderer, subTitles.get(i), subTitleX, subTitleY + 20 * (i - 1), 0xFFFFFF|a); 156 | } 157 | } 158 | if (task != null) { 159 | task.renderPreview(matrix, subTitleX - 8, subTitleY + 20 * (subTitleCount - 1)); 160 | } 161 | } 162 | matrix.pop(); 163 | } 164 | } -------------------------------------------------------------------------------- /src/main/java/com/unascribed/blockrenderer/RenderTask.java: -------------------------------------------------------------------------------- 1 | package com.unascribed.blockrenderer; 2 | 3 | import com.mojang.blaze3d.matrix.MatrixStack; 4 | 5 | import net.minecraft.util.ResourceLocation; 6 | import net.minecraft.util.text.ITextComponent; 7 | 8 | public abstract class RenderTask { 9 | 10 | public abstract String getCategory(); 11 | 12 | public abstract ITextComponent getPreviewDisplayName(); 13 | public abstract String getDisplayName(); 14 | public abstract ResourceLocation getId(); 15 | 16 | public abstract void renderPreview(MatrixStack matrices, int x, int y); 17 | public abstract void render(int renderSize); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/mods.toml: -------------------------------------------------------------------------------- 1 | modLoader="javafml" 2 | loaderVersion="${loader_version}" 3 | license="MIT" 4 | 5 | [[mods]] 6 | modId="blockrenderer" 7 | version="${version}" 8 | displayName="BlockRenderer" 9 | displayURL="https://unascribed.com" 10 | authors="Una" 11 | description='''It renders blocks and items.''' 12 | 13 | [[dependencies.blockrenderer]] 14 | modId="minecraft" 15 | mandatory=true 16 | versionRange="${mc_version}" 17 | side="BOTH" 18 | [[dependencies.blockrenderer]] 19 | modId="forge" 20 | mandatory=true 21 | versionRange="${forge_version}" 22 | side="BOTH" -------------------------------------------------------------------------------- /src/main/resources/assets/blockrenderer/lang/en_us.json: -------------------------------------------------------------------------------- 1 | { 2 | "key.blockrenderer.render": "Render", 3 | "key.blockrenderer.category": "BlockRenderer", 4 | 5 | "msg.blockrenderer.render.fail": "Rendering failed, see game output for details.", 6 | "msg.blockrenderer.render.success": "Successfully rendered to %s.", 7 | 8 | "msg.blockrenderer.slot.empty": "That slot has no item.", 9 | "msg.blockrenderer.slot.absent": "There's no slot there.", 10 | "msg.blockrenderer.not_container": "Can't find anything to render.", 11 | "msg.blockrenderer.no_world": "You must be in a world to render things.", 12 | "msg.blockrenderer.rendered": "Rendered %1$s items from %2$s in %3$s ms.", 13 | 14 | "gui.blockrenderer.configure": "Configure Render", 15 | 16 | "gui.blockrenderer.namespace": "Namespace", 17 | 18 | "gui.blockrenderer.items": "Items & Blocks", 19 | "gui.blockrenderer.entities": "Entities", 20 | "gui.blockrenderer.structures": "Structures", 21 | 22 | "gui.blockrenderer.capped_both": "(Capped at %1$dx%1$d due to your window width and height)", 23 | "gui.blockrenderer.capped_height": "(Capped at %1$dx%1$d due to your window height)", 24 | "gui.blockrenderer.capped_width": "(Capped at %1$dx%1$d due to your window width)", 25 | "gui.blockrenderer.selected_dimensions": "%1$dx%1$d", 26 | 27 | "gui.blockrenderer.cancel": "Cancel", 28 | "gui.blockrenderer.render": "Render", 29 | "gui.blockrenderer.render_size": "Size", 30 | 31 | "gui.blockrenderer.rendering": "Rendering %1$s things from %2$s", 32 | "gui.blockrenderer.rendered": "Rendered %1$s things from %2$s", 33 | "gui.blockrenderer.render_cancelled": "Operation cancelled", 34 | "gui.blockrenderer.progress": "(%1$s/%2$s, %3$s to go)" 35 | } -------------------------------------------------------------------------------- /src/main/resources/pack.mcmeta: -------------------------------------------------------------------------------- 1 | { 2 | "pack": { 3 | "pack_format": 6, 4 | "description": "Resources used for BlockRenderer" 5 | } 6 | } --------------------------------------------------------------------------------