commit f460e229dfe66faf81aed1d2c17cf5a95ff8cdc0 Author: JozsefA Date: Wed Jun 16 12:57:52 2021 -0700 Squish - Squash all commits before separating flywheel from create diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..6ddef3d3c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 4 +continuation_indent_size = 8 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.json] +indent_style = space +indent_size = 2 + +[*.java] +indent_style = tab +ij_continuation_indent_size = 8 +ij_java_class_count_to_use_import_on_demand = 99 +ij_java_names_count_to_use_import_on_demand = 99 +ij_java_imports_layout = $*,|,java.**,|,javax.**,|,org.**,|,com.**,|,* diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..bbc1b9f54 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Auto detect text files and perform LF normalization +* text=auto +# Disable autocrlf on generated files, they always generate with LF +src/generated/**/*.json text eol=lf \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..0123b6707 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +## Based on GitHub's Eclipse .gitignore + +run/ +.gradle/ +build/ +gradle-app.setting + +## IntelliJ IDEA + +.idea/ +*.iml +*.iws +*.ipr + +## Eclipse + +eclipse/ +*.pydevproject +.project +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.classpath +.settings/ +.loadpath + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# PDT-specific +.buildpath diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..de3479305 --- /dev/null +++ b/build.gradle @@ -0,0 +1,160 @@ +buildscript { + repositories { + maven { url = 'https://files.minecraftforge.net/maven' } + jcenter() + mavenCentral() + maven { url='https://repo.spongepowered.org/repository/maven-public/' } + } + dependencies { + classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '3.+', changing: true + classpath group: 'org.spongepowered', name: 'mixingradle', version: '0.7-SNAPSHOT' + } +} +plugins { + id 'com.github.johnrengelman.shadow' version '5.2.0' + id 'com.matthewprenger.cursegradle' version '1.4.0' +} + +apply plugin: 'net.minecraftforge.gradle' +apply plugin: 'org.spongepowered.mixin' + +group = 'jozufozu' +version = '0.1' +archivesBaseName = 'flywheel' + +sourceCompatibility = targetCompatibility = compileJava.sourceCompatibility = compileJava.targetCompatibility = '1.8' + +minecraft { + mappings channel: 'snapshot', version: "${mcp_mappings}" + // makeObfSourceJar = false // an Srg named sources jar is made by default. uncomment this to disable. + + accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg') + + // Default run configurations. + // These can be tweaked, removed, or duplicated as needed. + runs { + client { + workingDirectory project.file('run') + + property 'forge.logging.markers', '' + property 'forge.logging.console.level', 'debug' + + arg "-mixin.config=flywheel.mixins.json" + + mods { + flywheel { + source sourceSets.main + } + } + } + + server { + workingDirectory project.file('run') + + // Recommended logging data for a userdev environment + // The markers can be changed as needed. + // "SCAN": For mods scan. + // "REGISTRIES": For firing of registry events. + // "REGISTRYDUMP": For getting the contents of all registries. + property 'forge.logging.markers', 'REGISTRIES' + + // Recommended logging level for the console + // You can set various levels here. + // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels + property 'forge.logging.console.level', 'debug' + + arg "-mixin.config=flywheel.mixins.json" + + mods { + flywheel { + source sourceSets.main + } + } + } + + data { + workingDirectory project.file('run') + + // Recommended logging data for a userdev environment + // The markers can be changed as needed. + // "SCAN": For mods scan. + // "REGISTRIES": For firing of registry events. + // "REGISTRYDUMP": For getting the contents of all registries. + property 'forge.logging.markers', 'REGISTRIES' + + // Recommended logging level for the console + // You can set various levels here. + // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels + property 'forge.logging.console.level', 'debug' + + // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. + args '--mod', 'flywheel', '--all', '--output', file('src/generated/resources/'), '--existing', file('src/main/resources/') + + mods { + flywheel { + source sourceSets.main + } + } + } + } +} + +mixin { + add sourceSets.main, "flywheel.refmap.json" +} + +// Include resources generated by data generators. +sourceSets.main.resources { srcDir 'src/generated/resources' } + +repositories { + maven { + //location of the maven for mixed mappings and registrate + name = "tterrag maven" + url = "https://maven.tterrag.com/" + } +} + +dependencies { + // Specify the version of Minecraft to use, If this is any group other then 'net.minecraft' it is assumed + // that the dep is a ForgeGradle 'patcher' dependency. And it's patches will be applied. + // The userdev artifact is a special name and will get all sorts of transformations applied to it. + minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}" + + // You may put jars on which you depend on in ./libs or you may define them like so.. + // compile "some.group:artifact:version:classifier" + // compile "some.group:artifact:version" + + // Real examples + // compile 'com.mod-buildcraft:buildcraft:6.0.8:dev' // adds buildcraft to the dev env + // compile 'com.googlecode.efficient-java-matrix-library:ejml:0.24' // adds ejml to the dev env + + // The 'provided' configuration is for optional dependencies that exist at compile-time but might not at runtime. + // provided 'com.mod-buildcraft:buildcraft:6.0.8:dev' + + // These dependencies get remapped to your current MCP mappings + // deobf 'com.mod-buildcraft:buildcraft:6.0.8:dev' + + // For more info... + // http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html + // http://www.gradle.org/docs/current/userguide/dependency_management.html + + annotationProcessor 'org.spongepowered:mixin:0.8:processor' +} + +// Example for how to get properties into the manifest for reading by the runtime.. +jar { + manifest { + attributes([ + "Specification-Title" : "flywheel", + //"Specification-Vendor": "flywheel authors", + "Specification-Version" : "1", // We are version 1 of ourselves + "Implementation-Title" : project.name, + "Implementation-Version" : project.version, + //"Implementation-Vendor": "flywheel authors", + "MixinConfigs" : "flywheel.mixins.json", + "Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ") + ]) + } +} + +jar.finalizedBy('reobfJar') diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..5d55b81e7 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,19 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false + +# mod version info +mod_version=0.0.1 +minecraft_version=1.16.5 +forge_version=36.0.42 +mcp_mappings=20200920-mixed-1.16.3 + +# dependency versions +registrate_version=1.0.4 +jei_version=7.6.1.71 + +# curseforge information +# projectId=486392 +# curse_type=beta + +# github information +github_project=Jozufozu/Flywheel diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..7a3265ee9 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..a055b814c --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.3-bin.zip diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..cccdd3d51 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..f9553162f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/com/jozufozu/flywheel/Flywheel.java b/src/main/java/com/jozufozu/flywheel/Flywheel.java new file mode 100644 index 000000000..e929fa06c --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/Flywheel.java @@ -0,0 +1,84 @@ +package com.jozufozu.flywheel; + +import net.minecraft.block.Block; +import net.minecraft.block.Blocks; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.RegistryEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.InterModComms; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; +import net.minecraftforge.fml.event.lifecycle.InterModEnqueueEvent; +import net.minecraftforge.fml.event.lifecycle.InterModProcessEvent; +import net.minecraftforge.fml.event.server.FMLServerStartingEvent; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.stream.Collectors; + +// The value here should match an entry in the META-INF/mods.toml file +@Mod("flywheel") +public class Flywheel { + + public static final String ID = "flywheel"; + // Directly reference a log4j logger. + private static final Logger LOGGER = LogManager.getLogger(); + + public Flywheel() { + // Register the setup method for modloading + FMLJavaModLoadingContext.get().getModEventBus().addListener(this::setup); + // Register the enqueueIMC method for modloading + FMLJavaModLoadingContext.get().getModEventBus().addListener(this::enqueueIMC); + // Register the processIMC method for modloading + FMLJavaModLoadingContext.get().getModEventBus().addListener(this::processIMC); + // Register the doClientStuff method for modloading + FMLJavaModLoadingContext.get().getModEventBus().addListener(this::doClientStuff); + + // Register ourselves for server and other game events we are interested in + MinecraftForge.EVENT_BUS.register(this); + } + + private void setup(final FMLCommonSetupEvent event) { + // some preinit code + LOGGER.info("HELLO FROM PREINIT"); + LOGGER.info("DIRT BLOCK >> {}", Blocks.DIRT.getRegistryName()); + } + + private void doClientStuff(final FMLClientSetupEvent event) { + // do something that can only be done on the client + LOGGER.info("Got game settings {}", event.getMinecraftSupplier().get().gameSettings); + } + + private void enqueueIMC(final InterModEnqueueEvent event) { + // some example code to dispatch IMC to another mod + InterModComms.sendTo("flywheel", "helloworld", () -> { + LOGGER.info("Hello world from the MDK"); + return "Hello world"; + }); + } + + private void processIMC(final InterModProcessEvent event) { + // some example code to receive and process InterModComms from other mods + LOGGER.info("Got IMC {}", event.getIMCStream().map(m -> m.getMessageSupplier().get()).collect(Collectors.toList())); + } + + // You can use SubscribeEvent and let the Event Bus discover methods to call + @SubscribeEvent + public void onServerStarting(FMLServerStartingEvent event) { + // do something when the server starts + LOGGER.info("HELLO from server starting"); + } + + // You can use EventBusSubscriber to automatically subscribe events on the contained class (this is subscribing to the MOD + // Event bus for receiving Registry Events) + @Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD) + public static class RegistryEvents { + @SubscribeEvent + public static void onBlocksRegistry(final RegistryEvent.Register blockRegistryEvent) { + // register a new block here + LOGGER.info("HELLO from Register Block"); + } + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/Backend.java b/src/main/java/com/jozufozu/flywheel/backend/Backend.java new file mode 100644 index 000000000..4c541eef4 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/Backend.java @@ -0,0 +1,184 @@ +package com.jozufozu.flywheel.backend; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.lwjgl.opengl.GL; +import org.lwjgl.opengl.GLCapabilities; + +import com.jozufozu.flywheel.backend.gl.versioned.GlCompat; +import com.jozufozu.flywheel.backend.instancing.InstanceData; +import com.jozufozu.flywheel.backend.instancing.MaterialSpec; +import com.jozufozu.flywheel.core.shader.spec.ProgramSpec; + +import net.minecraft.client.Minecraft; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.math.vector.Matrix4f; +import net.minecraft.world.IWorld; +import net.minecraft.world.World; + +public class Backend { + public static final Logger log = LogManager.getLogger(Backend.class); + + protected static final Backend INSTANCE = new Backend(); + + public static Backend getInstance() { + return INSTANCE; + } + + public final Minecraft minecraft; + public ShaderSources sources; + + public GLCapabilities capabilities; + public GlCompat compat; + + private Matrix4f projectionMatrix = new Matrix4f(); + private boolean instancedArrays; + private boolean enabled; + + private final List> contexts = new ArrayList<>(); + private final Map> materialRegistry = new HashMap<>(); + private final Map programSpecRegistry = new HashMap<>(); + + protected Backend() { + // Can be null when running datagenerators due to the unfortunate time we call this + minecraft = Minecraft.getInstance(); + if (minecraft == null) return; + + sources = new ShaderSources(this); + + OptifineHandler.init(); + } + + void clearContexts() { + SpecMetaRegistry.clear(); + contexts.forEach(IShaderContext::delete); + materialRegistry.clear(); + } + + /** + * Get a string describing the Flywheel backend. When there are eventually multiple backends + * (Meshlet, MDI, GL31 Draw Instanced are planned), this will name which one is in use. + */ + public String getBackendDescriptor() { + if (canUseInstancing()) { + return "GL33 Instanced Arrays"; + } + + if (canUseVBOs()) { + return "VBOs"; + } + + return "Disabled"; + } + + /** + * Register a shader program. + */ + public ProgramSpec register(ProgramSpec spec) { + ResourceLocation name = spec.name; + if (programSpecRegistry.containsKey(name)) { + throw new IllegalStateException("Program spec '" + name + "' already registered."); + } + programSpecRegistry.put(name, spec); + return spec; + } + + /** + * Register a shader context. + */ + public > C register(C spec) { + contexts.add(spec); + return spec; + } + + /** + * Register an instancing material. + */ + public MaterialSpec register(MaterialSpec spec) { + ResourceLocation name = spec.name; + if (materialRegistry.containsKey(name)) { + throw new IllegalStateException("Material spec '" + name + "' already registered."); + } + materialRegistry.put(name, spec); + return spec; + } + + public ProgramSpec getSpec(ResourceLocation name) { + return programSpecRegistry.get(name); + } + + public boolean available() { + return canUseVBOs(); + } + + public boolean canUseInstancing() { + return enabled && instancedArrays; + } + + public boolean canUseVBOs() { + return enabled && gl20(); + } + + public boolean gl33() { + return capabilities.OpenGL33; + } + + public boolean gl20() { + return capabilities.OpenGL20; + } + + public void refresh() { + OptifineHandler.refresh(); + capabilities = GL.createCapabilities(); + + compat = new GlCompat(capabilities); + + instancedArrays = compat.vertexArrayObjectsSupported() && + compat.drawInstancedSupported() && + compat.instancedArraysSupported(); + + // TODO: Config + enabled = !OptifineHandler.usingShaders(); + } + + public boolean canUseInstancing(World world) { + return canUseInstancing() && isFlywheelWorld(world); + } + + public Collection> allMaterials() { + return materialRegistry.values(); + } + + public Collection allPrograms() { + return programSpecRegistry.values(); + } + + public Collection> allContexts() { + return contexts; + } + + public Matrix4f getProjectionMatrix() { + return projectionMatrix; + } + + public void setProjectionMatrix(Matrix4f projectionMatrix) { + this.projectionMatrix = projectionMatrix; + } + + /** + * Used to avoid calling Flywheel functions on (fake) worlds that don't specifically support it. + */ + public static boolean isFlywheelWorld(IWorld world) { + return (world instanceof IFlywheelWorld && ((IFlywheelWorld) world).supportsFlywheel()) || world == Minecraft.getInstance().world; + } + + public static void reloadWorldRenderers() { + RenderWork.enqueue(Minecraft.getInstance().worldRenderer::loadRenderers); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/IFlywheelWorld.java b/src/main/java/com/jozufozu/flywheel/backend/IFlywheelWorld.java new file mode 100644 index 000000000..52f1712ed --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/IFlywheelWorld.java @@ -0,0 +1,13 @@ +package com.jozufozu.flywheel.backend; + +/** + * A marker interface custom worlds can override to indicate + * that tiles inside the world should render with Flywheel. + * + * Minecraft.getInstance().world is special cased and will support Flywheel by default. + */ +public interface IFlywheelWorld { + default boolean supportsFlywheel() { + return true; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/IShaderContext.java b/src/main/java/com/jozufozu/flywheel/backend/IShaderContext.java new file mode 100644 index 000000000..733ed5739 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/IShaderContext.java @@ -0,0 +1,23 @@ +package com.jozufozu.flywheel.backend; + +import java.util.function.Supplier; + +import com.jozufozu.flywheel.backend.gl.shader.GlProgram; + +import net.minecraft.util.ResourceLocation; + +public interface IShaderContext

{ + + default P getProgram(ResourceLocation loc) { + return this.getProgramSupplier(loc).get(); + } + + Supplier

getProgramSupplier(ResourceLocation loc); + + /** + * Load all programs associated with this context. This might be just one, if the context is very specialized. + */ + void load(); + + void delete(); +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/OptifineHandler.java b/src/main/java/com/jozufozu/flywheel/backend/OptifineHandler.java new file mode 100644 index 000000000..c4d825c90 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/OptifineHandler.java @@ -0,0 +1,86 @@ +package com.jozufozu.flywheel.backend; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.util.Optional; + +import net.minecraft.client.Minecraft; + +public class OptifineHandler { + public static final String OPTIFINE_ROOT_PACKAGE = "net.optifine"; + public static final String SHADER_PACKAGE = "net.optifine.shaders"; + + private static Package optifine; + private static OptifineHandler handler; + + public final boolean usingShaders; + + public OptifineHandler(boolean usingShaders) { + this.usingShaders = usingShaders; + } + + /** + * Get information about the current Optifine configuration. + * + * @return {@link Optional#empty()} if Optifine is not installed. + */ + public static Optional get() { + return Optional.ofNullable(handler); + } + + public static boolean optifineInstalled() { + return optifine != null; + } + + public static boolean usingShaders() { + return OptifineHandler.get() + .map(OptifineHandler::isUsingShaders) + .orElse(false); + } + + public static void init() { + optifine = Package.getPackage(OPTIFINE_ROOT_PACKAGE); + + if (optifine == null) { + Backend.log.info("Optifine not detected."); + } else { + Backend.log.info("Optifine detected."); + + refresh(); + } + } + + public static void refresh() { + if (optifine == null) return; + + File dir = Minecraft.getInstance().gameDir; + + File shaderOptions = new File(dir, "optionsshaders.txt"); + + boolean shadersOff = true; + try { + BufferedReader reader = new BufferedReader(new FileReader(shaderOptions)); + + shadersOff = reader.lines() + .anyMatch(it -> { + String line = it.replaceAll("\\s", ""); + if (line.startsWith("shaderPack=")) { + String setting = line.substring("shaderPack=".length()); + + return setting.equals("OFF") || setting.equals("(internal)"); + } + return false; + }); + } catch (FileNotFoundException e) { + Backend.log.info("No shader config found."); + } + + handler = new OptifineHandler(!shadersOff); + } + + public boolean isUsingShaders() { + return usingShaders; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/RenderWork.java b/src/main/java/com/jozufozu/flywheel/backend/RenderWork.java new file mode 100644 index 000000000..37b72652f --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/RenderWork.java @@ -0,0 +1,30 @@ +package com.jozufozu.flywheel.backend; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.client.event.RenderWorldLastEvent; +import net.minecraftforge.eventbus.api.EventPriority; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +@Mod.EventBusSubscriber(value = Dist.CLIENT) +public class RenderWork { + private static final Queue runs = new ConcurrentLinkedQueue<>(); + + + @SubscribeEvent(priority = EventPriority.LOWEST) + public static void onRenderWorldLast(RenderWorldLastEvent event) { + while (!runs.isEmpty()) { + runs.remove().run(); + } + } + + /** + * Queue work to be executed at the end of a frame + */ + public static void enqueue(Runnable run) { + runs.add(run); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/ResourceUtil.java b/src/main/java/com/jozufozu/flywheel/backend/ResourceUtil.java new file mode 100644 index 000000000..a8294d3d1 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/ResourceUtil.java @@ -0,0 +1,19 @@ +package com.jozufozu.flywheel.backend; + +import net.minecraft.util.ResourceLocation; + +public class ResourceUtil { + + public static ResourceLocation subPath(ResourceLocation root, String subPath) { + return new ResourceLocation(root.getNamespace(), root.getPath() + subPath); + } + + public static ResourceLocation removePrefixUnchecked(ResourceLocation full, String root) { + return new ResourceLocation(full.getNamespace(), full.getPath().substring(root.length())); + } + + public static ResourceLocation trim(ResourceLocation loc, String prefix, String suffix) { + String path = loc.getPath(); + return new ResourceLocation(loc.getNamespace(), path.substring(prefix.length(), path.length() - suffix.length())); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/ShaderContext.java b/src/main/java/com/jozufozu/flywheel/backend/ShaderContext.java new file mode 100644 index 000000000..9cbfe3d54 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/ShaderContext.java @@ -0,0 +1,88 @@ +package com.jozufozu.flywheel.backend; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import javax.annotation.Nullable; + +import com.jozufozu.flywheel.backend.gl.GlObject; +import com.jozufozu.flywheel.backend.gl.shader.GlProgram; +import com.jozufozu.flywheel.backend.gl.shader.GlShader; +import com.jozufozu.flywheel.backend.gl.shader.ShaderType; +import com.jozufozu.flywheel.backend.loading.Program; +import com.jozufozu.flywheel.backend.loading.Shader; +import com.jozufozu.flywheel.core.shader.IMultiProgram; +import com.jozufozu.flywheel.core.shader.spec.ProgramSpec; +import com.jozufozu.flywheel.core.shader.spec.ProgramState; + +import net.minecraft.util.ResourceLocation; + +public abstract class ShaderContext

implements IShaderContext

{ + + protected final Map> programs = new HashMap<>(); + + public final Backend backend; + + public ShaderContext(Backend backend) { + this.backend = backend; + } + + @Override + public Supplier

getProgramSupplier(ResourceLocation spec) { + return programs.get(spec); + } + + public Program loadAndLink(ProgramSpec spec, @Nullable ProgramState state) { + Shader vertexFile = getSource(ShaderType.VERTEX, spec.vert); + Shader fragmentFile = getSource(ShaderType.FRAGMENT, spec.frag); + + if (state != null) { + vertexFile.defineAll(state.getDefines()); + fragmentFile.defineAll(state.getDefines()); + } + + return link(buildProgram(spec.name, vertexFile, fragmentFile)); + } + + protected Shader getSource(ShaderType type, ResourceLocation name) { + return backend.sources.source(name, type); + } + + protected Program link(Program program) { + return program.link(); + } + + @Override + public void delete() { + programs.values().forEach(IMultiProgram::delete); + } + + /** + * Ingests the given shaders, compiling them and linking them together after applying the transformer to the source. + * + * @param name What should we call this program if something goes wrong? + * @param shaders What are the different shader stages that should be linked together? + * @return A program with all provided shaders attached + */ + protected static Program buildProgram(ResourceLocation name, Shader... shaders) { + List compiled = new ArrayList<>(shaders.length); + try { + Program builder = new Program(name); + + for (Shader shader : shaders) { + GlShader sh = new GlShader(shader); + compiled.add(sh); + + builder.attachShader(shader, sh); + } + + return builder; + } finally { + compiled.forEach(GlObject::delete); + } + } + +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/ShaderSources.java b/src/main/java/com/jozufozu/flywheel/backend/ShaderSources.java new file mode 100644 index 000000000..03fa61d27 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/ShaderSources.java @@ -0,0 +1,212 @@ +package com.jozufozu.flywheel.backend; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import javax.annotation.Nonnull; +import javax.annotation.ParametersAreNonnullByDefault; + +import org.lwjgl.system.MemoryUtil; + +import com.google.common.collect.Lists; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.jozufozu.flywheel.backend.gl.shader.ShaderType; +import com.jozufozu.flywheel.backend.loading.Shader; +import com.jozufozu.flywheel.backend.loading.ShaderLoadingException; +import com.jozufozu.flywheel.core.shader.spec.ProgramSpec; +import com.jozufozu.flywheel.event.GatherContextEvent; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.JsonOps; + +import net.minecraft.resources.IReloadableResourceManager; +import net.minecraft.resources.IResource; +import net.minecraft.resources.IResourceManager; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.fml.ModLoader; +import net.minecraftforge.resource.IResourceType; +import net.minecraftforge.resource.ISelectiveResourceReloadListener; +import net.minecraftforge.resource.VanillaResourceType; + +@ParametersAreNonnullByDefault +public class ShaderSources implements ISelectiveResourceReloadListener { + public static final String SHADER_DIR = "flywheel/shaders/"; + public static final String PROGRAM_DIR = "flywheel/programs/"; + public static final ArrayList EXTENSIONS = Lists.newArrayList(".vert", ".vsh", ".frag", ".fsh", ".glsl"); + private static final Gson GSON = new GsonBuilder().create(); + + private final Map shaderSource = new HashMap<>(); + + private boolean shouldCrash; + private final Backend backend; + + public ShaderSources(Backend backend) { + this.backend = backend; + IResourceManager manager = backend.minecraft.getResourceManager(); + if (manager instanceof IReloadableResourceManager) { + ((IReloadableResourceManager) manager).addReloadListener(this); + } + } + + @Override + public void onResourceManagerReload(IResourceManager manager, Predicate predicate) { + if (predicate.test(VanillaResourceType.SHADERS)) { + backend.refresh(); + + if (backend.gl20()) { + shaderSource.clear(); + + shouldCrash = false; + + backend.clearContexts(); + ModLoader.get().postEvent(new GatherContextEvent(backend)); + + loadProgramSpecs(manager); + loadShaderSources(manager); + + for (IShaderContext context : backend.allContexts()) { + context.load(); + } + + if (shouldCrash) { + throw new ShaderLoadingException("Could not load all shaders, see log for details"); + } + + Backend.log.info("Loaded all shader programs."); + + // no need to hog all that memory + shaderSource.clear(); + } + } + } + + private void loadProgramSpecs(IResourceManager manager) { + Collection programSpecs = manager.getAllResourceLocations(PROGRAM_DIR, s -> s.endsWith(".json")); + + for (ResourceLocation location : programSpecs) { + try { + IResource file = manager.getResource(location); + + String s = readToString(file.getInputStream()); + + ResourceLocation specName = ResourceUtil.trim(location, PROGRAM_DIR, ".json"); + + DataResult> result = ProgramSpec.CODEC.decode(JsonOps.INSTANCE, GSON.fromJson(s, JsonElement.class)); + + ProgramSpec spec = result.get().orThrow().getFirst(); + + spec.setName(specName); + + backend.register(spec); + } catch (Exception e) { + Backend.log.error(e); + } + } + } + + public void notifyError() { + shouldCrash = true; + } + + @Nonnull + public String getShaderSource(ResourceLocation loc) { + String source = shaderSource.get(loc); + + if (source == null) { + throw new ShaderLoadingException(String.format("shader '%s' does not exist", loc)); + } + + return source; + } + + private void loadShaderSources(IResourceManager manager) { + Collection allShaders = manager.getAllResourceLocations(SHADER_DIR, s -> { + for (String ext : EXTENSIONS) { + if (s.endsWith(ext)) return true; + } + return false; + }); + + for (ResourceLocation location : allShaders) { + try { + IResource resource = manager.getResource(location); + + String file = readToString(resource.getInputStream()); + + ResourceLocation name = ResourceUtil.removePrefixUnchecked(location, SHADER_DIR); + + shaderSource.put(name, file); + } catch (IOException e) { + + } + } + } + + public Shader source(ResourceLocation name, ShaderType type) { + return new Shader(this, type, name, getShaderSource(name)); + } + + public static Stream lines(String s) { + return new BufferedReader(new StringReader(s)).lines(); + } + + public String readToString(InputStream is) { + RenderSystem.assertThread(RenderSystem::isOnRenderThread); + ByteBuffer bytebuffer = null; + + try { + bytebuffer = readToBuffer(is); + int i = bytebuffer.position(); + bytebuffer.rewind(); + return MemoryUtil.memASCII(bytebuffer, i); + } catch (IOException e) { + + } finally { + if (bytebuffer != null) { + MemoryUtil.memFree(bytebuffer); + } + + } + + return null; + } + + public ByteBuffer readToBuffer(InputStream is) throws IOException { + ByteBuffer bytebuffer; + if (is instanceof FileInputStream) { + FileInputStream fileinputstream = (FileInputStream) is; + FileChannel filechannel = fileinputstream.getChannel(); + bytebuffer = MemoryUtil.memAlloc((int) filechannel.size() + 1); + + while (filechannel.read(bytebuffer) != -1) { + } + } else { + bytebuffer = MemoryUtil.memAlloc(8192); + ReadableByteChannel readablebytechannel = Channels.newChannel(is); + + while (readablebytechannel.read(bytebuffer) != -1) { + if (bytebuffer.remaining() == 0) { + bytebuffer = MemoryUtil.memRealloc(bytebuffer, bytebuffer.capacity() * 2); + } + } + } + + return bytebuffer; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/SpecMetaRegistry.java b/src/main/java/com/jozufozu/flywheel/backend/SpecMetaRegistry.java new file mode 100644 index 000000000..a19ed77f7 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/SpecMetaRegistry.java @@ -0,0 +1,56 @@ +package com.jozufozu.flywheel.backend; + +import java.util.HashMap; +import java.util.Map; + +import com.jozufozu.flywheel.core.shader.extension.IProgramExtension; +import com.jozufozu.flywheel.core.shader.gamestate.IGameStateProvider; + +import net.minecraft.util.ResourceLocation; + +public class SpecMetaRegistry { + + private static final Map registeredExtensions = new HashMap<>(); + private static final Map registeredStateProviders = new HashMap<>(); + + static void clear() { + registeredExtensions.clear(); + registeredStateProviders.clear(); + } + + public static IGameStateProvider getStateProvider(ResourceLocation location) { + IGameStateProvider out = registeredStateProviders.get(location); + + if (out == null) { + throw new IllegalArgumentException("State provider '" + location + "' does not exist."); + } + + return out; + } + + public static IProgramExtension getExtension(ResourceLocation location) { + IProgramExtension out = registeredExtensions.get(location); + + if (out == null) { + throw new IllegalArgumentException("Extension '" + location + "' does not exist."); + } + + return out; + } + + public static void register(IGameStateProvider context) { + if (registeredStateProviders.containsKey(context.getID())) { + throw new IllegalStateException("Duplicate game state provider: " + context.getID()); + } + + registeredStateProviders.put(context.getID(), context); + } + + public static void register(IProgramExtension extender) { + if (registeredStateProviders.containsKey(extender.getID())) { + throw new IllegalStateException("Duplicate shader extension: " + extender.getID()); + } + + registeredExtensions.put(extender.getID(), extender); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/GlNumericType.java b/src/main/java/com/jozufozu/flywheel/backend/gl/GlNumericType.java new file mode 100644 index 000000000..29baffd7f --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/GlNumericType.java @@ -0,0 +1,67 @@ +package com.jozufozu.flywheel.backend.gl; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +import org.lwjgl.opengl.GL11; + +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +@OnlyIn(Dist.CLIENT) +public enum GlNumericType { + FLOAT(4, "float", GL11.GL_FLOAT), + UBYTE(1, "ubyte", GL11.GL_UNSIGNED_BYTE), + BYTE(1, "byte", GL11.GL_BYTE), + USHORT(2, "ushort", GL11.GL_UNSIGNED_SHORT), + SHORT(2, "short", GL11.GL_SHORT), + UINT(4, "uint", GL11.GL_UNSIGNED_INT), + INT(4, "int", GL11.GL_INT), + ; + + private static final GlNumericType[] VALUES = values(); + private static final Map NAME_LOOKUP = Arrays.stream(VALUES) + .collect(Collectors.toMap(GlNumericType::getDisplayName, type -> type)); + + private final int byteWidth; + private final String displayName; + private final int glEnum; + + GlNumericType(int bytes, String name, int glEnum) { + this.byteWidth = bytes; + this.displayName = name; + this.glEnum = glEnum; + } + + public int getByteWidth() { + return this.byteWidth; + } + + public String getDisplayName() { + return this.displayName; + } + + public int getGlEnum() { + return this.glEnum; + } + + public void castAndBuffer(ByteBuffer buf, int val) { + if (this == UBYTE || this == BYTE) { + buf.put((byte) val); + } else if (this == USHORT || this == SHORT) { + buf.putShort((short) val); + } else if (this == UINT || this == INT) { + buf.putInt(val); + } + } + + @Nullable + public static GlNumericType byName(String name) { + return name == null ? null : NAME_LOOKUP.get(name.toLowerCase(Locale.ROOT)); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/GlObject.java b/src/main/java/com/jozufozu/flywheel/backend/gl/GlObject.java new file mode 100644 index 000000000..d07872204 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/GlObject.java @@ -0,0 +1,43 @@ +package com.jozufozu.flywheel.backend.gl; + +// Utility class for safely dealing with gl object handles. +public abstract class GlObject { + private static final int INVALID_HANDLE = Integer.MIN_VALUE; + + private int handle = INVALID_HANDLE; + + protected final void setHandle(int handle) { + this.handle = handle; + } + + public final int handle() { + this.checkHandle(); + + return this.handle; + } + + protected final void checkHandle() { + if (!this.isHandleValid()) { + throw new IllegalStateException("Handle is not valid"); + } + } + + protected final boolean isHandleValid() { + return this.handle != INVALID_HANDLE; + } + + protected final void invalidateHandle() { + this.handle = INVALID_HANDLE; + } + + public void delete() { + if (!isHandleValid()) { + throw new IllegalStateException("Handle already deleted."); + } + + deleteInternal(handle); + invalidateHandle(); + } + + protected abstract void deleteInternal(int handle); +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/GlPrimitive.java b/src/main/java/com/jozufozu/flywheel/backend/gl/GlPrimitive.java new file mode 100644 index 000000000..71a8d5353 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/GlPrimitive.java @@ -0,0 +1,23 @@ +package com.jozufozu.flywheel.backend.gl; + +import org.lwjgl.opengl.GL11; + +public enum GlPrimitive { + POINTS(GL11.GL_POINTS), + LINES(GL11.GL_LINES), + LINE_LOOP(GL11.GL_LINE_LOOP), + LINE_STRIP(GL11.GL_LINE_STRIP), + TRIANGLES(GL11.GL_TRIANGLES), + TRIANGLE_STRIP(GL11.GL_TRIANGLE_STRIP), + TRIANGLE_FAN(GL11.GL_TRIANGLE_FAN), + QUADS(GL11.GL_QUADS), + QUAD_STRIP(GL11.GL_QUAD_STRIP), + POLYGON(GL11.GL_POLYGON), + ; + + public final int glEnum; + + GlPrimitive(int glEnum) { + this.glEnum = glEnum; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/GlTexture.java b/src/main/java/com/jozufozu/flywheel/backend/gl/GlTexture.java new file mode 100644 index 000000000..76e637a6e --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/GlTexture.java @@ -0,0 +1,25 @@ +package com.jozufozu.flywheel.backend.gl; + +import org.lwjgl.opengl.GL20; + +public class GlTexture extends GlObject { + private final int textureType; + + public GlTexture(int textureType) { + this.textureType = textureType; + setHandle(GL20.glGenTextures()); + } + + @Override + protected void deleteInternal(int handle) { + GL20.glDeleteTextures(handle); + } + + public void bind() { + GL20.glBindTexture(textureType, handle()); + } + + public void unbind() { + GL20.glBindTexture(textureType, 0); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/GlVertexArray.java b/src/main/java/com/jozufozu/flywheel/backend/gl/GlVertexArray.java new file mode 100644 index 000000000..64544c54b --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/GlVertexArray.java @@ -0,0 +1,21 @@ +package com.jozufozu.flywheel.backend.gl; + +import com.jozufozu.flywheel.backend.Backend; + +public class GlVertexArray extends GlObject { + public GlVertexArray() { + setHandle(Backend.getInstance().compat.vao.genVertexArrays()); + } + + public void bind() { + Backend.getInstance().compat.vao.bindVertexArray(handle()); + } + + public void unbind() { + Backend.getInstance().compat.vao.bindVertexArray(0); + } + + protected void deleteInternal(int handle) { + Backend.getInstance().compat.vao.deleteVertexArrays(handle); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/attrib/CommonAttributes.java b/src/main/java/com/jozufozu/flywheel/backend/gl/attrib/CommonAttributes.java new file mode 100644 index 000000000..a1726fae0 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/attrib/CommonAttributes.java @@ -0,0 +1,21 @@ +package com.jozufozu.flywheel.backend.gl.attrib; + +import com.jozufozu.flywheel.backend.gl.GlNumericType; + +public class CommonAttributes { + + public static final VertexAttribSpec VEC4 = new VertexAttribSpec(GlNumericType.FLOAT, 4); + public static final VertexAttribSpec VEC3 = new VertexAttribSpec(GlNumericType.FLOAT, 3); + public static final VertexAttribSpec VEC2 = new VertexAttribSpec(GlNumericType.FLOAT, 2); + public static final VertexAttribSpec FLOAT = new VertexAttribSpec(GlNumericType.FLOAT, 1); + + public static final VertexAttribSpec QUATERNION = new VertexAttribSpec(GlNumericType.FLOAT, 4); + public static final VertexAttribSpec NORMAL = new VertexAttribSpec(GlNumericType.BYTE, 3, true); + public static final VertexAttribSpec UV = new VertexAttribSpec(GlNumericType.FLOAT, 2); + + public static final VertexAttribSpec RGBA = new VertexAttribSpec(GlNumericType.UBYTE, 4, true); + public static final VertexAttribSpec RGB = new VertexAttribSpec(GlNumericType.UBYTE, 3, true); + public static final VertexAttribSpec LIGHT = new VertexAttribSpec(GlNumericType.UBYTE, 2, true); + + public static final VertexAttribSpec NORMALIZED_BYTE = new VertexAttribSpec(GlNumericType.BYTE, 1, true); +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/attrib/IAttribSpec.java b/src/main/java/com/jozufozu/flywheel/backend/gl/attrib/IAttribSpec.java new file mode 100644 index 000000000..779d5c502 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/attrib/IAttribSpec.java @@ -0,0 +1,10 @@ +package com.jozufozu.flywheel.backend.gl.attrib; + +public interface IAttribSpec { + + void vertexAttribPointer(int stride, int index, int pointer); + + int getSize(); + + int getAttributeCount(); +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/attrib/MatrixAttributes.java b/src/main/java/com/jozufozu/flywheel/backend/gl/attrib/MatrixAttributes.java new file mode 100644 index 000000000..0a16dd11f --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/attrib/MatrixAttributes.java @@ -0,0 +1,37 @@ +package com.jozufozu.flywheel.backend.gl.attrib; + +import org.lwjgl.opengl.GL20; + +import com.jozufozu.flywheel.backend.gl.GlNumericType; + +public enum MatrixAttributes implements IAttribSpec { + MAT3(3, 3), + MAT4(4, 4), + ; + + private final int rows; + private final int cols; + + MatrixAttributes(int rows, int cols) { + this.rows = rows; + this.cols = cols; + } + + @Override + public void vertexAttribPointer(int stride, int index, int pointer) { + for (int i = 0; i < rows; i++) { + long attribPointer = pointer + (long) i * cols * GlNumericType.FLOAT.getByteWidth(); + GL20.glVertexAttribPointer(index + i, cols, GlNumericType.FLOAT.getGlEnum(), false, stride, attribPointer); + } + } + + @Override + public int getSize() { + return GlNumericType.FLOAT.getByteWidth() * rows * cols; + } + + @Override + public int getAttributeCount() { + return rows; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/attrib/VertexAttribSpec.java b/src/main/java/com/jozufozu/flywheel/backend/gl/attrib/VertexAttribSpec.java new file mode 100644 index 000000000..373f4cfb6 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/attrib/VertexAttribSpec.java @@ -0,0 +1,41 @@ +package com.jozufozu.flywheel.backend.gl.attrib; + +import org.lwjgl.opengl.GL20; + +import com.jozufozu.flywheel.backend.gl.GlNumericType; + +public class VertexAttribSpec implements IAttribSpec { + + private final GlNumericType type; + private final int count; + private final int size; + private final int attributeCount; + private final boolean normalized; + + public VertexAttribSpec(GlNumericType type, int count) { + this(type, count, false); + } + + public VertexAttribSpec(GlNumericType type, int count, boolean normalized) { + this.type = type; + this.count = count; + this.size = type.getByteWidth() * count; + this.attributeCount = (this.size + 15) / 16; // ceiling division. GLSL vertex attributes can only be 16 bytes wide + this.normalized = normalized; + } + + @Override + public void vertexAttribPointer(int stride, int index, int pointer) { + GL20.glVertexAttribPointer(index, count, type.getGlEnum(), normalized, stride, pointer); + } + + @Override + public int getSize() { + return size; + } + + @Override + public int getAttributeCount() { + return attributeCount; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/attrib/VertexFormat.java b/src/main/java/com/jozufozu/flywheel/backend/gl/attrib/VertexFormat.java new file mode 100644 index 000000000..9d48fd142 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/attrib/VertexFormat.java @@ -0,0 +1,61 @@ +package com.jozufozu.flywheel.backend.gl.attrib; + +import java.util.ArrayList; +import java.util.Collections; + +public class VertexFormat { + + private final ArrayList allAttributes; + + private final int numAttributes; + private final int stride; + + public VertexFormat(ArrayList allAttributes) { + this.allAttributes = allAttributes; + + int numAttributes = 0, stride = 0; + for (IAttribSpec spec : allAttributes) { + numAttributes += spec.getAttributeCount(); + stride += spec.getSize(); + } + this.numAttributes = numAttributes; + this.stride = stride; + } + + public int getAttributeCount() { + return numAttributes; + } + + public int getStride() { + return stride; + } + + public void vertexAttribPointers(int index) { + int offset = 0; + for (IAttribSpec spec : this.allAttributes) { + spec.vertexAttribPointer(stride, index, offset); + index += spec.getAttributeCount(); + offset += spec.getSize(); + } + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private final ArrayList allAttributes = new ArrayList<>(); + + public Builder() { + } + + public Builder addAttributes(IAttribSpec... attributes) { + Collections.addAll(allAttributes, attributes); + return this; + } + + public VertexFormat build() { + return new VertexFormat(allAttributes); + } + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/buffer/GlBuffer.java b/src/main/java/com/jozufozu/flywheel/backend/gl/buffer/GlBuffer.java new file mode 100644 index 000000000..d52804652 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/buffer/GlBuffer.java @@ -0,0 +1,69 @@ +package com.jozufozu.flywheel.backend.gl.buffer; + +import java.nio.ByteBuffer; + +import org.lwjgl.opengl.GL15; +import org.lwjgl.opengl.GL20; +import org.lwjgl.opengl.GL30; + +import com.jozufozu.flywheel.backend.Backend; +import com.jozufozu.flywheel.backend.gl.GlObject; +import com.jozufozu.flywheel.backend.gl.versioned.MapBufferRange; + +public class GlBuffer extends GlObject { + + protected final GlBufferType type; + protected final GlBufferUsage usage; + + public GlBuffer(GlBufferType type) { + this(type, GlBufferUsage.STATIC_DRAW); + } + + public GlBuffer(GlBufferType type, GlBufferUsage usage) { + setHandle(GL20.glGenBuffers()); + this.type = type; + this.usage = usage; + } + + public GlBufferType getBufferTarget() { + return type; + } + + public void bind() { + bind(type); + } + + public void bind(GlBufferType type) { + GL20.glBindBuffer(type.glEnum, handle()); + } + + public void unbind() { + unbind(type); + } + + public void unbind(GlBufferType bufferType) { + GL20.glBindBuffer(bufferType.glEnum, 0); + } + + public void alloc(int size) { + GL15.glBufferData(type.glEnum, size, usage.glEnum); + } + + public void upload(ByteBuffer directBuffer) { + GL15.glBufferData(type.glEnum, directBuffer, usage.glEnum); + } + + public MappedBuffer getBuffer(int offset, int length) { + if (Backend.getInstance().compat.mapBufferRange != MapBufferRange.UNSUPPORTED) { + return new MappedBufferRange(this, offset, length, GL30.GL_MAP_WRITE_BIT); + } else { + MappedFullBuffer fullBuffer = new MappedFullBuffer(this, MappedBufferUsage.WRITE_ONLY); + fullBuffer.position(offset); + return fullBuffer; + } + } + + protected void deleteInternal(int handle) { + GL20.glDeleteBuffers(handle); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/buffer/GlBufferType.java b/src/main/java/com/jozufozu/flywheel/backend/gl/buffer/GlBufferType.java new file mode 100644 index 000000000..7ba9d0073 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/buffer/GlBufferType.java @@ -0,0 +1,32 @@ +package com.jozufozu.flywheel.backend.gl.buffer; + +import org.lwjgl.opengl.GL15C; +import org.lwjgl.opengl.GL21; +import org.lwjgl.opengl.GL30; +import org.lwjgl.opengl.GL31; +import org.lwjgl.opengl.GL40; +import org.lwjgl.opengl.GL42; +import org.lwjgl.opengl.GL43; + +public enum GlBufferType { + ARRAY_BUFFER(GL15C.GL_ARRAY_BUFFER), + ELEMENT_ARRAY_BUFFER(GL15C.GL_ELEMENT_ARRAY_BUFFER), + PIXEL_PACK_BUFFER(GL21.GL_PIXEL_PACK_BUFFER), + PIXEL_UNPACK_BUFFER(GL21.GL_PIXEL_UNPACK_BUFFER), + TRANSFORM_FEEDBACK_BUFFER(GL30.GL_TRANSFORM_FEEDBACK_BUFFER), + UNIFORM_BUFFER(GL31.GL_UNIFORM_BUFFER), + TEXTURE_BUFFER(GL31.GL_TEXTURE_BUFFER), + COPY_READ_BUFFER(GL31.GL_COPY_READ_BUFFER), + COPY_WRITE_BUFFER(GL31.GL_COPY_WRITE_BUFFER), + DRAW_INDIRECT_BUFFER(GL40.GL_DRAW_INDIRECT_BUFFER), + ATOMIC_COUNTER_BUFFER(GL42.GL_ATOMIC_COUNTER_BUFFER), + DISPATCH_INDIRECT_BUFFER(GL43.GL_DISPATCH_INDIRECT_BUFFER), + SHADER_STORAGE_BUFFER(GL43.GL_SHADER_STORAGE_BUFFER), + ; + + public final int glEnum; + + GlBufferType(int glEnum) { + this.glEnum = glEnum; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/buffer/GlBufferUsage.java b/src/main/java/com/jozufozu/flywheel/backend/gl/buffer/GlBufferUsage.java new file mode 100644 index 000000000..e01f28afe --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/buffer/GlBufferUsage.java @@ -0,0 +1,22 @@ +package com.jozufozu.flywheel.backend.gl.buffer; + +import org.lwjgl.opengl.GL15; + +public enum GlBufferUsage { + STREAM_DRAW(GL15.GL_STREAM_DRAW), + STREAM_READ(GL15.GL_STREAM_READ), + STREAM_COPY(GL15.GL_STREAM_COPY), + STATIC_DRAW(GL15.GL_STATIC_DRAW), + STATIC_READ(GL15.GL_STATIC_READ), + STATIC_COPY(GL15.GL_STATIC_COPY), + DYNAMIC_DRAW(GL15.GL_DYNAMIC_DRAW), + DYNAMIC_READ(GL15.GL_DYNAMIC_READ), + DYNAMIC_COPY(GL15.GL_DYNAMIC_COPY), + ; + + public final int glEnum; + + GlBufferUsage(int glEnum) { + this.glEnum = glEnum; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/buffer/MappedBuffer.java b/src/main/java/com/jozufozu/flywheel/backend/gl/buffer/MappedBuffer.java new file mode 100644 index 000000000..2c8762452 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/buffer/MappedBuffer.java @@ -0,0 +1,110 @@ +package com.jozufozu.flywheel.backend.gl.buffer; + +import java.nio.ByteBuffer; + +import org.lwjgl.opengl.GL15; + +public abstract class MappedBuffer extends VecBuffer implements AutoCloseable { + + protected boolean mapped; + protected final GlBuffer owner; + + public MappedBuffer(GlBuffer owner) { + this.owner = owner; + } + + protected abstract void checkAndMap(); + + /** + * Make the changes in client memory available to the GPU. + */ + public void flush() { + if (mapped) { + GL15.glUnmapBuffer(owner.type.glEnum); + mapped = false; + setInternal(null); + } + } + + @Override + public void close() throws Exception { + flush(); + } + + public MappedBuffer putFloatArray(float[] floats) { + checkAndMap(); + super.putFloatArray(floats); + return this; + } + + public MappedBuffer putByteArray(byte[] bytes) { + checkAndMap(); + super.putByteArray(bytes); + return this; + } + + /** + * Position this buffer relative to the 0-index in GPU memory. + * + * @return This buffer. + */ + public MappedBuffer position(int p) { + checkAndMap(); + super.position(p); + return this; + } + + public MappedBuffer putFloat(float f) { + checkAndMap(); + super.putFloat(f); + return this; + } + + public MappedBuffer putInt(int i) { + checkAndMap(); + super.putInt(i); + return this; + } + + public MappedBuffer put(byte b) { + checkAndMap(); + super.put(b); + return this; + } + + public MappedBuffer put(ByteBuffer b) { + checkAndMap(); + super.put(b); + return this; + } + + public MappedBuffer putVec4(float x, float y, float z, float w) { + checkAndMap(); + super.putVec4(x, y, z, w); + return this; + } + + public MappedBuffer putVec3(float x, float y, float z) { + checkAndMap(); + super.putVec3(x, y, z); + return this; + } + + public MappedBuffer putVec2(float x, float y) { + checkAndMap(); + super.putVec2(x, y); + return this; + } + + public MappedBuffer putVec3(byte x, byte y, byte z) { + checkAndMap(); + super.putVec3(x, y, z); + return this; + } + + public MappedBuffer putVec2(byte x, byte y) { + checkAndMap(); + super.putVec2(x, y); + return this; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/buffer/MappedBufferRange.java b/src/main/java/com/jozufozu/flywheel/backend/gl/buffer/MappedBufferRange.java new file mode 100644 index 000000000..af0485c1e --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/buffer/MappedBufferRange.java @@ -0,0 +1,33 @@ +package com.jozufozu.flywheel.backend.gl.buffer; + +import com.jozufozu.flywheel.backend.Backend; + +public class MappedBufferRange extends MappedBuffer { + + long offset, length; + int access; + + public MappedBufferRange(GlBuffer buffer, long offset, long length, int access) { + super(buffer); + this.offset = offset; + this.length = length; + this.access = access; + } + + + @Override + public MappedBuffer position(int p) { + if (p < offset || p >= offset + length) { + throw new IndexOutOfBoundsException("Index " + p + " is not mapped"); + } + return super.position(p - (int) offset); + } + + @Override + protected void checkAndMap() { + if (!mapped) { + setInternal(Backend.getInstance().compat.mapBufferRange.mapBuffer(owner.type, offset, length, access)); + mapped = true; + } + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/buffer/MappedBufferUsage.java b/src/main/java/com/jozufozu/flywheel/backend/gl/buffer/MappedBufferUsage.java new file mode 100644 index 000000000..30e60a348 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/buffer/MappedBufferUsage.java @@ -0,0 +1,16 @@ +package com.jozufozu.flywheel.backend.gl.buffer; + +import org.lwjgl.opengl.GL15C; + +public enum MappedBufferUsage { + READ_ONLY(GL15C.GL_READ_ONLY), + WRITE_ONLY(GL15C.GL_WRITE_ONLY), + READ_WRITE(GL15C.GL_READ_WRITE), + ; + + int glEnum; + + MappedBufferUsage(int glEnum) { + this.glEnum = glEnum; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/buffer/MappedFullBuffer.java b/src/main/java/com/jozufozu/flywheel/backend/gl/buffer/MappedFullBuffer.java new file mode 100644 index 000000000..36ebb41d2 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/buffer/MappedFullBuffer.java @@ -0,0 +1,21 @@ +package com.jozufozu.flywheel.backend.gl.buffer; + +import org.lwjgl.opengl.GL15; + +public class MappedFullBuffer extends MappedBuffer { + + MappedBufferUsage usage; + + public MappedFullBuffer(GlBuffer buffer, MappedBufferUsage usage) { + super(buffer); + this.usage = usage; + } + + @Override + protected void checkAndMap() { + if (!mapped) { + setInternal(GL15.glMapBuffer(owner.type.glEnum, usage.glEnum)); + mapped = true; + } + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/buffer/VecBuffer.java b/src/main/java/com/jozufozu/flywheel/backend/gl/buffer/VecBuffer.java new file mode 100644 index 000000000..03540e7fb --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/buffer/VecBuffer.java @@ -0,0 +1,116 @@ +package com.jozufozu.flywheel.backend.gl.buffer; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class VecBuffer { + + protected ByteBuffer internal; + + public VecBuffer() { + } + + public VecBuffer(ByteBuffer internal) { + this.internal = internal; + } + + public static VecBuffer allocate(int bytes) { + ByteBuffer buffer = ByteBuffer.allocate(bytes); + buffer.order(ByteOrder.nativeOrder()); + return new VecBuffer(buffer); + } + + protected void setInternal(ByteBuffer internal) { + this.internal = internal; + } + + public ByteBuffer unwrap() { + return internal; + } + + public VecBuffer rewind() { + internal.rewind(); + + return this; + } + + public VecBuffer putFloatArray(float[] floats) { + + for (float f : floats) { + internal.putFloat(f); + } +// internal.asFloatBuffer().put(floats); +// internal.position(internal.position() + floats.length * 4); + + return this; + } + + public VecBuffer putByteArray(byte[] bytes) { + internal.put(bytes); + return this; + } + + /** + * Position this buffer relative to the 0-index in GPU memory. + * + * @return This buffer. + */ + public VecBuffer position(int p) { + internal.position(p); + return this; + } + + public VecBuffer putFloat(float f) { + internal.putFloat(f); + return this; + } + + public VecBuffer putInt(int i) { + internal.putInt(i); + return this; + } + + public VecBuffer put(byte b) { + internal.put(b); + return this; + } + + public VecBuffer put(ByteBuffer b) { + internal.put(b); + return this; + } + + public VecBuffer putVec4(float x, float y, float z, float w) { + internal.putFloat(x); + internal.putFloat(y); + internal.putFloat(z); + internal.putFloat(w); + return this; + } + + public VecBuffer putVec3(float x, float y, float z) { + internal.putFloat(x); + internal.putFloat(y); + internal.putFloat(z); + return this; + } + + public VecBuffer putVec2(float x, float y) { + internal.putFloat(x); + internal.putFloat(y); + return this; + } + + public VecBuffer putVec3(byte x, byte y, byte z) { + internal.put(x); + internal.put(y); + internal.put(z); + return this; + } + + public VecBuffer putVec2(byte x, byte y) { + internal.put(x); + internal.put(y); + return this; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/shader/GlProgram.java b/src/main/java/com/jozufozu/flywheel/backend/gl/shader/GlProgram.java new file mode 100644 index 000000000..e62ba84df --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/shader/GlProgram.java @@ -0,0 +1,77 @@ +package com.jozufozu.flywheel.backend.gl.shader; + +import static org.lwjgl.opengl.GL20.glDeleteProgram; +import static org.lwjgl.opengl.GL20.glGetUniformLocation; +import static org.lwjgl.opengl.GL20.glUniform1i; +import static org.lwjgl.opengl.GL20.glUniformMatrix4fv; +import static org.lwjgl.opengl.GL20.glUseProgram; + +import com.jozufozu.flywheel.backend.Backend; +import com.jozufozu.flywheel.backend.gl.GlObject; +import com.jozufozu.flywheel.backend.loading.Program; +import com.jozufozu.flywheel.util.RenderUtil; + +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.math.vector.Matrix4f; + +public abstract class GlProgram extends GlObject { + + public final ResourceLocation name; + + protected GlProgram(Program program) { + setHandle(program.program); + this.name = program.name; + } + + public void bind() { + glUseProgram(handle()); + } + + public void unbind() { + glUseProgram(0); + } + + /** + * Retrieves the index of the uniform with the given name. + * + * @param uniform The name of the uniform to find the index of + * @return The uniform's index + */ + public int getUniformLocation(String uniform) { + int index = glGetUniformLocation(this.handle(), uniform); + + if (index < 0) { + Backend.log.debug("No active uniform '{}' exists in program '{}'. Could be unused.", uniform, this.name); + } + + return index; + } + + /** + * Binds a sampler uniform to the given texture unit. + * + * @param name The name of the sampler uniform. + * @param binding The index of the texture unit. + * @return The sampler uniform's index. + * @throws NullPointerException If no uniform exists with the given name. + */ + public int setSamplerBinding(String name, int binding) { + int samplerUniform = getUniformLocation(name); + + if (samplerUniform >= 0) { + glUniform1i(samplerUniform, binding); + } + + return samplerUniform; + } + + protected static void uploadMatrixUniform(int uniform, Matrix4f mat) { + glUniformMatrix4fv(uniform, false, RenderUtil.writeMatrix(mat)); + } + + @Override + protected void deleteInternal(int handle) { + glDeleteProgram(handle); + } + +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/shader/GlShader.java b/src/main/java/com/jozufozu/flywheel/backend/gl/shader/GlShader.java new file mode 100644 index 000000000..09fbe99bb --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/shader/GlShader.java @@ -0,0 +1,46 @@ +package com.jozufozu.flywheel.backend.gl.shader; + +import org.lwjgl.opengl.GL20; + +import com.jozufozu.flywheel.backend.Backend; +import com.jozufozu.flywheel.backend.gl.GlObject; +import com.jozufozu.flywheel.backend.gl.versioned.GlCompat; +import com.jozufozu.flywheel.backend.loading.Shader; + +import net.minecraft.util.ResourceLocation; + +public class GlShader extends GlObject { + + public final ResourceLocation name; + public final ShaderType type; + + public GlShader(Shader shader) { + this(shader.type, shader.name, shader.getSource()); + } + + public GlShader(ShaderType type, ResourceLocation name, String source) { + this.type = type; + this.name = name; + int handle = GL20.glCreateShader(type.glEnum); + + GlCompat.safeShaderSource(handle, source); + GL20.glCompileShader(handle); + + String log = GL20.glGetShaderInfoLog(handle); + + if (!log.isEmpty()) { + Backend.log.error("Shader compilation log for " + name + ": " + log); + } + + if (GL20.glGetShaderi(handle, GL20.GL_COMPILE_STATUS) != GL20.GL_TRUE) { + throw new RuntimeException("Could not compile " + name + ". See log for details."); + } + + setHandle(handle); + } + + @Override + protected void deleteInternal(int handle) { + GL20.glDeleteShader(handle); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/shader/ShaderType.java b/src/main/java/com/jozufozu/flywheel/backend/gl/shader/ShaderType.java new file mode 100644 index 000000000..2ac5a3721 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/shader/ShaderType.java @@ -0,0 +1,17 @@ +package com.jozufozu.flywheel.backend.gl.shader; + +import org.lwjgl.opengl.GL20; + +public enum ShaderType { + VERTEX("vertex", GL20.GL_VERTEX_SHADER), + FRAGMENT("fragment", GL20.GL_FRAGMENT_SHADER), + ; + + public final String name; + public final int glEnum; + + ShaderType(String name, int glEnum) { + this.name = name; + this.glEnum = glEnum; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/GlCompat.java b/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/GlCompat.java new file mode 100644 index 000000000..438fbb734 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/GlCompat.java @@ -0,0 +1,113 @@ +package com.jozufozu.flywheel.backend.gl.versioned; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +import org.lwjgl.PointerBuffer; +import org.lwjgl.opengl.GL20C; +import org.lwjgl.opengl.GLCapabilities; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; + +import com.jozufozu.flywheel.backend.gl.versioned.framebuffer.Blit; +import com.jozufozu.flywheel.backend.gl.versioned.framebuffer.Framebuffer; +import com.jozufozu.flywheel.backend.gl.versioned.instancing.DrawInstanced; +import com.jozufozu.flywheel.backend.gl.versioned.instancing.InstancedArrays; +import com.jozufozu.flywheel.backend.gl.versioned.instancing.VertexArrayObject; + +/** + * An instance of this class stores information about what OpenGL features are available. + *
+ * Each field stores an enum variant that provides access to the most appropriate version of a feature for the current + * system. + */ +public class GlCompat { + public final MapBufferRange mapBufferRange; + + public final VertexArrayObject vao; + public final InstancedArrays instancedArrays; + public final DrawInstanced drawInstanced; + public final Blit blit; + public final Framebuffer fbo; + + public final RGPixelFormat pixelFormat; + + public GlCompat(GLCapabilities caps) { + mapBufferRange = getLatest(MapBufferRange.class, caps); + + vao = getLatest(VertexArrayObject.class, caps); + instancedArrays = getLatest(InstancedArrays.class, caps); + drawInstanced = getLatest(DrawInstanced.class, caps); + blit = getLatest(Blit.class, caps); + fbo = getLatest(Framebuffer.class, caps); + + pixelFormat = getLatest(RGPixelFormat.class, caps); + } + + public boolean vertexArrayObjectsSupported() { + return vao != VertexArrayObject.UNSUPPORTED; + } + + public boolean instancedArraysSupported() { + return instancedArrays != InstancedArrays.UNSUPPORTED; + } + + public boolean drawInstancedSupported() { + return drawInstanced != DrawInstanced.UNSUPPORTED; + } + + public boolean fbosSupported() { + return fbo != Framebuffer.UNSUPPORTED; + } + + public boolean blitSupported() { + return blit != Blit.UNSUPPORTED; + } + + /** + * Get the most compatible version of a specific OpenGL feature by iterating over enum constants in order. + * + * @param clazz The class of the versioning enum. + * @param caps The current system's supported features. + * @param The type of the versioning enum. + * @return The first defined enum variant to return true. + */ + public static & GlVersioned> V getLatest(Class clazz, GLCapabilities caps) { + V[] constants = clazz.getEnumConstants(); + V last = constants[constants.length - 1]; + if (!last.supported(caps)) { + throw new IllegalStateException(""); + } + + return Arrays.stream(constants).filter(it -> it.supported(caps)).findFirst().get(); + } + + /** + * Copied from: + *
https://github.com/grondag/canvas/commit/820bf754092ccaf8d0c169620c2ff575722d7d96 + * + *

Identical in function to {@link GL20C#glShaderSource(int, CharSequence)} but + * passes a null pointer for string length to force the driver to rely on the null + * terminator for string length. This is a workaround for an apparent flaw with some + * AMD drivers that don't receive or interpret the length correctly, resulting in + * an access violation when the driver tries to read past the string memory. + * + *

Hat tip to fewizz for the find and the fix. + */ + public static void safeShaderSource(int glId, CharSequence source) { + final MemoryStack stack = MemoryStack.stackGet(); + final int stackPointer = stack.getPointer(); + + try { + final ByteBuffer sourceBuffer = MemoryUtil.memUTF8(source, true); + final PointerBuffer pointers = stack.mallocPointer(1); + pointers.put(sourceBuffer); + + GL20C.nglShaderSource(glId, 1, pointers.address0(), 0); + org.lwjgl.system.APIUtil.apiArrayFree(pointers.address0(), 1); + } finally { + stack.setPointer(stackPointer); + } + } +} + diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/GlVersioned.java b/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/GlVersioned.java new file mode 100644 index 000000000..d019832b7 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/GlVersioned.java @@ -0,0 +1,17 @@ +package com.jozufozu.flywheel.backend.gl.versioned; + +import org.lwjgl.opengl.GLCapabilities; + +/** + * This interface should be implemented by enums such that the + * last defined variant always returns true. + */ +public interface GlVersioned { + /** + * Queries whether this variant is supported by the current system. + * + * @param caps The {@link GLCapabilities} reported by the current system. + * @return true if this variant is supported, or if this is the last defined variant. + */ + boolean supported(GLCapabilities caps); +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/MapBufferRange.java b/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/MapBufferRange.java new file mode 100644 index 000000000..85f75bc2c --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/MapBufferRange.java @@ -0,0 +1,49 @@ +package com.jozufozu.flywheel.backend.gl.versioned; + +import java.nio.ByteBuffer; + +import org.lwjgl.opengl.ARBMapBufferRange; +import org.lwjgl.opengl.GL30; +import org.lwjgl.opengl.GLCapabilities; + +import com.jozufozu.flywheel.backend.gl.buffer.GlBufferType; + +public enum MapBufferRange implements GlVersioned { + + GL30_RANGE { + @Override + public boolean supported(GLCapabilities caps) { + return caps.OpenGL30; + } + + @Override + public ByteBuffer mapBuffer(GlBufferType target, long offset, long length, int access) { + return GL30.glMapBufferRange(target.glEnum, offset, length, access); + } + }, + ARB_RANGE { + @Override + public boolean supported(GLCapabilities caps) { + return caps.GL_ARB_map_buffer_range; + } + + @Override + public ByteBuffer mapBuffer(GlBufferType target, long offset, long length, int access) { + return ARBMapBufferRange.glMapBufferRange(target.glEnum, offset, length, access); + } + }, + UNSUPPORTED { + @Override + public boolean supported(GLCapabilities caps) { + return true; + } + + @Override + public ByteBuffer mapBuffer(GlBufferType target, long offset, long length, int access) { + throw new UnsupportedOperationException("glMapBuffer not supported"); + } + }; + + + public abstract ByteBuffer mapBuffer(GlBufferType target, long offset, long length, int access); +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/RGPixelFormat.java b/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/RGPixelFormat.java new file mode 100644 index 000000000..339289894 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/RGPixelFormat.java @@ -0,0 +1,77 @@ +package com.jozufozu.flywheel.backend.gl.versioned; + +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL30; +import org.lwjgl.opengl.GLCapabilities; + +public enum RGPixelFormat implements GlVersioned { + GL30_RG { + @Override + public boolean supported(GLCapabilities caps) { + return caps.OpenGL30; + } + + @Override + public int internalFormat() { + return GL30.GL_RG8; + } + + @Override + public int format() { + return GL30.GL_RG; + } + + @Override + public int byteCount() { + return 2; + } + }, + GL11_RGB { + @Override + public boolean supported(GLCapabilities caps) { + return caps.OpenGL11; + } + + @Override + public int internalFormat() { + return GL11.GL_RGB8; + } + + @Override + public int format() { + return GL11.GL_RGB; + } + + @Override + public int byteCount() { + return 3; + } + }, + UNSUPPORTED { + @Override + public boolean supported(GLCapabilities caps) { + return true; + } + + @Override + public int internalFormat() { + throw new UnsupportedOperationException(); + } + + @Override + public int format() { + throw new UnsupportedOperationException(); + } + + @Override + public int byteCount() { + throw new UnsupportedOperationException(); + } + }; + + public abstract int internalFormat(); + + public abstract int format(); + + public abstract int byteCount(); +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/framebuffer/Blit.java b/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/framebuffer/Blit.java new file mode 100644 index 000000000..fdf5473d1 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/framebuffer/Blit.java @@ -0,0 +1,45 @@ +package com.jozufozu.flywheel.backend.gl.versioned.framebuffer; + +import org.lwjgl.opengl.EXTFramebufferBlit; +import org.lwjgl.opengl.GL30; +import org.lwjgl.opengl.GLCapabilities; + +import com.jozufozu.flywheel.backend.gl.versioned.GlVersioned; + +public enum Blit implements GlVersioned { + CORE { + @Override + public boolean supported(GLCapabilities caps) { + return caps.OpenGL30; + } + + @Override + public void blitFramebuffer(int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, int mask, int filter) { + GL30.glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); + } + }, + EXT { + @Override + public boolean supported(GLCapabilities caps) { + return caps.GL_EXT_framebuffer_blit; + } + + @Override + public void blitFramebuffer(int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, int mask, int filter) { + EXTFramebufferBlit.glBlitFramebufferEXT(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); + } + }, + UNSUPPORTED { + @Override + public boolean supported(GLCapabilities caps) { + return true; + } + + @Override + public void blitFramebuffer(int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, int mask, int filter) { + throw new UnsupportedOperationException("Framebuffer blitting not supported."); + } + }; + + public abstract void blitFramebuffer(int srcX0, int srcY0, int srcX1, int srcY1, int dstX0, int dstY0, int dstX1, int dstY1, int mask, int filter); +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/framebuffer/Framebuffer.java b/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/framebuffer/Framebuffer.java new file mode 100644 index 000000000..a0e2b3e1b --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/framebuffer/Framebuffer.java @@ -0,0 +1,57 @@ +package com.jozufozu.flywheel.backend.gl.versioned.framebuffer; + +import org.lwjgl.opengl.ARBFramebufferObject; +import org.lwjgl.opengl.EXTFramebufferObject; +import org.lwjgl.opengl.GL30C; +import org.lwjgl.opengl.GLCapabilities; + +import com.jozufozu.flywheel.backend.gl.versioned.GlVersioned; + +public enum Framebuffer implements GlVersioned { + CORE { + @Override + public boolean supported(GLCapabilities caps) { + return caps.OpenGL30; + } + + @Override + public void bindFramebuffer(int target, int framebuffer) { + GL30C.glBindFramebuffer(target, framebuffer); + } + }, + ARB { + @Override + public boolean supported(GLCapabilities caps) { + return caps.GL_ARB_framebuffer_object; + } + + @Override + public void bindFramebuffer(int target, int framebuffer) { + ARBFramebufferObject.glBindFramebuffer(target, framebuffer); + } + }, + EXT { + @Override + public boolean supported(GLCapabilities caps) { + return caps.GL_EXT_framebuffer_object; + } + + @Override + public void bindFramebuffer(int target, int framebuffer) { + EXTFramebufferObject.glBindFramebufferEXT(target, framebuffer); + } + }, + UNSUPPORTED { + @Override + public boolean supported(GLCapabilities caps) { + return true; + } + + @Override + public void bindFramebuffer(int target, int framebuffer) { + throw new UnsupportedOperationException("Framebuffers not supported"); + } + }; + + public abstract void bindFramebuffer(int target, int framebuffer); +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/instancing/DrawInstanced.java b/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/instancing/DrawInstanced.java new file mode 100644 index 000000000..c2d73e0c4 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/instancing/DrawInstanced.java @@ -0,0 +1,76 @@ +package com.jozufozu.flywheel.backend.gl.versioned.instancing; + +import org.lwjgl.opengl.ARBDrawInstanced; +import org.lwjgl.opengl.EXTDrawInstanced; +import org.lwjgl.opengl.GL31; +import org.lwjgl.opengl.GLCapabilities; + +import com.jozufozu.flywheel.backend.gl.GlNumericType; +import com.jozufozu.flywheel.backend.gl.GlPrimitive; +import com.jozufozu.flywheel.backend.gl.versioned.GlVersioned; + +public enum DrawInstanced implements GlVersioned { + GL31_DRAW_INSTANCED { + @Override + public boolean supported(GLCapabilities caps) { + return caps.OpenGL31; + } + + @Override + public void drawArraysInstanced(GlPrimitive mode, int first, int count, int primcount) { + GL31.glDrawArraysInstanced(mode.glEnum, first, count, primcount); + } + + @Override + public void drawElementsInstanced(GlPrimitive mode, int elementCount, GlNumericType type, long indices, int primcount) { + GL31.glDrawElementsInstanced(mode.glEnum, elementCount, type.getGlEnum(), indices, primcount); + } + }, + ARB_DRAW_INSTANCED { + @Override + public boolean supported(GLCapabilities caps) { + return caps.GL_ARB_draw_instanced; + } + + @Override + public void drawArraysInstanced(GlPrimitive mode, int first, int count, int primcount) { + ARBDrawInstanced.glDrawArraysInstancedARB(mode.glEnum, first, count, primcount); + } + + @Override + public void drawElementsInstanced(GlPrimitive mode, int elementCount, GlNumericType type, long indices, int primcount) { + ARBDrawInstanced.glDrawElementsInstancedARB(mode.glEnum, elementCount, type.getGlEnum(), indices, primcount); + } + }, + EXT_DRAW_INSTANCED { + @Override + public boolean supported(GLCapabilities caps) { + return caps.GL_EXT_draw_instanced; + } + + @Override + public void drawArraysInstanced(GlPrimitive mode, int first, int count, int primcount) { + EXTDrawInstanced.glDrawArraysInstancedEXT(mode.glEnum, first, count, primcount); + } + + @Override + public void drawElementsInstanced(GlPrimitive mode, int elementCount, GlNumericType type, long indices, int primcount) { + EXTDrawInstanced.glDrawElementsInstancedEXT(mode.glEnum, elementCount, type.getGlEnum(), indices, primcount); + } + }, + UNSUPPORTED { + @Override + public boolean supported(GLCapabilities caps) { + return true; + } + }; + + + public void drawArraysInstanced(GlPrimitive mode, int first, int count, int primcount) { + throw new UnsupportedOperationException(); + } + + public void drawElementsInstanced(GlPrimitive mode, int elementCount, GlNumericType type, long indices, int primcount) { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/instancing/InstancedArrays.java b/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/instancing/InstancedArrays.java new file mode 100644 index 000000000..2c681d41d --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/instancing/InstancedArrays.java @@ -0,0 +1,45 @@ +package com.jozufozu.flywheel.backend.gl.versioned.instancing; + +import org.lwjgl.opengl.ARBInstancedArrays; +import org.lwjgl.opengl.GL33; +import org.lwjgl.opengl.GLCapabilities; + +import com.jozufozu.flywheel.backend.gl.versioned.GlVersioned; + +public enum InstancedArrays implements GlVersioned { + GL33_INSTANCED_ARRAYS { + @Override + public boolean supported(GLCapabilities caps) { + return caps.OpenGL33; + } + + @Override + public void vertexAttribDivisor(int index, int divisor) { + GL33.glVertexAttribDivisor(index, divisor); + } + }, + ARB_INSTANCED_ARRAYS { + @Override + public boolean supported(GLCapabilities caps) { + return caps.GL_ARB_instanced_arrays; + } + + @Override + public void vertexAttribDivisor(int index, int divisor) { + ARBInstancedArrays.glVertexAttribDivisorARB(index, divisor); + } + }, + UNSUPPORTED { + @Override + public boolean supported(GLCapabilities caps) { + return true; + } + + @Override + public void vertexAttribDivisor(int index, int divisor) { + throw new UnsupportedOperationException(); + } + }; + + public abstract void vertexAttribDivisor(int index, int divisor); +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/instancing/VertexArrayObject.java b/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/instancing/VertexArrayObject.java new file mode 100644 index 000000000..4a2d13c11 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/gl/versioned/instancing/VertexArrayObject.java @@ -0,0 +1,79 @@ +package com.jozufozu.flywheel.backend.gl.versioned.instancing; + +import org.lwjgl.opengl.ARBVertexArrayObject; +import org.lwjgl.opengl.GL30; +import org.lwjgl.opengl.GLCapabilities; + +import com.jozufozu.flywheel.backend.gl.versioned.GlVersioned; + +public enum VertexArrayObject implements GlVersioned { + GL30_VAO { + @Override + public boolean supported(GLCapabilities caps) { + return caps.OpenGL30; + } + + @Override + public int genVertexArrays() { + return GL30.glGenVertexArrays(); + } + + @Override + public void bindVertexArray(int array) { + GL30.glBindVertexArray(array); + } + + @Override + public void deleteVertexArrays(int array) { + GL30.glDeleteVertexArrays(array); + } + }, + ARB_VAO { + @Override + public boolean supported(GLCapabilities caps) { + return caps.GL_ARB_vertex_array_object; + } + + @Override + public int genVertexArrays() { + return ARBVertexArrayObject.glGenVertexArrays(); + } + + @Override + public void bindVertexArray(int array) { + ARBVertexArrayObject.glBindVertexArray(array); + } + + @Override + public void deleteVertexArrays(int array) { + ARBVertexArrayObject.glDeleteVertexArrays(array); + } + }, + UNSUPPORTED { + @Override + public boolean supported(GLCapabilities caps) { + return true; + } + + @Override + public int genVertexArrays() { + throw new UnsupportedOperationException(); + } + + @Override + public void bindVertexArray(int array) { + throw new UnsupportedOperationException(); + } + + @Override + public void deleteVertexArrays(int array) { + throw new UnsupportedOperationException(); + } + }; + + public abstract int genVertexArrays(); + + public abstract void bindVertexArray(int array); + + public abstract void deleteVertexArrays(int array); +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/IDynamicInstance.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/IDynamicInstance.java new file mode 100644 index 000000000..fd17da753 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/IDynamicInstance.java @@ -0,0 +1,32 @@ +package com.jozufozu.flywheel.backend.instancing; + +import com.jozufozu.flywheel.backend.instancing.tile.TileEntityInstance; + +/** + * An interface giving {@link TileEntityInstance}s a hook to have a function called at + * the start of a frame. By implementing {@link IDynamicInstance}, a {@link TileEntityInstance} + * can animate its models in ways that could not be easily achieved by shader attribute + * parameterization. + * + *

If your goal is offloading work to shaders, but you're unsure exactly how you need + * to parameterize the instances, you're encouraged to implement this for prototyping. + */ +public interface IDynamicInstance extends IInstance { + /** + * Called every frame. + */ + void beginFrame(); + + /** + * As a further optimization, dynamic instances that are far away are ticked less often. + * This behavior can be disabled by returning false. + * + *
You might want to opt out of this if you want your animations to remain smooth + * even when far away from the camera. It is recommended to keep this as is, however. + * + * @return true if your instance should be slow ticked. + */ + default boolean decreaseFramerateWithDistance() { + return true; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/IInstance.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/IInstance.java new file mode 100644 index 000000000..fa424ffa6 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/IInstance.java @@ -0,0 +1,22 @@ +package com.jozufozu.flywheel.backend.instancing; + +import com.jozufozu.flywheel.backend.instancing.tile.TileInstanceManager; + +import net.minecraft.util.math.BlockPos; + +/** + * A general interface providing information about any type of thing that could use Flywheel's instanced rendering. + * Right now, that's only {@link TileInstanceManager}, but there could be an entity equivalent in the future. + */ +public interface IInstance { + + BlockPos getWorldPosition(); + + void updateLight(); + + void remove(); + + boolean shouldReset(); + + void update(); +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/IInstanceFactory.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/IInstanceFactory.java new file mode 100644 index 000000000..f2364392c --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/IInstanceFactory.java @@ -0,0 +1,5 @@ +package com.jozufozu.flywheel.backend.instancing; + +public interface IInstanceFactory { + D create(Instancer owner); +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/IInstanceRendered.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/IInstanceRendered.java new file mode 100644 index 000000000..71d9a13af --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/IInstanceRendered.java @@ -0,0 +1,18 @@ +package com.jozufozu.flywheel.backend.instancing; + +import net.minecraft.world.World; + +/** + * Something (a TileEntity or Entity) that can be rendered using the instancing API. + */ +public interface IInstanceRendered { + + /** + * @return true if there are parts of the renderer that cannot be implemented with Flywheel. + */ + default boolean shouldRenderNormally() { + return false; + } + + World getWorld(); +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/ITickableInstance.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/ITickableInstance.java new file mode 100644 index 000000000..4c3265d0c --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/ITickableInstance.java @@ -0,0 +1,40 @@ +package com.jozufozu.flywheel.backend.instancing; + +import com.jozufozu.flywheel.backend.instancing.tile.TileEntityInstance; + +/** + * An interface giving {@link TileEntityInstance}s a hook to have a function called at + * the end of every tick. By implementing {@link ITickableInstance}, a {@link TileEntityInstance} + * can update frequently, but not every frame. + *
There are a few cases in which this should be considered over {@link IDynamicInstance}: + *

    + *
  • + * You'd like to change something about the instance every now and then. + * eg. adding or removing parts, snapping to a different rotation, etc. + *
  • + *
  • + * Your TileEntity does animate, but the animation doesn't have + * to be smooth, in which case this could be an optimization. + *
  • + *
+ */ +public interface ITickableInstance extends IInstance { + + /** + * Called every tick. + */ + void tick(); + + /** + * As a further optimization, tickable instances that are far away are ticked less often. + * This behavior can be disabled by returning false. + * + *
You might want to opt out of this if you want your animations to remain smooth + * even when far away from the camera. It is recommended to keep this as is, however. + * + * @return true if your instance should be slow ticked. + */ + default boolean decreaseTickRateWithDistance() { + return true; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/InstanceData.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/InstanceData.java new file mode 100644 index 000000000..cfd7b5c8f --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/InstanceData.java @@ -0,0 +1,28 @@ +package com.jozufozu.flywheel.backend.instancing; + +import com.jozufozu.flywheel.backend.gl.buffer.MappedBuffer; + +public abstract class InstanceData { + + protected final Instancer owner; + + boolean dirty; + boolean removed; + + protected InstanceData(Instancer owner) { + this.owner = owner; + } + + public abstract void write(MappedBuffer buf); + + public void markDirty() { + owner.anyToUpdate = true; + dirty = true; + } + + public void delete() { + owner.anyToRemove = true; + removed = true; + } + +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/InstanceManager.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/InstanceManager.java new file mode 100644 index 000000000..250880e61 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/InstanceManager.java @@ -0,0 +1,245 @@ +package com.jozufozu.flywheel.backend.instancing; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.annotation.Nullable; + +import com.jozufozu.flywheel.backend.Backend; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.client.renderer.ActiveRenderInfo; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.vector.Vector3f; + +public abstract class InstanceManager implements MaterialManager.OriginShiftListener { + + public final MaterialManager materialManager; + + protected final ArrayList queuedAdditions; + protected final ConcurrentHashMap.KeySetView queuedUpdates; + + protected final Map instances; + protected final Object2ObjectOpenHashMap tickableInstances; + protected final Object2ObjectOpenHashMap dynamicInstances; + + protected int frame; + protected int tick; + + public InstanceManager(MaterialManager materialManager) { + this.materialManager = materialManager; + this.queuedUpdates = ConcurrentHashMap.newKeySet(64); + this.queuedAdditions = new ArrayList<>(64); + this.instances = new HashMap<>(); + + this.dynamicInstances = new Object2ObjectOpenHashMap<>(); + this.tickableInstances = new Object2ObjectOpenHashMap<>(); + + materialManager.onOriginShift(this); + } + + public void tick(double cameraX, double cameraY, double cameraZ) { + tick++; + + // integer camera pos + int cX = (int) cameraX; + int cY = (int) cameraY; + int cZ = (int) cameraZ; + + if (tickableInstances.size() > 0) { + for (ITickableInstance instance : tickableInstances.values()) { + if (!instance.decreaseTickRateWithDistance()) { + instance.tick(); + continue; + } + + BlockPos pos = instance.getWorldPosition(); + + int dX = pos.getX() - cX; + int dY = pos.getY() - cY; + int dZ = pos.getZ() - cZ; + + if ((tick % getUpdateDivisor(dX, dY, dZ)) == 0) + instance.tick(); + } + } + + queuedUpdates.forEach(te -> { + queuedUpdates.remove(te); + + update(te); + }); + } + + public void beginFrame(ActiveRenderInfo info) { + frame++; + processQueuedAdditions(); + + Vector3f look = info.getHorizontalPlane(); + float lookX = look.getX(); + float lookY = look.getY(); + float lookZ = look.getZ(); + + // integer camera pos + int cX = (int) info.getProjectedView().x; + int cY = (int) info.getProjectedView().y; + int cZ = (int) info.getProjectedView().z; + + if (dynamicInstances.size() > 0) { + dynamicInstances.object2ObjectEntrySet().fastForEach(e -> { + IDynamicInstance dyn = e.getValue(); + if (!dyn.decreaseFramerateWithDistance() || shouldFrameUpdate(dyn.getWorldPosition(), lookX, lookY, lookZ, cX, cY, cZ)) + dyn.beginFrame(); + }); + } + } + + public void add(T obj) { + if (!Backend.getInstance().canUseInstancing()) return; + + if (obj instanceof IInstanceRendered) { + addInternal(obj); + } + } + + public synchronized void queueAdd(T obj) { + if (!Backend.getInstance().canUseInstancing()) return; + + queuedAdditions.add(obj); + } + + public void update(T obj) { + if (!Backend.getInstance().canUseInstancing()) return; + + if (obj instanceof IInstanceRendered) { + IInstance instance = getInstance(obj, false); + + if (instance != null) { + + if (instance.shouldReset()) { + removeInternal(obj, instance); + + createInternal(obj); + } else { + instance.update(); + } + } + } + } + + public synchronized void queueUpdate(T obj) { + if (!Backend.getInstance().canUseInstancing()) return; + + queuedUpdates.add(obj); + } + + public void onLightUpdate(T obj) { + if (!Backend.getInstance().canUseInstancing()) return; + + if (obj instanceof IInstanceRendered) { + IInstance instance = getInstance(obj, false); + + if (instance != null) + instance.updateLight(); + } + } + + public void remove(T obj) { + if (!Backend.getInstance().canUseInstancing()) return; + + if (obj instanceof IInstanceRendered) { + IInstance instance = getInstance(obj, false); + if (instance != null) + removeInternal(obj, instance); + } + } + + public void invalidate() { + instances.clear(); + dynamicInstances.clear(); + tickableInstances.clear(); + } + + @SuppressWarnings("unchecked") + @Nullable + protected IInstance getInstance(I obj, boolean create) { + if (!Backend.getInstance().canUseInstancing()) return null; + + IInstance instance = instances.get(obj); + + if (instance != null) { + return instance; + } else if (create && canCreateInstance(obj)) { + return createInternal(obj); + } else { + return null; + } + } + + protected synchronized void processQueuedAdditions() { + if (queuedAdditions.size() > 0) { + queuedAdditions.forEach(this::addInternal); + queuedAdditions.clear(); + } + } + + protected boolean shouldFrameUpdate(BlockPos worldPos, float lookX, float lookY, float lookZ, int cX, int cY, int cZ) { + int dX = worldPos.getX() - cX; + int dY = worldPos.getY() - cY; + int dZ = worldPos.getZ() - cZ; + + // is it more than 2 blocks behind the camera? + int dist = 2; + float dot = (dX + lookX * dist) * lookX + (dY + lookY * dist) * lookY + (dZ + lookZ * dist) * lookZ; + if (dot < 0) return false; + + return (frame % getUpdateDivisor(dX, dY, dZ)) == 0; + } + + protected int getUpdateDivisor(int dX, int dY, int dZ) { + int dSq = dX * dX + dY * dY + dZ * dZ; + + return (dSq / 1024) + 1; + } + + protected void addInternal(T tile) { + getInstance(tile, true); + } + + protected void removeInternal(T obj, IInstance instance) { + instance.remove(); + instances.remove(obj); + dynamicInstances.remove(obj); + tickableInstances.remove(obj); + } + + protected IInstance createInternal(T obj) { + IInstance renderer = createRaw(obj); + + if (renderer != null) { + renderer.updateLight(); + instances.put(obj, renderer); + + if (renderer instanceof IDynamicInstance) + dynamicInstances.put(obj, (IDynamicInstance) renderer); + + if (renderer instanceof ITickableInstance) + tickableInstances.put(obj, ((ITickableInstance) renderer)); + } + + return renderer; + } + + @Override + public void onOriginShift() { + ArrayList instancedTiles = new ArrayList<>(instances.keySet()); + invalidate(); + instancedTiles.forEach(this::add); + } + + protected abstract IInstance createRaw(T obj); + + protected abstract boolean canCreateInstance(T entity); +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/InstanceMaterial.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/InstanceMaterial.java new file mode 100644 index 000000000..6f1a1a63e --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/InstanceMaterial.java @@ -0,0 +1,170 @@ +package com.jozufozu.flywheel.backend.instancing; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.apache.commons.lang3.tuple.Pair; +import org.lwjgl.opengl.GL11; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.jozufozu.flywheel.backend.RenderWork; +import com.jozufozu.flywheel.backend.gl.attrib.VertexFormat; +import com.jozufozu.flywheel.backend.model.BufferedModel; +import com.jozufozu.flywheel.backend.model.IndexedModel; +import com.jozufozu.flywheel.core.PartialModel; +import com.jozufozu.flywheel.util.BufferBuilderReader; +import com.jozufozu.flywheel.util.RenderUtil; +import com.jozufozu.flywheel.util.VirtualEmptyModelData; +import com.mojang.blaze3d.matrix.MatrixStack; + +import net.minecraft.block.BlockState; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.BlockModelRenderer; +import net.minecraft.client.renderer.BlockRendererDispatcher; +import net.minecraft.client.renderer.BufferBuilder; +import net.minecraft.client.renderer.model.IBakedModel; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.client.renderer.vertex.DefaultVertexFormats; +import net.minecraft.util.Direction; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.vector.Vector3i; + +public class InstanceMaterial { + + protected final Supplier originCoordinate; + protected final Cache> models; + protected final MaterialSpec spec; + private final VertexFormat modelFormat; + + public InstanceMaterial(Supplier renderer, MaterialSpec spec) { + this.originCoordinate = renderer; + this.spec = spec; + + this.models = CacheBuilder.newBuilder() + .removalListener(notification -> { + Instancer instancer = (Instancer) notification.getValue(); + RenderWork.enqueue(instancer::delete); + }) + .build(); + modelFormat = this.spec.getModelFormat(); + } + + public boolean nothingToRender() { + return models.size() > 0 && models.asMap().values().stream().allMatch(Instancer::empty); + } + + public void delete() { + models.invalidateAll(); + } + + /** + * Clear all instance data without freeing resources. + */ + public void clear() { + models.asMap().values().forEach(Instancer::clear); + } + + public void forEachInstancer(Consumer> f) { + for (Instancer model : models.asMap().values()) { + f.accept(model); + } + } + + public Instancer getModel(PartialModel partial, BlockState referenceState) { + return get(partial, () -> buildModel(partial.get(), referenceState)); + } + + public Instancer getModel(PartialModel partial, BlockState referenceState, Direction dir) { + return getModel(partial, referenceState, dir, RenderUtil.rotateToFace(dir)); + } + + public Instancer getModel(PartialModel partial, BlockState referenceState, Direction dir, Supplier modelTransform) { + return get(Pair.of(dir, partial), + () -> buildModel(partial.get(), referenceState, modelTransform.get())); + } + + public Instancer getModel(BlockState toRender) { + return get(toRender, () -> buildModel(toRender)); + } + + public Instancer get(Object key, Supplier supplier) { + try { + return models.get(key, () -> new Instancer<>(supplier.get(), originCoordinate, spec)); + } catch (ExecutionException e) { + e.printStackTrace(); + return null; + } + } + + private BufferedModel buildModel(BlockState renderedState) { + BlockRendererDispatcher dispatcher = Minecraft.getInstance().getBlockRendererDispatcher(); + return buildModel(dispatcher.getModelForState(renderedState), renderedState); + } + + private BufferedModel buildModel(IBakedModel model, BlockState renderedState) { + return buildModel(model, renderedState, new MatrixStack()); + } + + private BufferedModel buildModel(IBakedModel model, BlockState referenceState, MatrixStack ms) { + BufferBuilderReader reader = new BufferBuilderReader(getBufferBuilder(model, referenceState, ms)); + + int vertexCount = reader.getVertexCount(); + + ByteBuffer vertices = ByteBuffer.allocate(vertexCount * modelFormat.getStride()); + vertices.order(ByteOrder.nativeOrder()); + + for (int i = 0; i < vertexCount; i++) { + vertices.putFloat(reader.getX(i)); + vertices.putFloat(reader.getY(i)); + vertices.putFloat(reader.getZ(i)); + + vertices.put(reader.getNX(i)); + vertices.put(reader.getNY(i)); + vertices.put(reader.getNZ(i)); + + vertices.putFloat(reader.getU(i)); + vertices.putFloat(reader.getV(i)); + } + + vertices.rewind(); + + // return new BufferedModel(GlPrimitive.QUADS, format, vertices, vertexCount); + + return IndexedModel.fromSequentialQuads(modelFormat, vertices, vertexCount); + } + + // DOWN, UP, NORTH, SOUTH, WEST, EAST, null + private static final Direction[] dirs; + + static { + Direction[] directions = Direction.values(); + + dirs = Arrays.copyOf(directions, directions.length + 1); + } + + public static BufferBuilder getBufferBuilder(IBakedModel model, BlockState referenceState, MatrixStack ms) { + Minecraft mc = Minecraft.getInstance(); + BlockRendererDispatcher dispatcher = mc.getBlockRendererDispatcher(); + BlockModelRenderer blockRenderer = dispatcher.getBlockModelRenderer(); + BufferBuilder builder = new BufferBuilder(512); + +// BakedQuadWrapper quadReader = new BakedQuadWrapper(); +// +// IModelData modelData = model.getModelData(mc.world, BlockPos.ZERO.up(255), referenceState, VirtualEmptyModelData.INSTANCE); +// List quads = Arrays.stream(dirs) +// .flatMap(dir -> model.getQuads(referenceState, dir, mc.world.rand, modelData).stream()) +// .collect(Collectors.toList()); + + builder.begin(GL11.GL_QUADS, DefaultVertexFormats.BLOCK); + blockRenderer.renderModel(mc.world, model, referenceState, BlockPos.ZERO.up(255), ms, builder, true, + mc.world.rand, 42, OverlayTexture.DEFAULT_UV, VirtualEmptyModelData.INSTANCE); + builder.finishDrawing(); + return builder; + } + +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/InstancedRenderDispatcher.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/InstancedRenderDispatcher.java new file mode 100644 index 000000000..d07779765 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/InstancedRenderDispatcher.java @@ -0,0 +1,180 @@ +package com.jozufozu.flywheel.backend.instancing; + +import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D; +import static org.lwjgl.opengl.GL11.glBindTexture; +import static org.lwjgl.opengl.GL13.GL_TEXTURE0; +import static org.lwjgl.opengl.GL13.GL_TEXTURE4; +import static org.lwjgl.opengl.GL13.glActiveTexture; + +import java.util.BitSet; +import java.util.SortedSet; +import java.util.Vector; + +import javax.annotation.Nonnull; + +import com.jozufozu.flywheel.backend.Backend; +import com.jozufozu.flywheel.backend.instancing.entity.EntityInstanceManager; +import com.jozufozu.flywheel.backend.instancing.tile.TileInstanceManager; +import com.jozufozu.flywheel.core.Contexts; +import com.jozufozu.flywheel.core.CrumblingInstanceManager; +import com.jozufozu.flywheel.event.BeginFrameEvent; +import com.jozufozu.flywheel.event.ReloadRenderersEvent; +import com.jozufozu.flywheel.event.RenderLayerEvent; +import com.jozufozu.flywheel.util.WorldAttached; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.ActiveRenderInfo; +import net.minecraft.client.renderer.DestroyBlockProgress; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.WorldRenderer; +import net.minecraft.client.renderer.model.ModelBakery; +import net.minecraft.client.renderer.texture.Texture; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.Entity; +import net.minecraft.inventory.container.PlayerContainer; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.LazyValue; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.vector.Matrix4f; +import net.minecraft.world.IWorld; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +@Mod.EventBusSubscriber(value = Dist.CLIENT) +public class InstancedRenderDispatcher { + + private static final WorldAttached entityInstanceManager = new WorldAttached<>(world -> new EntityInstanceManager(Contexts.WORLD.getMaterialManager(world))); + private static final WorldAttached tileInstanceManager = new WorldAttached<>(world -> new TileInstanceManager(Contexts.WORLD.getMaterialManager(world))); + + private static final LazyValue> blockBreaking = new LazyValue<>(() -> { + Vector renderers = new Vector<>(10); + for (int i = 0; i < 10; i++) { + renderers.add(new CrumblingInstanceManager()); + } + return renderers; + }); + + @Nonnull + public static TileInstanceManager getTiles(IWorld world) { + return tileInstanceManager.get(world); + } + + @Nonnull + public static EntityInstanceManager getEntities(IWorld world) { + return entityInstanceManager.get(world); + } + + public static void tick() { + Minecraft mc = Minecraft.getInstance(); + ClientWorld world = mc.world; + + Entity renderViewEntity = mc.renderViewEntity != null ? mc.renderViewEntity : mc.player; + + if (renderViewEntity == null) return; + + getTiles(world).tick(renderViewEntity.getX(), renderViewEntity.getY(), renderViewEntity.getZ()); + getEntities(world).tick(renderViewEntity.getX(), renderViewEntity.getY(), renderViewEntity.getZ()); + } + + public static void enqueueUpdate(TileEntity te) { + getTiles(te.getWorld()).queueUpdate(te); + } + + public static void enqueueUpdate(Entity entity) { + getEntities(entity.world).queueUpdate(entity); + } + + @SubscribeEvent + public static void onBeginFrame(BeginFrameEvent event) { + Contexts.WORLD.getMaterialManager(event.getWorld()) + .checkAndShiftOrigin(event.getInfo()); + + getTiles(event.getWorld()).beginFrame(event.getInfo()); + getEntities(event.getWorld()).beginFrame(event.getInfo()); + } + + @SubscribeEvent + public static void renderLayer(RenderLayerEvent event) { + ClientWorld world = event.getWorld(); + if (!Backend.getInstance().canUseInstancing(world)) return; + + event.type.startDrawing(); + + Contexts.WORLD.getMaterialManager(world) + .render(event.type, event.viewProjection, event.camX, event.camY, event.camZ); + + event.type.endDrawing(); + } + + @SubscribeEvent + public static void onReloadRenderers(ReloadRenderersEvent event) { + ClientWorld world = event.getWorld(); + if (Backend.getInstance().canUseInstancing() && world != null) { + Contexts.WORLD.getMaterialManager(world).delete(); + + TileInstanceManager tiles = getTiles(world); + tiles.invalidate(); + world.loadedTileEntityList.forEach(tiles::add); + + EntityInstanceManager entities = getEntities(world); + entities.invalidate(); + world.getAllEntities().forEach(entities::add); + } + } + + private static final RenderType crumblingLayer = ModelBakery.BLOCK_DESTRUCTION_RENDER_LAYERS.get(0); + + public static void renderBreaking(ClientWorld world, Matrix4f viewProjection, double cameraX, double cameraY, double cameraZ) { + if (!Backend.getInstance().canUseInstancing(world)) return; + + WorldRenderer worldRenderer = Minecraft.getInstance().worldRenderer; + Long2ObjectMap> breakingProgressions = worldRenderer.blockBreakingProgressions; + + if (breakingProgressions.isEmpty()) return; + Vector renderers = blockBreaking.getValue(); + + BitSet bitSet = new BitSet(10); + + for (Long2ObjectMap.Entry> entry : breakingProgressions.long2ObjectEntrySet()) { + BlockPos breakingPos = BlockPos.fromLong(entry.getLongKey()); + + SortedSet progresses = entry.getValue(); + if (progresses != null && !progresses.isEmpty()) { + int blockDamage = progresses.last().getPartialBlockDamage(); + bitSet.set(blockDamage); + renderers.get(blockDamage).add(world.getTileEntity(breakingPos)); + } + } + + TextureManager textureManager = Minecraft.getInstance().textureManager; + ActiveRenderInfo info = Minecraft.getInstance().gameRenderer.getActiveRenderInfo(); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, textureManager.getTexture(PlayerContainer.BLOCK_ATLAS_TEXTURE).getGlTextureId()); + + glActiveTexture(GL_TEXTURE4); + + crumblingLayer.startDrawing(); + bitSet.stream().forEach(i -> { + Texture breaking = textureManager.getTexture(ModelBakery.BLOCK_DESTRUCTION_STAGE_TEXTURES.get(i)); + CrumblingInstanceManager renderer = renderers.get(i); + renderer.beginFrame(info); + + if (breaking != null) { + glBindTexture(GL_TEXTURE_2D, breaking.getGlTextureId()); + renderer.materialManager.render(RenderType.getCutoutMipped(), viewProjection, cameraX, cameraY, cameraZ); + } + + renderer.invalidate(); + }); + crumblingLayer.endDrawing(); + + glActiveTexture(GL_TEXTURE0); + Texture breaking = textureManager.getTexture(ModelBakery.BLOCK_DESTRUCTION_STAGE_TEXTURES.get(0)); + if (breaking != null) + glBindTexture(GL_TEXTURE_2D, breaking.getGlTextureId()); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/InstancedRenderRegistry.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/InstancedRenderRegistry.java new file mode 100644 index 000000000..31f2b5611 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/InstancedRenderRegistry.java @@ -0,0 +1,57 @@ +package com.jozufozu.flywheel.backend.instancing; + +import java.util.Map; + +import javax.annotation.Nullable; + +import com.google.common.collect.Maps; +import com.jozufozu.flywheel.backend.instancing.entity.EntityInstance; +import com.jozufozu.flywheel.backend.instancing.entity.IEntityInstanceFactory; +import com.jozufozu.flywheel.backend.instancing.tile.ITileInstanceFactory; +import com.jozufozu.flywheel.backend.instancing.tile.TileEntityInstance; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.tileentity.TileEntityType; + +public class InstancedRenderRegistry { + private static final InstancedRenderRegistry INSTANCE = new InstancedRenderRegistry(); + + public static InstancedRenderRegistry getInstance() { + return INSTANCE; + } + + private final Map, ITileInstanceFactory> tiles = Maps.newHashMap(); + private final Map, IEntityInstanceFactory> entities = Maps.newHashMap(); + + public void register(TileEntityType type, ITileInstanceFactory rendererFactory) { + this.tiles.put(type, rendererFactory); + } + + public void register(EntityType type, IEntityInstanceFactory rendererFactory) { + this.entities.put(type, rendererFactory); + } + + @SuppressWarnings("unchecked") + @Nullable + public TileEntityInstance create(MaterialManager manager, T tile) { + TileEntityType type = tile.getType(); + ITileInstanceFactory factory = (ITileInstanceFactory) this.tiles.get(type); + + if (factory == null) return null; + else return factory.create(manager, tile); + } + + + @SuppressWarnings("unchecked") + @Nullable + public EntityInstance create(MaterialManager manager, T tile) { + EntityType type = tile.getType(); + IEntityInstanceFactory factory = (IEntityInstanceFactory) this.entities.get(type); + + if (factory == null) return null; + else return factory.create(manager, tile); + } + +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/Instancer.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/Instancer.java new file mode 100644 index 000000000..7cb3fafc5 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/Instancer.java @@ -0,0 +1,252 @@ +package com.jozufozu.flywheel.backend.instancing; + + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.function.Supplier; + +import com.jozufozu.flywheel.backend.Backend; +import com.jozufozu.flywheel.backend.gl.GlVertexArray; +import com.jozufozu.flywheel.backend.gl.attrib.VertexFormat; +import com.jozufozu.flywheel.backend.gl.buffer.GlBuffer; +import com.jozufozu.flywheel.backend.gl.buffer.GlBufferType; +import com.jozufozu.flywheel.backend.gl.buffer.MappedBuffer; +import com.jozufozu.flywheel.backend.model.BufferedModel; +import com.jozufozu.flywheel.util.AttribUtil; + +import net.minecraft.util.math.vector.Vector3i; + +public class Instancer { + + public final Supplier originCoordinate; + + protected final BufferedModel model; + + protected final VertexFormat instanceFormat; + protected final IInstanceFactory factory; + protected GlVertexArray vao; + protected GlBuffer instanceVBO; + protected int glBufferSize = -1; + protected int glInstanceCount = 0; + private boolean deleted; + + protected final ArrayList data = new ArrayList<>(); + + boolean anyToRemove; + boolean anyToUpdate; + + public Instancer(BufferedModel model, Supplier originCoordinate, MaterialSpec spec) { + this.model = model; + this.factory = spec.getInstanceFactory(); + this.instanceFormat = spec.getInstanceFormat(); + this.originCoordinate = originCoordinate; + + if (model.getVertexCount() <= 0) + throw new IllegalArgumentException("Refusing to instance a model with no vertices."); + + vao = new GlVertexArray(); + instanceVBO = new GlBuffer(GlBufferType.ARRAY_BUFFER); + + vao.bind(); + + // bind the model's vbo to our vao + model.setupState(); + + AttribUtil.enableArrays(model.getAttributeCount() + instanceFormat.getAttributeCount()); + + vao.unbind(); + + model.clearState(); + } + + public void render() { + if (deleted) return; + + vao.bind(); + renderSetup(); + + if (glInstanceCount > 0) + model.drawInstances(glInstanceCount); + + vao.unbind(); + } + + public D createInstance() { + D instanceData = factory.create(this); + instanceData.dirty = true; + anyToUpdate = true; + data.add(instanceData); + + return instanceData; + } + + public boolean empty() { + return !anyToUpdate && !anyToRemove && glInstanceCount == 0; + } + + /** + * Clear all instance data without freeing resources. + */ + public void clear() { + data.clear(); + anyToRemove = true; + } + + /** + * Free acquired resources. Attempting to use this after calling delete is undefined behavior. + */ + public void delete() { + if (deleted) return; + + deleted = true; + + model.delete(); + + instanceVBO.delete(); + vao.delete(); + } + + protected void renderSetup() { + if (anyToRemove) { + removeDeletedInstances(); + } + + instanceVBO.bind(); + if (!realloc()) { + + if (anyToRemove) { + clearBufferTail(); + } + + if (anyToUpdate) { + updateBuffer(); + } + + glInstanceCount = data.size(); + } + + instanceVBO.unbind(); + + anyToRemove = anyToUpdate = false; + } + + private void clearBufferTail() { + int size = data.size(); + final int offset = size * instanceFormat.getStride(); + final int length = glBufferSize - offset; + if (length > 0) { + instanceVBO.getBuffer(offset, length) + .putByteArray(new byte[length]) + .flush(); + } + } + + private void updateBuffer() { + final int size = data.size(); + + if (size <= 0) return; + + final int stride = instanceFormat.getStride(); + final BitSet dirtySet = getDirtyBitSet(); + + if (dirtySet.isEmpty()) return; + + final int firstDirty = dirtySet.nextSetBit(0); + final int lastDirty = dirtySet.previousSetBit(size); + + final int offset = firstDirty * stride; + final int length = (1 + lastDirty - firstDirty) * stride; + + if (length > 0) { + MappedBuffer mapped = instanceVBO.getBuffer(offset, length); + + dirtySet.stream().forEach(i -> { + final D d = data.get(i); + + mapped.position(i * stride); + d.write(mapped); + }); + mapped.flush(); + } + } + + private BitSet getDirtyBitSet() { + final int size = data.size(); + final BitSet dirtySet = new BitSet(size); + + for (int i = 0; i < size; i++) { + D element = data.get(i); + if (element.dirty) { + dirtySet.set(i); + + element.dirty = false; + } + } + return dirtySet; + } + + private boolean realloc() { + int size = this.data.size(); + int stride = instanceFormat.getStride(); + int requiredSize = size * stride; + if (requiredSize > glBufferSize) { + glBufferSize = requiredSize + stride * 16; + instanceVBO.alloc(glBufferSize); + + MappedBuffer buffer = instanceVBO.getBuffer(0, glBufferSize); + for (D datum : data) { + datum.write(buffer); + } + buffer.flush(); + + glInstanceCount = size; + + informAttribDivisors(); + + return true; + } + return false; + } + + private void removeDeletedInstances() { + // Figure out which elements are to be removed. + final int oldSize = this.data.size(); + int removeCount = 0; + final BitSet removeSet = new BitSet(oldSize); + for (int i = 0; i < oldSize; i++) { + final D element = this.data.get(i); + if (element.removed) { + removeSet.set(i); + removeCount++; + } + } + + final int newSize = oldSize - removeCount; + + // shift surviving elements left over the spaces left by removed elements + for (int i = 0, j = 0; (i < oldSize) && (j < newSize); i++, j++) { + i = removeSet.nextClearBit(i); + + if (i != j) { + D element = data.get(i); + data.set(j, element); + element.dirty = true; + } + } + + anyToUpdate = true; + + data.subList(newSize, oldSize).clear(); + + } + + private void informAttribDivisors() { + int staticAttributes = model.getAttributeCount(); + instanceFormat.vertexAttribPointers(staticAttributes); + + for (int i = 0; i < instanceFormat.getAttributeCount(); i++) { + Backend.getInstance().compat.instancedArrays.vertexAttribDivisor(i + staticAttributes, 1); + } + } + +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/MaterialManager.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/MaterialManager.java new file mode 100644 index 000000000..96c18077e --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/MaterialManager.java @@ -0,0 +1,161 @@ +package com.jozufozu.flywheel.backend.instancing; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import com.jozufozu.flywheel.backend.Backend; +import com.jozufozu.flywheel.core.Materials; +import com.jozufozu.flywheel.core.WorldContext; +import com.jozufozu.flywheel.core.materials.ModelData; +import com.jozufozu.flywheel.core.materials.OrientedData; +import com.jozufozu.flywheel.core.shader.IProgramCallback; +import com.jozufozu.flywheel.core.shader.WorldProgram; +import com.jozufozu.flywheel.util.WeakHashSet; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.ActiveRenderInfo; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.vector.Matrix4f; +import net.minecraft.util.math.vector.Vector3i; + +public class MaterialManager

{ + + public static int MAX_ORIGIN_DISTANCE = 100; + + protected final WorldContext

context; + + protected final Map, InstanceMaterial> atlasMaterials; + protected final ArrayList> atlasRenderers; + + protected final Map>> renderers; + protected final Map, InstanceMaterial>> materials; + + private BlockPos originCoordinate = BlockPos.ZERO; + + private final WeakHashSet listeners; + + public MaterialManager(WorldContext

context) { + this.context = context; + + this.atlasMaterials = new HashMap<>(); + this.atlasRenderers = new ArrayList<>(Backend.getInstance().allMaterials().size()); + + this.materials = new HashMap<>(); + this.renderers = new HashMap<>(); + + this.listeners = new WeakHashSet<>(); + } + + /** + * Render every model for every material. + * + * @param layer Which vanilla {@link RenderType} is being drawn? + * @param viewProjection How do we get from camera space to clip space? + */ + public void render(RenderType layer, Matrix4f viewProjection, double camX, double camY, double camZ) { + render(layer, viewProjection, camX, camY, camZ, null); + } + + /** + * Render every model for every material. + * + * @param layer Which vanilla {@link RenderType} is being drawn? + * @param viewProjection How do we get from camera space to clip space? + * @param callback Provide additional uniforms or state here. + */ + public void render(RenderType layer, Matrix4f viewProjection, double camX, double camY, double camZ, IProgramCallback

callback) { + camX -= originCoordinate.getX(); + camY -= originCoordinate.getY(); + camZ -= originCoordinate.getZ(); + + Matrix4f translate = Matrix4f.translate((float) -camX, (float) -camY, (float) -camZ); + + translate.multiplyBackward(viewProjection); + + for (MaterialRenderer

material : atlasRenderers) { + material.render(layer, translate, camX, camY, camZ, callback); + } + + for (Map.Entry>> entry : renderers.entrySet()) { + Minecraft.getInstance().textureManager.bindTexture(entry.getKey()); + + for (MaterialRenderer

materialRenderer : entry.getValue()) { + materialRenderer.render(layer, translate, camX, camY, camZ, callback); + } + } + } + + public void delete() { + atlasMaterials.values().forEach(InstanceMaterial::delete); + + materials.values().stream().flatMap(m -> m.values().stream()).forEach(InstanceMaterial::delete); + } + + @SuppressWarnings("unchecked") + public InstanceMaterial getMaterial(MaterialSpec materialType) { + return (InstanceMaterial) this.atlasMaterials.computeIfAbsent(materialType, type -> { + InstanceMaterial material = new InstanceMaterial<>(this::getOriginCoordinate, type); + + this.atlasRenderers.add(new MaterialRenderer<>(context.getProgramSupplier(type.getProgramName()), material)); + + return material; + }); + } + + @SuppressWarnings("unchecked") + public InstanceMaterial getMaterial(MaterialSpec materialType, ResourceLocation texture) { + return (InstanceMaterial) materials.computeIfAbsent(texture, $ -> new HashMap<>()) + .computeIfAbsent(materialType, type -> { + InstanceMaterial material = new InstanceMaterial<>(this::getOriginCoordinate, type); + + this.renderers.computeIfAbsent(texture, $ -> new ArrayList<>()) + .add(new MaterialRenderer<>(context.getProgramSupplier(type.getProgramName()), material)); + + return material; + }); + } + + public InstanceMaterial getTransformMaterial() { + return getMaterial(Materials.TRANSFORMED); + } + + public InstanceMaterial getOrientedMaterial() { + return getMaterial(Materials.ORIENTED); + } + + public Vector3i getOriginCoordinate() { + return originCoordinate; + } + + public void onOriginShift(OriginShiftListener listener) { + listeners.add(listener); + } + + public void checkAndShiftOrigin(ActiveRenderInfo info) { + int cX = MathHelper.floor(info.getProjectedView().x); + int cY = MathHelper.floor(info.getProjectedView().y); + int cZ = MathHelper.floor(info.getProjectedView().z); + + int dX = cX - originCoordinate.getX(); + int dY = cY - originCoordinate.getY(); + int dZ = cZ - originCoordinate.getZ(); + + if (Math.abs(dX) > MAX_ORIGIN_DISTANCE || Math.abs(dY) > MAX_ORIGIN_DISTANCE || Math.abs(dZ) > MAX_ORIGIN_DISTANCE) { + + originCoordinate = new BlockPos(cX, cY, cZ); + + materials.values().stream().flatMap(m -> m.values().stream()).forEach(InstanceMaterial::clear); + atlasMaterials.values().forEach(InstanceMaterial::clear); + listeners.forEach(OriginShiftListener::onOriginShift); + } + } + + @FunctionalInterface + public interface OriginShiftListener { + void onOriginShift(); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/MaterialRenderer.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/MaterialRenderer.java new file mode 100644 index 000000000..1a8b8d1da --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/MaterialRenderer.java @@ -0,0 +1,40 @@ +package com.jozufozu.flywheel.backend.instancing; + +import java.util.function.Supplier; + +import com.jozufozu.flywheel.core.shader.IProgramCallback; +import com.jozufozu.flywheel.core.shader.WorldProgram; + +import net.minecraft.client.renderer.RenderType; +import net.minecraft.util.math.vector.Matrix4f; + +public class MaterialRenderer

{ + + private final Supplier

program; + private final InstanceMaterial material; + + public MaterialRenderer(Supplier

programSupplier, InstanceMaterial material) { + this.program = programSupplier; + this.material = material; + } + + public void render(RenderType layer, Matrix4f viewProjection, double camX, double camY, double camZ, IProgramCallback

setup) { + if (!(layer == RenderType.getCutoutMipped())) return; + + if (material.nothingToRender()) return; + + P program = this.program.get(); + + program.bind(); + program.uploadViewProjection(viewProjection); + program.uploadCameraPos(camX, camY, camZ); + + if (setup != null) setup.call(program); + + makeRenderCalls(); + } + + protected void makeRenderCalls() { + material.forEachInstancer(Instancer::render); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/MaterialSpec.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/MaterialSpec.java new file mode 100644 index 000000000..405bc4772 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/MaterialSpec.java @@ -0,0 +1,47 @@ +package com.jozufozu.flywheel.backend.instancing; + +import com.jozufozu.flywheel.backend.gl.attrib.VertexFormat; + +import net.minecraft.inventory.container.PlayerContainer; +import net.minecraft.util.ResourceLocation; + +public class MaterialSpec { + + public final ResourceLocation name; + + private final ResourceLocation programSpec; + private final VertexFormat modelFormat; + private final VertexFormat instanceFormat; + private final IInstanceFactory instanceFactory; + private final ResourceLocation texture; + + public MaterialSpec(ResourceLocation name, ResourceLocation programSpec, VertexFormat modelFormat, VertexFormat instanceFormat, IInstanceFactory instanceFactory) { + this(name, programSpec, modelFormat, instanceFormat, PlayerContainer.BLOCK_ATLAS_TEXTURE, instanceFactory); + } + + public MaterialSpec(ResourceLocation name, ResourceLocation programSpec, VertexFormat modelFormat, VertexFormat instanceFormat, ResourceLocation texture, IInstanceFactory instanceFactory) { + this.name = name; + this.programSpec = programSpec; + this.modelFormat = modelFormat; + this.instanceFormat = instanceFormat; + this.instanceFactory = instanceFactory; + this.texture = texture; + } + + public ResourceLocation getProgramName() { + return programSpec; + } + + public VertexFormat getModelFormat() { + return modelFormat; + } + + public VertexFormat getInstanceFormat() { + return instanceFormat; + } + + public IInstanceFactory getInstanceFactory() { + return instanceFactory; + } + +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/entity/EntityInstance.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/entity/EntityInstance.java new file mode 100644 index 000000000..2500ac960 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/entity/EntityInstance.java @@ -0,0 +1,133 @@ +package com.jozufozu.flywheel.backend.instancing.entity; + +import java.util.Arrays; +import java.util.stream.Stream; + +import com.jozufozu.flywheel.backend.instancing.IDynamicInstance; +import com.jozufozu.flywheel.backend.instancing.IInstance; +import com.jozufozu.flywheel.backend.instancing.ITickableInstance; +import com.jozufozu.flywheel.backend.instancing.InstanceMaterial; +import com.jozufozu.flywheel.backend.instancing.MaterialManager; +import com.jozufozu.flywheel.backend.instancing.tile.TileInstanceManager; +import com.jozufozu.flywheel.core.materials.IFlatLight; +import com.jozufozu.flywheel.core.materials.ModelData; +import com.jozufozu.flywheel.core.materials.OrientedData; + +import net.minecraft.entity.Entity; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.vector.Vector3d; +import net.minecraft.util.math.vector.Vector3f; +import net.minecraft.util.math.vector.Vector3i; +import net.minecraft.world.LightType; +import net.minecraft.world.World; + +/** + * The layer between a {@link TileEntity} and the Flywheel backend. + ** + *

There are a few additional features that overriding classes can opt in to: + *

    + *
  • {@link IDynamicInstance}
  • + *
  • {@link ITickableInstance}
  • + *
+ * See the interfaces' documentation for more information about each one. + * + *
Implementing one or more of these will give a {@link EntityInstance} access + * to more interesting and regular points within a tick or a frame. + * + * @param The type of {@link Entity} your class is an instance of. + */ +public abstract class EntityInstance implements IInstance { + + protected final MaterialManager materialManager; + protected final E entity; + protected final World world; + + public EntityInstance(MaterialManager materialManager, E entity) { + this.materialManager = materialManager; + this.entity = entity; + this.world = entity.world; + } + + /** + * Free any acquired resources. + */ + public abstract void remove(); + + /** + * Update instance data here. Good for when data doesn't change very often and when animations are GPU based. + * Don't query lighting data here, that's handled separately in {@link #updateLight()}. + * + *

If your animations are complex or more CPU driven, see {@link IDynamicInstance} or {@link ITickableInstance}. + */ + public void update() { + } + + /** + * Called after construction and when a light update occurs in the world. + * + *
If your model needs it, update light here. + */ + public void updateLight() { + } + + /** + * Just before {@link #update()} would be called, shouldReset() is checked. + * If this function returns true, then this instance will be {@link #remove removed}, + * and another instance will be constructed to replace it. This allows for more sane resource + * acquisition compared to trying to update everything within the lifetime of an instance. + * + * @return true if this instance should be discarded and refreshed. + */ + public boolean shouldReset() { + return false; + } + + /** + * In order to accommodate for floating point precision errors at high coordinates, + * {@link TileInstanceManager}s are allowed to arbitrarily adjust the origin, and + * shift the world matrix provided as a shader uniform accordingly. + * + * @return The {@link BlockPos position} of the {@link Entity} this instance + * represents should be rendered at to appear in the correct location. + */ + public Vector3f getInstancePosition() { + Vector3d pos = entity.getPositionVec(); + Vector3i origin = materialManager.getOriginCoordinate(); + return new Vector3f( + (float) (pos.x - origin.getX()), + (float) (pos.y - origin.getY()), + (float) (pos.z - origin.getZ()) + ); + } + + @Override + public BlockPos getWorldPosition() { + return entity.getBlockPos(); + } + + protected void relight(BlockPos pos, IFlatLight... models) { + relight(world.getLightLevel(LightType.BLOCK, pos), world.getLightLevel(LightType.SKY, pos), models); + } + + protected > void relight(BlockPos pos, Stream models) { + relight(world.getLightLevel(LightType.BLOCK, pos), world.getLightLevel(LightType.SKY, pos), models); + } + + protected void relight(int block, int sky, IFlatLight... models) { + relight(block, sky, Arrays.stream(models)); + } + + protected > void relight(int block, int sky, Stream models) { + models.forEach(model -> model.setBlockLight(block).setSkyLight(sky)); + } + + protected InstanceMaterial getTransformMaterial() { + return materialManager.getTransformMaterial(); + } + + protected InstanceMaterial getOrientedMaterial() { + return materialManager.getOrientedMaterial(); + } + +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/entity/EntityInstanceManager.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/entity/EntityInstanceManager.java new file mode 100644 index 000000000..24934c8ab --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/entity/EntityInstanceManager.java @@ -0,0 +1,43 @@ +package com.jozufozu.flywheel.backend.instancing.entity; + +import com.jozufozu.flywheel.backend.Backend; +import com.jozufozu.flywheel.backend.instancing.IInstance; +import com.jozufozu.flywheel.backend.instancing.InstanceManager; +import com.jozufozu.flywheel.backend.instancing.InstancedRenderRegistry; +import com.jozufozu.flywheel.backend.instancing.MaterialManager; + +import net.minecraft.entity.Entity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.IBlockReader; +import net.minecraft.world.World; + +public class EntityInstanceManager extends InstanceManager { + + public EntityInstanceManager(MaterialManager materialManager) { + super(materialManager); + } + + @Override + protected IInstance createRaw(Entity obj) { + return InstancedRenderRegistry.getInstance().create(materialManager, obj); + } + + @Override + protected boolean canCreateInstance(Entity entity) { + if (!entity.isAlive()) return false; + + World world = entity.world; + + if (world == null) return false; + + if (Backend.isFlywheelWorld(world)) { + BlockPos pos = entity.getBlockPos(); + + IBlockReader existingChunk = world.getExistingChunk(pos.getX() >> 4, pos.getZ() >> 4); + + return existingChunk != null; + } + + return false; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/entity/IEntityInstanceFactory.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/entity/IEntityInstanceFactory.java new file mode 100644 index 000000000..7915947e4 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/entity/IEntityInstanceFactory.java @@ -0,0 +1,10 @@ +package com.jozufozu.flywheel.backend.instancing.entity; + +import com.jozufozu.flywheel.backend.instancing.MaterialManager; + +import net.minecraft.entity.Entity; + +@FunctionalInterface +public interface IEntityInstanceFactory { + EntityInstance create(MaterialManager manager, E te); +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/tile/ITileInstanceFactory.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/tile/ITileInstanceFactory.java new file mode 100644 index 000000000..b46187ab6 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/tile/ITileInstanceFactory.java @@ -0,0 +1,10 @@ +package com.jozufozu.flywheel.backend.instancing.tile; + +import com.jozufozu.flywheel.backend.instancing.MaterialManager; + +import net.minecraft.tileentity.TileEntity; + +@FunctionalInterface +public interface ITileInstanceFactory { + TileEntityInstance create(MaterialManager manager, T te); +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/tile/TileEntityInstance.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/tile/TileEntityInstance.java new file mode 100644 index 000000000..e3929101c --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/tile/TileEntityInstance.java @@ -0,0 +1,130 @@ +package com.jozufozu.flywheel.backend.instancing.tile; + +import java.util.Arrays; +import java.util.stream.Stream; + +import com.jozufozu.flywheel.backend.instancing.IDynamicInstance; +import com.jozufozu.flywheel.backend.instancing.IInstance; +import com.jozufozu.flywheel.backend.instancing.ITickableInstance; +import com.jozufozu.flywheel.backend.instancing.InstanceMaterial; +import com.jozufozu.flywheel.backend.instancing.MaterialManager; +import com.jozufozu.flywheel.core.materials.IFlatLight; +import com.jozufozu.flywheel.core.materials.ModelData; +import com.jozufozu.flywheel.core.materials.OrientedData; + +import net.minecraft.block.BlockState; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.LightType; +import net.minecraft.world.World; + +/** + * The layer between a {@link TileEntity} and the Flywheel backend. + * + *

{@link #updateLight()} is called after construction. + * + *

There are a few additional features that overriding classes can opt in to: + *
    + *
  • {@link IDynamicInstance}
  • + *
  • {@link ITickableInstance}
  • + *
+ * See the interfaces' documentation for more information about each one. + * + *
Implementing one or more of these will give a {@link TileEntityInstance} access + * to more interesting and regular points within a tick or a frame. + * + * @param The type of {@link TileEntity} your class is an instance of. + */ +public abstract class TileEntityInstance implements IInstance { + + protected final MaterialManager materialManager; + protected final T tile; + protected final World world; + protected final BlockPos pos; + protected final BlockPos instancePos; + protected final BlockState blockState; + + public TileEntityInstance(MaterialManager materialManager, T tile) { + this.materialManager = materialManager; + this.tile = tile; + this.world = tile.getWorld(); + this.pos = tile.getPos(); + this.blockState = tile.getBlockState(); + this.instancePos = pos.subtract(materialManager.getOriginCoordinate()); + } + + /** + * Update instance data here. Good for when data doesn't change very often and when animations are GPU based. + * Don't query lighting data here, that's handled separately in {@link #updateLight()}. + * + *

If your animations are complex or more CPU driven, see {@link IDynamicInstance} or {@link ITickableInstance}. + */ + public void update() { + } + + /** + * Called after construction and when a light update occurs in the world. + * + *
If your model needs it, update light here. + */ + public void updateLight() { + } + + /** + * Free any acquired resources. + */ + public abstract void remove(); + + /** + * Just before {@link #update()} would be called, shouldReset() is checked. + * If this function returns true, then this instance will be {@link #remove removed}, + * and another instance will be constructed to replace it. This allows for more sane resource + * acquisition compared to trying to update everything within the lifetime of an instance. + * + * @return true if this instance should be discarded and refreshed. + */ + public boolean shouldReset() { + return tile.getBlockState() != blockState; + } + + /** + * In order to accommodate for floating point precision errors at high coordinates, + * {@link TileInstanceManager}s are allowed to arbitrarily adjust the origin, and + * shift the world matrix provided as a shader uniform accordingly. + * + * @return The {@link BlockPos position} of the {@link TileEntity} this instance + * represents should be rendered at to appear in the correct location. + */ + public BlockPos getInstancePosition() { + return pos.subtract(materialManager.getOriginCoordinate()); + } + + @Override + public BlockPos getWorldPosition() { + return pos; + } + + protected void relight(BlockPos pos, IFlatLight... models) { + relight(world.getLightLevel(LightType.BLOCK, pos), world.getLightLevel(LightType.SKY, pos), models); + } + + protected > void relight(BlockPos pos, Stream models) { + relight(world.getLightLevel(LightType.BLOCK, pos), world.getLightLevel(LightType.SKY, pos), models); + } + + protected void relight(int block, int sky, IFlatLight... models) { + relight(block, sky, Arrays.stream(models)); + } + + protected > void relight(int block, int sky, Stream models) { + models.forEach(model -> model.setBlockLight(block).setSkyLight(sky)); + } + + protected InstanceMaterial getTransformMaterial() { + return materialManager.getTransformMaterial(); + } + + protected InstanceMaterial getOrientedMaterial() { + return materialManager.getOrientedMaterial(); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/instancing/tile/TileInstanceManager.java b/src/main/java/com/jozufozu/flywheel/backend/instancing/tile/TileInstanceManager.java new file mode 100644 index 000000000..1359ceafd --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/instancing/tile/TileInstanceManager.java @@ -0,0 +1,45 @@ +package com.jozufozu.flywheel.backend.instancing.tile; + +import com.jozufozu.flywheel.backend.Backend; +import com.jozufozu.flywheel.backend.instancing.IInstance; +import com.jozufozu.flywheel.backend.instancing.InstanceManager; +import com.jozufozu.flywheel.backend.instancing.InstancedRenderRegistry; +import com.jozufozu.flywheel.backend.instancing.MaterialManager; + +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.IBlockReader; +import net.minecraft.world.World; + +public class TileInstanceManager extends InstanceManager { + + public TileInstanceManager(MaterialManager materialManager) { + super(materialManager); + } + + @Override + protected IInstance createRaw(TileEntity obj) { + return InstancedRenderRegistry.getInstance().create(materialManager, obj); + } + + @Override + protected boolean canCreateInstance(TileEntity tile) { + if (tile.isRemoved()) return false; + + World world = tile.getWorld(); + + if (world == null) return false; + + if (world.isAirBlock(tile.getPos())) return false; + + if (Backend.isFlywheelWorld(world)) { + BlockPos pos = tile.getPos(); + + IBlockReader existingChunk = world.getExistingChunk(pos.getX() >> 4, pos.getZ() >> 4); + + return existingChunk != null; + } + + return false; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/loading/IProcessingStage.java b/src/main/java/com/jozufozu/flywheel/backend/loading/IProcessingStage.java new file mode 100644 index 000000000..2a60aa8c2 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/loading/IProcessingStage.java @@ -0,0 +1,7 @@ +package com.jozufozu.flywheel.backend.loading; + +@FunctionalInterface +public interface IProcessingStage { + + void process(Shader shader); +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/loading/InstancedArraysTemplate.java b/src/main/java/com/jozufozu/flywheel/backend/loading/InstancedArraysTemplate.java new file mode 100644 index 000000000..9d1ba5841 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/loading/InstancedArraysTemplate.java @@ -0,0 +1,39 @@ +package com.jozufozu.flywheel.backend.loading; + +import com.jozufozu.flywheel.Flywheel; +import com.jozufozu.flywheel.backend.ShaderSources; +import com.jozufozu.flywheel.backend.gl.shader.ShaderType; + +import net.minecraft.util.ResourceLocation; + +public class InstancedArraysTemplate extends ProgramTemplate { + + public static final String vertexData = "VertexData"; + public static final String instanceData = "InstanceData"; + public static final String fragment = "Fragment"; + + public static final String vertexPrefix = "a_v_"; + public static final String instancePrefix = "a_i_"; + + public static final String[] requiredVert = new String[]{instanceData, vertexData, fragment}; + + public static final String[] requiredFrag = {fragment}; + + public static final ResourceLocation vert = new ResourceLocation(Flywheel.ID, "template/instanced/instanced.vert"); + public static final ResourceLocation frag = new ResourceLocation(Flywheel.ID, "template/instanced/instanced.frag"); + + public InstancedArraysTemplate(ShaderSources loader) { + super(loader); + + templates.put(ShaderType.VERTEX, new ShaderTemplate(requiredVert, loader.getShaderSource(vert))); + templates.put(ShaderType.FRAGMENT, new ShaderTemplate(requiredFrag, loader.getShaderSource(frag))); + } + + @Override + public void attachAttributes(Program builder) { + Shader shader = builder.attached.get(ShaderType.VERTEX); + + shader.getTag(vertexData).addPrefixedAttributes(builder, vertexPrefix); + shader.getTag(instanceData).addPrefixedAttributes(builder, instancePrefix); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/loading/LayoutTag.java b/src/main/java/com/jozufozu/flywheel/backend/loading/LayoutTag.java new file mode 100644 index 000000000..c7646e658 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/loading/LayoutTag.java @@ -0,0 +1,19 @@ +package com.jozufozu.flywheel.backend.loading; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.jozufozu.flywheel.backend.gl.GlNumericType; + +public class LayoutTag { + + public static final Pattern pattern = Pattern.compile("Layout\\((\\w+)(?:\\s*,\\s*(\\w*))?\\)"); + + final GlNumericType type; + final boolean normalized; + + public LayoutTag(Matcher matcher) { + type = GlNumericType.byName(matcher.group(1)); + normalized = Boolean.parseBoolean(matcher.group(2)); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/loading/ModelTemplate.java b/src/main/java/com/jozufozu/flywheel/backend/loading/ModelTemplate.java new file mode 100644 index 000000000..74e3810b1 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/loading/ModelTemplate.java @@ -0,0 +1,35 @@ +package com.jozufozu.flywheel.backend.loading; + +import com.jozufozu.flywheel.Flywheel; +import com.jozufozu.flywheel.backend.ShaderSources; +import com.jozufozu.flywheel.backend.gl.shader.ShaderType; + +import net.minecraft.util.ResourceLocation; + +public class ModelTemplate extends ProgramTemplate { + public static final String vertexData = "VertexData"; + public static final String fragment = "Fragment"; + + public static final String vertexPrefix = "a_v_"; + + public static final String[] requiredVert = new String[]{vertexData, fragment}; + + public static final String[] requiredFrag = {fragment}; + + public static final ResourceLocation vert = new ResourceLocation(Flywheel.ID, "template/model/model.vert"); + public static final ResourceLocation frag = new ResourceLocation(Flywheel.ID, "template/model/model.frag"); + + public ModelTemplate(ShaderSources loader) { + super(loader); + + templates.put(ShaderType.VERTEX, new ShaderTemplate(requiredVert, loader.getShaderSource(vert))); + templates.put(ShaderType.FRAGMENT, new ShaderTemplate(requiredFrag, loader.getShaderSource(frag))); + } + + @Override + public void attachAttributes(Program builder) { + Shader shader = builder.attached.get(ShaderType.VERTEX); + + shader.getTag(vertexData).addPrefixedAttributes(builder, vertexPrefix); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/loading/Program.java b/src/main/java/com/jozufozu/flywheel/backend/loading/Program.java new file mode 100644 index 000000000..43f6fefe8 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/loading/Program.java @@ -0,0 +1,69 @@ +package com.jozufozu.flywheel.backend.loading; + +import static org.lwjgl.opengl.GL20.GL_LINK_STATUS; +import static org.lwjgl.opengl.GL20.GL_TRUE; +import static org.lwjgl.opengl.GL20.glAttachShader; +import static org.lwjgl.opengl.GL20.glBindAttribLocation; +import static org.lwjgl.opengl.GL20.glCreateProgram; +import static org.lwjgl.opengl.GL20.glGetProgramInfoLog; +import static org.lwjgl.opengl.GL20.glGetProgrami; +import static org.lwjgl.opengl.GL20.glLinkProgram; + +import java.util.EnumMap; +import java.util.Map; + +import com.jozufozu.flywheel.backend.Backend; +import com.jozufozu.flywheel.backend.gl.shader.GlShader; +import com.jozufozu.flywheel.backend.gl.shader.ShaderType; + +import net.minecraft.util.ResourceLocation; + +public class Program { + public final ResourceLocation name; + public final int program; + + private int attributeIndex; + + public final Map attached; + + public Program(ResourceLocation name) { + this.name = name; + this.program = glCreateProgram(); + attached = new EnumMap<>(ShaderType.class); + } + + public Program attachShader(Shader shader, GlShader glShader) { + glAttachShader(this.program, glShader.handle()); + + attached.put(shader.type, shader); + + return this; + } + + public Program addAttribute(String name, int attributeCount) { + glBindAttribLocation(this.program, attributeIndex, name); + attributeIndex += attributeCount; + return this; + } + + /** + * Links the attached shaders to this program. + */ + public Program link() { + glLinkProgram(this.program); + + String log = glGetProgramInfoLog(this.program); + + if (!log.isEmpty()) { + Backend.log.debug("Program link log for " + this.name + ": " + log); + } + + int result = glGetProgrami(this.program, GL_LINK_STATUS); + + if (result != GL_TRUE) { + throw new RuntimeException("Shader program linking failed, see log for details"); + } + + return this; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/loading/ProgramTemplate.java b/src/main/java/com/jozufozu/flywheel/backend/loading/ProgramTemplate.java new file mode 100644 index 000000000..a04f004fa --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/loading/ProgramTemplate.java @@ -0,0 +1,30 @@ +package com.jozufozu.flywheel.backend.loading; + +import java.util.EnumMap; +import java.util.Map; + +import com.jozufozu.flywheel.backend.ShaderSources; +import com.jozufozu.flywheel.backend.gl.shader.ShaderType; + +public abstract class ProgramTemplate implements IProcessingStage { + + protected final ShaderSources loader; + protected Map templates = new EnumMap<>(ShaderType.class); + + public ProgramTemplate(ShaderSources loader) { + this.loader = loader; + } + + @Override + public void process(Shader shader) { + ShaderTemplate template = templates.get(shader.type); + + if (template == null) return; + + shader.setSource(template.apply(shader)); + } + + public void attachAttributes(Program builder) { + + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/loading/Shader.java b/src/main/java/com/jozufozu/flywheel/backend/loading/Shader.java new file mode 100644 index 000000000..32ac93007 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/loading/Shader.java @@ -0,0 +1,149 @@ +package com.jozufozu.flywheel.backend.loading; + +import java.io.BufferedReader; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.jozufozu.flywheel.backend.Backend; +import com.jozufozu.flywheel.backend.ShaderSources; +import com.jozufozu.flywheel.backend.gl.shader.ShaderType; + +import net.minecraft.util.ResourceLocation; + +public class Shader { + // #flwinclude <"valid_namespace:valid/path_to_file.glsl"> + private static final Pattern includePattern = Pattern.compile("#flwinclude <\"([\\w\\d_]+:[\\w\\d_./]+)\">"); + + public static final Pattern versionDetector = Pattern.compile("#version[^\\n]*"); + private static final Pattern decorator = Pattern.compile("#\\[([\\w_]*)]"); + + public final ResourceLocation name; + public ShaderType type; + private String source; + private final ShaderSources loader; + + private boolean parsed = false; + final List structs = new ArrayList<>(3); + final Map tag2Struct = new HashMap<>(); + final Map name2Struct = new HashMap<>(); + + public Shader(ShaderSources loader, ShaderType type, ResourceLocation name, String source) { + this.loader = loader; + this.type = type; + this.name = name; + this.source = source; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + public TaggedStruct getTag(String tag) { + checkAndParse(); + return tag2Struct.get(tag); + } + + public TaggedStruct getStruct(String name) { + checkAndParse(); + return name2Struct.get(name); + } + + private void checkAndParse() { + if (!parsed) { + parsed = true; + parseStructs(); + } + } + + public void defineAll(Collection defines) { + Matcher matcher = versionDetector.matcher(source); + + if (matcher.find()) { + StringBuffer sourceWithDefines = new StringBuffer(); + String lines = defines.stream().map(it -> "#define " + it).collect(Collectors.joining("\n")); + + matcher.appendReplacement(sourceWithDefines, matcher.group() + '\n' + lines); + + matcher.appendTail(sourceWithDefines); + + source = sourceWithDefines.toString(); + } + } + + public void parseStructs() { + Matcher structMatcher = TaggedStruct.taggedStruct.matcher(source); + + StringBuffer strippedSrc = new StringBuffer(); + + while (structMatcher.find()) { + TaggedStruct struct = new TaggedStruct(structMatcher); + + structs.add(struct); + + structMatcher.appendReplacement(strippedSrc, decorator.matcher(struct.source).replaceFirst("")); + + tag2Struct.put(struct.tag, struct); + name2Struct.put(struct.name, struct); + } + structMatcher.appendTail(strippedSrc); + + this.source = strippedSrc.toString(); + } + + public void processIncludes() { + HashSet seen = new HashSet<>(); + seen.add(name); + + source = includeRecursive(source, seen).collect(Collectors.joining("\n")); + } + + private Stream includeRecursive(String source, Set seen) { + return lines(source).flatMap(line -> { + + Matcher matcher = includePattern.matcher(line); + + if (matcher.find()) { + String includeName = matcher.group(1); + + ResourceLocation include = new ResourceLocation(includeName); + + if (seen.add(include)) { + try { + return includeRecursive(loader.getShaderSource(include), seen); + } catch (ShaderLoadingException e) { + throw new ShaderLoadingException("could not resolve import: " + e.getMessage()); + } + } + + } + + return Stream.of(line); + }); + } + + public void printSource() { + Backend.log.debug("Source for shader '" + name + "':"); + int i = 1; + for (String s : source.split("\n")) { + Backend.log.debug(String.format("%1$4s: ", i++) + s); + } + } + + public static Stream lines(String s) { + return new BufferedReader(new StringReader(s)).lines(); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/loading/ShaderLoadingException.java b/src/main/java/com/jozufozu/flywheel/backend/loading/ShaderLoadingException.java new file mode 100644 index 000000000..5c32c7d98 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/loading/ShaderLoadingException.java @@ -0,0 +1,22 @@ +package com.jozufozu.flywheel.backend.loading; + +public class ShaderLoadingException extends RuntimeException { + public ShaderLoadingException() { + } + + public ShaderLoadingException(String message) { + super(message); + } + + public ShaderLoadingException(String message, Throwable cause) { + super(message, cause); + } + + public ShaderLoadingException(Throwable cause) { + super(cause); + } + + public ShaderLoadingException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/loading/ShaderTemplate.java b/src/main/java/com/jozufozu/flywheel/backend/loading/ShaderTemplate.java new file mode 100644 index 000000000..d2ed593f4 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/loading/ShaderTemplate.java @@ -0,0 +1,121 @@ +package com.jozufozu.flywheel.backend.loading; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ShaderTemplate { + + private static final String delimiter = "#flwbeginbody"; + private static final Pattern headerFinder = Pattern.compile(delimiter); + + private static final Pattern prefixer = Pattern.compile("#FLWPrefixFields\\((\\w+),\\s*(\\w+),\\s*([\\w\\d]+)\\)"); + private static final Pattern assigner = Pattern.compile("#FLWAssignFields\\(([\\w\\d_]+),\\s*([\\w\\d_.]+),\\s*([\\w\\d_.]+)\\)"); + + final String[] requiredStructs; + + final String header; + final String body; + + public ShaderTemplate(String[] requiredStructs, String templateSrc) { + this.requiredStructs = requiredStructs; + Matcher matcher = headerFinder.matcher(templateSrc); + + if (!matcher.find()) { + throw new RuntimeException("Shader template must have a header and footer delimited by '" + delimiter + "'"); + } + + this.header = templateSrc.substring(0, matcher.start()); + this.body = templateSrc.substring(matcher.end()); + + } + + public String apply(Shader shader) { + + return header + + shader.getSource() + + processBody(shader); + } + + public String processBody(Shader shader) { + String s = body; + + List missing = new ArrayList<>(); + + for (String name : requiredStructs) { + TaggedStruct struct = shader.getTag(name); + + if (struct != null) { + s = s.replace(name, struct.name); + } else { + missing.add(name); + } + } + + if (!missing.isEmpty()) { + String err = shader.name + " is missing: " + String.join(", ", missing); + throw new RuntimeException(err); + } + + s = fillPrefixes(shader, s); + s = fillAssigns(shader, s); + + return s; + } + + private String fillPrefixes(Shader shader, String s) { + Matcher prefixMatches = prefixer.matcher(s); + + StringBuffer out = new StringBuffer(); + while (prefixMatches.find()) { + String structName = prefixMatches.group(1); + String modifier = prefixMatches.group(2); + String prefix = prefixMatches.group(3); + + TaggedStruct struct = shader.getStruct(structName); + + StringBuilder builder = new StringBuilder(); + for (TaggedField field : struct.fields) { + builder.append(modifier); + builder.append(' '); + builder.append(field.getType()); + builder.append(' '); + builder.append(prefix); + builder.append(field.getName()); + builder.append(";\n"); + } + + prefixMatches.appendReplacement(out, builder.toString()); + } + prefixMatches.appendTail(out); + return out.toString(); + } + + private String fillAssigns(Shader shader, String s) { + Matcher assignMatches = assigner.matcher(s); + + StringBuffer out = new StringBuffer(); + while (assignMatches.find()) { + String structName = assignMatches.group(1); + String lhs = assignMatches.group(2); + String rhs = assignMatches.group(3); + + TaggedStruct struct = shader.getStruct(structName); + + StringBuilder builder = new StringBuilder(); + for (TaggedField field : struct.fields) { + builder.append(lhs); + builder.append(field.getName()); + builder.append(" = "); + builder.append(rhs); + builder.append(field.getName()); + builder.append(";\n"); + } + + assignMatches.appendReplacement(out, builder.toString()); + } + assignMatches.appendTail(out); + return out.toString(); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/loading/ShaderTransformer.java b/src/main/java/com/jozufozu/flywheel/backend/loading/ShaderTransformer.java new file mode 100644 index 000000000..4d156f8a7 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/loading/ShaderTransformer.java @@ -0,0 +1,38 @@ +package com.jozufozu.flywheel.backend.loading; + +import java.util.LinkedList; + +public class ShaderTransformer { + + private final LinkedList stages = new LinkedList<>(); + + public ShaderTransformer() { + } + + public ShaderTransformer pushStage(IProcessingStage stage) { + if (stage != null) { + stages.addLast(stage); + } + return this; + } + + public ShaderTransformer popStage() { + stages.removeLast(); + return this; + } + + public ShaderTransformer prependStage(IProcessingStage stage) { + if (stage != null) { + stages.addFirst(stage); + } + return this; + } + + public void transformSource(Shader shader) { + + for (IProcessingStage stage : this.stages) { + stage.process(shader); + } + } + +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/loading/TaggedField.java b/src/main/java/com/jozufozu/flywheel/backend/loading/TaggedField.java new file mode 100644 index 000000000..2d2cfdcdb --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/loading/TaggedField.java @@ -0,0 +1,48 @@ +package com.jozufozu.flywheel.backend.loading; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class TaggedField { + public static final Pattern fieldPattern = Pattern.compile("(?:#\\[([^\\n]*)]\\s*)?(\\S+)\\s*(\\S+);"); + + public String annotation; + public String name; + public String type; + public LayoutTag layout; + + public TaggedField(Matcher fieldMatcher) { + annotation = fieldMatcher.group(1); + type = fieldMatcher.group(2); + name = fieldMatcher.group(3); + + if (annotation != null) { + Matcher matcher = LayoutTag.pattern.matcher(annotation); + + if (matcher.find()) { + layout = new LayoutTag(matcher); + } + } + } + + public String getAnnotation() { + return annotation; + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + + @Override + public String toString() { + return "TaggedField{" + + "name='" + name + '\'' + + ", type='" + type + '\'' + + '}'; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/loading/TaggedStruct.java b/src/main/java/com/jozufozu/flywheel/backend/loading/TaggedStruct.java new file mode 100644 index 000000000..eac1ee079 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/loading/TaggedStruct.java @@ -0,0 +1,49 @@ +package com.jozufozu.flywheel.backend.loading; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class TaggedStruct { + + // https://regexr.com/5t207 + static final Pattern taggedStruct = Pattern.compile("#\\[(\\w*)]\\s*struct\\s+([\\w\\d]*)\\s*\\{([\\w\\d \\t#\\[\\](),;\\n]*)}\\s*;"); + + int srcStart, srcEnd; + String source; + String tag; + String name; + String body; + + List fields = new ArrayList<>(4); + Map fields2Types = new HashMap<>(); + + public TaggedStruct(Matcher foundMatcher) { + this.source = foundMatcher.group(); + + srcStart = foundMatcher.start(); + srcEnd = foundMatcher.end(); + + tag = foundMatcher.group(1); + name = foundMatcher.group(2); + body = foundMatcher.group(3); + + Matcher fielder = TaggedField.fieldPattern.matcher(body); + + while (fielder.find()) { + fields.add(new TaggedField(fielder)); + fields2Types.put(fielder.group(2), fielder.group(1)); + } + } + + public void addPrefixedAttributes(Program builder, String prefix) { + for (TaggedField field : fields) { + int attributeCount = TypeHelper.getAttributeCount(field.type); + + builder.addAttribute(prefix + field.name, attributeCount); + } + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/loading/TypeHelper.java b/src/main/java/com/jozufozu/flywheel/backend/loading/TypeHelper.java new file mode 100644 index 000000000..2d67ff460 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/loading/TypeHelper.java @@ -0,0 +1,37 @@ +package com.jozufozu.flywheel.backend.loading; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class TypeHelper { + + public static final Pattern vecType = Pattern.compile("^[biud]?vec([234])$"); + public static final Pattern matType = Pattern.compile("^mat([234])(?:x([234]))?$"); + + public static int getElementCount(String type) { + Matcher vec = vecType.matcher(type); + if (vec.find()) return Integer.parseInt(vec.group(1)); + + Matcher mat = matType.matcher(type); + if (mat.find()) { + int n = Integer.parseInt(mat.group(1)); + + String m = mat.group(2); + + if (m != null) return Integer.parseInt(m) * n; + + return n; + } + + return 1; + } + + public static int getAttributeCount(String type) { + Matcher mat = matType.matcher(type); + if (mat.find()) { + return Integer.parseInt(mat.group(1)); + } + + return 1; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/model/ArrayModelRenderer.java b/src/main/java/com/jozufozu/flywheel/backend/model/ArrayModelRenderer.java new file mode 100644 index 000000000..5347e74d1 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/model/ArrayModelRenderer.java @@ -0,0 +1,28 @@ +package com.jozufozu.flywheel.backend.model; + +import com.jozufozu.flywheel.backend.gl.GlVertexArray; + +public class ArrayModelRenderer extends ModelRenderer { + + protected GlVertexArray vao; + + public ArrayModelRenderer(BufferedModel model) { + super(model); + vao = new GlVertexArray(); + + vao.bind(); + model.setupState(); + vao.unbind(); + model.clearState(); + } + + public void draw() { + if (!model.valid()) return; + + vao.bind(); + + model.drawCall(); + + vao.unbind(); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/model/BufferedModel.java b/src/main/java/com/jozufozu/flywheel/backend/model/BufferedModel.java new file mode 100644 index 000000000..3c775cfbb --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/model/BufferedModel.java @@ -0,0 +1,93 @@ +package com.jozufozu.flywheel.backend.model; + +import static org.lwjgl.opengl.GL20.glDrawArrays; + +import java.nio.ByteBuffer; + +import com.jozufozu.flywheel.backend.Backend; +import com.jozufozu.flywheel.backend.gl.GlPrimitive; +import com.jozufozu.flywheel.backend.gl.attrib.VertexFormat; +import com.jozufozu.flywheel.backend.gl.buffer.GlBuffer; +import com.jozufozu.flywheel.backend.gl.buffer.GlBufferType; +import com.jozufozu.flywheel.util.AttribUtil; + +public class BufferedModel { + + protected final GlPrimitive primitiveMode; + protected final ByteBuffer data; + protected final VertexFormat format; + protected final int vertexCount; + protected GlBuffer vbo; + protected boolean deleted; + + public BufferedModel(GlPrimitive primitiveMode, VertexFormat format, ByteBuffer data, int vertices) { + this.primitiveMode = primitiveMode; + this.data = data; + this.format = format; + this.vertexCount = vertices; + + vbo = new GlBuffer(GlBufferType.ARRAY_BUFFER); + + vbo.bind(); + // allocate the buffer on the gpu + vbo.alloc(this.data.capacity()); + + // mirror it in system memory so we can write to it, and upload our model. + vbo.getBuffer(0, this.data.capacity()) + .put(this.data) + .flush(); + vbo.unbind(); + } + + public VertexFormat getFormat() { + return format; + } + + public int getVertexCount() { + return vertexCount; + } + + public boolean valid() { + return vertexCount > 0 && !deleted; + } + + /** + * The VBO/VAO should be bound externally. + */ + public void setupState() { + vbo.bind(); + AttribUtil.enableArrays(getAttributeCount()); + format.vertexAttribPointers(0); + } + + public void clearState() { + AttribUtil.disableArrays(getAttributeCount()); + vbo.unbind(); + } + + public void drawCall() { + glDrawArrays(primitiveMode.glEnum, 0, vertexCount); + } + + /** + * Draws many instances of this model, assuming the appropriate state is already bound. + */ + public void drawInstances(int instanceCount) { + if (!valid()) return; + + Backend.getInstance().compat.drawInstanced.drawArraysInstanced(primitiveMode, 0, vertexCount, instanceCount); + } + + public void delete() { + if (deleted) return; + + deleted = true; + vbo.delete(); + } + + public int getAttributeCount() { + return format.getAttributeCount(); + } + +} + diff --git a/src/main/java/com/jozufozu/flywheel/backend/model/ElementBuffer.java b/src/main/java/com/jozufozu/flywheel/backend/model/ElementBuffer.java new file mode 100644 index 000000000..8227ad019 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/model/ElementBuffer.java @@ -0,0 +1,25 @@ +package com.jozufozu.flywheel.backend.model; + +import com.jozufozu.flywheel.backend.gl.GlNumericType; +import com.jozufozu.flywheel.backend.gl.buffer.GlBuffer; + +public class ElementBuffer { + + private final GlBuffer buffer; + public final int elementCount; + public final GlNumericType eboIndexType; + + public ElementBuffer(GlBuffer backing, int elementCount, GlNumericType indexType) { + this.buffer = backing; + this.eboIndexType = indexType; + this.elementCount = elementCount; + } + + public void bind() { + buffer.bind(); + } + + public void unbind() { + buffer.unbind(); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/model/IndexedModel.java b/src/main/java/com/jozufozu/flywheel/backend/model/IndexedModel.java new file mode 100644 index 000000000..7c70e984c --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/model/IndexedModel.java @@ -0,0 +1,59 @@ +package com.jozufozu.flywheel.backend.model; + +import java.nio.ByteBuffer; + +import org.lwjgl.opengl.GL20; + +import com.jozufozu.flywheel.backend.Backend; +import com.jozufozu.flywheel.backend.gl.GlPrimitive; +import com.jozufozu.flywheel.backend.gl.attrib.VertexFormat; +import com.jozufozu.flywheel.core.QuadConverter; + +/** + * An indexed triangle model. Just what the driver ordered. + * + *
This should be favored over a normal BufferedModel. + */ +public class IndexedModel extends BufferedModel { + + protected ElementBuffer ebo; + + public IndexedModel(VertexFormat modelFormat, ByteBuffer buf, int vertices, ElementBuffer ebo) { + super(GlPrimitive.TRIANGLES, modelFormat, buf, vertices); + + this.ebo = ebo; + } + + public static IndexedModel fromSequentialQuads(VertexFormat modelFormat, ByteBuffer quads, int vertices) { + return new IndexedModel(modelFormat, quads, vertices, QuadConverter.getInstance().quads2Tris(vertices / 4)); + } + + @Override + public void setupState() { + super.setupState(); + ebo.bind(); + } + + @Override + public void clearState() { + super.clearState(); + ebo.unbind(); + } + + @Override + public void drawCall() { + GL20.glDrawElements(primitiveMode.glEnum, ebo.elementCount, ebo.eboIndexType.getGlEnum(), 0); + } + + @Override + public void drawInstances(int instanceCount) { + if (vertexCount <= 0 || deleted) return; + + Backend.getInstance().compat.drawInstanced.drawElementsInstanced(primitiveMode, ebo.elementCount, ebo.eboIndexType, 0, instanceCount); + } + + @Override + public void delete() { + super.delete(); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/backend/model/ModelRenderer.java b/src/main/java/com/jozufozu/flywheel/backend/model/ModelRenderer.java new file mode 100644 index 000000000..fd1efd896 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/model/ModelRenderer.java @@ -0,0 +1,25 @@ +package com.jozufozu.flywheel.backend.model; + +public class ModelRenderer { + + protected BufferedModel model; + + public ModelRenderer(BufferedModel model) { + this.model = model; + } + + /** + * Renders this model, checking first if there is anything to render. + */ + public void draw() { + if (!model.valid()) return; + + model.setupState(); + model.drawCall(); + model.clearState(); + } + + public void delete() { + model.delete(); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/AtlasStitcher.java b/src/main/java/com/jozufozu/flywheel/core/AtlasStitcher.java new file mode 100644 index 000000000..ef71f9366 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/AtlasStitcher.java @@ -0,0 +1,40 @@ +package com.jozufozu.flywheel.core; + +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.inventory.container.PlayerContainer; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.client.event.TextureStitchEvent; + +/** + * This is primarily for hacking entity textures into the block atlas. + */ +public class AtlasStitcher { + protected static final AtlasStitcher INSTANCE = new AtlasStitcher(); + + public static AtlasStitcher getInstance() { + return INSTANCE; + } + + private final List sprites = new ArrayList<>(); + + public StitchedSprite get(ResourceLocation loc) { + StitchedSprite sprite = new StitchedSprite(loc); + + sprites.add(sprite); + + return sprite; + } + + public void onTextureStitch(TextureStitchEvent.Pre event) { + if (!event.getMap() + .getId() + .equals(PlayerContainer.BLOCK_ATLAS_TEXTURE)) + return; + + sprites.stream() + .map(StitchedSprite::getLoc) + .forEach(event::addSprite); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/Contexts.java b/src/main/java/com/jozufozu/flywheel/core/Contexts.java new file mode 100644 index 000000000..18433bf76 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/Contexts.java @@ -0,0 +1,49 @@ +package com.jozufozu.flywheel.core; + +import com.jozufozu.flywheel.Flywheel; +import com.jozufozu.flywheel.backend.Backend; +import com.jozufozu.flywheel.backend.SpecMetaRegistry; +import com.jozufozu.flywheel.backend.gl.shader.ShaderType; +import com.jozufozu.flywheel.core.shader.WorldFog; +import com.jozufozu.flywheel.core.shader.WorldProgram; +import com.jozufozu.flywheel.core.shader.gamestate.FogStateProvider; +import com.jozufozu.flywheel.core.shader.gamestate.NormalDebugStateProvider; +import com.jozufozu.flywheel.event.GatherContextEvent; + +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +@Mod.EventBusSubscriber(value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.MOD) +public class Contexts { + + public static WorldContext WORLD; + public static WorldContext CRUMBLING; + + @SubscribeEvent + public static void flwInit(GatherContextEvent event) { + Backend backend = event.getBackend(); + + SpecMetaRegistry.register(FogStateProvider.INSTANCE); + SpecMetaRegistry.register(NormalDebugStateProvider.INSTANCE); + + SpecMetaRegistry.register(WorldFog.LINEAR); + SpecMetaRegistry.register(WorldFog.EXP2); + + CRUMBLING = backend.register(new WorldContext<>(backend, CrumblingProgram::new) + .withName(Names.CRUMBLING) + .withBuiltin(ShaderType.FRAGMENT, Names.CRUMBLING, "/builtin.frag") + .withBuiltin(ShaderType.VERTEX, Names.CRUMBLING, "/builtin.vert")); + + WORLD = backend.register(new WorldContext<>(backend, WorldProgram::new) + .withName(Names.WORLD) + .withBuiltin(ShaderType.FRAGMENT, Names.WORLD, "/builtin.frag") + .withBuiltin(ShaderType.VERTEX, Names.WORLD, "/builtin.vert")); + } + + public static class Names { + public static final ResourceLocation CRUMBLING = new ResourceLocation(Flywheel.ID, "context/crumbling"); + public static final ResourceLocation WORLD = new ResourceLocation(Flywheel.ID, "context/world"); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/CrumblingInstanceManager.java b/src/main/java/com/jozufozu/flywheel/core/CrumblingInstanceManager.java new file mode 100644 index 000000000..bbea00ada --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/CrumblingInstanceManager.java @@ -0,0 +1,17 @@ +package com.jozufozu.flywheel.core; + +import com.jozufozu.flywheel.backend.instancing.MaterialManager; +import com.jozufozu.flywheel.backend.instancing.tile.TileInstanceManager; + +import net.minecraft.util.math.BlockPos; + +public class CrumblingInstanceManager extends TileInstanceManager { + public CrumblingInstanceManager() { + super(new MaterialManager<>(Contexts.CRUMBLING)); + } + + @Override + protected boolean shouldFrameUpdate(BlockPos worldPos, float lookX, float lookY, float lookZ, int cX, int cY, int cZ) { + return true; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/CrumblingProgram.java b/src/main/java/com/jozufozu/flywheel/core/CrumblingProgram.java new file mode 100644 index 000000000..dff507f42 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/CrumblingProgram.java @@ -0,0 +1,31 @@ +package com.jozufozu.flywheel.core; + +import static org.lwjgl.opengl.GL20.glUniform2f; + +import java.util.List; + +import com.jozufozu.flywheel.backend.loading.Program; +import com.jozufozu.flywheel.core.shader.WorldProgram; +import com.jozufozu.flywheel.core.shader.extension.IProgramExtension; + +public class CrumblingProgram extends WorldProgram { + protected final int uTextureScale; + protected int uCrumbling; + + public CrumblingProgram(Program program, List extensions) { + super(program, extensions); + + uTextureScale = getUniformLocation("uTextureScale"); + } + + @Override + protected void registerSamplers() { + super.registerSamplers(); + uCrumbling = setSamplerBinding("uCrumbling", 4); + } + + public void setTextureScale(float x, float y) { + glUniform2f(uTextureScale, x, y); + } + +} diff --git a/src/main/java/com/jozufozu/flywheel/core/Formats.java b/src/main/java/com/jozufozu/flywheel/core/Formats.java new file mode 100644 index 000000000..060cae933 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/Formats.java @@ -0,0 +1,24 @@ +package com.jozufozu.flywheel.core; + +import com.jozufozu.flywheel.backend.gl.attrib.CommonAttributes; +import com.jozufozu.flywheel.backend.gl.attrib.MatrixAttributes; +import com.jozufozu.flywheel.backend.gl.attrib.VertexFormat; + +public class Formats { + public static final VertexFormat UNLIT_MODEL = VertexFormat.builder() + .addAttributes(CommonAttributes.VEC3, CommonAttributes.NORMAL, CommonAttributes.UV) + .build(); + + public static final VertexFormat TRANSFORMED = litInstance() + .addAttributes(MatrixAttributes.MAT4, + MatrixAttributes.MAT3) + .build(); + public static final VertexFormat ORIENTED = litInstance() + .addAttributes(CommonAttributes.VEC3, CommonAttributes.VEC3, CommonAttributes.QUATERNION) + .build(); + + public static VertexFormat.Builder litInstance() { + return VertexFormat.builder() + .addAttributes(CommonAttributes.LIGHT, CommonAttributes.RGBA); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/FullscreenQuad.java b/src/main/java/com/jozufozu/flywheel/core/FullscreenQuad.java new file mode 100644 index 000000000..c22cae5b1 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/FullscreenQuad.java @@ -0,0 +1,61 @@ +package com.jozufozu.flywheel.core; + +import org.lwjgl.opengl.GL20; + +import com.jozufozu.flywheel.backend.gl.GlNumericType; +import com.jozufozu.flywheel.backend.gl.GlVertexArray; +import com.jozufozu.flywheel.backend.gl.buffer.GlBuffer; +import com.jozufozu.flywheel.backend.gl.buffer.GlBufferType; + +import net.minecraftforge.common.util.Lazy; + +public class FullscreenQuad { + + public static final Lazy INSTANCE = Lazy.of(FullscreenQuad::new); + + private static final float[] vertices = { + // pos // tex + -1.0f, -1.0f, 0.0f, 0.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + -1.0f, 1.0f, 0.0f, 1.0f, + + -1.0f, -1.0f, 0.0f, 0.0f, + 1.0f, -1.0f, 1.0f, 0.0f, + 1.0f, 1.0f, 1.0f, 1.0f + }; + + private static final int bufferSize = vertices.length * 4; + + private final GlVertexArray vao; + private final GlBuffer vbo; + + private FullscreenQuad() { + vbo = new GlBuffer(GlBufferType.ARRAY_BUFFER); + vbo.bind(); + vbo.alloc(bufferSize); + vbo.getBuffer(0, bufferSize) + .putFloatArray(vertices) + .flush(); + + vao = new GlVertexArray(); + vao.bind(); + + GL20.glEnableVertexAttribArray(0); + + GL20.glVertexAttribPointer(0, 4, GlNumericType.FLOAT.getGlEnum(), false, 4 * 4, 0); + + vao.unbind(); + vbo.unbind(); + } + + public void draw() { + vao.bind(); + GL20.glDrawArrays(GL20.GL_TRIANGLES, 0, 6); + vao.unbind(); + } + + public void delete() { + vao.delete(); + vbo.delete(); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/Materials.java b/src/main/java/com/jozufozu/flywheel/core/Materials.java new file mode 100644 index 000000000..38d6b08b1 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/Materials.java @@ -0,0 +1,34 @@ +package com.jozufozu.flywheel.core; + +import com.jozufozu.flywheel.backend.Backend; +import com.jozufozu.flywheel.backend.instancing.InstanceData; +import com.jozufozu.flywheel.backend.instancing.MaterialSpec; +import com.jozufozu.flywheel.core.materials.ModelData; +import com.jozufozu.flywheel.core.materials.OrientedData; +import com.jozufozu.flywheel.event.GatherContextEvent; + +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +@Mod.EventBusSubscriber(value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.MOD) +public class Materials { + public static final MaterialSpec ORIENTED = register(new MaterialSpec<>(Locations.ORIENTED, Programs.ORIENTED, Formats.UNLIT_MODEL, Formats.ORIENTED, OrientedData::new)); + public static final MaterialSpec TRANSFORMED = register(new MaterialSpec<>(Locations.MODEL, Programs.TRANSFORMED, Formats.UNLIT_MODEL, Formats.TRANSFORMED, ModelData::new)); + + public static MaterialSpec register(MaterialSpec spec) { + return Backend.getInstance().register(spec); + } + + @SubscribeEvent + public static void flwInit(GatherContextEvent event) { + register(ORIENTED); + register(TRANSFORMED); + } + + public static class Locations { + public static final ResourceLocation MODEL = new ResourceLocation("create", "model"); + public static final ResourceLocation ORIENTED = new ResourceLocation("create", "oriented"); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/PartialModel.java b/src/main/java/com/jozufozu/flywheel/core/PartialModel.java new file mode 100644 index 000000000..f769f542b --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/PartialModel.java @@ -0,0 +1,56 @@ +package com.jozufozu.flywheel.core; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import net.minecraft.client.renderer.model.IBakedModel; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.client.event.ModelBakeEvent; +import net.minecraftforge.client.event.ModelRegistryEvent; +import net.minecraftforge.client.model.ModelLoader; + +/** + * A helper class for loading and accessing json models. + *

+ * Creating a PartialModel will make the associated modelLocation automatically load. + * As such, PartialModels must be initialized at or before {@link ModelRegistryEvent}. + * Once {@link ModelBakeEvent} finishes, all PartialModels (with valid modelLocations) + * will have their bakedModel fields populated. + *

+ * Attempting to create a PartialModel after ModelRegistryEvent will cause an error. + */ +public class PartialModel { + + private static boolean tooLate = false; + private static final List all = new ArrayList<>(); + + protected final ResourceLocation modelLocation; + protected IBakedModel bakedModel; + + public PartialModel(ResourceLocation modelLocation) { + + if (tooLate) throw new RuntimeException("PartialModel '" + modelLocation + "' loaded after ModelRegistryEvent"); + + this.modelLocation = modelLocation; + all.add(this); + } + + public static void onModelRegistry(ModelRegistryEvent event) { + for (PartialModel partial : all) + ModelLoader.addSpecialModel(partial.modelLocation); + + tooLate = true; + } + + public static void onModelBake(ModelBakeEvent event) { + Map modelRegistry = event.getModelRegistry(); + for (PartialModel partial : all) + partial.bakedModel = modelRegistry.get(partial.modelLocation); + } + + public IBakedModel get() { + return bakedModel; + } + +} diff --git a/src/main/java/com/jozufozu/flywheel/core/Programs.java b/src/main/java/com/jozufozu/flywheel/core/Programs.java new file mode 100644 index 000000000..a6553208f --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/Programs.java @@ -0,0 +1,10 @@ +package com.jozufozu.flywheel.core; + +import com.jozufozu.flywheel.Flywheel; + +import net.minecraft.util.ResourceLocation; + +public class Programs { + public static final ResourceLocation TRANSFORMED = new ResourceLocation(Flywheel.ID, "model"); + public static final ResourceLocation ORIENTED = new ResourceLocation(Flywheel.ID, "oriented"); +} diff --git a/src/main/java/com/jozufozu/flywheel/core/QuadConverter.java b/src/main/java/com/jozufozu/flywheel/core/QuadConverter.java new file mode 100644 index 000000000..6fa635f59 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/QuadConverter.java @@ -0,0 +1,174 @@ +package com.jozufozu.flywheel.core; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.EnumMap; +import java.util.Map; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; + +import com.jozufozu.flywheel.backend.gl.GlNumericType; +import com.jozufozu.flywheel.backend.gl.buffer.GlBuffer; +import com.jozufozu.flywheel.backend.gl.buffer.GlBufferType; +import com.jozufozu.flywheel.backend.model.ElementBuffer; +import com.jozufozu.flywheel.event.ReloadRenderersEvent; + +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.eventbus.api.EventPriority; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +/** + * A class to manage EBOs that index quads as triangles. + */ +@Mod.EventBusSubscriber(value = Dist.CLIENT) +public class QuadConverter { + + public static final int STARTING_CAPACITY = 42; + + private static QuadConverter INSTANCE; + + @Nonnull + public static QuadConverter getInstance() { + if (INSTANCE == null) { + INSTANCE = new QuadConverter(STARTING_CAPACITY); // 255 / 6 = 42 + } + + return INSTANCE; + } + + @Nullable + public static QuadConverter getNullable() { + return INSTANCE; + } + + Map ebos; + int[] capacities; + + public QuadConverter(int initialCapacity) { + this.ebos = new EnumMap<>(GlNumericType.class); + initCapacities(); + + fillBuffer(initialCapacity); + } + + public ElementBuffer quads2Tris(int quads) { + int indexCount = quads * 6; + GlNumericType type = getSmallestIndexType(indexCount); + + if (quads > getCapacity(type)) { + fillBuffer(quads, indexCount, type); + } + + return new ElementBuffer(getBuffer(type), indexCount, type); + } + + private void initCapacities() { + this.capacities = new int[GlNumericType.values().length]; + } + + private int getCapacity(GlNumericType type) { + return capacities[type.ordinal()]; + } + + private void updateCapacity(GlNumericType type, int capacity) { + if (getCapacity(type) < capacity) { + capacities[type.ordinal()] = capacity; + } + } + + public void free() { + ebos.values().forEach(GlBuffer::delete); + ebos.clear(); + initCapacities(); + } + + private void fillBuffer(int quads) { + int indexCount = quads * 6; + + fillBuffer(quads, indexCount, getSmallestIndexType(indexCount)); + } + + private void fillBuffer(int quads, int indexCount, GlNumericType type) { + MemoryStack stack = MemoryStack.stackPush(); + int bytes = indexCount * type.getByteWidth(); + + ByteBuffer indices; + if (bytes > stack.getSize()) { + indices = MemoryUtil.memAlloc(bytes); // not enough space on the preallocated stack + } else { + stack.push(); + indices = stack.malloc(bytes); + } + + indices.order(ByteOrder.nativeOrder()); + + fillBuffer(indices, type, quads); + + GlBuffer buffer = getBuffer(type); + + buffer.bind(); + buffer.upload(indices); + buffer.unbind(); + + if (bytes > stack.getSize()) { + MemoryUtil.memFree(indices); + } else { + stack.pop(); + } + + updateCapacity(type, quads); + } + + private void fillBuffer(ByteBuffer indices, GlNumericType type, int quads) { + for (int i = 0, max = 4 * quads; i < max; i += 4) { + // triangle a + type.castAndBuffer(indices, i); + type.castAndBuffer(indices, i + 1); + type.castAndBuffer(indices, i + 2); + // triangle b + type.castAndBuffer(indices, i); + type.castAndBuffer(indices, i + 2); + type.castAndBuffer(indices, i + 3); + } + indices.flip(); + } + + private GlBuffer getBuffer(GlNumericType type) { + return ebos.computeIfAbsent(type, $ -> new GlBuffer(GlBufferType.ELEMENT_ARRAY_BUFFER)); + } + + /** + * Given the needed number of indices, what is the smallest bit width type that can index everything?
+ * + *

+	 * | indexCount   | type  |
+	 * |--------------|-------|
+	 * | [0, 255)     | byte  |
+	 * | [256, 65536)	| short	|
+	 * | [65537, )	| int	|
+	 * 
+ */ + private static GlNumericType getSmallestIndexType(int indexCount) { + indexCount = indexCount >>> 8; + if (indexCount == 0) { + return GlNumericType.UBYTE; + } + indexCount = indexCount >>> 8; + if (indexCount == 0) { + return GlNumericType.USHORT; + } + + return GlNumericType.UINT; + } + + // make sure this gets reset first so it has a chance to repopulate + @SubscribeEvent(priority = EventPriority.HIGHEST) + public static void onRendererReload(ReloadRenderersEvent event) { + if (INSTANCE != null) INSTANCE.free(); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/StitchedSprite.java b/src/main/java/com/jozufozu/flywheel/core/StitchedSprite.java new file mode 100644 index 000000000..b9536b7ec --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/StitchedSprite.java @@ -0,0 +1,31 @@ +package com.jozufozu.flywheel.core; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.inventory.container.PlayerContainer; +import net.minecraft.util.ResourceLocation; + +public class StitchedSprite { + + private final ResourceLocation loc; + + TextureAtlasSprite sprite; + + public StitchedSprite(ResourceLocation loc) { + this.loc = loc; + } + + public ResourceLocation getLoc() { + return loc; + } + + public TextureAtlasSprite getSprite() { + if (sprite == null) { + sprite = Minecraft.getInstance() + .getSpriteAtlas(PlayerContainer.BLOCK_ATLAS_TEXTURE) + .apply(loc); + } + + return sprite; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/WorldContext.java b/src/main/java/com/jozufozu/flywheel/core/WorldContext.java new file mode 100644 index 000000000..1aff5eee7 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/WorldContext.java @@ -0,0 +1,165 @@ +package com.jozufozu.flywheel.core; + +import java.util.EnumMap; +import java.util.Map; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import com.jozufozu.flywheel.backend.Backend; +import com.jozufozu.flywheel.backend.ResourceUtil; +import com.jozufozu.flywheel.backend.ShaderContext; +import com.jozufozu.flywheel.backend.ShaderSources; +import com.jozufozu.flywheel.backend.gl.shader.ShaderType; +import com.jozufozu.flywheel.backend.instancing.MaterialManager; +import com.jozufozu.flywheel.backend.instancing.MaterialSpec; +import com.jozufozu.flywheel.backend.loading.InstancedArraysTemplate; +import com.jozufozu.flywheel.backend.loading.Program; +import com.jozufozu.flywheel.backend.loading.ProgramTemplate; +import com.jozufozu.flywheel.backend.loading.Shader; +import com.jozufozu.flywheel.backend.loading.ShaderLoadingException; +import com.jozufozu.flywheel.backend.loading.ShaderTransformer; +import com.jozufozu.flywheel.core.shader.ExtensibleGlProgram; +import com.jozufozu.flywheel.core.shader.IMultiProgram; +import com.jozufozu.flywheel.core.shader.StateSensitiveMultiProgram; +import com.jozufozu.flywheel.core.shader.WorldProgram; +import com.jozufozu.flywheel.util.WorldAttached; + +import net.minecraft.util.ResourceLocation; +import net.minecraft.world.IWorld; + +public class WorldContext

extends ShaderContext

{ + + private static final String declaration = "#flwbuiltins"; + private static final Pattern builtinPattern = Pattern.compile(declaration); + + protected ResourceLocation name; + protected Supplier> specStream; + protected TemplateFactory templateFactory; + + private final WorldAttached> materialManager = new WorldAttached<>($ -> new MaterialManager<>(this)); + + private final Map builtins = new EnumMap<>(ShaderType.class); + private final Map builtinSources = new EnumMap<>(ShaderType.class); + + private final ExtensibleGlProgram.Factory

factory; + + public WorldContext(Backend backend, ExtensibleGlProgram.Factory

factory) { + super(backend); + this.factory = factory; + + specStream = () -> backend.allMaterials() + .stream() + .map(MaterialSpec::getProgramName); + + templateFactory = InstancedArraysTemplate::new; + } + + public WorldContext

withName(ResourceLocation name) { + this.name = name; + return this; + } + + public WorldContext

withBuiltin(ShaderType shaderType, ResourceLocation folder, String file) { + return withBuiltin(shaderType, ResourceUtil.subPath(folder, file)); + } + + public WorldContext

withBuiltin(ShaderType shaderType, ResourceLocation file) { + builtins.put(shaderType, file); + return this; + } + + public MaterialManager

getMaterialManager(IWorld world) { + return materialManager.get(world); + } + + public WorldContext

withSpecStream(Supplier> specStream) { + this.specStream = specStream; + return this; + } + + public WorldContext

withTemplateFactory(TemplateFactory templateFactory) { + this.templateFactory = templateFactory; + return this; + } + + protected ShaderTransformer transformer; + protected ProgramTemplate template; + + @Override + public void load() { + programs.values().forEach(IMultiProgram::delete); + programs.clear(); + + Backend.log.info("Loading context '{}'", name); + + try { + builtins.forEach((type, resourceLocation) -> builtinSources.put(type, backend.sources.getShaderSource(resourceLocation))); + } catch (ShaderLoadingException e) { + backend.sources.notifyError(); + + Backend.log.error(String.format("Could not find builtin: %s", e.getMessage())); + + return; + } + + template = templateFactory.create(backend.sources); + transformer = new ShaderTransformer() + .pushStage(this::injectBuiltins) + .pushStage(Shader::processIncludes) + .pushStage(template) + .pushStage(Shader::processIncludes); + + specStream.get() + .map(backend::getSpec) + .forEach(spec -> { + + try { + programs.put(spec.name, new StateSensitiveMultiProgram<>(factory, this, spec)); + + Backend.log.debug("Loaded program {}", spec.name); + } catch (Exception e) { + Backend.log.error("Program '{}': {}", spec.name, e); + backend.sources.notifyError(); + } + }); + } + + @Override + public void delete() { + super.delete(); + + materialManager.forEach(MaterialManager::delete); + } + + @Override + protected Shader getSource(ShaderType type, ResourceLocation name) { + Shader source = super.getSource(type, name); + transformer.transformSource(source); + return source; + } + + @Override + protected Program link(Program program) { + template.attachAttributes(program); + + return super.link(program); + } + + /** + * Replace #flwbuiltins with whatever expansion this context provides for the given shader. + */ + public void injectBuiltins(Shader shader) { + Matcher matcher = builtinPattern.matcher(shader.getSource()); + + if (matcher.find()) + shader.setSource(matcher.replaceFirst(builtinSources.get(shader.type))); + else + throw new ShaderLoadingException(String.format("%s is missing %s, cannot use in World Context", shader.type.name, declaration)); + } + + public interface TemplateFactory { + ProgramTemplate create(ShaderSources loader); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/instancing/ConditionalInstance.java b/src/main/java/com/jozufozu/flywheel/core/instancing/ConditionalInstance.java new file mode 100644 index 000000000..251b00db6 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/instancing/ConditionalInstance.java @@ -0,0 +1,61 @@ +package com.jozufozu.flywheel.core.instancing; + +import java.util.Optional; +import java.util.function.Consumer; + +import javax.annotation.Nullable; + +import com.jozufozu.flywheel.backend.instancing.InstanceData; +import com.jozufozu.flywheel.backend.instancing.Instancer; + +public class ConditionalInstance { + + final Instancer model; + ICondition condition; + + Consumer setupFunc; + + @Nullable + private D instance; + + public ConditionalInstance(Instancer model) { + this.model = model; + this.condition = () -> true; + } + + public ConditionalInstance withSetupFunc(Consumer setupFunc) { + this.setupFunc = setupFunc; + return this; + } + + public ConditionalInstance withCondition(ICondition condition) { + this.condition = condition; + return this; + } + + public ConditionalInstance update() { + boolean shouldShow = condition.shouldShow(); + if (shouldShow && instance == null) { + instance = model.createInstance(); + if (setupFunc != null) setupFunc.accept(instance); + } else if (!shouldShow && instance != null) { + instance.delete(); + instance = null; + } + + return this; + } + + public Optional get() { + return Optional.ofNullable(instance); + } + + public void delete() { + if (instance != null) instance.delete(); + } + + @FunctionalInterface + public interface ICondition { + boolean shouldShow(); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/instancing/GroupInstance.java b/src/main/java/com/jozufozu/flywheel/core/instancing/GroupInstance.java new file mode 100644 index 000000000..c9084b6c0 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/instancing/GroupInstance.java @@ -0,0 +1,84 @@ +package com.jozufozu.flywheel.core.instancing; + +import java.util.AbstractCollection; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import com.jozufozu.flywheel.backend.instancing.InstanceData; +import com.jozufozu.flywheel.backend.instancing.Instancer; + +public class GroupInstance extends AbstractCollection { + + final Instancer model; + final List backing; + + public GroupInstance(Instancer model) { + this.model = model; + + this.backing = new ArrayList<>(); + } + + public GroupInstance(Instancer model, int size) { + this.model = model; + + this.backing = new ArrayList<>(size); + + for (int i = 0; i < size; i++) { + addInstance(); + } + } + + /** + * @param count + * @return True if the number of elements changed. + */ + public boolean resize(int count) { + int size = size(); + if (count == size) return false; + + if (count <= 0) { + clear(); + return size > 0; + } + + if (count > size) { + for (int i = size; i < count; i++) { + addInstance(); + } + } else { + List unnecessary = backing.subList(count, size); + unnecessary.forEach(InstanceData::delete); + unnecessary.clear(); + } + + return true; + } + + public InstanceData addInstance() { + D instance = model.createInstance(); + backing.add(instance); + + return instance; + } + + public D get(int index) { + return backing.get(index); + } + + @Override + public Iterator iterator() { + return backing.iterator(); + } + + @Override + public int size() { + return backing.size(); + } + + @Override + public void clear() { + backing.forEach(InstanceData::delete); + backing.clear(); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/instancing/SelectInstance.java b/src/main/java/com/jozufozu/flywheel/core/instancing/SelectInstance.java new file mode 100644 index 000000000..b0f812fb3 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/instancing/SelectInstance.java @@ -0,0 +1,61 @@ +package com.jozufozu.flywheel.core.instancing; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import javax.annotation.Nullable; + +import com.jozufozu.flywheel.backend.instancing.InstanceData; +import com.jozufozu.flywheel.backend.instancing.Instancer; + +public class SelectInstance { + + final List> models; + + ModelSelector selector; + + private int last = -1; + @Nullable + private D current; + + public SelectInstance(ModelSelector selector) { + this.models = new ArrayList<>(); + this.selector = selector; + } + + public SelectInstance addModel(Instancer model) { + models.add(model); + return this; + } + + public SelectInstance update() { + int i = selector.modelIndexToShow(); + + if (i < 0 || i >= models.size()) { + if (current != null) { + current.delete(); + current = null; + } + } else if (i != last) { + if (current != null) current.delete(); + + current = models.get(i).createInstance(); + } + + last = i; + return this; + } + + public Optional get() { + return Optional.ofNullable(current); + } + + public void delete() { + if (current != null) current.delete(); + } + + public interface ModelSelector { + int modelIndexToShow(); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/materials/BasicData.java b/src/main/java/com/jozufozu/flywheel/core/materials/BasicData.java new file mode 100644 index 000000000..2fc7fc285 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/materials/BasicData.java @@ -0,0 +1,77 @@ +package com.jozufozu.flywheel.core.materials; + +import com.jozufozu.flywheel.backend.gl.buffer.MappedBuffer; +import com.jozufozu.flywheel.backend.instancing.InstanceData; +import com.jozufozu.flywheel.backend.instancing.Instancer; + +public abstract class BasicData extends InstanceData implements IFlatLight { + + protected byte blockLight; + protected byte skyLight; + + protected byte r = (byte) 0xFF; + protected byte g = (byte) 0xFF; + protected byte b = (byte) 0xFF; + protected byte a = (byte) 0xFF; + + public BasicData(Instancer owner) { + super(owner); + } + + @Override + public BasicData setBlockLight(int blockLight) { + this.blockLight = (byte) (blockLight << 4); + markDirty(); + return this; + } + + @Override + public BasicData setSkyLight(int skyLight) { + this.skyLight = (byte) (skyLight << 4); + markDirty(); + return this; + } + + public BasicData setColor(int color) { + return setColor(color, false); + } + + public BasicData setColor(int color, boolean alpha) { + byte r = (byte) ((color >> 16) & 0xFF); + byte g = (byte) ((color >> 8) & 0xFF); + byte b = (byte) (color & 0xFF); + + if (alpha) { + byte a = (byte) ((color >> 24) & 0xFF); + return setColor(r, g, b, a); + } else { + return setColor(r, g, b); + } + } + + public BasicData setColor(int r, int g, int b) { + return setColor((byte) r, (byte) g, (byte) b); + } + + public BasicData setColor(byte r, byte g, byte b) { + this.r = r; + this.g = g; + this.b = b; + markDirty(); + return this; + } + + public BasicData setColor(byte r, byte g, byte b, byte a) { + this.r = r; + this.g = g; + this.b = b; + this.a = a; + markDirty(); + return this; + } + + @Override + public void write(MappedBuffer buf) { + buf.putByteArray(new byte[]{blockLight, skyLight, r, g, b, a}); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/materials/IFlatLight.java b/src/main/java/com/jozufozu/flywheel/core/materials/IFlatLight.java new file mode 100644 index 000000000..93c90dbea --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/materials/IFlatLight.java @@ -0,0 +1,27 @@ +package com.jozufozu.flywheel.core.materials; + +import com.jozufozu.flywheel.backend.instancing.InstanceData; + +/** + * An interface that implementors of {@link InstanceData} should also implement + * if they wish to make use of Flywheel's provided light update methods. + *

+ * This only covers flat lighting, smooth lighting is still TODO. + * + * @param The name of the class that implements this interface. + */ +public interface IFlatLight> { + /** + * @param blockLight An integer in the range [0, 15] representing the + * amount of block light this instance should receive. + * @return this + */ + D setBlockLight(int blockLight); + + /** + * @param skyLight An integer in the range [0, 15] representing the + * amount of sky light this instance should receive. + * @return this + */ + D setSkyLight(int skyLight); +} diff --git a/src/main/java/com/jozufozu/flywheel/core/materials/ModelData.java b/src/main/java/com/jozufozu/flywheel/core/materials/ModelData.java new file mode 100644 index 000000000..f7c99122a --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/materials/ModelData.java @@ -0,0 +1,28 @@ +package com.jozufozu.flywheel.core.materials; + +import com.jozufozu.flywheel.backend.gl.buffer.MappedBuffer; +import com.jozufozu.flywheel.backend.instancing.Instancer; +import com.jozufozu.flywheel.util.RenderUtil; +import com.mojang.blaze3d.matrix.MatrixStack; + +public class ModelData extends BasicData { + private static final float[] empty = new float[25]; + + private float[] matrices = empty; + + public ModelData(Instancer owner) { + super(owner); + } + + public ModelData setTransform(MatrixStack stack) { + matrices = RenderUtil.writeMatrixStack(stack); + markDirty(); + return this; + } + + @Override + public void write(MappedBuffer buf) { + super.write(buf); + buf.putFloatArray(matrices); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/materials/OrientedData.java b/src/main/java/com/jozufozu/flywheel/core/materials/OrientedData.java new file mode 100644 index 000000000..843d4cb00 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/materials/OrientedData.java @@ -0,0 +1,99 @@ +package com.jozufozu.flywheel.core.materials; + +import com.jozufozu.flywheel.backend.gl.buffer.MappedBuffer; +import com.jozufozu.flywheel.backend.instancing.Instancer; + +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.vector.Quaternion; +import net.minecraft.util.math.vector.Vector3d; +import net.minecraft.util.math.vector.Vector3f; + +public class OrientedData extends BasicData { + + private float posX; + private float posY; + private float posZ; + private float pivotX = 0.5f; + private float pivotY = 0.5f; + private float pivotZ = 0.5f; + private float qX; + private float qY; + private float qZ; + private float qW; + + public OrientedData(Instancer owner) { + super(owner); + } + + public OrientedData setPosition(BlockPos pos) { + return setPosition(pos.getX(), pos.getY(), pos.getZ()); + } + + public OrientedData setPosition(Vector3f pos) { + return setPosition(pos.getX(), pos.getY(), pos.getZ()); + } + + public OrientedData setPosition(float x, float y, float z) { + this.posX = x; + this.posY = y; + this.posZ = z; + markDirty(); + return this; + } + + public OrientedData nudge(float x, float y, float z) { + this.posX += x; + this.posY += y; + this.posZ += z; + markDirty(); + return this; + } + + public OrientedData setPivot(Vector3f pos) { + return setPosition(pos.getX(), pos.getY(), pos.getZ()); + } + + public OrientedData setPivot(Vector3d pos) { + return setPosition((float) pos.getX(), (float) pos.getY(), (float) pos.getZ()); + } + + public OrientedData setPivot(float x, float y, float z) { + this.pivotX = x; + this.pivotY = y; + this.pivotZ = z; + markDirty(); + return this; + } + + public OrientedData setRotation(Quaternion q) { + return setRotation(q.getX(), q.getY(), q.getZ(), q.getW()); + } + + public OrientedData setRotation(float x, float y, float z, float w) { + this.qX = x; + this.qY = y; + this.qZ = z; + this.qW = w; + markDirty(); + return this; + } + + @Override + public void write(MappedBuffer buf) { + super.write(buf); + + buf.putFloatArray(new float[]{ + posX, + posY, + posZ, + pivotX, + pivotY, + pivotZ, + qX, + qY, + qZ, + qW + }); + } +} + diff --git a/src/main/java/com/jozufozu/flywheel/core/shader/ExtensibleGlProgram.java b/src/main/java/com/jozufozu/flywheel/core/shader/ExtensibleGlProgram.java new file mode 100644 index 000000000..7da1e8b14 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/shader/ExtensibleGlProgram.java @@ -0,0 +1,71 @@ +package com.jozufozu.flywheel.core.shader; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import com.jozufozu.flywheel.backend.gl.shader.GlProgram; +import com.jozufozu.flywheel.backend.loading.Program; +import com.jozufozu.flywheel.core.shader.extension.IExtensionInstance; +import com.jozufozu.flywheel.core.shader.extension.IProgramExtension; + +/** + * A shader program that be arbitrarily "extended". This class can take in any number of program extensions, and + * will initialize them and then call their {@link IExtensionInstance#bind() bind} function every subsequent time this + * program is bound. An "extension" is something that interacts with the shader program in a way that is invisible to + * the caller using the program. This is used by some programs to implement the different fog modes. Other uses might + * include binding extra textures to allow for blocks to have normal maps, for example. As the extensions are + * per-program, this also allows for same extra specialization within a + * {@link com.jozufozu.flywheel.backend.ShaderContext ShaderContext}. + */ +public class ExtensibleGlProgram extends GlProgram { + + protected final List extensions; + + public ExtensibleGlProgram(Program program, @Nullable List extensions) { + super(program); + + if (extensions != null) { + List list = new ArrayList<>(); + for (IProgramExtension e : extensions) { + IExtensionInstance extension = e.create(this); + list.add(extension); + } + this.extensions = list; + } else { + this.extensions = Collections.emptyList(); + } + } + + @Override + public void bind() { + super.bind(); + + extensions.forEach(IExtensionInstance::bind); + } + + @Override + public String toString() { + return "ExtensibleGlProgram{" + + "name=" + name + + ", extensions=" + extensions + + '}'; + } + + /** + * A factory interface to create {@link GlProgram}s parameterized by a list of extensions. This doesn't necessarily + * have to return an {@link ExtensibleGlProgram} if implementors want more flexibility for whatever reason. + */ + public interface Factory

{ + + @Nonnull + P create(Program program, @Nullable List extensions); + + default P create(Program program) { + return create(program, null); + } + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/shader/FogMode.java b/src/main/java/com/jozufozu/flywheel/core/shader/FogMode.java new file mode 100644 index 000000000..2ff577b0a --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/shader/FogMode.java @@ -0,0 +1,60 @@ +package com.jozufozu.flywheel.core.shader; + +import org.lwjgl.opengl.GL20; + +import com.jozufozu.flywheel.Flywheel; +import com.jozufozu.flywheel.backend.gl.shader.GlProgram; +import com.jozufozu.flywheel.core.shader.extension.IExtensionInstance; + +import net.minecraft.util.ResourceLocation; + +public abstract class FogMode { + + public static class Linear implements IExtensionInstance { + + public static final ResourceLocation NAME = new ResourceLocation(Flywheel.ID, "fog_linear"); + + private final int uFogColor; + private final int uFogRange; + + public Linear(GlProgram program) { + this.uFogColor = program.getUniformLocation("uFogColor"); + this.uFogRange = program.getUniformLocation("uFogRange"); + } + + @Override + public void bind() { + GL20.glUniform2f(uFogRange, GlFog.getFogStart(), GlFog.getFogEnd()); + GL20.glUniform4fv(uFogColor, GlFog.FOG_COLOR); + } + + @Override + public ResourceLocation name() { + return NAME; + } + } + + public static class Exp2 implements IExtensionInstance { + + public static final ResourceLocation NAME = new ResourceLocation(Flywheel.ID, "fog_exp2"); + + private final int uFogColor; + private final int uFogDensity; + + public Exp2(GlProgram program) { + this.uFogColor = program.getUniformLocation("uFogColor"); + this.uFogDensity = program.getUniformLocation("uFogDensity"); + } + + @Override + public void bind() { + GL20.glUniform1f(uFogDensity, GlFog.getFogDensity()); + GL20.glUniform4fv(uFogColor, GlFog.FOG_COLOR); + } + + @Override + public ResourceLocation name() { + return NAME; + } + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/shader/GlFog.java b/src/main/java/com/jozufozu/flywheel/core/shader/GlFog.java new file mode 100644 index 000000000..55428f94d --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/shader/GlFog.java @@ -0,0 +1,47 @@ +package com.jozufozu.flywheel.core.shader; + +import org.lwjgl.opengl.GL11; + +import com.mojang.blaze3d.platform.GlStateManager; + +public class GlFog { + public static float[] FOG_COLOR = new float[]{0, 0, 0, 0}; + + public static boolean fogEnabled() { + return GlStateManager.FOG.field_179049_a.field_179201_b; + } + + public static int getFogModeGlEnum() { + return GlStateManager.FOG.field_179047_b; + } + + public static float getFogDensity() { + return GlStateManager.FOG.field_179048_c; + } + + public static float getFogEnd() { + return GlStateManager.FOG.field_179046_e; + } + + public static float getFogStart() { + return GlStateManager.FOG.field_179045_d; + } + + public static WorldFog getFogMode() { + if (!fogEnabled()) { + return WorldFog.NONE; + } + + int mode = getFogModeGlEnum(); + + switch (mode) { + case GL11.GL_EXP2: + case GL11.GL_EXP: + return WorldFog.EXP2; + case GL11.GL_LINEAR: + return WorldFog.LINEAR; + default: + throw new UnsupportedOperationException("Unknown fog mode: " + mode); + } + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/shader/IMultiProgram.java b/src/main/java/com/jozufozu/flywheel/core/shader/IMultiProgram.java new file mode 100644 index 000000000..924a989a5 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/shader/IMultiProgram.java @@ -0,0 +1,26 @@ +package com.jozufozu.flywheel.core.shader; + +import java.util.function.Supplier; + +import com.jozufozu.flywheel.backend.gl.shader.GlProgram; + +/** + * Encapsulates any number of shader programs for use in similar contexts. + * Allows the implementor to choose which shader program to use based on arbitrary state. + * + * @param

+ */ +public interface IMultiProgram

extends Supplier

{ + + /** + * Get the shader program most suited for the current game state. + * + * @return The one true program. + */ + P get(); + + /** + * Delete all shader programs encapsulated by your implementation. + */ + void delete(); +} diff --git a/src/main/java/com/jozufozu/flywheel/core/shader/IProgramCallback.java b/src/main/java/com/jozufozu/flywheel/core/shader/IProgramCallback.java new file mode 100644 index 000000000..4dae733e9 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/shader/IProgramCallback.java @@ -0,0 +1,19 @@ +package com.jozufozu.flywheel.core.shader; + +import com.jozufozu.flywheel.backend.gl.shader.GlProgram; + +/** + * Used to define shader uniforms. + */ +@FunctionalInterface +public interface IProgramCallback

{ + + void call(P program); + + default IProgramCallback

andThen(IProgramCallback

other) { + return program -> { + call(program); + other.call(program); + }; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/shader/StateSensitiveMultiProgram.java b/src/main/java/com/jozufozu/flywheel/core/shader/StateSensitiveMultiProgram.java new file mode 100644 index 000000000..22311cbfc --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/shader/StateSensitiveMultiProgram.java @@ -0,0 +1,52 @@ +package com.jozufozu.flywheel.core.shader; + +import java.util.ArrayList; +import java.util.List; + +import com.jozufozu.flywheel.backend.ShaderContext; +import com.jozufozu.flywheel.backend.gl.shader.GlProgram; +import com.jozufozu.flywheel.backend.loading.Program; +import com.jozufozu.flywheel.core.shader.spec.IContextCondition; +import com.jozufozu.flywheel.core.shader.spec.ProgramSpec; +import com.jozufozu.flywheel.core.shader.spec.ProgramState; +import com.jozufozu.flywheel.util.Pair; + +public class StateSensitiveMultiProgram

implements IMultiProgram

{ + + List> variants; + P fallback; + + public StateSensitiveMultiProgram(ExtensibleGlProgram.Factory

factory, ShaderContext

context, ProgramSpec p) { + variants = new ArrayList<>(p.states.size()); + + for (ProgramState state : p.states) { + + Program variant = context.loadAndLink(p, state); + + Pair pair = Pair.of(state.getContext(), factory.create(variant, state.getExtensions())); + + variants.add(pair); + } + + fallback = factory.create(context.loadAndLink(p, null)); + } + + @Override + public P get() { + for (Pair variant : variants) { + if (variant.getFirst().get()) + return variant.getSecond(); + } + + return fallback; + } + + @Override + public void delete() { + for (Pair variant : variants) { + variant.getSecond().delete(); + } + + fallback.delete(); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/shader/WorldFog.java b/src/main/java/com/jozufozu/flywheel/core/shader/WorldFog.java new file mode 100644 index 000000000..0f8243f85 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/shader/WorldFog.java @@ -0,0 +1,47 @@ +package com.jozufozu.flywheel.core.shader; + +import java.util.function.Function; + +import com.jozufozu.flywheel.Flywheel; +import com.jozufozu.flywheel.backend.gl.shader.GlProgram; +import com.jozufozu.flywheel.core.shader.extension.IExtensionInstance; +import com.jozufozu.flywheel.core.shader.extension.IProgramExtension; +import com.jozufozu.flywheel.core.shader.extension.UnitExtensionInstance; + +import net.minecraft.util.ResourceLocation; + +public enum WorldFog implements IProgramExtension { + NONE("none", UnitExtensionInstance::new), + LINEAR("linear", FogMode.Linear::new), + EXP2("exp2", FogMode.Exp2::new), + ; + + private final ResourceLocation id; + private final String name; + private final Function fogFactory; + + WorldFog(String name, Function fogFactory) { + this.id = new ResourceLocation(Flywheel.ID, "fog_" + name); + this.name = name; + this.fogFactory = fogFactory; + } + + public String getName() { + return name; + } + + @Override + public IExtensionInstance create(GlProgram program) { + return fogFactory.apply(program); + } + + @Override + public ResourceLocation getID() { + return id; + } + + @Override + public String toString() { + return name; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/shader/WorldProgram.java b/src/main/java/com/jozufozu/flywheel/core/shader/WorldProgram.java new file mode 100644 index 000000000..f9a6a940f --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/shader/WorldProgram.java @@ -0,0 +1,56 @@ +package com.jozufozu.flywheel.core.shader; + +import static org.lwjgl.opengl.GL20.glUniform1f; +import static org.lwjgl.opengl.GL20.glUniform3f; + +import java.util.List; + +import com.jozufozu.flywheel.backend.loading.Program; +import com.jozufozu.flywheel.core.shader.extension.IProgramExtension; + +import com.jozufozu.flywheel.util.AnimationTickHolder; +import net.minecraft.util.math.vector.Matrix4f; + +public class WorldProgram extends ExtensibleGlProgram { + protected final int uTime; + protected final int uViewProjection; + protected final int uCameraPos; + + protected int uBlockAtlas; + protected int uLightMap; + + public WorldProgram(Program program, List extensions) { + super(program, extensions); + uTime = getUniformLocation("uTime"); + uViewProjection = getUniformLocation("uViewProjection"); + uCameraPos = getUniformLocation("uCameraPos"); + + bind(); + registerSamplers(); + unbind(); + } + + protected void registerSamplers() { + uBlockAtlas = setSamplerBinding("uBlockAtlas", 0); + uLightMap = setSamplerBinding("uLightMap", 2); + } + + public void uploadViewProjection(Matrix4f viewProjection) { + uploadMatrixUniform(uViewProjection, viewProjection); + } + + public void uploadCameraPos(double camX, double camY, double camZ) { + glUniform3f(uCameraPos, (float) camX, (float) camY, (float) camZ); + } + + public void uploadTime(float renderTime) { + glUniform1f(uTime, renderTime); + } + + @Override + public void bind() { + super.bind(); + + uploadTime(AnimationTickHolder.getRenderTime()); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/shader/extension/IExtensionInstance.java b/src/main/java/com/jozufozu/flywheel/core/shader/extension/IExtensionInstance.java new file mode 100644 index 000000000..8cd43311e --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/shader/extension/IExtensionInstance.java @@ -0,0 +1,14 @@ +package com.jozufozu.flywheel.core.shader.extension; + +import net.minecraft.util.ResourceLocation; + +public interface IExtensionInstance { + + /** + * Bind the extra program state. It is recommended to grab the state information from global variables, + * or local variables passed through a {@link IProgramExtension}. + */ + void bind(); + + ResourceLocation name(); +} diff --git a/src/main/java/com/jozufozu/flywheel/core/shader/extension/IProgramExtension.java b/src/main/java/com/jozufozu/flywheel/core/shader/extension/IProgramExtension.java new file mode 100644 index 000000000..bdc3701ec --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/shader/extension/IProgramExtension.java @@ -0,0 +1,26 @@ +package com.jozufozu.flywheel.core.shader.extension; + +import com.jozufozu.flywheel.backend.SpecMetaRegistry; +import com.jozufozu.flywheel.backend.gl.shader.GlProgram; +import com.mojang.serialization.Codec; + +import net.minecraft.util.ResourceLocation; + +/** + * A factory interface for creating {@link IExtensionInstance}s. These are what end up being passed in + * during shader program construction. + */ +public interface IProgramExtension { + + Codec CODEC = ResourceLocation.CODEC.xmap(SpecMetaRegistry::getExtension, IProgramExtension::getID); + + /** + * Construct the extension, binding any necessary information using the provided {@link GlProgram}. + * + * @param program The program being extended. + * @return An extension object, possibly initialized using the program. + */ + IExtensionInstance create(GlProgram program); + + ResourceLocation getID(); +} diff --git a/src/main/java/com/jozufozu/flywheel/core/shader/extension/UnitExtensionInstance.java b/src/main/java/com/jozufozu/flywheel/core/shader/extension/UnitExtensionInstance.java new file mode 100644 index 000000000..0fc5d76f9 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/shader/extension/UnitExtensionInstance.java @@ -0,0 +1,23 @@ +package com.jozufozu.flywheel.core.shader.extension; + +import com.jozufozu.flywheel.Flywheel; +import com.jozufozu.flywheel.backend.gl.shader.GlProgram; + +import net.minecraft.util.ResourceLocation; + +public class UnitExtensionInstance implements IExtensionInstance { + + public static final ResourceLocation NAME = new ResourceLocation(Flywheel.ID, "unit"); + + public UnitExtensionInstance(GlProgram program) { } + + @Override + public void bind() { + + } + + @Override + public ResourceLocation name() { + return NAME; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/shader/gamestate/FogStateProvider.java b/src/main/java/com/jozufozu/flywheel/core/shader/gamestate/FogStateProvider.java new file mode 100644 index 000000000..8423896ea --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/shader/gamestate/FogStateProvider.java @@ -0,0 +1,22 @@ +package com.jozufozu.flywheel.core.shader.gamestate; + +import com.jozufozu.flywheel.Flywheel; +import com.jozufozu.flywheel.core.shader.GlFog; + +import net.minecraft.util.ResourceLocation; + +public class FogStateProvider implements IGameStateProvider { + + public static final FogStateProvider INSTANCE = new FogStateProvider(); + public static final ResourceLocation NAME = new ResourceLocation(Flywheel.ID, "fog_mode"); + + @Override + public ResourceLocation getID() { + return NAME; + } + + @Override + public Object getValue() { + return GlFog.getFogMode(); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/shader/gamestate/IGameStateProvider.java b/src/main/java/com/jozufozu/flywheel/core/shader/gamestate/IGameStateProvider.java new file mode 100644 index 000000000..17ece39ec --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/shader/gamestate/IGameStateProvider.java @@ -0,0 +1,15 @@ +package com.jozufozu.flywheel.core.shader.gamestate; + +import com.jozufozu.flywheel.backend.SpecMetaRegistry; +import com.mojang.serialization.Codec; + +import net.minecraft.util.ResourceLocation; + +public interface IGameStateProvider { + + Codec CODEC = ResourceLocation.CODEC.xmap(SpecMetaRegistry::getStateProvider, IGameStateProvider::getID); + + ResourceLocation getID(); + + Object getValue(); +} diff --git a/src/main/java/com/jozufozu/flywheel/core/shader/gamestate/NormalDebugStateProvider.java b/src/main/java/com/jozufozu/flywheel/core/shader/gamestate/NormalDebugStateProvider.java new file mode 100644 index 000000000..e05890003 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/shader/gamestate/NormalDebugStateProvider.java @@ -0,0 +1,26 @@ +package com.jozufozu.flywheel.core.shader.gamestate; + +import com.jozufozu.flywheel.Flywheel; +import com.jozufozu.flywheel.core.shader.spec.IBooleanStateProvider; + +import net.minecraft.util.ResourceLocation; + +public class NormalDebugStateProvider implements IBooleanStateProvider { + + public static final NormalDebugStateProvider INSTANCE = new NormalDebugStateProvider(); + public static final ResourceLocation NAME = new ResourceLocation(Flywheel.ID, "normal_debug"); + + protected NormalDebugStateProvider() { + + } + + @Override + public boolean isTrue() { + return false; + } + + @Override + public ResourceLocation getID() { + return NAME; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/shader/spec/BooleanContextCondition.java b/src/main/java/com/jozufozu/flywheel/core/shader/spec/BooleanContextCondition.java new file mode 100644 index 000000000..a61a69f51 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/shader/spec/BooleanContextCondition.java @@ -0,0 +1,37 @@ +package com.jozufozu.flywheel.core.shader.spec; + +import com.jozufozu.flywheel.core.shader.gamestate.IGameStateProvider; +import com.mojang.serialization.Codec; + +import net.minecraft.util.ResourceLocation; + +public class BooleanContextCondition implements IContextCondition { + + public static final Codec BOOLEAN_SUGAR = IGameStateProvider.CODEC.xmap(gameContext -> { + if (gameContext instanceof IBooleanStateProvider) { + return new BooleanContextCondition(((IBooleanStateProvider) gameContext)); + } + + return null; + }, IContextCondition::contextProvider); + protected final IBooleanStateProvider context; + + public BooleanContextCondition(IBooleanStateProvider context) { + this.context = context; + } + + @Override + public ResourceLocation getID() { + return context.getID(); + } + + @Override + public IGameStateProvider contextProvider() { + return context; + } + + @Override + public boolean get() { + return context.isTrue(); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/shader/spec/IBooleanStateProvider.java b/src/main/java/com/jozufozu/flywheel/core/shader/spec/IBooleanStateProvider.java new file mode 100644 index 000000000..1f1265724 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/shader/spec/IBooleanStateProvider.java @@ -0,0 +1,13 @@ +package com.jozufozu.flywheel.core.shader.spec; + +import com.jozufozu.flywheel.core.shader.gamestate.IGameStateProvider; + +public interface IBooleanStateProvider extends IGameStateProvider { + + boolean isTrue(); + + @Override + default Boolean getValue() { + return isTrue(); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/shader/spec/IContextCondition.java b/src/main/java/com/jozufozu/flywheel/core/shader/spec/IContextCondition.java new file mode 100644 index 000000000..5d6d7ee42 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/shader/spec/IContextCondition.java @@ -0,0 +1,14 @@ +package com.jozufozu.flywheel.core.shader.spec; + +import com.jozufozu.flywheel.core.shader.gamestate.IGameStateProvider; + +import net.minecraft.util.ResourceLocation; + +public interface IContextCondition { + + ResourceLocation getID(); + + IGameStateProvider contextProvider(); + + boolean get(); +} diff --git a/src/main/java/com/jozufozu/flywheel/core/shader/spec/ProgramSpec.java b/src/main/java/com/jozufozu/flywheel/core/shader/spec/ProgramSpec.java new file mode 100644 index 000000000..391cc7c65 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/shader/spec/ProgramSpec.java @@ -0,0 +1,61 @@ +package com.jozufozu.flywheel.core.shader.spec; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +import net.minecraft.util.ResourceLocation; + +public class ProgramSpec { + + // TODO: Block model style inheritance? + public static final Codec CODEC = RecordCodecBuilder.create(instance -> + instance.group( + ResourceLocation.CODEC.fieldOf("vert") + .forGetter(ProgramSpec::getVert), + ResourceLocation.CODEC.fieldOf("frag") + .forGetter(ProgramSpec::getFrag), + ProgramState.CODEC.listOf() + .optionalFieldOf("states", Collections.emptyList()) + .forGetter(ProgramSpec::getStates) + ).apply(instance, ProgramSpec::new)); + + public ResourceLocation name; + public final ResourceLocation vert; + public final ResourceLocation frag; + + public final List states; + + public ProgramSpec(ResourceLocation vert, ResourceLocation frag, List states) { + this.vert = vert; + this.frag = frag; + this.states = states; + } + + public ProgramSpec(ResourceLocation name, ResourceLocation vert, ResourceLocation frag) { + this.name = name; + this.vert = vert; + this.frag = frag; + this.states = new ArrayList<>(); + } + + public void setName(ResourceLocation name) { + this.name = name; + } + + public ResourceLocation getVert() { + return vert; + } + + public ResourceLocation getFrag() { + return frag; + } + + public List getStates() { + return states; + } + +} diff --git a/src/main/java/com/jozufozu/flywheel/core/shader/spec/ProgramState.java b/src/main/java/com/jozufozu/flywheel/core/shader/spec/ProgramState.java new file mode 100644 index 000000000..cd2f74547 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/shader/spec/ProgramState.java @@ -0,0 +1,67 @@ +package com.jozufozu.flywheel.core.shader.spec; + +import java.util.Collections; +import java.util.List; + +import com.jozufozu.flywheel.core.shader.extension.IProgramExtension; +import com.jozufozu.flywheel.util.CodecUtil; +import com.mojang.datafixers.util.Either; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +public class ProgramState { + + // TODO: Use Codec.dispatch + private static final Codec WHEN = Codec.either( + BooleanContextCondition.BOOLEAN_SUGAR, + SpecificValueCondition.CODEC + ).flatXmap( + either -> either.map(DataResult::success, DataResult::success), + any -> { + if (any instanceof BooleanContextCondition) { + return DataResult.success(Either.left((BooleanContextCondition) any)); + } + + if (any instanceof SpecificValueCondition) { + return DataResult.success(Either.right((SpecificValueCondition) any)); + } + + return DataResult.error("unknown context condition"); + } + ); + + public static final Codec CODEC = RecordCodecBuilder.create(state -> + state.group( + WHEN.fieldOf("when") + .forGetter(ProgramState::getContext), + CodecUtil.oneOrMore(Codec.STRING) + .optionalFieldOf("define", Collections.emptyList()) + .forGetter(ProgramState::getDefines), + CodecUtil.oneOrMore(IProgramExtension.CODEC) + .optionalFieldOf("extend", Collections.emptyList()) + .forGetter(ProgramState::getExtensions) + ).apply(state, ProgramState::new)); + + private final IContextCondition context; + private final List defines; + private final List extensions; + + public ProgramState(IContextCondition context, List defines, List extensions) { + this.context = context; + this.defines = defines; + this.extensions = extensions; + } + + public IContextCondition getContext() { + return context; + } + + public List getDefines() { + return defines; + } + + public List getExtensions() { + return extensions; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/core/shader/spec/SpecificValueCondition.java b/src/main/java/com/jozufozu/flywheel/core/shader/spec/SpecificValueCondition.java new file mode 100644 index 000000000..cefc0d868 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/core/shader/spec/SpecificValueCondition.java @@ -0,0 +1,42 @@ +package com.jozufozu.flywheel.core.shader.spec; + +import com.jozufozu.flywheel.core.shader.gamestate.IGameStateProvider; +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +import net.minecraft.util.ResourceLocation; + +public class SpecificValueCondition implements IContextCondition { + + public static final Codec CODEC = RecordCodecBuilder.create(condition -> condition.group( + IGameStateProvider.CODEC.fieldOf("provider").forGetter(SpecificValueCondition::contextProvider), + Codec.STRING.fieldOf("value").forGetter(SpecificValueCondition::getValue) + ).apply(condition, SpecificValueCondition::new)); + + private final String required; + private final IGameStateProvider context; + + public SpecificValueCondition(IGameStateProvider context, String required) { + this.required = required; + this.context = context; + } + + @Override + public ResourceLocation getID() { + return context.getID(); + } + + public String getValue() { + return required; + } + + @Override + public IGameStateProvider contextProvider() { + return context; + } + + @Override + public boolean get() { + return required.equals(context.getValue().toString()); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/event/BeginFrameEvent.java b/src/main/java/com/jozufozu/flywheel/event/BeginFrameEvent.java new file mode 100644 index 000000000..042cc1acd --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/event/BeginFrameEvent.java @@ -0,0 +1,45 @@ +package com.jozufozu.flywheel.event; + +import com.mojang.blaze3d.matrix.MatrixStack; + +import net.minecraft.client.renderer.ActiveRenderInfo; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.world.ClientWorld; +import net.minecraftforge.eventbus.api.Event; + +public class BeginFrameEvent extends Event { + private final ClientWorld world; + private final MatrixStack stack; + private final ActiveRenderInfo info; + private final GameRenderer gameRenderer; + private final LightTexture lightTexture; + + public BeginFrameEvent(ClientWorld world, MatrixStack stack, ActiveRenderInfo info, GameRenderer gameRenderer, LightTexture lightTexture) { + this.world = world; + this.stack = stack; + this.info = info; + this.gameRenderer = gameRenderer; + this.lightTexture = lightTexture; + } + + public ClientWorld getWorld() { + return world; + } + + public MatrixStack getStack() { + return stack; + } + + public ActiveRenderInfo getInfo() { + return info; + } + + public GameRenderer getGameRenderer() { + return gameRenderer; + } + + public LightTexture getLightTexture() { + return lightTexture; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/event/EntityWorldHandler.java b/src/main/java/com/jozufozu/flywheel/event/EntityWorldHandler.java new file mode 100644 index 000000000..8c668503e --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/event/EntityWorldHandler.java @@ -0,0 +1,27 @@ +package com.jozufozu.flywheel.event; + +import com.jozufozu.flywheel.backend.instancing.InstancedRenderDispatcher; + +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.event.entity.EntityJoinWorldEvent; +import net.minecraftforge.event.entity.EntityLeaveWorldEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +@Mod.EventBusSubscriber(Dist.CLIENT) +public class EntityWorldHandler { + + @SubscribeEvent + public static void onEntityJoinWorld(EntityJoinWorldEvent event) { + if (event.getWorld().isRemote) + InstancedRenderDispatcher.getEntities(event.getWorld()) + .queueAdd(event.getEntity()); + } + + @SubscribeEvent + public static void onEntityLeaveWorld(EntityLeaveWorldEvent event) { + if (event.getWorld().isRemote) + InstancedRenderDispatcher.getEntities(event.getWorld()) + .remove(event.getEntity()); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/event/ForgeEvents.java b/src/main/java/com/jozufozu/flywheel/event/ForgeEvents.java new file mode 100644 index 000000000..e89ccd26d --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/event/ForgeEvents.java @@ -0,0 +1,56 @@ +package com.jozufozu.flywheel.event; + +import java.util.ArrayList; + +import com.jozufozu.flywheel.backend.Backend; +import com.jozufozu.flywheel.backend.instancing.InstancedRenderDispatcher; +import com.jozufozu.flywheel.backend.instancing.entity.EntityInstanceManager; +import com.jozufozu.flywheel.backend.instancing.tile.TileInstanceManager; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.world.IWorld; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.client.event.RenderGameOverlayEvent; +import net.minecraftforge.event.world.WorldEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +@Mod.EventBusSubscriber(value = Dist.CLIENT) +public class ForgeEvents { + + @SubscribeEvent + public static void addToDebugScreen(RenderGameOverlayEvent.Text event) { + + if (Minecraft.getInstance().gameSettings.showDebugInfo) { + + ArrayList right = event.getRight(); + + String text = "Flywheel: " + Backend.getInstance().getBackendDescriptor(); + if (right.size() < 10) { + right.add(""); + right.add(text); + } else { + right.add(9, ""); + right.add(10, text); + } + } + } + + @SubscribeEvent + public static void onLoadWorld(WorldEvent.Load event) { + IWorld world = event.getWorld(); + + if (Backend.isFlywheelWorld(world)) { + ClientWorld clientWorld = (ClientWorld) world; + + TileInstanceManager tiles = InstancedRenderDispatcher.getTiles(world); + tiles.invalidate(); + clientWorld.loadedTileEntityList.forEach(tiles::add); + + EntityInstanceManager entities = InstancedRenderDispatcher.getEntities(world); + entities.invalidate(); + clientWorld.getAllEntities().forEach(entities::add); + } + } +} diff --git a/src/main/java/com/jozufozu/flywheel/event/GatherContextEvent.java b/src/main/java/com/jozufozu/flywheel/event/GatherContextEvent.java new file mode 100644 index 000000000..a409d4363 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/event/GatherContextEvent.java @@ -0,0 +1,19 @@ +package com.jozufozu.flywheel.event; + +import com.jozufozu.flywheel.backend.Backend; + +import net.minecraftforge.eventbus.api.Event; +import net.minecraftforge.fml.event.lifecycle.IModBusEvent; + +public class GatherContextEvent extends Event implements IModBusEvent { + + private final Backend backend; + + public GatherContextEvent(Backend backend) { + this.backend = backend; + } + + public Backend getBackend() { + return backend; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/event/ReloadRenderersEvent.java b/src/main/java/com/jozufozu/flywheel/event/ReloadRenderersEvent.java new file mode 100644 index 000000000..dc7bac5fb --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/event/ReloadRenderersEvent.java @@ -0,0 +1,16 @@ +package com.jozufozu.flywheel.event; + +import net.minecraft.client.world.ClientWorld; +import net.minecraftforge.eventbus.api.Event; + +public class ReloadRenderersEvent extends Event { + private final ClientWorld world; + + public ReloadRenderersEvent(ClientWorld world) { + this.world = world; + } + + public ClientWorld getWorld() { + return world; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/event/RenderLayerEvent.java b/src/main/java/com/jozufozu/flywheel/event/RenderLayerEvent.java new file mode 100644 index 000000000..a22816f71 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/event/RenderLayerEvent.java @@ -0,0 +1,48 @@ +package com.jozufozu.flywheel.event; + +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.util.math.vector.Matrix4f; +import net.minecraftforge.eventbus.api.Event; + +public class RenderLayerEvent extends Event { + private final ClientWorld world; + public final RenderType type; + public final Matrix4f viewProjection; + public final double camX; + public final double camY; + public final double camZ; + + public RenderLayerEvent(ClientWorld world, RenderType type, Matrix4f viewProjection, double camX, double camY, double camZ) { + this.world = world; + this.type = type; + this.viewProjection = viewProjection; + this.camX = camX; + this.camY = camY; + this.camZ = camZ; + } + + public ClientWorld getWorld() { + return world; + } + + public RenderType getType() { + return type; + } + + public Matrix4f getViewProjection() { + return viewProjection; + } + + public double getCamX() { + return camX; + } + + public double getCamY() { + return camY; + } + + public double getCamZ() { + return camZ; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/light/GridAlignedBB.java b/src/main/java/com/jozufozu/flywheel/light/GridAlignedBB.java new file mode 100644 index 000000000..29e66663a --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/light/GridAlignedBB.java @@ -0,0 +1,323 @@ +package com.jozufozu.flywheel.light; + +import static com.jozufozu.flywheel.util.RenderUtil.isPowerOf2; + +import com.jozufozu.flywheel.util.RenderUtil; + +import net.minecraft.util.Direction; +import net.minecraft.util.math.AxisAlignedBB; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.SectionPos; +import net.minecraft.util.math.vector.Vector3i; + +public class GridAlignedBB { + public int minX; + public int minY; + public int minZ; + public int maxX; + public int maxY; + public int maxZ; + + public GridAlignedBB(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) { + this.minX = minX; + this.minY = minY; + this.minZ = minZ; + this.maxX = maxX; + this.maxY = maxY; + this.maxZ = maxZ; + } + + public static GridAlignedBB ofRadius(int radius) { + return new GridAlignedBB(-radius, -radius, -radius, radius + 1, radius + 1, radius + 1); + } + + public static GridAlignedBB copy(GridAlignedBB bb) { + return new GridAlignedBB(bb.minX, bb.minY, bb.minZ, bb.maxX, bb.maxY, bb.maxZ); + } + + public static GridAlignedBB from(AxisAlignedBB aabb) { + int minX = (int) Math.floor(aabb.minX); + int minY = (int) Math.floor(aabb.minY); + int minZ = (int) Math.floor(aabb.minZ); + int maxX = (int) Math.ceil(aabb.maxX); + int maxY = (int) Math.ceil(aabb.maxY); + int maxZ = (int) Math.ceil(aabb.maxZ); + return new GridAlignedBB(minX, minY, minZ, maxX, maxY, maxZ); + } + + public static GridAlignedBB from(SectionPos pos) { + return new GridAlignedBB(pos.getWorldStartX(), + pos.getWorldStartY(), + pos.getWorldStartZ(), + pos.getWorldEndX() + 1, + pos.getWorldEndY() + 1, + pos.getWorldEndZ() + 1); + } + + public static GridAlignedBB from(BlockPos start, BlockPos end) { + return new GridAlignedBB(start.getX(), + start.getY(), + start.getZ(), + end.getX() + 1, + end.getY() + 1, + end.getZ() + 1); + } + + public static GridAlignedBB from(int sectionX, int sectionZ) { + int startX = sectionX << 4; + int startZ = sectionZ << 4; + return new GridAlignedBB(startX, + 0, + startZ, + startX + 16, + 256, + startZ + 16); + } + + public static AxisAlignedBB toAABB(GridAlignedBB bb) { + return new AxisAlignedBB(bb.minX, bb.minY, bb.minZ, bb.maxX, bb.maxY, bb.maxZ); + } + + public GridAlignedBB copy() { + return copy(this); + } + + public boolean sameAs(GridAlignedBB other) { + return minX == other.minX && + minY == other.minY && + minZ == other.minZ && + maxX == other.maxX && + maxY == other.maxY && + maxZ == other.maxZ; + } + + public void fixMinMax() { + int minX = Math.min(this.minX, this.maxX); + int minY = Math.min(this.minY, this.maxY); + int minZ = Math.min(this.minZ, this.maxZ); + int maxX = Math.max(this.minX, this.maxX); + int maxY = Math.max(this.minY, this.maxY); + int maxZ = Math.max(this.minZ, this.maxZ); + + this.minX = minX; + this.minY = minY; + this.minZ = minZ; + this.maxX = maxX; + this.maxY = maxY; + this.maxZ = maxZ; + } + + public int sizeX() { + return maxX - minX; + } + + public int sizeY() { + return maxY - minY; + } + + public int sizeZ() { + return maxZ - minZ; + } + + public int volume() { + return sizeX() * sizeY() * sizeZ(); + } + + public boolean empty() { + // if any dimension has side length 0 this box contains no volume + return minX == maxX || + minY == maxY || + minZ == maxZ; + } + + public void translate(Vector3i by) { + translate(by.getX(), by.getY(), by.getZ()); + } + + public void translate(int x, int y, int z) { + minX += x; + maxX += x; + minY += y; + maxY += y; + minZ += z; + maxZ += z; + } + + public void mirrorAbout(Direction.Axis axis) { + Vector3i axisVec = Direction.getFacingFromAxis(Direction.AxisDirection.POSITIVE, axis).getDirectionVec(); + int flipX = axisVec.getX() - 1; + int flipY = axisVec.getY() - 1; + int flipZ = axisVec.getZ() - 1; + + int maxX = this.maxX * flipX; + int maxY = this.maxY * flipY; + int maxZ = this.maxZ * flipZ; + this.maxX = this.minX * flipX; + this.maxY = this.minY * flipY; + this.maxZ = this.minZ * flipZ; + this.minX = maxX; + this.minY = maxY; + this.minZ = maxZ; + } + + /** + * Grow this bounding box to have power of 2 side length, scaling from the center. + */ + public void nextPowerOf2Centered() { + int sizeX = sizeX(); + int sizeY = sizeY(); + int sizeZ = sizeZ(); + + int newSizeX = RenderUtil.nextPowerOf2(sizeX); + int newSizeY = RenderUtil.nextPowerOf2(sizeY); + int newSizeZ = RenderUtil.nextPowerOf2(sizeZ); + + int diffX = newSizeX - sizeX; + int diffY = newSizeY - sizeY; + int diffZ = newSizeZ - sizeZ; + + minX -= diffX / 2; // floor division for the minimums + minY -= diffY / 2; + minZ -= diffZ / 2; + maxX += (diffX + 1) / 2; // ceiling divison for the maximums + maxY += (diffY + 1) / 2; + maxZ += (diffZ + 1) / 2; + } + + /** + * Grow this bounding box to have power of 2 side lengths, scaling from the minimum coords. + */ + public void nextPowerOf2() { + int sizeX = RenderUtil.nextPowerOf2(sizeX()); + int sizeY = RenderUtil.nextPowerOf2(sizeY()); + int sizeZ = RenderUtil.nextPowerOf2(sizeZ()); + + this.maxX = this.minX + sizeX; + this.maxY = this.minY + sizeY; + this.maxZ = this.minZ + sizeZ; + } + + public boolean hasPowerOf2Sides() { + // this is only true if all individual side lengths are powers of 2 + return isPowerOf2(volume()); + } + + public void grow(int s) { + this.grow(s, s, s); + } + + public void grow(int x, int y, int z) { + minX -= x; + minY -= y; + minZ -= z; + maxX += x; + maxY += y; + maxZ += z; + } + + public GridAlignedBB intersect(GridAlignedBB other) { + int minX = Math.max(this.minX, other.minX); + int minY = Math.max(this.minY, other.minY); + int minZ = Math.max(this.minZ, other.minZ); + int maxX = Math.min(this.maxX, other.maxX); + int maxY = Math.min(this.maxY, other.maxY); + int maxZ = Math.min(this.maxZ, other.maxZ); + return new GridAlignedBB(minX, minY, minZ, maxX, maxY, maxZ); + } + + public void intersectAssign(GridAlignedBB other) { + this.minX = Math.max(this.minX, other.minX); + this.minY = Math.max(this.minY, other.minY); + this.minZ = Math.max(this.minZ, other.minZ); + this.maxX = Math.min(this.maxX, other.maxX); + this.maxY = Math.min(this.maxY, other.maxY); + this.maxZ = Math.min(this.maxZ, other.maxZ); + } + + public GridAlignedBB union(GridAlignedBB other) { + int minX = Math.min(this.minX, other.minX); + int minY = Math.min(this.minY, other.minY); + int minZ = Math.min(this.minZ, other.minZ); + int maxX = Math.max(this.maxX, other.maxX); + int maxY = Math.max(this.maxY, other.maxY); + int maxZ = Math.max(this.maxZ, other.maxZ); + return new GridAlignedBB(minX, minY, minZ, maxX, maxY, maxZ); + } + + public void unionAssign(GridAlignedBB other) { + this.minX = Math.min(this.minX, other.minX); + this.minY = Math.min(this.minY, other.minY); + this.minZ = Math.min(this.minZ, other.minZ); + this.maxX = Math.max(this.maxX, other.maxX); + this.maxY = Math.max(this.maxY, other.maxY); + this.maxZ = Math.max(this.maxZ, other.maxZ); + } + + public void unionAssign(AxisAlignedBB other) { + this.minX = Math.min(this.minX, (int) Math.floor(other.minX)); + this.minY = Math.min(this.minY, (int) Math.floor(other.minY)); + this.minZ = Math.min(this.minZ, (int) Math.floor(other.minZ)); + this.maxX = Math.max(this.maxX, (int) Math.ceil(other.maxX)); + this.maxY = Math.max(this.maxY, (int) Math.ceil(other.maxY)); + this.maxZ = Math.max(this.maxZ, (int) Math.ceil(other.maxZ)); + } + + public boolean intersects(GridAlignedBB other) { + return this.intersects(other.minX, other.minY, other.minZ, other.maxX, other.maxY, other.maxZ); + } + + public boolean contains(GridAlignedBB other) { + return other.minX >= this.minX && + other.maxX <= this.maxX && + other.minY >= this.minY && + other.maxY <= this.maxY && + other.minZ >= this.minZ && + other.maxZ <= this.maxZ; + } + + public boolean isContainedBy(GridAlignedBB other) { + return other.contains(this); + } + + public boolean intersects(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) { + return this.minX < maxX && this.maxX > minX && this.minY < maxY && this.maxY > minY && this.minZ < maxZ && this.maxZ > minZ; + } + + public void forEachContained(ICoordinateConsumer func) { + if (empty()) return; + + for (int x = minX; x < maxX; x++) { + for (int y = Math.max(minY, 0); y < Math.min(maxY, 255); y++) { // clamp to world height limits + for (int z = minZ; z < maxZ; z++) { + func.consume(x, y, z); + } + } + } + } + + public AxisAlignedBB toAABB() { + return toAABB(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + GridAlignedBB that = (GridAlignedBB) o; + + return this.sameAs(that); + } + + @Override + public int hashCode() { + int result = minX; + result = 31 * result + minY; + result = 31 * result + minZ; + result = 31 * result + maxX; + result = 31 * result + maxY; + result = 31 * result + maxZ; + return result; + } + +} diff --git a/src/main/java/com/jozufozu/flywheel/light/ICoordinateConsumer.java b/src/main/java/com/jozufozu/flywheel/light/ICoordinateConsumer.java new file mode 100644 index 000000000..a4d77b32e --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/light/ICoordinateConsumer.java @@ -0,0 +1,6 @@ +package com.jozufozu.flywheel.light; + +@FunctionalInterface +public interface ICoordinateConsumer { + void consume(int x, int y, int z); +} diff --git a/src/main/java/com/jozufozu/flywheel/light/ILightUpdateListener.java b/src/main/java/com/jozufozu/flywheel/light/ILightUpdateListener.java new file mode 100644 index 000000000..570467411 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/light/ILightUpdateListener.java @@ -0,0 +1,32 @@ +package com.jozufozu.flywheel.light; + +import net.minecraft.world.IBlockDisplayReader; +import net.minecraft.world.LightType; + +/** + * Anything can implement this, implementors should call {@link LightUpdater#startListening} + * appropriately to make sure they get the updates they want. + */ +public interface ILightUpdateListener { + + /** + * Called when a light updates in a chunk the implementor cares about. + * + * @return true if this object is no longer valid and should not receive any more updates. + */ + boolean onLightUpdate(IBlockDisplayReader world, LightType type, GridAlignedBB changed); + + /** + * Called when the server sends light data to the client. + * + * @return true if this object is no longer valid and should not receive any more updates. + */ + default boolean onLightPacket(IBlockDisplayReader world, int chunkX, int chunkZ) { + GridAlignedBB changedVolume = GridAlignedBB.from(chunkX, chunkZ); + + if (onLightUpdate(world, LightType.BLOCK, changedVolume)) + return true; + + return onLightUpdate(world, LightType.SKY, changedVolume); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/light/LightUpdater.java b/src/main/java/com/jozufozu/flywheel/light/LightUpdater.java new file mode 100644 index 000000000..563b75dc8 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/light/LightUpdater.java @@ -0,0 +1,193 @@ +package com.jozufozu.flywheel.light; + +import java.util.WeakHashMap; +import java.util.function.LongConsumer; + +import com.jozufozu.flywheel.util.WeakHashSet; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongRBTreeSet; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.SectionPos; +import net.minecraft.world.IBlockDisplayReader; +import net.minecraft.world.LightType; + +/** + * By using WeakReferences we can automatically remove listeners when they are garbage collected. + * This allows us to easily be more clever about how we store the listeners. Each listener is associated + * with 2 sets of longs indicating what chunks and sections each listener is in. Additionally, a reverse + * mapping is created to allow for fast lookups when light updates. The reverse mapping is more interesting, + * but {@link #listenersToSections}, and {@link #listenersToChunks} are used to know what sections and + * chunks we need to remove the listeners from if they re-subscribe. Otherwise, listeners could get updates + * they no longer care about. This is done in {@link #clearSections} and {@link #clearChunks} + */ +public class LightUpdater { + + private static LightUpdater instance; + + public static LightUpdater getInstance() { + if (instance == null) + instance = new LightUpdater(); + + return instance; + } + + private final Long2ObjectMap> sections; + private final WeakHashMap listenersToSections; + + private final Long2ObjectMap> chunks; + private final WeakHashMap listenersToChunks; + + public LightUpdater() { + sections = new Long2ObjectOpenHashMap<>(); + listenersToSections = new WeakHashMap<>(); + + chunks = new Long2ObjectOpenHashMap<>(); + listenersToChunks = new WeakHashMap<>(); + } + + /** + * Add a listener associated with the given {@link BlockPos}. + *

+ * When a light update occurs in the chunk the position is contained in, + * {@link ILightUpdateListener#onLightUpdate} will be called. + * + * @param pos The position in the world that the listener cares about. + * @param listener The object that wants to receive light update notifications. + */ + public void startListening(BlockPos pos, ILightUpdateListener listener) { + LongRBTreeSet sections = clearSections(listener); + LongRBTreeSet chunks = clearChunks(listener); + + long sectionPos = worldToSection(pos); + addToSection(sectionPos, listener); + sections.add(sectionPos); + + long chunkPos = sectionToChunk(sectionPos); + addToChunk(chunkPos, listener); + chunks.add(chunkPos); + } + + /** + * Add a listener associated with the given {@link GridAlignedBB}. + *

+ * When a light update occurs in any chunk spanning the given volume, + * {@link ILightUpdateListener#onLightUpdate} will be called. + * + * @param volume The volume in the world that the listener cares about. + * @param listener The object that wants to receive light update notifications. + */ + public void startListening(GridAlignedBB volume, ILightUpdateListener listener) { + LongRBTreeSet sections = clearSections(listener); + LongRBTreeSet chunks = clearSections(listener); + + int minX = SectionPos.toChunk(volume.minX); + int minY = SectionPos.toChunk(volume.minY); + int minZ = SectionPos.toChunk(volume.minZ); + int maxX = SectionPos.toChunk(volume.maxX); + int maxY = SectionPos.toChunk(volume.maxY); + int maxZ = SectionPos.toChunk(volume.maxZ); + + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + for (int y = minY; y <= maxY; y++) { + long sectionPos = SectionPos.asLong(x, y, z); + addToSection(sectionPos, listener); + sections.add(sectionPos); + } + long chunkPos = SectionPos.asLong(x, 0, z); + addToChunk(chunkPos, listener); + chunks.add(chunkPos); + } + } + } + + /** + * Dispatch light updates to all registered {@link ILightUpdateListener}s. + * + * @param world The world in which light was updated. + * @param type The type of light that changed. + * @param sectionPos A long representing the section position where light changed. + */ + public void onLightUpdate(IBlockDisplayReader world, LightType type, long sectionPos) { + WeakHashSet set = sections.get(sectionPos); + + if (set == null || set.isEmpty()) return; + + GridAlignedBB chunkBox = GridAlignedBB.from(SectionPos.from(sectionPos)); + + set.removeIf(listener -> listener.onLightUpdate(world, type, chunkBox.copy())); + } + + /** + * Dispatch light updates to all registered {@link ILightUpdateListener}s + * when the server sends lighting data for an entire chunk. + * + * @param world The world in which light was updated. + */ + public void onLightPacket(IBlockDisplayReader world, int chunkX, int chunkZ) { + + long chunkPos = SectionPos.asLong(chunkX, 0, chunkZ); + + WeakHashSet set = chunks.get(chunkPos); + + if (set == null || set.isEmpty()) return; + + set.removeIf(listener -> listener.onLightPacket(world, chunkX, chunkZ)); + } + + private LongRBTreeSet clearChunks(ILightUpdateListener listener) { + return clear(listener, listenersToChunks, chunks); + } + + private LongRBTreeSet clearSections(ILightUpdateListener listener) { + return clear(listener, listenersToSections, sections); + } + + private LongRBTreeSet clear(ILightUpdateListener listener, WeakHashMap listeners, Long2ObjectMap> lookup) { + LongRBTreeSet set = listeners.get(listener); + + if (set == null) { + set = new LongRBTreeSet(); + listeners.put(listener, set); + } else { + set.forEach((LongConsumer) l -> { + WeakHashSet listeningSections = lookup.get(l); + + if (listeningSections != null) listeningSections.remove(listener); + }); + + set.clear(); + } + + return set; + } + + private void addToSection(long sectionPos, ILightUpdateListener listener) { + getOrCreate(sections, sectionPos).add(listener); + } + + private void addToChunk(long chunkPos, ILightUpdateListener listener) { + getOrCreate(chunks, chunkPos).add(listener); + } + + private WeakHashSet getOrCreate(Long2ObjectMap> sections, long chunkPos) { + WeakHashSet set = sections.get(chunkPos); + + if (set == null) { + set = new WeakHashSet<>(); + sections.put(chunkPos, set); + } + + return set; + } + + public static long worldToSection(BlockPos pos) { + return SectionPos.asLong(pos.getX(), pos.getY(), pos.getZ()); + } + + public static long sectionToChunk(long sectionPos) { + return sectionPos & 0xFFFFFFFFFFF_00000L; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/light/LightVolume.java b/src/main/java/com/jozufozu/flywheel/light/LightVolume.java new file mode 100644 index 000000000..736b8e06d --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/light/LightVolume.java @@ -0,0 +1,328 @@ +package com.jozufozu.flywheel.light; + +import static org.lwjgl.opengl.GL20.GL_LINEAR; +import static org.lwjgl.opengl.GL20.GL_MIRRORED_REPEAT; +import static org.lwjgl.opengl.GL20.GL_TEXTURE0; +import static org.lwjgl.opengl.GL20.GL_TEXTURE4; +import static org.lwjgl.opengl.GL20.GL_TEXTURE_3D; +import static org.lwjgl.opengl.GL20.GL_TEXTURE_MAG_FILTER; +import static org.lwjgl.opengl.GL20.GL_TEXTURE_MIN_FILTER; +import static org.lwjgl.opengl.GL20.GL_TEXTURE_WRAP_R; +import static org.lwjgl.opengl.GL20.GL_TEXTURE_WRAP_S; +import static org.lwjgl.opengl.GL20.GL_TEXTURE_WRAP_T; +import static org.lwjgl.opengl.GL20.GL_UNPACK_ALIGNMENT; +import static org.lwjgl.opengl.GL20.GL_UNPACK_IMAGE_HEIGHT; +import static org.lwjgl.opengl.GL20.GL_UNPACK_ROW_LENGTH; +import static org.lwjgl.opengl.GL20.GL_UNPACK_SKIP_IMAGES; +import static org.lwjgl.opengl.GL20.GL_UNPACK_SKIP_PIXELS; +import static org.lwjgl.opengl.GL20.GL_UNPACK_SKIP_ROWS; +import static org.lwjgl.opengl.GL20.GL_UNSIGNED_BYTE; +import static org.lwjgl.opengl.GL20.glActiveTexture; +import static org.lwjgl.opengl.GL20.glPixelStorei; +import static org.lwjgl.opengl.GL20.glTexImage3D; +import static org.lwjgl.opengl.GL20.glTexParameteri; +import static org.lwjgl.opengl.GL20.glTexSubImage3D; + +import java.nio.ByteBuffer; + +import org.lwjgl.system.MemoryUtil; + +import com.jozufozu.flywheel.backend.Backend; +import com.jozufozu.flywheel.backend.gl.GlTexture; +import com.jozufozu.flywheel.backend.gl.versioned.RGPixelFormat; + +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.IBlockDisplayReader; +import net.minecraft.world.LightType; + +public class LightVolume { + + private GridAlignedBB sampleVolume; + private GridAlignedBB textureVolume; + private ByteBuffer lightData; + + private boolean bufferDirty; + private boolean removed; + + private final GlTexture glTexture; + + private final RGPixelFormat pixelFormat; + + public LightVolume(GridAlignedBB sampleVolume) { + setSampleVolume(sampleVolume); + + pixelFormat = Backend.getInstance().compat.pixelFormat; + + this.glTexture = new GlTexture(GL_TEXTURE_3D); + this.lightData = MemoryUtil.memAlloc(this.textureVolume.volume() * pixelFormat.byteCount()); + + // allocate space for the texture + glActiveTexture(GL_TEXTURE4); + glTexture.bind(); + + int sizeX = textureVolume.sizeX(); + int sizeY = textureVolume.sizeY(); + int sizeZ = textureVolume.sizeZ(); + glTexImage3D(GL_TEXTURE_3D, 0, pixelFormat.internalFormat(), sizeX, sizeY, sizeZ, 0, pixelFormat.format(), GL_UNSIGNED_BYTE, 0); + + glTexture.unbind(); + glActiveTexture(GL_TEXTURE0); + } + + private void setSampleVolume(GridAlignedBB sampleVolume) { + this.sampleVolume = sampleVolume; + this.textureVolume = sampleVolume.copy(); + this.textureVolume.nextPowerOf2Centered(); + } + + public GridAlignedBB getTextureVolume() { + return GridAlignedBB.copy(textureVolume); + } + + public GridAlignedBB getSampleVolume() { + return GridAlignedBB.copy(sampleVolume); + } + + public int getMinX() { + return textureVolume.minX; + } + + public int getMinY() { + return textureVolume.minY; + } + + public int getMinZ() { + return textureVolume.minZ; + } + + public int getMaxX() { + return textureVolume.maxX; + } + + public int getMaxY() { + return textureVolume.maxY; + } + + public int getMaxZ() { + return textureVolume.maxZ; + } + + public int getSizeX() { + return textureVolume.sizeX(); + } + + public int getSizeY() { + return textureVolume.sizeY(); + } + + public int getSizeZ() { + return textureVolume.sizeZ(); + } + + public void move(IBlockDisplayReader world, GridAlignedBB newSampleVolume) { + if (textureVolume.contains(newSampleVolume)) { + if (newSampleVolume.intersects(sampleVolume)) { + GridAlignedBB newArea = newSampleVolume.intersect(sampleVolume); + sampleVolume = newSampleVolume; + + copyLight(world, newArea); + } else { + sampleVolume = newSampleVolume; + initialize(world); + } + } else { + setSampleVolume(newSampleVolume); + int volume = textureVolume.volume(); + if (volume * 2 > lightData.capacity()) { + lightData = MemoryUtil.memRealloc(lightData, volume * 2); + } + initialize(world); + } + } + + public void notifyLightUpdate(IBlockDisplayReader world, LightType type, GridAlignedBB changedVolume) { + if (removed) + return; + + if (!changedVolume.intersects(sampleVolume)) + return; + changedVolume = changedVolume.intersect(sampleVolume); // compute the region contained by us that has dirty lighting data. + + if (type == LightType.BLOCK) copyBlock(world, changedVolume); + else if (type == LightType.SKY) copySky(world, changedVolume); + } + + public void notifyLightPacket(IBlockDisplayReader world, int chunkX, int chunkZ) { + if (removed) return; + + GridAlignedBB changedVolume = GridAlignedBB.from(chunkX, chunkZ); + if (!changedVolume.intersects(sampleVolume)) + return; + changedVolume.intersectAssign(sampleVolume); // compute the region contained by us that has dirty lighting data. + + copyLight(world, changedVolume); + } + + /** + * Completely (re)populate this volume with block and sky lighting data. + * This is expensive and should be avoided. + */ + public void initialize(IBlockDisplayReader world) { + BlockPos.Mutable pos = new BlockPos.Mutable(); + + int shiftX = textureVolume.minX; + int shiftY = textureVolume.minY; + int shiftZ = textureVolume.minZ; + + sampleVolume.forEachContained((x, y, z) -> { + pos.setPos(x, y, z); + + int blockLight = world.getLightLevel(LightType.BLOCK, pos); + int skyLight = world.getLightLevel(LightType.SKY, pos); + + writeLight(x - shiftX, y - shiftY, z - shiftZ, blockLight, skyLight); + }); + + bufferDirty = true; + } + + /** + * Copy block light from the world into this volume. + * + * @param worldVolume the region in the world to copy data from. + */ + public void copyBlock(IBlockDisplayReader world, GridAlignedBB worldVolume) { + BlockPos.Mutable pos = new BlockPos.Mutable(); + + int xShift = textureVolume.minX; + int yShift = textureVolume.minY; + int zShift = textureVolume.minZ; + + worldVolume.forEachContained((x, y, z) -> { + pos.setPos(x, y, z); + + int light = world.getLightLevel(LightType.BLOCK, pos); + + writeBlock(x - xShift, y - yShift, z - zShift, light); + }); + + bufferDirty = true; + } + + /** + * Copy sky light from the world into this volume. + * + * @param worldVolume the region in the world to copy data from. + */ + public void copySky(IBlockDisplayReader world, GridAlignedBB worldVolume) { + BlockPos.Mutable pos = new BlockPos.Mutable(); + + int xShift = textureVolume.minX; + int yShift = textureVolume.minY; + int zShift = textureVolume.minZ; + + worldVolume.forEachContained((x, y, z) -> { + pos.setPos(x, y, z); + + int light = world.getLightLevel(LightType.SKY, pos); + + writeSky(x - xShift, y - yShift, z - zShift, light); + }); + + bufferDirty = true; + } + + /** + * Copy all light from the world into this volume. + * + * @param worldVolume the region in the world to copy data from. + */ + public void copyLight(IBlockDisplayReader world, GridAlignedBB worldVolume) { + BlockPos.Mutable pos = new BlockPos.Mutable(); + + int xShift = textureVolume.minX; + int yShift = textureVolume.minY; + int zShift = textureVolume.minZ; + + worldVolume.forEachContained((x, y, z) -> { + pos.setPos(x, y, z); + + int block = world.getLightLevel(LightType.BLOCK, pos); + int sky = world.getLightLevel(LightType.SKY, pos); + + writeLight(x - xShift, y - yShift, z - zShift, block, sky); + }); + + bufferDirty = true; + } + + public void bind() { + // just in case something goes wrong or we accidentally call this before this volume is properly disposed of. + if (lightData == null || removed) return; + + glActiveTexture(GL_TEXTURE4); + glTexture.bind(); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_MIRRORED_REPEAT); + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT); + + uploadTexture(); + } + + private void uploadTexture() { + if (bufferDirty) { + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); + glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); + glPixelStorei(GL_UNPACK_SKIP_IMAGES, 0); + glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0); + glPixelStorei(GL_UNPACK_ALIGNMENT, 2); + int sizeX = textureVolume.sizeX(); + int sizeY = textureVolume.sizeY(); + int sizeZ = textureVolume.sizeZ(); + + glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, sizeX, sizeY, sizeZ, pixelFormat.format(), GL_UNSIGNED_BYTE, lightData); + + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); // 4 is the default + bufferDirty = false; + } + } + + public void unbind() { + glTexture.unbind(); + } + + public void delete() { + removed = true; + glTexture.delete(); + MemoryUtil.memFree(lightData); + lightData = null; + } + + private void writeLight(int x, int y, int z, int block, int sky) { + byte b = (byte) ((block & 0xF) << 4); + byte s = (byte) ((sky & 0xF) << 4); + + int i = posToIndex(x, y, z); + lightData.put(i, b); + lightData.put(i + 1, s); + } + + private void writeBlock(int x, int y, int z, int block) { + byte b = (byte) ((block & 0xF) << 4); + + lightData.put(posToIndex(x, y, z), b); + } + + private void writeSky(int x, int y, int z, int sky) { + byte b = (byte) ((sky & 0xF) << 4); + + lightData.put(posToIndex(x, y, z) + 1, b); + } + + private int posToIndex(int x, int y, int z) { + return (x + textureVolume.sizeX() * (y + z * textureVolume.sizeY())) * pixelFormat.byteCount(); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/mixin/CancelEntityRenderMixin.java b/src/main/java/com/jozufozu/flywheel/mixin/CancelEntityRenderMixin.java new file mode 100644 index 000000000..069563702 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/mixin/CancelEntityRenderMixin.java @@ -0,0 +1,32 @@ +package com.jozufozu.flywheel.mixin; + +import java.util.ArrayList; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import com.google.common.collect.Lists; +import com.jozufozu.flywheel.backend.Backend; +import com.jozufozu.flywheel.backend.instancing.IInstanceRendered; + +import net.minecraft.client.renderer.WorldRenderer; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.Entity; + +@Mixin(WorldRenderer.class) +public class CancelEntityRenderMixin { + + @Redirect(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/world/ClientWorld;getAllEntities()Ljava/lang/Iterable;")) + private Iterable filterEntities(ClientWorld world) { + Iterable entities = world.getAllEntities(); + if (Backend.getInstance().canUseInstancing()) { + + ArrayList filtered = Lists.newArrayList(entities); + filtered.removeIf(tile -> tile instanceof IInstanceRendered && !((IInstanceRendered) tile).shouldRenderNormally()); + + return filtered; + } + return entities; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/mixin/CancelTileEntityRenderMixin.java b/src/main/java/com/jozufozu/flywheel/mixin/CancelTileEntityRenderMixin.java new file mode 100644 index 000000000..392ba06a6 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/mixin/CancelTileEntityRenderMixin.java @@ -0,0 +1,36 @@ +package com.jozufozu.flywheel.mixin; + +import java.util.List; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import com.jozufozu.flywheel.backend.Backend; +import com.jozufozu.flywheel.backend.instancing.IInstanceRendered; + +import net.minecraft.client.renderer.chunk.ChunkRenderDispatcher; +import net.minecraft.tileentity.TileEntity; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +@OnlyIn(Dist.CLIENT) +@Mixin(ChunkRenderDispatcher.CompiledChunk.class) +public class CancelTileEntityRenderMixin { + + /** + * JUSTIFICATION: when instanced rendering is enabled, many tile entities no longer need + * to be processed by the normal game renderer. This method is only called to retrieve the + * list of tile entities to render. By filtering the output here, we prevent the game from + * doing unnecessary light lookups and frustum checks. + */ + @Inject(at = @At("RETURN"), method = "getTileEntities", cancellable = true) + private void noRenderInstancedTiles(CallbackInfoReturnable> cir) { + if (Backend.getInstance().canUseInstancing()) { + List tiles = cir.getReturnValue(); + + tiles.removeIf(tile -> tile instanceof IInstanceRendered && !((IInstanceRendered) tile).shouldRenderNormally()); + } + } +} diff --git a/src/main/java/com/jozufozu/flywheel/mixin/FogColorTrackerMixin.java b/src/main/java/com/jozufozu/flywheel/mixin/FogColorTrackerMixin.java new file mode 100644 index 000000000..c9326d4ae --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/mixin/FogColorTrackerMixin.java @@ -0,0 +1,21 @@ +package com.jozufozu.flywheel.mixin; + +import org.lwjgl.opengl.GL11; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.jozufozu.flywheel.core.shader.GlFog; +import com.mojang.blaze3d.platform.GlStateManager; + +@Mixin(GlStateManager.class) +public class FogColorTrackerMixin { + + @Inject(at = @At("TAIL"), method = "fog") + private static void copyFogColor(int pname, float[] params, CallbackInfo ci) { + if (pname == GL11.GL_FOG_COLOR) { + GlFog.FOG_COLOR = params; + } + } +} diff --git a/src/main/java/com/jozufozu/flywheel/mixin/RenderHooksMixin.java b/src/main/java/com/jozufozu/flywheel/mixin/RenderHooksMixin.java new file mode 100644 index 000000000..5d75d3c5e --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/mixin/RenderHooksMixin.java @@ -0,0 +1,103 @@ +package com.jozufozu.flywheel.mixin; + +import org.lwjgl.opengl.GL20; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.jozufozu.flywheel.backend.Backend; +import com.jozufozu.flywheel.backend.instancing.InstancedRenderDispatcher; +import com.jozufozu.flywheel.event.BeginFrameEvent; +import com.jozufozu.flywheel.event.ReloadRenderersEvent; +import com.jozufozu.flywheel.event.RenderLayerEvent; +import com.mojang.blaze3d.matrix.MatrixStack; + +import net.minecraft.block.BlockState; +import net.minecraft.client.renderer.ActiveRenderInfo; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.WorldRenderer; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.vector.Matrix4f; +import net.minecraft.util.math.vector.Vector3d; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.common.MinecraftForge; + +@OnlyIn(Dist.CLIENT) +@Mixin(WorldRenderer.class) +public class RenderHooksMixin { + + @Shadow + private ClientWorld world; + + @Inject(at = @At(value = "INVOKE", target = "net.minecraft.client.renderer.WorldRenderer.updateChunks(J)V"), method = "render") + private void setupFrame(MatrixStack stack, float p_228426_2_, long p_228426_3_, boolean p_228426_5_, + ActiveRenderInfo info, GameRenderer gameRenderer, LightTexture lightTexture, Matrix4f p_228426_9_, + CallbackInfo ci) { + MinecraftForge.EVENT_BUS.post(new BeginFrameEvent(world, stack, info, gameRenderer, lightTexture)); + } + + /** + * JUSTIFICATION: This method is called once per layer per frame. It allows us to perform + * layer-correct custom rendering. RenderWorldLast is not refined enough for rendering world objects. + * This should probably be a forge event. + */ + @Inject(at = @At("TAIL"), method = "renderLayer") + private void renderLayer(RenderType type, MatrixStack stack, double camX, double camY, double camZ, + CallbackInfo ci) { + if (!Backend.getInstance().available()) + return; + + Matrix4f view = stack.peek() + .getModel(); + Matrix4f viewProjection = view.copy(); + viewProjection.multiplyBackward(Backend.getInstance().getProjectionMatrix()); + + MinecraftForge.EVENT_BUS.post(new RenderLayerEvent(world, type, viewProjection, camX, camY, camZ)); + GL20.glUseProgram(0); + } + + @Inject(at = @At("TAIL"), method = "loadRenderers") + private void refresh(CallbackInfo ci) { + Backend.getInstance().refresh(); + + MinecraftForge.EVENT_BUS.post(new ReloadRenderersEvent(world)); + } + + + @Inject(at = + @At( + value = "INVOKE", + target = "Lnet/minecraft/client/renderer/WorldRenderer;checkEmpty(Lcom/mojang/blaze3d/matrix/MatrixStack;)V", + ordinal = 2 // after the game renders the breaking overlay normally + ), + method = "render") + private void renderBlockBreaking(MatrixStack stack, float p_228426_2_, long p_228426_3_, boolean p_228426_5_, + ActiveRenderInfo info, GameRenderer gameRenderer, LightTexture lightTexture, Matrix4f p_228426_9_, + CallbackInfo ci) { + if (!Backend.getInstance().available()) + return; + + Matrix4f view = stack.peek() + .getModel(); + Matrix4f viewProjection = view.copy(); + viewProjection.multiplyBackward(Backend.getInstance().getProjectionMatrix()); + + Vector3d cameraPos = info.getProjectedView(); + InstancedRenderDispatcher.renderBreaking(world, viewProjection, cameraPos.x, cameraPos.y, cameraPos.z); + GL20.glUseProgram(0); + } + + // Instancing + + @Inject(at = @At("TAIL"), method = "scheduleBlockRerenderIfNeeded") + private void checkUpdate(BlockPos pos, BlockState lastState, BlockState newState, CallbackInfo ci) { + InstancedRenderDispatcher.getTiles(world) + .update(world.getTileEntity(pos)); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/mixin/ShaderCloseMixin.java b/src/main/java/com/jozufozu/flywheel/mixin/ShaderCloseMixin.java new file mode 100644 index 000000000..d03e35735 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/mixin/ShaderCloseMixin.java @@ -0,0 +1,35 @@ +package com.jozufozu.flywheel.mixin; + +import javax.annotation.Nullable; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.jozufozu.flywheel.backend.OptifineHandler; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.VideoSettingsScreen; + +@Mixin(Minecraft.class) +public class ShaderCloseMixin { + + @Shadow + @Nullable + public Screen currentScreen; + + @Inject(at = @At("HEAD"), method = "displayGuiScreen") + private void whenScreenChanges(Screen screen, CallbackInfo info) { + if (OptifineHandler.optifineInstalled() && screen instanceof VideoSettingsScreen) { + Screen old = this.currentScreen; + if (old != null && old.getClass() + .getName() + .startsWith(OptifineHandler.SHADER_PACKAGE)) { + OptifineHandler.refresh(); + } + } + } +} diff --git a/src/main/java/com/jozufozu/flywheel/mixin/StoreProjectionMatrixMixin.java b/src/main/java/com/jozufozu/flywheel/mixin/StoreProjectionMatrixMixin.java new file mode 100644 index 000000000..39ca73614 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/mixin/StoreProjectionMatrixMixin.java @@ -0,0 +1,37 @@ +package com.jozufozu.flywheel.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.jozufozu.flywheel.backend.Backend; +import com.mojang.blaze3d.matrix.MatrixStack; + +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.util.math.vector.Matrix4f; + +@Mixin(GameRenderer.class) +public abstract class StoreProjectionMatrixMixin { + + @Unique + private boolean shouldCopy = false; + + /** + * We only want to copy the projection matrix if it is going to be used to render the world. + * We don't care about the mat for your hand. + */ + @Inject(method = "renderWorld", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/GameRenderer;loadProjectionMatrix(Lnet/minecraft/util/math/vector/Matrix4f;)V")) + private void projectionMatrixReady(float p_228378_1_, long p_228378_2_, MatrixStack p_228378_4_, CallbackInfo ci) { + shouldCopy = true; + } + + @Inject(method = "loadProjectionMatrix", at = @At("TAIL")) + private void onProjectionMatrixLoad(Matrix4f projection, CallbackInfo ci) { + if (shouldCopy) { + Backend.getInstance().setProjectionMatrix(projection.copy()); + shouldCopy = false; + } + } +} diff --git a/src/main/java/com/jozufozu/flywheel/mixin/TileRemoveMixin.java b/src/main/java/com/jozufozu/flywheel/mixin/TileRemoveMixin.java new file mode 100644 index 000000000..84a55ff04 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/mixin/TileRemoveMixin.java @@ -0,0 +1,30 @@ +package com.jozufozu.flywheel.mixin; + +import javax.annotation.Nullable; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.jozufozu.flywheel.backend.instancing.InstancedRenderDispatcher; + +import net.minecraft.client.world.ClientWorld; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.world.World; + +@Mixin(TileEntity.class) +public class TileRemoveMixin { + + @Shadow + @Nullable + protected World world; + + @Inject(at = @At("TAIL"), method = "remove") + private void onRemove(CallbackInfo ci) { + if (world instanceof ClientWorld) + InstancedRenderDispatcher.getTiles(this.world) + .remove((TileEntity) (Object) this); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/mixin/TileWorldHookMixin.java b/src/main/java/com/jozufozu/flywheel/mixin/TileWorldHookMixin.java new file mode 100644 index 000000000..790485990 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/mixin/TileWorldHookMixin.java @@ -0,0 +1,55 @@ +package com.jozufozu.flywheel.mixin; + +import java.util.Set; + +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import com.jozufozu.flywheel.backend.instancing.InstancedRenderDispatcher; +import com.jozufozu.flywheel.backend.instancing.tile.TileInstanceManager; + +import net.minecraft.tileentity.TileEntity; +import net.minecraft.world.World; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +@OnlyIn(Dist.CLIENT) +@Mixin(value = World.class, priority = 1100) // this and create.mixins.json have high priority to load after Performant +public class TileWorldHookMixin { + + final World self = (World) (Object) this; + + @Shadow + @Final + public boolean isRemote; + + @Shadow + @Final + protected Set tileEntitiesToBeRemoved; + + @Inject(at = @At("TAIL"), method = "addTileEntity") + private void onAddTile(TileEntity te, CallbackInfoReturnable cir) { + if (isRemote) { + InstancedRenderDispatcher.getTiles(self) + .queueAdd(te); + } + } + + /** + * Without this we don't unload instances when a chunk unloads. + */ + @Inject(at = @At(value = "INVOKE", target = "Ljava/util/Set;clear()V", ordinal = 0), method = "tickBlockEntities") + private void onChunkUnload(CallbackInfo ci) { + if (isRemote) { + TileInstanceManager kineticRenderer = InstancedRenderDispatcher.getTiles(self); + for (TileEntity tile : tileEntitiesToBeRemoved) { + kineticRenderer.remove(tile); + } + } + } +} diff --git a/src/main/java/com/jozufozu/flywheel/mixin/light/LightUpdateMixin.java b/src/main/java/com/jozufozu/flywheel/mixin/light/LightUpdateMixin.java new file mode 100644 index 000000000..59611c72e --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/mixin/light/LightUpdateMixin.java @@ -0,0 +1,58 @@ +package com.jozufozu.flywheel.mixin.light; + +import java.util.Map; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.jozufozu.flywheel.backend.instancing.InstancedRenderDispatcher; +import com.jozufozu.flywheel.light.LightUpdater; + +import net.minecraft.client.multiplayer.ClientChunkProvider; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.util.math.SectionPos; +import net.minecraft.world.LightType; +import net.minecraft.world.chunk.AbstractChunkProvider; +import net.minecraft.world.chunk.Chunk; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +@OnlyIn(Dist.CLIENT) +@Mixin(ClientChunkProvider.class) +public abstract class LightUpdateMixin extends AbstractChunkProvider { + + /** + * JUSTIFICATION: This method is called after a lighting tick once per subchunk where a + * lighting change occurred that tick. On the client, Minecraft uses this method to inform + * the rendering system that it needs to redraw a chunk. It does all that work asynchronously, + * and we should too. + */ + @Inject(at = @At("HEAD"), method = "markLightChanged") + private void onLightUpdate(LightType type, SectionPos pos, CallbackInfo ci) { + ClientChunkProvider thi = ((ClientChunkProvider) (Object) this); + ClientWorld world = (ClientWorld) thi.getWorld(); + + Chunk chunk = thi.getChunk(pos.getSectionX(), pos.getSectionZ(), false); + + int sectionY = pos.getSectionY(); + + if (chunk != null) { + chunk.getTileEntityMap() + .entrySet() + .stream() + .filter(entry -> SectionPos.toChunk(entry.getKey() + .getY()) == sectionY) + .map(Map.Entry::getValue) + .forEach(InstancedRenderDispatcher.getTiles(world)::onLightUpdate); + + if (sectionY >= 0) // TODO: 1.17 + chunk.getEntityLists()[sectionY] + .forEach(InstancedRenderDispatcher.getEntities(world)::onLightUpdate); + } + + LightUpdater.getInstance() + .onLightUpdate(world, type, pos.asLong()); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/mixin/light/NetworkLightUpdateMixin.java b/src/main/java/com/jozufozu/flywheel/mixin/light/NetworkLightUpdateMixin.java new file mode 100644 index 000000000..16a31725f --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/mixin/light/NetworkLightUpdateMixin.java @@ -0,0 +1,52 @@ +package com.jozufozu.flywheel.mixin.light; + +import java.util.Arrays; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import com.jozufozu.flywheel.backend.RenderWork; +import com.jozufozu.flywheel.backend.instancing.InstancedRenderDispatcher; +import com.jozufozu.flywheel.light.LightUpdater; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.network.play.ClientPlayNetHandler; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.network.play.server.SUpdateLightPacket; +import net.minecraft.util.ClassInheritanceMultiMap; +import net.minecraft.world.chunk.Chunk; + +@Mixin(ClientPlayNetHandler.class) +public class NetworkLightUpdateMixin { + + @Inject(at = @At("TAIL"), method = "handleUpdateLight") + private void onLightPacket(SUpdateLightPacket packet, CallbackInfo ci) { + RenderWork.enqueue(() -> { + ClientWorld world = Minecraft.getInstance().world; + + if (world == null) + return; + + int chunkX = packet.getChunkX(); + int chunkZ = packet.getChunkZ(); + + Chunk chunk = world.getChunkProvider() + .getChunk(chunkX, chunkZ, false); + + if (chunk != null) { + chunk.getTileEntityMap() + .values() + .forEach(InstancedRenderDispatcher.getTiles(world)::onLightUpdate); + + Arrays.stream(chunk.getEntityLists()) + .flatMap(ClassInheritanceMultiMap::stream) + .forEach(InstancedRenderDispatcher.getEntities(world)::onLightUpdate); + } + + LightUpdater.getInstance() + .onLightPacket(world, chunkX, chunkZ); + }); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/util/AngleHelper.java b/src/main/java/com/jozufozu/flywheel/util/AngleHelper.java new file mode 100644 index 000000000..b20537a04 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/util/AngleHelper.java @@ -0,0 +1,57 @@ +package com.jozufozu.flywheel.util; + +import net.minecraft.util.Direction; +import net.minecraft.util.Direction.Axis; + +public class AngleHelper { + + /** + * Legacy method. See {@link #horizontalAngleNew(Direction)} for new method. + */ + public static float horizontalAngle(Direction facing) { + float angle = facing.getHorizontalAngle(); + if (facing.getAxis() == Axis.X) + angle = -angle; + return angle; + } + + /** + * Same as {@link #horizontalAngle(Direction)}, but returns 0 instead of -90 for vertical directions. + */ + public static float horizontalAngleNew(Direction facing) { + if (facing.getAxis().isVertical()) { + return 0; + } + float angle = facing.getHorizontalAngle(); + if (facing.getAxis() == Axis.X) + angle = -angle; + return angle; + } + + public static float verticalAngle(Direction facing) { + return facing == Direction.UP ? -90 : facing == Direction.DOWN ? 90 : 0; + } + + public static float rad(double angle) { + if (angle == 0) + return 0; + return (float) (angle / 180 * Math.PI); + } + + public static float deg(double angle) { + if (angle == 0) + return 0; + return (float) (angle * 180 / Math.PI); + } + + public static float angleLerp(double pct, double current, double target) { + return (float) (current + getShortestAngleDiff(current, target) * pct); + } + + public static float getShortestAngleDiff(double current, double target) { + current = current % 360; + target = target % 360; + return (float) (((((target - current) % 360) + 540) % 360) - 180); + } + +} diff --git a/src/main/java/com/jozufozu/flywheel/util/AnimationTickHolder.java b/src/main/java/com/jozufozu/flywheel/util/AnimationTickHolder.java new file mode 100644 index 000000000..476664944 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/util/AnimationTickHolder.java @@ -0,0 +1,41 @@ +package com.jozufozu.flywheel.util; + +import net.minecraft.client.Minecraft; +import net.minecraft.world.IWorld; + +public class AnimationTickHolder { + + private static int ticks; + private static int paused_ticks; + + public static void reset() { + ticks = 0; + paused_ticks = 0; + } + + public static void tick() { + if (!Minecraft.getInstance() + .isGamePaused()) { + ticks = (ticks + 1) % 1_728_000; // wrap around every 24 hours so we maintain enough floating point precision + } else { + paused_ticks = (paused_ticks + 1) % 1_728_000; + } + } + + public static int getTicks() { + return getTicks(false); + } + + public static int getTicks(boolean includePaused) { + return includePaused ? ticks + paused_ticks : ticks; + } + + public static float getRenderTime() { + return getTicks() + getPartialTicks(); + } + + public static float getPartialTicks() { + Minecraft mc = Minecraft.getInstance(); + return (mc.isGamePaused() ? mc.renderPartialTicksPaused : mc.getRenderPartialTicks()); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/util/AttribUtil.java b/src/main/java/com/jozufozu/flywheel/util/AttribUtil.java new file mode 100644 index 000000000..eb1c7aa89 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/util/AttribUtil.java @@ -0,0 +1,26 @@ +package com.jozufozu.flywheel.util; + +import org.lwjgl.opengl.GL20; + +public class AttribUtil { + + public static void enableArrays(int count) { + enableArrays(0, count); + } + + public static void enableArrays(int fromInclusive, int toExclusive) { + for (int i = fromInclusive; i < toExclusive; i++) { + GL20.glEnableVertexAttribArray(i); + } + } + + public static void disableArrays(int count) { + disableArrays(0, count); + } + + public static void disableArrays(int fromInclusive, int toExclusive) { + for (int i = fromInclusive; i < toExclusive; i++) { + GL20.glDisableVertexAttribArray(i); + } + } +} diff --git a/src/main/java/com/jozufozu/flywheel/util/BakedQuadWrapper.java b/src/main/java/com/jozufozu/flywheel/util/BakedQuadWrapper.java new file mode 100644 index 000000000..87b9fdd99 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/util/BakedQuadWrapper.java @@ -0,0 +1,216 @@ +package com.jozufozu.flywheel.util; + +import net.minecraft.client.renderer.model.BakedQuad; +import net.minecraft.client.renderer.vertex.DefaultVertexFormats; +import net.minecraft.client.renderer.vertex.VertexFormat; +import net.minecraft.client.renderer.vertex.VertexFormatElement; +import net.minecraft.util.math.vector.Vector2f; +import net.minecraft.util.math.vector.Vector3f; + +public class BakedQuadWrapper { + private final FormatCache formatCache = new FormatCache(); + private BakedQuad quad; + private int[] vertexData; + + public BakedQuadWrapper() { + } + + public BakedQuadWrapper(BakedQuad quad) { + this.quad = quad; + this.vertexData = quad.getVertexData(); + } + + public void setQuad(BakedQuad quad) { + this.quad = quad; + this.vertexData = this.quad.getVertexData(); + } + + public static BakedQuadWrapper of(BakedQuad quad) { + return new BakedQuadWrapper(quad); + } + + public void refreshFormat() { + formatCache.refresh(); + } + + public BakedQuad getQuad() { + return quad; + } + + public void clear() { + quad = null; + vertexData = null; + } + + // Getters + + public float getPosX(int vertexIndex) { + return Float.intBitsToFloat(vertexData[vertexIndex * formatCache.vertexSize + formatCache.position]); + } + + public float getPosY(int vertexIndex) { + return Float.intBitsToFloat(vertexData[vertexIndex * formatCache.vertexSize + formatCache.position + 1]); + } + + public float getPosZ(int vertexIndex) { + return Float.intBitsToFloat(vertexData[vertexIndex * formatCache.vertexSize + formatCache.position + 2]); + } + + public Vector3f getPos(int vertexIndex) { + return new Vector3f(getPosX(vertexIndex), getPosY(vertexIndex), getPosZ(vertexIndex)); + } + + public void copyPos(int vertexIndex, Vector3f pos) { + pos.set(getPosX(vertexIndex), getPosY(vertexIndex), getPosZ(vertexIndex)); + } + + public int getColor(int vertexIndex) { + return vertexData[vertexIndex * formatCache.vertexSize + formatCache.color]; + } + + public float getTexU(int vertexIndex) { + return Float.intBitsToFloat(vertexData[vertexIndex * formatCache.vertexSize + formatCache.texture]); + } + + public float getTexV(int vertexIndex) { + return Float.intBitsToFloat(vertexData[vertexIndex * formatCache.vertexSize + formatCache.texture + 1]); + } + + public Vector2f getTex(int vertexIndex) { + return new Vector2f(getTexU(vertexIndex), getTexV(vertexIndex)); + } + + public int getLight(int vertexIndex) { + return vertexData[vertexIndex * formatCache.vertexSize + formatCache.light]; + } + + public float getNormalX(int vertexIndex) { + return Float.intBitsToFloat(vertexData[vertexIndex * formatCache.vertexSize + formatCache.normal]); + } + + public float getNormalY(int vertexIndex) { + return Float.intBitsToFloat(vertexData[vertexIndex * formatCache.vertexSize + formatCache.normal + 1]); + } + + public float getNormalZ(int vertexIndex) { + return Float.intBitsToFloat(vertexData[vertexIndex * formatCache.vertexSize + formatCache.normal + 2]); + } + + public Vector3f getNormal(int vertexIndex) { + return new Vector3f(getNormalX(vertexIndex), getNormalY(vertexIndex), getNormalZ(vertexIndex)); + } + + public void copyNormal(int vertexIndex, Vector3f normal) { + normal.set(getNormalX(vertexIndex), getNormalY(vertexIndex), getNormalZ(vertexIndex)); + } + + // Setters + + public void setPosX(int vertexIndex, float x) { + vertexData[vertexIndex * formatCache.vertexSize + formatCache.position] = Float.floatToRawIntBits(x); + } + + public void setPosY(int vertexIndex, float y) { + vertexData[vertexIndex * formatCache.vertexSize + formatCache.position + 1] = Float.floatToRawIntBits(y); + } + + public void setPosZ(int vertexIndex, float z) { + vertexData[vertexIndex * formatCache.vertexSize + formatCache.position + 2] = Float.floatToRawIntBits(z); + } + + public void setPos(int vertexIndex, float x, float y, float z) { + setPosX(vertexIndex, x); + setPosY(vertexIndex, y); + setPosZ(vertexIndex, z); + } + + public void setPos(int vertexIndex, Vector3f pos) { + setPos(vertexIndex, pos.getX(), pos.getY(), pos.getZ()); + } + + public void setColor(int vertexIndex, int color) { + vertexData[vertexIndex * formatCache.vertexSize + formatCache.color] = color; + } + + public void setTexU(int vertexIndex, float u) { + vertexData[vertexIndex * formatCache.vertexSize + formatCache.texture] = Float.floatToRawIntBits(u); + } + + public void setTexV(int vertexIndex, float v) { + vertexData[vertexIndex * formatCache.vertexSize + formatCache.texture + 1] = Float.floatToRawIntBits(v); + } + + public void setTex(int vertexIndex, float u, float v) { + setTexU(vertexIndex, u); + setTexV(vertexIndex, v); + } + + public void setTex(int vertexIndex, Vector2f tex) { + setTex(vertexIndex, tex.x, tex.y); + } + + public void setLight(int vertexIndex, int light) { + vertexData[vertexIndex * formatCache.vertexSize + formatCache.light] = light; + } + + public void setNormalX(int vertexIndex, float normalX) { + vertexData[vertexIndex * formatCache.vertexSize + formatCache.normal] = Float.floatToRawIntBits(normalX); + } + + public void setNormalY(int vertexIndex, float normalY) { + vertexData[vertexIndex * formatCache.vertexSize + formatCache.normal + 1] = Float.floatToRawIntBits(normalY); + } + + public void setNormalZ(int vertexIndex, float normalZ) { + vertexData[vertexIndex * formatCache.vertexSize + formatCache.normal + 2] = Float.floatToRawIntBits(normalZ); + } + + public void setNormal(int vertexIndex, float normalX, float normalY, float normalZ) { + setNormalX(vertexIndex, normalX); + setNormalY(vertexIndex, normalY); + setNormalZ(vertexIndex, normalZ); + } + + public void setNormal(int vertexIndex, Vector3f normal) { + setNormal(vertexIndex, normal.getX(), normal.getY(), normal.getZ()); + } + + private static class FormatCache { + private static final VertexFormat FORMAT = DefaultVertexFormats.BLOCK; + + public FormatCache() { + refresh(); + } + + // Integer size + public int vertexSize; + + // Element integer offsets + public int position; + public int color; + public int texture; + public int light; + public int normal; + + public void refresh() { + vertexSize = FORMAT.getIntegerSize(); + for (int elementId = 0; elementId < FORMAT.getElements().size(); elementId++) { + VertexFormatElement element = FORMAT.getElements().get(elementId); + int intOffset = FORMAT.getOffset(elementId) / Integer.BYTES; + if (element.getUsage() == VertexFormatElement.Usage.POSITION) { + position = intOffset; + } else if (element.getUsage() == VertexFormatElement.Usage.COLOR) { + color = intOffset; + } else if (element.getUsage() == VertexFormatElement.Usage.UV) { + if (element.getIndex() == 0) { + texture = intOffset; + } else if (element.getIndex() == 2) { + light = intOffset; + } + } else if (element.getUsage() == VertexFormatElement.Usage.NORMAL) { + normal = intOffset; + } + } + } + } +} diff --git a/src/main/java/com/jozufozu/flywheel/util/BufferBuilderReader.java b/src/main/java/com/jozufozu/flywheel/util/BufferBuilderReader.java new file mode 100644 index 000000000..15a6e1390 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/util/BufferBuilderReader.java @@ -0,0 +1,106 @@ +package com.jozufozu.flywheel.util; + +import java.nio.ByteBuffer; + +import com.mojang.datafixers.util.Pair; + +import net.minecraft.client.renderer.BufferBuilder; +import net.minecraft.client.renderer.vertex.VertexFormat; + +public class BufferBuilderReader { + + private final ByteBuffer buffer; + private final int vertexCount; + private final int formatSize; + private final int size; + + public BufferBuilderReader(BufferBuilder builder) { + VertexFormat vertexFormat = builder.getVertexFormat(); + Pair data = builder.popData(); + buffer = data.getSecond(); + + formatSize = vertexFormat.getSize(); + + vertexCount = data.getFirst() + .getCount(); + + size = vertexCount * formatSize; + + // TODO: adjust the getters based on the input format +// ImmutableList elements = vertexFormat.getElements(); +// for (int i = 0, size = elements.size(); i < size; i++) { +// VertexFormatElement element = elements.get(i); +// int offset = vertexFormat.getOffset(i); +// +// element.getUsage() +// } + } + + public boolean isEmpty() { + return vertexCount == 0; + } + + public int vertIdx(int vertexIndex) { + return vertexIndex * formatSize; + } + + public float getX(int index) { + return buffer.getFloat(vertIdx(index)); + } + + public float getY(int index) { + return buffer.getFloat(vertIdx(index) + 4); + } + + public float getZ(int index) { + return buffer.getFloat(vertIdx(index) + 8); + } + + public byte getR(int index) { + return buffer.get(vertIdx(index) + 12); + } + + public byte getG(int index) { + return buffer.get(vertIdx(index) + 13); + } + + public byte getB(int index) { + return buffer.get(vertIdx(index) + 14); + } + + public byte getA(int index) { + return buffer.get(vertIdx(index) + 15); + } + + public float getU(int index) { + return buffer.getFloat(vertIdx(index) + 16); + } + + public float getV(int index) { + return buffer.getFloat(vertIdx(index) + 20); + } + + public int getLight(int index) { + return buffer.getInt(vertIdx(index) + 24); + } + + public byte getNX(int index) { + return buffer.get(vertIdx(index) + 28); + } + + public byte getNY(int index) { + return buffer.get(vertIdx(index) + 29); + } + + public byte getNZ(int index) { + return buffer.get(vertIdx(index) + 30); + } + + public int getVertexCount() { + return vertexCount; + } + + public int getSize() { + return size; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/util/CodecUtil.java b/src/main/java/com/jozufozu/flywheel/util/CodecUtil.java new file mode 100644 index 000000000..3b83fac86 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/util/CodecUtil.java @@ -0,0 +1,26 @@ +package com.jozufozu.flywheel.util; + +import java.util.Collections; +import java.util.List; + +import com.mojang.datafixers.util.Either; +import com.mojang.serialization.Codec; + +public class CodecUtil { + + /** + * Creates a list codec that can be parsed from either a single element or a complete list. + */ + public static Codec> oneOrMore(Codec codec) { + return Codec.either(codec.listOf(), codec) + .xmap( + either -> either.map(l -> l, Collections::singletonList), + list -> { + if (list.size() == 1) { + return Either.right(list.get(0)); + } else { + return Either.left(list); + } + }); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/util/Pair.java b/src/main/java/com/jozufozu/flywheel/util/Pair.java new file mode 100644 index 000000000..b1e232037 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/util/Pair.java @@ -0,0 +1,68 @@ +package com.jozufozu.flywheel.util; + +import java.util.Objects; + +public class Pair { + + F first; + S second; + + protected Pair(F first, S second) { + this.first = first; + this.second = second; + } + + public static Pair of(F first, S second) { + return new Pair<>(first, second); + } + + public F getFirst() { + return first; + } + + public S getSecond() { + return second; + } + + public void setFirst(F first) { + this.first = first; + } + + public void setSecond(S second) { + this.second = second; + } + + public Pair copy() { + return Pair.of(first, second); + } + + @Override + public boolean equals(final Object obj) { + if (obj == this) + return true; + if (obj instanceof Pair) { + final Pair other = (Pair) obj; + return Objects.equals(first, other.first) && Objects.equals(second, other.second); + } + return false; + } + + @Override + public int hashCode() { + return (nullHash(first) * 31) ^ nullHash(second); + } + + int nullHash(Object o) { + return o == null ? 0 : o.hashCode(); + } + + @Override + public String toString() { + return "(" + first + ", " + second + ")"; + } + + public Pair swap() { + return Pair.of(second, first); + } + +} diff --git a/src/main/java/com/jozufozu/flywheel/util/RenderUtil.java b/src/main/java/com/jozufozu/flywheel/util/RenderUtil.java new file mode 100644 index 000000000..82fec9995 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/util/RenderUtil.java @@ -0,0 +1,102 @@ +package com.jozufozu.flywheel.util; + +import java.util.function.Supplier; + +import com.mojang.blaze3d.matrix.MatrixStack; + +import net.minecraft.util.Direction; +import net.minecraft.util.math.vector.Matrix3f; +import net.minecraft.util.math.vector.Matrix4f; +import net.minecraft.util.math.vector.Vector3f; + +public class RenderUtil { + public static int nextPowerOf2(int a) { + int h = Integer.highestOneBit(a); + return (h == a) ? h : (h << 1); + } + + public static boolean isPowerOf2(int n) { + int b = n & (n - 1); + return b == 0 && n != 0; + } + + public static double lengthSqr(double x, double y, double z) { + return x * x + y * y + z * z; + } + + public static double length(double x, double y, double z) { + return Math.sqrt(lengthSqr(x, y, z)); + } + + public static float[] writeMatrixStack(MatrixStack stack) { + return writeMatrixStack(stack.peek().getModel(), stack.peek().getNormal()); + } + + // GPUs want matrices in column major order. + public static float[] writeMatrixStack(Matrix4f model, Matrix3f normal) { + return new float[]{ + model.a00, + model.a10, + model.a20, + model.a30, + model.a01, + model.a11, + model.a21, + model.a31, + model.a02, + model.a12, + model.a22, + model.a32, + model.a03, + model.a13, + model.a23, + model.a33, + normal.a00, + normal.a10, + normal.a20, + normal.a01, + normal.a11, + normal.a21, + normal.a02, + normal.a12, + normal.a22, + }; + } + + public static float[] writeMatrix(Matrix4f model) { + return new float[]{ + model.a00, + model.a10, + model.a20, + model.a30, + model.a01, + model.a11, + model.a21, + model.a31, + model.a02, + model.a12, + model.a22, + model.a32, + model.a03, + model.a13, + model.a23, + model.a33, + }; + } + + public static Supplier rotateToFace(Direction facing) { + return () -> { + MatrixStack stack = new MatrixStack(); +// MatrixStacker.of(stack) +// .centre() +// .rotateY(AngleHelper.horizontalAngle(facing)) +// .rotateX(AngleHelper.verticalAngle(facing)) +// .unCentre(); + stack.peek().getModel().setTranslation(0.5f, 0.5f, 0.5f); + stack.multiply(Vector3f.POSITIVE_Y.getDegreesQuaternion(AngleHelper.horizontalAngle(facing))); + stack.multiply(Vector3f.POSITIVE_X.getDegreesQuaternion(AngleHelper.verticalAngle(facing))); + stack.translate(-0.5f, -0.5f, -0.5f); + return stack; + }; + } +} diff --git a/src/main/java/com/jozufozu/flywheel/util/VirtualEmptyModelData.java b/src/main/java/com/jozufozu/flywheel/util/VirtualEmptyModelData.java new file mode 100644 index 000000000..e61807f85 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/util/VirtualEmptyModelData.java @@ -0,0 +1,30 @@ +package com.jozufozu.flywheel.util; + +import net.minecraftforge.client.model.data.IModelData; +import net.minecraftforge.client.model.data.ModelProperty; + +/** + * This model data instance is passed whenever a model is rendered without + * available in-world context. IBakedModel#getModelData can react accordingly + * and avoid looking for model data itself + **/ +public enum VirtualEmptyModelData implements IModelData { + + INSTANCE; + + @Override + public boolean hasProperty(ModelProperty prop) { + return false; + } + + @Override + public T getData(ModelProperty prop) { + return null; + } + + @Override + public T setData(ModelProperty prop, T data) { + return null; + } + +} diff --git a/src/main/java/com/jozufozu/flywheel/util/WeakHashSet.java b/src/main/java/com/jozufozu/flywheel/util/WeakHashSet.java new file mode 100644 index 000000000..d0fdd7207 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/util/WeakHashSet.java @@ -0,0 +1,119 @@ +package com.jozufozu.flywheel.util; + +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.WeakHashMap; + +import net.minecraft.util.Unit; + +public class WeakHashSet extends AbstractSet { + + WeakHashMap map; + + public WeakHashSet() { + map = new WeakHashMap<>(); + } + + /** + * Constructs a new set containing the elements in the specified + * collection. The HashMap is created with default load factor + * (0.75) and an initial capacity sufficient to contain the elements in + * the specified collection. + * + * @param c the collection whose elements are to be placed into this set + * @throws NullPointerException if the specified collection is null + */ + public WeakHashSet(Collection c) { + map = new WeakHashMap<>(Math.max((int) (c.size() / .75f) + 1, 16)); + addAll(c); + } + + /** + * Constructs a new, empty set; the backing HashMap instance has + * the specified initial capacity and the specified load factor. + * + * @param initialCapacity the initial capacity of the hash map + * @param loadFactor the load factor of the hash map + * @throws IllegalArgumentException if the initial capacity is less + * than zero, or if the load factor is nonpositive + */ + public WeakHashSet(int initialCapacity, float loadFactor) { + map = new WeakHashMap<>(initialCapacity, loadFactor); + } + + /** + * Constructs a new, empty set; the backing HashMap instance has + * the specified initial capacity and default load factor (0.75). + * + * @param initialCapacity the initial capacity of the hash table + * @throws IllegalArgumentException if the initial capacity is less + * than zero + */ + public WeakHashSet(int initialCapacity) { + map = new WeakHashMap<>(initialCapacity); + } + + @Override + public Iterator iterator() { + return map.keySet() + .iterator(); + } + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean add(T t) { + return map.put(t, Unit.INSTANCE) == null; + } + + @Override + public boolean remove(Object o) { + return map.remove((T) o) != null; + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return map.containsKey((T) o); + } + + @Override + public Object[] toArray() { + return map.keySet() + .toArray(); + } + + @Override + public boolean containsAll(Collection c) { + return c.stream() + .allMatch(map::containsKey); + } + + @Override + public boolean addAll(Collection c) { + return false; + } + + @Override + public boolean retainAll(Collection c) { + return false; + } + + @Override + public boolean removeAll(Collection c) { + return false; + } + + @Override + public void clear() { + map.clear(); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/util/WorldAttached.java b/src/main/java/com/jozufozu/flywheel/util/WorldAttached.java new file mode 100644 index 000000000..74531aa96 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/util/WorldAttached.java @@ -0,0 +1,52 @@ +package com.jozufozu.flywheel.util; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + +import javax.annotation.Nonnull; + +import net.minecraft.world.IWorld; + +public class WorldAttached { + + Map attached; + private final WorldAttacher factory; + + public WorldAttached(WorldAttacher factory) { + this.factory = factory; + attached = new HashMap<>(); + } + + @Nonnull + public T get(IWorld world) { + T t = attached.get(world); + if (t != null) + return t; + T entry = factory.attach(world); + put(world, entry); + return entry; + } + + public void put(IWorld world, T entry) { + attached.put(world, entry); + } + + public void forEach(Consumer consumer) { + attached.values() + .forEach(consumer); + } + + @FunctionalInterface + public interface WorldAttacher extends Function { + @Nonnull + T attach(IWorld world); + + @Override + default T apply(IWorld world) { + return attach(world); + } + } + +} diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg new file mode 100644 index 000000000..c47d220df --- /dev/null +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -0,0 +1,96 @@ +public net.minecraft.network.play.ServerPlayNetHandler field_147365_f # floatingTickCount +public net.minecraft.client.network.play.ClientPlayNetHandler field_217287_m # viewDistance + +# CubeParticle +protected net.minecraft.client.particle.Particle field_228343_B_ # collidedY + +# Needed for ChunkUtil, maybe remove these for releases +# ChunkManager +public net.minecraft.world.server.ChunkManager func_219212_a(JLnet/minecraft/world/server/ChunkHolder;)V #scheduleSave +public net.minecraft.world.server.ChunkManager field_219251_e #loadedChunks +public net.minecraft.world.server.ChunkManager field_219262_p #immutableLoadedChunksDirty +public net.minecraft.world.server.ChunkManager field_219253_g #chunksToUnload + +# ChunkStatus +public-f net.minecraft.world.chunk.ChunkStatus field_222617_m #FULL +public net.minecraft.world.chunk.ChunkStatus$IGenerationWorker +public net.minecraft.world.chunk.ChunkStatus$ILoadingWorker + +# PotionBrewing +public net.minecraft.potion.PotionBrewing field_185215_c # POTION_ITEMS + +public net.minecraft.client.gui.FontRenderer func_238419_a_(Lnet/minecraft/util/ResourceLocation;)Lnet/minecraft/client/gui/fonts/Font; # getFontStorage +protected net.minecraft.entity.Entity func_226266_a_(Lnet/minecraft/entity/Entity;Lnet/minecraft/entity/Entity$IMoveCallback;)V # updatePassengerPosition +public net.minecraft.world.server.ChunkManager field_219266_t # field_219266_t +public net.minecraft.world.biome.BiomeManager field_226833_b_ # seed + +public net.minecraft.client.renderer.ItemRenderer field_175057_n # textureManager + +# Beacon +public net.minecraft.tileentity.BeaconTileEntity field_174909_f # beamSegments + +# Server Tick List (For stopping placed fluids from spilling) +public net.minecraft.world.server.ServerTickList field_205374_d # pendingTickListEntriesHashSet +public net.minecraft.world.server.ServerTickList field_205375_e # pendingTickListEntriesTreeSet + +# Lightmap information for instanced rendering +public net.minecraft.client.renderer.LightTexture field_205112_c #resourceLocation +public net.minecraft.client.Minecraft field_193996_ah #renderPartialTicksPaused + +# Functions needed to setup a projection matrix +public net.minecraft.client.renderer.GameRenderer field_78529_t #rendererUpdateCount +public net.minecraft.client.renderer.GameRenderer func_228380_a_(Lcom/mojang/blaze3d/matrix/MatrixStack;F)V #bobViewWhenHurt +public net.minecraft.client.renderer.GameRenderer func_228383_b_(Lcom/mojang/blaze3d/matrix/MatrixStack;F)V #bobView + +# Expose fog state to the public +public com.mojang.blaze3d.platform.GlStateManager$FogState +public com.mojang.blaze3d.platform.GlStateManager field_225663_h_ #FOG +public com.mojang.blaze3d.platform.GlStateManager$BooleanState +public com.mojang.blaze3d.platform.GlStateManager$BooleanState field_179201_b #field_179201_b + +# GameRenderer +public net.minecraft.client.renderer.GameRenderer func_215311_a(Lnet/minecraft/client/renderer/ActiveRenderInfo;FZ)D #getFOVModifier + +# FirstPersonRenderer +public net.minecraft.client.renderer.FirstPersonRenderer field_187467_d # itemStackMainHand +public net.minecraft.client.renderer.FirstPersonRenderer field_187468_e # itemStackOffHand + +# EntityRendererManager +public net.minecraft.client.renderer.entity.EntityRendererManager field_178637_m # playerRenderer + +# IResizeCallback +public net.minecraft.util.palette.IResizeCallback + +# Entity +public net.minecraft.entity.Entity func_205011_p()V # updateAquatics +public net.minecraft.entity.Entity field_70122_E #onGround + +# For uploading matrices as vertex attributes. +public net.minecraft.util.math.vector.Matrix3f field_226097_a_ #a00 +public net.minecraft.util.math.vector.Matrix3f field_226098_b_ #a01 +public net.minecraft.util.math.vector.Matrix3f field_226099_c_ #a02 +public net.minecraft.util.math.vector.Matrix3f field_226100_d_ #a10 +public net.minecraft.util.math.vector.Matrix3f field_226101_e_ #a11 +public net.minecraft.util.math.vector.Matrix3f field_226102_f_ #a12 +public net.minecraft.util.math.vector.Matrix3f field_226103_g_ #a20 +public net.minecraft.util.math.vector.Matrix3f field_226104_h_ #a21 +public net.minecraft.util.math.vector.Matrix3f field_226105_i_ #a22 + +public net.minecraft.util.math.vector.Matrix4f field_226575_a_ #a00 +public net.minecraft.util.math.vector.Matrix4f field_226576_b_ #a01 +public net.minecraft.util.math.vector.Matrix4f field_226577_c_ #a02 +public net.minecraft.util.math.vector.Matrix4f field_226578_d_ #a03 +public net.minecraft.util.math.vector.Matrix4f field_226579_e_ #a10 +public net.minecraft.util.math.vector.Matrix4f field_226580_f_ #a11 +public net.minecraft.util.math.vector.Matrix4f field_226581_g_ #a12 +public net.minecraft.util.math.vector.Matrix4f field_226582_h_ #a13 +public net.minecraft.util.math.vector.Matrix4f field_226583_i_ #a20 +public net.minecraft.util.math.vector.Matrix4f field_226584_j_ #a21 +public net.minecraft.util.math.vector.Matrix4f field_226585_k_ #a22 +public net.minecraft.util.math.vector.Matrix4f field_226586_l_ #a23 +public net.minecraft.util.math.vector.Matrix4f field_226587_m_ #a30 +public net.minecraft.util.math.vector.Matrix4f field_226588_n_ #a31 +public net.minecraft.util.math.vector.Matrix4f field_226589_o_ #a32 +public net.minecraft.util.math.vector.Matrix4f field_226590_p_ #a33 + +public net.minecraft.client.renderer.WorldRenderer field_228407_B_ #blockBreakingProgressions diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml new file mode 100644 index 000000000..01536bdaf --- /dev/null +++ b/src/main/resources/META-INF/mods.toml @@ -0,0 +1,26 @@ +modLoader = "javafml" +loaderVersion = "[36,)" +license = "LGPL3" + +[[mods]] +modId = "flywheel" +version = "${file.jarVersion}" +displayName = "Flywheel" +authors="Jozufozu" +description = ''' +A modern engine for modded minecraft. +''' + +[[dependencies.flywheel]] +modId = "forge" +mandatory = true +versionRange = "[36,)" +ordering = "NONE" +side = "BOTH" + +[[dependencies.flywheel]] +modId = "minecraft" +mandatory = true +versionRange = "[1.16.5,1.17)" +ordering = "NONE" +side = "BOTH" diff --git a/src/main/resources/assets/flywheel/flywheel/programs/model.json b/src/main/resources/assets/flywheel/flywheel/programs/model.json new file mode 100644 index 000000000..b30bb0fe2 --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/programs/model.json @@ -0,0 +1,29 @@ +{ + "vert": "flywheel:model.vert", + "frag": "flywheel:block.frag", + "states": [ + { + "when": { + "provider": "flywheel:normal_debug", + "value": "true" + }, + "define": "DEBUG_NORMAL" + }, + { + "when": { + "provider": "flywheel:fog_mode", + "value": "linear" + }, + "define": ["USE_FOG", "USE_FOG_LINEAR"], + "extend": "flywheel:fog_linear" + }, + { + "when": { + "provider": "flywheel:fog_mode", + "value": "exp2" + }, + "define": ["USE_FOG", "USE_FOG_EXP2"], + "extend": "flywheel:fog_exp2" + } + ] +} diff --git a/src/main/resources/assets/flywheel/flywheel/programs/oriented.json b/src/main/resources/assets/flywheel/flywheel/programs/oriented.json new file mode 100644 index 000000000..0340b5602 --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/programs/oriented.json @@ -0,0 +1,29 @@ +{ + "vert": "flywheel:oriented.vert", + "frag": "flywheel:block.frag", + "states": [ + { + "when": { + "provider": "flywheel:normal_debug", + "value": "true" + }, + "define": "DEBUG_NORMAL" + }, + { + "when": { + "provider": "flywheel:fog_mode", + "value": "linear" + }, + "define": ["USE_FOG", "USE_FOG_LINEAR"], + "extend": "flywheel:fog_linear" + }, + { + "when": { + "provider": "flywheel:fog_mode", + "value": "exp2" + }, + "define": ["USE_FOG", "USE_FOG_EXP2"], + "extend": "flywheel:fog_exp2" + } + ] +} diff --git a/src/main/resources/assets/flywheel/flywheel/shaders/block.frag b/src/main/resources/assets/flywheel/flywheel/shaders/block.frag new file mode 100644 index 000000000..1eae7e5cc --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/shaders/block.frag @@ -0,0 +1,17 @@ +#flwbuiltins + +#flwinclude <"flywheel:data/blockfragment.glsl"> + +void FLWMain(BlockFrag r) { + vec4 tex = FLWBlockTexture(r.texCoords); + + vec4 color = vec4(tex.rgb * FLWLight(r.light).rgb * r.diffuse, tex.a) * r.color; + +// flw_WorldPos = ; +// flw_Normal = ; +// flw_Albedo = tex.rgb; +// flw_Alpha = tex.a; +// flw_LightMap = r.light; +// flw_Tint = r.color; + FLWFinalizeColor(color); +} diff --git a/src/main/resources/assets/flywheel/flywheel/shaders/context/crumbling/builtin.frag b/src/main/resources/assets/flywheel/flywheel/shaders/context/crumbling/builtin.frag new file mode 100644 index 000000000..3f68e588c --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/shaders/context/crumbling/builtin.frag @@ -0,0 +1,29 @@ +#flwinclude <"flywheel:context/world/fog.glsl"> + +uniform vec2 uTextureScale; +uniform sampler2D uBlockAtlas; +uniform sampler2D uLightMap; +uniform sampler2D uCrumbling; + +vec4 FLWBlockTexture(vec2 texCoords) { + vec4 cr = texture2D(uCrumbling, texCoords * uTextureScale); + float diffuseAlpha = texture2D(uBlockAtlas, texCoords).a; + cr.a = cr.a * diffuseAlpha; + return cr; +} + +void FLWFinalizeColor(vec4 color) { + #if defined(USE_FOG) + float a = color.a; + float fog = clamp(FLWFogFactor(), 0., 1.); + + color = mix(uFogColor, color, fog); + color.a = a; + #endif + + gl_FragColor = color; +} + +vec4 FLWLight(vec2 lightCoords) { + return vec4(1.); +} diff --git a/src/main/resources/assets/flywheel/flywheel/shaders/context/crumbling/builtin.vert b/src/main/resources/assets/flywheel/flywheel/shaders/context/crumbling/builtin.vert new file mode 100644 index 000000000..730ca3556 --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/shaders/context/crumbling/builtin.vert @@ -0,0 +1 @@ +#flwinclude <"flywheel:context/world/builtin.vert"> diff --git a/src/main/resources/assets/flywheel/flywheel/shaders/context/world/builtin.frag b/src/main/resources/assets/flywheel/flywheel/shaders/context/world/builtin.frag new file mode 100644 index 000000000..e8b615c75 --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/shaders/context/world/builtin.frag @@ -0,0 +1,25 @@ +#flwinclude <"flywheel:context/world/fog.glsl"> +#flwinclude <"flywheel:core/lightutil.glsl"> + +uniform sampler2D uBlockAtlas; +uniform sampler2D uLightMap; + +vec4 FLWBlockTexture(vec2 texCoords) { + return texture2D(uBlockAtlas, texCoords); +} + +void FLWFinalizeColor(vec4 color) { + #if defined(USE_FOG) + float a = color.a; + float fog = clamp(FLWFogFactor(), 0., 1.); + + color = mix(uFogColor, color, fog); + color.a = a; + #endif + + gl_FragColor = color; +} + +vec4 FLWLight(vec2 lightCoords) { + return texture2D(uLightMap, shiftLight(lightCoords)); +} diff --git a/src/main/resources/assets/flywheel/flywheel/shaders/context/world/builtin.vert b/src/main/resources/assets/flywheel/flywheel/shaders/context/world/builtin.vert new file mode 100644 index 000000000..251267397 --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/shaders/context/world/builtin.vert @@ -0,0 +1,19 @@ +uniform float uTime; +uniform mat4 uViewProjection; +uniform vec3 uCameraPos; + +#if defined(USE_FOG) +varying float FragDistance; +#endif + +void FLWFinalizeWorldPos(inout vec4 worldPos) { + #if defined(USE_FOG) + FragDistance = length(worldPos.xyz - uCameraPos); + #endif + + gl_Position = uViewProjection * worldPos; +} + +void FLWFinalizeNormal(inout vec3 normal) { + // noop +} diff --git a/src/main/resources/assets/flywheel/flywheel/shaders/context/world/fog.glsl b/src/main/resources/assets/flywheel/flywheel/shaders/context/world/fog.glsl new file mode 100644 index 000000000..75a68d176 --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/shaders/context/world/fog.glsl @@ -0,0 +1,21 @@ +#if defined(USE_FOG) +varying float FragDistance; +uniform vec4 uFogColor; +#endif + +#if defined(USE_FOG_LINEAR) +uniform vec2 uFogRange; + +float FLWFogFactor() { + return (uFogRange.y - FragDistance) / (uFogRange.y - uFogRange.x); +} + #endif + + #if defined(USE_FOG_EXP2) +uniform float uFogDensity; + +float FLWFogFactor() { + float dist = FragDistance * uFogDensity; + return 1. / exp2(dist * dist); +} + #endif diff --git a/src/main/resources/assets/flywheel/flywheel/shaders/core/color.glsl b/src/main/resources/assets/flywheel/flywheel/shaders/core/color.glsl new file mode 100644 index 000000000..f6f9d271b --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/shaders/core/color.glsl @@ -0,0 +1,22 @@ +// All components are in the range [0…1], including hue. +vec3 rgb2hsv(vec3 c) { + const vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); + vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g)); + vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r)); + + float d = q.x - min(q.w, q.y); + float e = 1.0e-10; + return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); +} + +// All components are in the range [0…1], including hue. +vec3 hsv2rgb(vec3 hsv) { + const vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(hsv.xxx + K.xyz) * 6.0 - K.www); + return hsv.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), hsv.y); +} + +vec3 hsv2rgbWrapped(vec3 hsv) { + hsv.x = fract(hsv.x); + return hsv2rgb(hsv); +} diff --git a/src/main/resources/assets/flywheel/flywheel/shaders/core/diffuse.glsl b/src/main/resources/assets/flywheel/flywheel/shaders/core/diffuse.glsl new file mode 100644 index 000000000..bc5792231 --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/shaders/core/diffuse.glsl @@ -0,0 +1,5 @@ + +float diffuse(vec3 normal) { + vec3 n2 = normal * normal * vec3(.6, .25, .8); + return min(n2.x + n2.y * (3. + normal.y) + n2.z, 1.); +} diff --git a/src/main/resources/assets/flywheel/flywheel/shaders/core/lightutil.glsl b/src/main/resources/assets/flywheel/flywheel/shaders/core/lightutil.glsl new file mode 100644 index 000000000..9967c4f51 --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/shaders/core/lightutil.glsl @@ -0,0 +1,4 @@ +// Adjust the [0,1] normalized lightmap value based on the texture matrix from LightTexture#enableLightmap +vec2 shiftLight(vec2 lm) { + return lm * 0.99609375 + 0.03125;// * 255/256 + 1/32 +} diff --git a/src/main/resources/assets/flywheel/flywheel/shaders/core/matutils.glsl b/src/main/resources/assets/flywheel/flywheel/shaders/core/matutils.glsl new file mode 100644 index 000000000..d5f4d9c6c --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/shaders/core/matutils.glsl @@ -0,0 +1,29 @@ + +mat4 rotate(vec3 axis, float angle) { + float s = sin(angle); + float c = cos(angle); + float oc = 1. - c; + + vec3 sa = axis * s; + + mat4 mr = mat4(1.); + mr[0].xyz = oc * axis.xxz * axis.xyx + vec3(c, sa.z, -sa.y); + mr[1].xyz = oc * axis.xyy * axis.yyz + vec3(-sa.z, c, sa.x); + mr[2].xyz = oc * axis.zyz * axis.xzz + vec3(sa.y, -sa.x, c); + + return mr; +} + +mat4 rotation(vec3 rot) { + return rotate(vec3(0., 1., 0.), rot.y) * rotate(vec3(0., 0., 1.), rot.z) * rotate(vec3(1., 0., 0.), rot.x); +} + +mat3 modelToNormal(mat4 mat) { + // Discard the edges. This won't be accurate for scaled or skewed matrices, + // but we don't have to work with those often. + mat3 m; + m[0] = mat[0].xyz; + m[1] = mat[1].xyz; + m[2] = mat[2].xyz; + return m; +} diff --git a/src/main/resources/assets/flywheel/flywheel/shaders/core/quaternion.glsl b/src/main/resources/assets/flywheel/flywheel/shaders/core/quaternion.glsl new file mode 100644 index 000000000..fb3e1ae95 --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/shaders/core/quaternion.glsl @@ -0,0 +1,27 @@ + +#define PIOVER2 1.5707963268 + +vec4 quat(vec3 axis, float angle) { + float halfAngle = angle * PIOVER2 / 180.0; + vec2 cs = sin(vec2(PIOVER2 - halfAngle, halfAngle)); // compute sin and cos in one instruction + return vec4(axis.xyz * cs.y, cs.x); +} + +vec4 quatMult(vec4 q1, vec4 q2) { + // disgustingly vectorized quaternion multiplication + vec4 a = q1.w * q2.xyzw; + vec4 b = q1.x * q2.wzxy * vec4(1., -1., 1., -1.); + vec4 c = q1.y * q2.zwxy * vec4(1., 1., -1., -1.); + vec4 d = q1.z * q2.yxwz * vec4(-1., 1., 1., -1.); + + return a + b + c + d; +} + +vec3 rotateVertexByQuat(vec3 v, vec4 q) { + vec3 i = q.xyz; + return v + 2.0 * cross(i, cross(i, v) + q.w * v); +} + +vec3 rotateAbout(vec3 v, vec3 axis, float angle) { + return rotateVertexByQuat(v, quat(axis, angle)); +} diff --git a/src/main/resources/assets/flywheel/flywheel/shaders/data/blockfragment.glsl b/src/main/resources/assets/flywheel/flywheel/shaders/data/blockfragment.glsl new file mode 100644 index 000000000..e5d09eb16 --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/shaders/data/blockfragment.glsl @@ -0,0 +1,7 @@ +#[Fragment] +struct BlockFrag { + vec2 texCoords; + vec4 color; + float diffuse; + vec2 light; +}; diff --git a/src/main/resources/assets/flywheel/flywheel/shaders/data/modelvertex.glsl b/src/main/resources/assets/flywheel/flywheel/shaders/data/modelvertex.glsl new file mode 100644 index 000000000..5e34d2883 --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/shaders/data/modelvertex.glsl @@ -0,0 +1,6 @@ +#[VertexData] +struct Vertex { + vec3 pos; + vec3 normal; + vec2 texCoords; +}; diff --git a/src/main/resources/assets/flywheel/flywheel/shaders/model.vert b/src/main/resources/assets/flywheel/flywheel/shaders/model.vert new file mode 100644 index 000000000..efdfbf804 --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/shaders/model.vert @@ -0,0 +1,35 @@ +#flwbuiltins +#flwinclude <"flywheel:core/diffuse.glsl"> + +#flwinclude <"flywheel:data/modelvertex.glsl"> +#flwinclude <"flywheel:data/blockfragment.glsl"> + +#[InstanceData] +struct Instance { + vec2 light; + vec4 color; + mat4 transform; + mat3 normalMat; +}; + +BlockFrag FLWMain(Vertex v, Instance i) { + vec4 worldPos = i.transform * vec4(v.pos, 1.); + + vec3 norm = i.normalMat * v.normal; + + FLWFinalizeWorldPos(worldPos); + FLWFinalizeNormal(norm); + + norm = normalize(norm); + + BlockFrag b; + b.diffuse = diffuse(norm); + b.texCoords = v.texCoords; + b.light = i.light; + #if defined(DEBUG_NORMAL) + b.color = vec4(norm, 1.); + #else + b.color = i.color; + #endif + return b; +} diff --git a/src/main/resources/assets/flywheel/flywheel/shaders/oriented.vert b/src/main/resources/assets/flywheel/flywheel/shaders/oriented.vert new file mode 100644 index 000000000..5c040349d --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/shaders/oriented.vert @@ -0,0 +1,36 @@ +#flwbuiltins +#flwinclude <"flywheel:core/matutils.glsl"> +#flwinclude <"flywheel:core/quaternion.glsl"> +#flwinclude <"flywheel:core/diffuse.glsl"> + +#[InstanceData] +struct Oriented { + vec2 light; + vec4 color; + vec3 pos; + vec3 pivot; + vec4 rotation; +}; + +#flwinclude <"flywheel:data/modelvertex.glsl"> +#flwinclude <"flywheel:data/blockfragment.glsl"> + +BlockFrag FLWMain(Vertex v, Oriented o) { + vec4 worldPos = vec4(rotateVertexByQuat(v.pos - o.pivot, o.rotation) + o.pivot + o.pos, 1.); + + vec3 norm = rotateVertexByQuat(v.normal, o.rotation); + + FLWFinalizeWorldPos(worldPos); + FLWFinalizeNormal(norm); + + BlockFrag b; + b.diffuse = diffuse(norm); + b.texCoords = v.texCoords; + b.light = o.light; + #if defined(DEBUG_NORMAL) + b.color = vec4(norm, 1.); + #else + b.color = o.color; + #endif + return b; +} diff --git a/src/main/resources/assets/flywheel/flywheel/shaders/smooth_oriented.glsl b/src/main/resources/assets/flywheel/flywheel/shaders/smooth_oriented.glsl new file mode 100644 index 000000000..4b6aac8ce --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/shaders/smooth_oriented.glsl @@ -0,0 +1,55 @@ +#flwbuiltins +#flwinclude <"flywheel:core/matutils.glsl"> +#flwinclude <"flywheel:core/quaternion.glsl"> +#flwinclude <"flywheel:core/diffuse.glsl"> + +#[InstanceData] +struct Oriented { +// each vec 4 is 2 light coords packed +// x z + vec4 lightA;// lo, lo + vec4 lightB;// hi, lo + vec4 lightC;// hi, hi + vec4 lightD;// lo, hi + + vec3 loCorner; + vec3 size; + + vec4 color; + vec3 pos; + vec3 pivot; + vec4 rotation; +}; + +#flwinclude <"flywheel:data/modelvertex.glsl"> +#flwinclude <"flywheel:data/blockfragment.glsl"> + +BlockFrag FLWMain(Vertex v, Oriented o) { + vec4 worldPos = vec4(rotateVertexByQuat(v.pos - o.pivot, o.rotation) + o.pivot + o.pos, 1.); + + vec3 norm = rotateVertexByQuat(v.normal, o.rotation); + + FLWFinalizeWorldPos(worldPos); + FLWFinalizeNormal(norm); + + // manual trilinear interpolation + vec3 lightPos = (worldPos.xyz - o.loCorner) / o.size; + + vec4 lightLoZ = mix(lightA, lightB, lightPos.x);// lo z + vec4 lightHiZ = mix(lightD, lightC, lightPos.x);// hi z + + vec4 lightE = mix(lightLoZ, lightHiZ, lightPos.z);// + + vec2 lightCoord = mix(lightE.xy, lightE.zw, lightPos.y); + + BlockFrag b; + b.diffuse = diffuse(norm); + b.texCoords = v.texCoords; + b.light = lightCoord; + #if defined(DEBUG_NORMAL) + b.color = vec4(norm, 1.); + #else + b.color = o.color; + #endif + return b; +} diff --git a/src/main/resources/assets/flywheel/flywheel/shaders/template/instanced/instanced.frag b/src/main/resources/assets/flywheel/flywheel/shaders/template/instanced/instanced.frag new file mode 100644 index 000000000..2f1bd8e03 --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/shaders/template/instanced/instanced.frag @@ -0,0 +1,19 @@ +#version 110 + +#flwbeginbody + +#FLWPrefixFields(Fragment, varying, v2f_) + +//vec3 flw_WorldPos; +//vec3 flw_Normal; +//vec3 flw_Albedo; +//float flw_Alpha; +//vec2 flw_LightMap; +//vec4 flw_Tint; + +void main() { + Fragment f; + #FLWAssignFields(Fragment, f., v2f_) + + FLWMain(f); +} diff --git a/src/main/resources/assets/flywheel/flywheel/shaders/template/instanced/instanced.vert b/src/main/resources/assets/flywheel/flywheel/shaders/template/instanced/instanced.vert new file mode 100644 index 000000000..83cc28e2b --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/shaders/template/instanced/instanced.vert @@ -0,0 +1,19 @@ +#version 110 + +#flwbeginbody +#FLWPrefixFields(VertexData, attribute, a_v_) +#FLWPrefixFields(InstanceData, attribute, a_i_) + +#FLWPrefixFields(Fragment, varying, v2f_) + +void main() { + VertexData v; + #FLWAssignFields(VertexData, v., a_v_) + + InstanceData i; + #FLWAssignFields(InstanceData, i., a_i_) + + Fragment o = FLWMain(v, i); + + #FLWAssignFields(Fragment, v2f_, o.) +} diff --git a/src/main/resources/assets/flywheel/flywheel/shaders/template/meshlet/meshlet.glsl b/src/main/resources/assets/flywheel/flywheel/shaders/template/meshlet/meshlet.glsl new file mode 100644 index 000000000..c1c3f8328 --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/shaders/template/meshlet/meshlet.glsl @@ -0,0 +1,43 @@ +#version 450 +#extension GL_NV_mesh_shader : require + +layout(local_size_x=32) in; + +layout(max_vertices=64, max_primitives=32) out; + +layout (std430, binding = 1) buffer _vertices { + FLWVertexData vertices[]; +} vb; + +struct s_meshlet { + uint vertices[64]; + uint indices[96]; + uint vertex_count; + uint index_count; +}; + +layout (std430, binding = 2) buffer _meshlets { + s_meshlet meshlets[]; +} mbuf; + +layout (location = 0) out PerVertexData { + vec4 color; +} v_out[];// [max_vertices] + +void main() { + uint mi = gl_WorkGroupID.x; + uint thread_id = gl_LocalInvocationID.x; + + uint primIdx = thread_id * 3; + uint vertStartIdx = thread_id * 2; + + gl_MeshVerticesNV[vertStartIdx + 0].gl_Position; + gl_MeshVerticesNV[vertStartIdx + 1].gl_Position; + + gl_PrimitiveIndicesNV[primIdx + 0] = mbuf.meshlets[mi].indices[primIdx + 0]; + gl_PrimitiveIndicesNV[primIdx + 1] = mbuf.meshlets[mi].indices[primIdx + 1]; + gl_PrimitiveIndicesNV[primIdx + 2] = mbuf.meshlets[mi].indices[primIdx + 2]; + + gl_PrimitiveCountNV = mbuf.meshlets[mi].vertex_count / 2; + +} diff --git a/src/main/resources/assets/flywheel/flywheel/shaders/template/model/model.frag b/src/main/resources/assets/flywheel/flywheel/shaders/template/model/model.frag new file mode 100644 index 000000000..fb9620d7d --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/shaders/template/model/model.frag @@ -0,0 +1,12 @@ +#version 110 + +#flwbeginbody + +#FLWPrefixFields(Fragment, varying, v2f_) + +void main() { + Fragment f; + #FLWAssignFields(Fragment, f., v2f_) + + FLWMain(f); +} diff --git a/src/main/resources/assets/flywheel/flywheel/shaders/template/model/model.vert b/src/main/resources/assets/flywheel/flywheel/shaders/template/model/model.vert new file mode 100644 index 000000000..1a235e153 --- /dev/null +++ b/src/main/resources/assets/flywheel/flywheel/shaders/template/model/model.vert @@ -0,0 +1,15 @@ +#version 110 + +#flwbeginbody +#FLWPrefixFields(VertexData, attribute, a_v_) + +#FLWPrefixFields(Fragment, varying, v2f_) + +void main() { + VertexData v; + #FLWAssignFields(VertexData, v., a_v_) + + Fragment o = FLWMain(v); + + #FLWAssignFields(Fragment, v2f_, o.) +} diff --git a/src/main/resources/flywheel.mixins.json b/src/main/resources/flywheel.mixins.json new file mode 100644 index 000000000..713089060 --- /dev/null +++ b/src/main/resources/flywheel.mixins.json @@ -0,0 +1,24 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "com.jozufozu.flywheel.mixin", + "compatibilityLevel": "JAVA_8", + "refmap": "flywheel.refmap.json", + "mixins": [ + ], + "client": [ + "CancelEntityRenderMixin", + "CancelTileEntityRenderMixin", + "FogColorTrackerMixin", + "RenderHooksMixin", + "ShaderCloseMixin", + "StoreProjectionMatrixMixin", + "TileRemoveMixin", + "TileWorldHookMixin", + "light.LightUpdateMixin", + "light.NetworkLightUpdateMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta new file mode 100644 index 000000000..95e381309 --- /dev/null +++ b/src/main/resources/pack.mcmeta @@ -0,0 +1,7 @@ +{ + "pack": { + "description": "flywheel resources", + "pack_format": 6, + "_comment": "A pack_format of 6 requires json lang files and some texture changes from 1.16.2. Note: we require v6 pack meta for all mods." + } +}