From f460e229dfe66faf81aed1d2c17cf5a95ff8cdc0 Mon Sep 17 00:00:00 2001 From: JozsefA Date: Wed, 16 Jun 2021 12:57:52 -0700 Subject: [PATCH] Squish - Squash all commits before separating flywheel from create --- .editorconfig | 22 ++ .gitattributes | 4 + .gitignore | 42 +++ build.gradle | 160 +++++++++ gradle.properties | 19 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54708 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 172 +++++++++ gradlew.bat | 84 +++++ .../java/com/jozufozu/flywheel/Flywheel.java | 84 +++++ .../jozufozu/flywheel/backend/Backend.java | 184 ++++++++++ .../flywheel/backend/IFlywheelWorld.java | 13 + .../flywheel/backend/IShaderContext.java | 23 ++ .../flywheel/backend/OptifineHandler.java | 86 +++++ .../jozufozu/flywheel/backend/RenderWork.java | 30 ++ .../flywheel/backend/ResourceUtil.java | 19 + .../flywheel/backend/ShaderContext.java | 88 +++++ .../flywheel/backend/ShaderSources.java | 212 +++++++++++ .../flywheel/backend/SpecMetaRegistry.java | 56 +++ .../flywheel/backend/gl/GlNumericType.java | 67 ++++ .../flywheel/backend/gl/GlObject.java | 43 +++ .../flywheel/backend/gl/GlPrimitive.java | 23 ++ .../flywheel/backend/gl/GlTexture.java | 25 ++ .../flywheel/backend/gl/GlVertexArray.java | 21 ++ .../backend/gl/attrib/CommonAttributes.java | 21 ++ .../backend/gl/attrib/IAttribSpec.java | 10 + .../backend/gl/attrib/MatrixAttributes.java | 37 ++ .../backend/gl/attrib/VertexAttribSpec.java | 41 +++ .../backend/gl/attrib/VertexFormat.java | 61 ++++ .../flywheel/backend/gl/buffer/GlBuffer.java | 69 ++++ .../backend/gl/buffer/GlBufferType.java | 32 ++ .../backend/gl/buffer/GlBufferUsage.java | 22 ++ .../backend/gl/buffer/MappedBuffer.java | 110 ++++++ .../backend/gl/buffer/MappedBufferRange.java | 33 ++ .../backend/gl/buffer/MappedBufferUsage.java | 16 + .../backend/gl/buffer/MappedFullBuffer.java | 21 ++ .../flywheel/backend/gl/buffer/VecBuffer.java | 116 +++++++ .../flywheel/backend/gl/shader/GlProgram.java | 77 ++++ .../flywheel/backend/gl/shader/GlShader.java | 46 +++ .../backend/gl/shader/ShaderType.java | 17 + .../backend/gl/versioned/GlCompat.java | 113 ++++++ .../backend/gl/versioned/GlVersioned.java | 17 + .../backend/gl/versioned/MapBufferRange.java | 49 +++ .../backend/gl/versioned/RGPixelFormat.java | 77 ++++ .../gl/versioned/framebuffer/Blit.java | 45 +++ .../gl/versioned/framebuffer/Framebuffer.java | 57 +++ .../versioned/instancing/DrawInstanced.java | 76 ++++ .../versioned/instancing/InstancedArrays.java | 45 +++ .../instancing/VertexArrayObject.java | 79 +++++ .../backend/instancing/IDynamicInstance.java | 32 ++ .../backend/instancing/IInstance.java | 22 ++ .../backend/instancing/IInstanceFactory.java | 5 + .../backend/instancing/IInstanceRendered.java | 18 + .../backend/instancing/ITickableInstance.java | 40 +++ .../backend/instancing/InstanceData.java | 28 ++ .../backend/instancing/InstanceManager.java | 245 +++++++++++++ .../backend/instancing/InstanceMaterial.java | 170 +++++++++ .../instancing/InstancedRenderDispatcher.java | 180 ++++++++++ .../instancing/InstancedRenderRegistry.java | 57 +++ .../backend/instancing/Instancer.java | 252 ++++++++++++++ .../backend/instancing/MaterialManager.java | 161 +++++++++ .../backend/instancing/MaterialRenderer.java | 40 +++ .../backend/instancing/MaterialSpec.java | 47 +++ .../instancing/entity/EntityInstance.java | 133 +++++++ .../entity/EntityInstanceManager.java | 43 +++ .../entity/IEntityInstanceFactory.java | 10 + .../instancing/tile/ITileInstanceFactory.java | 10 + .../instancing/tile/TileEntityInstance.java | 130 +++++++ .../instancing/tile/TileInstanceManager.java | 45 +++ .../backend/loading/IProcessingStage.java | 7 + .../loading/InstancedArraysTemplate.java | 39 +++ .../flywheel/backend/loading/LayoutTag.java | 19 + .../backend/loading/ModelTemplate.java | 35 ++ .../flywheel/backend/loading/Program.java | 69 ++++ .../backend/loading/ProgramTemplate.java | 30 ++ .../flywheel/backend/loading/Shader.java | 149 ++++++++ .../loading/ShaderLoadingException.java | 22 ++ .../backend/loading/ShaderTemplate.java | 121 +++++++ .../backend/loading/ShaderTransformer.java | 38 ++ .../flywheel/backend/loading/TaggedField.java | 48 +++ .../backend/loading/TaggedStruct.java | 49 +++ .../flywheel/backend/loading/TypeHelper.java | 37 ++ .../backend/model/ArrayModelRenderer.java | 28 ++ .../flywheel/backend/model/BufferedModel.java | 93 +++++ .../flywheel/backend/model/ElementBuffer.java | 25 ++ .../flywheel/backend/model/IndexedModel.java | 59 ++++ .../flywheel/backend/model/ModelRenderer.java | 25 ++ .../jozufozu/flywheel/core/AtlasStitcher.java | 40 +++ .../com/jozufozu/flywheel/core/Contexts.java | 49 +++ .../core/CrumblingInstanceManager.java | 17 + .../flywheel/core/CrumblingProgram.java | 31 ++ .../com/jozufozu/flywheel/core/Formats.java | 24 ++ .../flywheel/core/FullscreenQuad.java | 61 ++++ .../com/jozufozu/flywheel/core/Materials.java | 34 ++ .../jozufozu/flywheel/core/PartialModel.java | 56 +++ .../com/jozufozu/flywheel/core/Programs.java | 10 + .../jozufozu/flywheel/core/QuadConverter.java | 174 ++++++++++ .../flywheel/core/StitchedSprite.java | 31 ++ .../jozufozu/flywheel/core/WorldContext.java | 165 +++++++++ .../core/instancing/ConditionalInstance.java | 61 ++++ .../core/instancing/GroupInstance.java | 84 +++++ .../core/instancing/SelectInstance.java | 61 ++++ .../flywheel/core/materials/BasicData.java | 77 ++++ .../flywheel/core/materials/IFlatLight.java | 27 ++ .../flywheel/core/materials/ModelData.java | 28 ++ .../flywheel/core/materials/OrientedData.java | 99 ++++++ .../core/shader/ExtensibleGlProgram.java | 71 ++++ .../flywheel/core/shader/FogMode.java | 60 ++++ .../jozufozu/flywheel/core/shader/GlFog.java | 47 +++ .../flywheel/core/shader/IMultiProgram.java | 26 ++ .../core/shader/IProgramCallback.java | 19 + .../shader/StateSensitiveMultiProgram.java | 52 +++ .../flywheel/core/shader/WorldFog.java | 47 +++ .../flywheel/core/shader/WorldProgram.java | 56 +++ .../shader/extension/IExtensionInstance.java | 14 + .../shader/extension/IProgramExtension.java | 26 ++ .../extension/UnitExtensionInstance.java | 23 ++ .../shader/gamestate/FogStateProvider.java | 22 ++ .../shader/gamestate/IGameStateProvider.java | 15 + .../gamestate/NormalDebugStateProvider.java | 26 ++ .../shader/spec/BooleanContextCondition.java | 37 ++ .../shader/spec/IBooleanStateProvider.java | 13 + .../core/shader/spec/IContextCondition.java | 14 + .../core/shader/spec/ProgramSpec.java | 61 ++++ .../core/shader/spec/ProgramState.java | 67 ++++ .../shader/spec/SpecificValueCondition.java | 42 +++ .../flywheel/event/BeginFrameEvent.java | 45 +++ .../flywheel/event/EntityWorldHandler.java | 27 ++ .../jozufozu/flywheel/event/ForgeEvents.java | 56 +++ .../flywheel/event/GatherContextEvent.java | 19 + .../flywheel/event/ReloadRenderersEvent.java | 16 + .../flywheel/event/RenderLayerEvent.java | 48 +++ .../flywheel/light/GridAlignedBB.java | 323 +++++++++++++++++ .../flywheel/light/ICoordinateConsumer.java | 6 + .../flywheel/light/ILightUpdateListener.java | 32 ++ .../jozufozu/flywheel/light/LightUpdater.java | 193 +++++++++++ .../jozufozu/flywheel/light/LightVolume.java | 328 ++++++++++++++++++ .../mixin/CancelEntityRenderMixin.java | 32 ++ .../mixin/CancelTileEntityRenderMixin.java | 36 ++ .../flywheel/mixin/FogColorTrackerMixin.java | 21 ++ .../flywheel/mixin/RenderHooksMixin.java | 103 ++++++ .../flywheel/mixin/ShaderCloseMixin.java | 35 ++ .../mixin/StoreProjectionMatrixMixin.java | 37 ++ .../flywheel/mixin/TileRemoveMixin.java | 30 ++ .../flywheel/mixin/TileWorldHookMixin.java | 55 +++ .../mixin/light/LightUpdateMixin.java | 58 ++++ .../mixin/light/NetworkLightUpdateMixin.java | 52 +++ .../jozufozu/flywheel/util/AngleHelper.java | 57 +++ .../flywheel/util/AnimationTickHolder.java | 41 +++ .../jozufozu/flywheel/util/AttribUtil.java | 26 ++ .../flywheel/util/BakedQuadWrapper.java | 216 ++++++++++++ .../flywheel/util/BufferBuilderReader.java | 106 ++++++ .../com/jozufozu/flywheel/util/CodecUtil.java | 26 ++ .../java/com/jozufozu/flywheel/util/Pair.java | 68 ++++ .../jozufozu/flywheel/util/RenderUtil.java | 102 ++++++ .../flywheel/util/VirtualEmptyModelData.java | 30 ++ .../jozufozu/flywheel/util/WeakHashSet.java | 119 +++++++ .../jozufozu/flywheel/util/WorldAttached.java | 52 +++ .../resources/META-INF/accesstransformer.cfg | 96 +++++ src/main/resources/META-INF/mods.toml | 26 ++ .../flywheel/flywheel/programs/model.json | 29 ++ .../flywheel/flywheel/programs/oriented.json | 29 ++ .../flywheel/flywheel/shaders/block.frag | 17 + .../shaders/context/crumbling/builtin.frag | 29 ++ .../shaders/context/crumbling/builtin.vert | 1 + .../shaders/context/world/builtin.frag | 25 ++ .../shaders/context/world/builtin.vert | 19 + .../flywheel/shaders/context/world/fog.glsl | 21 ++ .../flywheel/flywheel/shaders/core/color.glsl | 22 ++ .../flywheel/shaders/core/diffuse.glsl | 5 + .../flywheel/shaders/core/lightutil.glsl | 4 + .../flywheel/shaders/core/matutils.glsl | 29 ++ .../flywheel/shaders/core/quaternion.glsl | 27 ++ .../flywheel/shaders/data/blockfragment.glsl | 7 + .../flywheel/shaders/data/modelvertex.glsl | 6 + .../flywheel/flywheel/shaders/model.vert | 35 ++ .../flywheel/flywheel/shaders/oriented.vert | 36 ++ .../flywheel/shaders/smooth_oriented.glsl | 55 +++ .../shaders/template/instanced/instanced.frag | 19 + .../shaders/template/instanced/instanced.vert | 19 + .../shaders/template/meshlet/meshlet.glsl | 43 +++ .../shaders/template/model/model.frag | 12 + .../shaders/template/model/model.vert | 15 + src/main/resources/flywheel.mixins.json | 24 ++ src/main/resources/pack.mcmeta | 7 + 185 files changed, 10249 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 src/main/java/com/jozufozu/flywheel/Flywheel.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/Backend.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/IFlywheelWorld.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/IShaderContext.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/OptifineHandler.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/RenderWork.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/ResourceUtil.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/ShaderContext.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/ShaderSources.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/SpecMetaRegistry.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/GlNumericType.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/GlObject.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/GlPrimitive.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/GlTexture.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/GlVertexArray.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/attrib/CommonAttributes.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/attrib/IAttribSpec.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/attrib/MatrixAttributes.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/attrib/VertexAttribSpec.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/attrib/VertexFormat.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/buffer/GlBuffer.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/buffer/GlBufferType.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/buffer/GlBufferUsage.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/buffer/MappedBuffer.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/buffer/MappedBufferRange.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/buffer/MappedBufferUsage.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/buffer/MappedFullBuffer.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/buffer/VecBuffer.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/shader/GlProgram.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/shader/GlShader.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/shader/ShaderType.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/versioned/GlCompat.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/versioned/GlVersioned.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/versioned/MapBufferRange.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/versioned/RGPixelFormat.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/versioned/framebuffer/Blit.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/versioned/framebuffer/Framebuffer.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/versioned/instancing/DrawInstanced.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/versioned/instancing/InstancedArrays.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/gl/versioned/instancing/VertexArrayObject.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/instancing/IDynamicInstance.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/instancing/IInstance.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/instancing/IInstanceFactory.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/instancing/IInstanceRendered.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/instancing/ITickableInstance.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/instancing/InstanceData.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/instancing/InstanceManager.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/instancing/InstanceMaterial.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/instancing/InstancedRenderDispatcher.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/instancing/InstancedRenderRegistry.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/instancing/Instancer.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/instancing/MaterialManager.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/instancing/MaterialRenderer.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/instancing/MaterialSpec.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/instancing/entity/EntityInstance.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/instancing/entity/EntityInstanceManager.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/instancing/entity/IEntityInstanceFactory.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/instancing/tile/ITileInstanceFactory.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/instancing/tile/TileEntityInstance.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/instancing/tile/TileInstanceManager.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/loading/IProcessingStage.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/loading/InstancedArraysTemplate.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/loading/LayoutTag.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/loading/ModelTemplate.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/loading/Program.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/loading/ProgramTemplate.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/loading/Shader.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/loading/ShaderLoadingException.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/loading/ShaderTemplate.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/loading/ShaderTransformer.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/loading/TaggedField.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/loading/TaggedStruct.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/loading/TypeHelper.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/model/ArrayModelRenderer.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/model/BufferedModel.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/model/ElementBuffer.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/model/IndexedModel.java create mode 100644 src/main/java/com/jozufozu/flywheel/backend/model/ModelRenderer.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/AtlasStitcher.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/Contexts.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/CrumblingInstanceManager.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/CrumblingProgram.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/Formats.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/FullscreenQuad.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/Materials.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/PartialModel.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/Programs.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/QuadConverter.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/StitchedSprite.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/WorldContext.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/instancing/ConditionalInstance.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/instancing/GroupInstance.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/instancing/SelectInstance.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/materials/BasicData.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/materials/IFlatLight.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/materials/ModelData.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/materials/OrientedData.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/shader/ExtensibleGlProgram.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/shader/FogMode.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/shader/GlFog.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/shader/IMultiProgram.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/shader/IProgramCallback.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/shader/StateSensitiveMultiProgram.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/shader/WorldFog.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/shader/WorldProgram.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/shader/extension/IExtensionInstance.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/shader/extension/IProgramExtension.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/shader/extension/UnitExtensionInstance.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/shader/gamestate/FogStateProvider.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/shader/gamestate/IGameStateProvider.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/shader/gamestate/NormalDebugStateProvider.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/shader/spec/BooleanContextCondition.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/shader/spec/IBooleanStateProvider.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/shader/spec/IContextCondition.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/shader/spec/ProgramSpec.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/shader/spec/ProgramState.java create mode 100644 src/main/java/com/jozufozu/flywheel/core/shader/spec/SpecificValueCondition.java create mode 100644 src/main/java/com/jozufozu/flywheel/event/BeginFrameEvent.java create mode 100644 src/main/java/com/jozufozu/flywheel/event/EntityWorldHandler.java create mode 100644 src/main/java/com/jozufozu/flywheel/event/ForgeEvents.java create mode 100644 src/main/java/com/jozufozu/flywheel/event/GatherContextEvent.java create mode 100644 src/main/java/com/jozufozu/flywheel/event/ReloadRenderersEvent.java create mode 100644 src/main/java/com/jozufozu/flywheel/event/RenderLayerEvent.java create mode 100644 src/main/java/com/jozufozu/flywheel/light/GridAlignedBB.java create mode 100644 src/main/java/com/jozufozu/flywheel/light/ICoordinateConsumer.java create mode 100644 src/main/java/com/jozufozu/flywheel/light/ILightUpdateListener.java create mode 100644 src/main/java/com/jozufozu/flywheel/light/LightUpdater.java create mode 100644 src/main/java/com/jozufozu/flywheel/light/LightVolume.java create mode 100644 src/main/java/com/jozufozu/flywheel/mixin/CancelEntityRenderMixin.java create mode 100644 src/main/java/com/jozufozu/flywheel/mixin/CancelTileEntityRenderMixin.java create mode 100644 src/main/java/com/jozufozu/flywheel/mixin/FogColorTrackerMixin.java create mode 100644 src/main/java/com/jozufozu/flywheel/mixin/RenderHooksMixin.java create mode 100644 src/main/java/com/jozufozu/flywheel/mixin/ShaderCloseMixin.java create mode 100644 src/main/java/com/jozufozu/flywheel/mixin/StoreProjectionMatrixMixin.java create mode 100644 src/main/java/com/jozufozu/flywheel/mixin/TileRemoveMixin.java create mode 100644 src/main/java/com/jozufozu/flywheel/mixin/TileWorldHookMixin.java create mode 100644 src/main/java/com/jozufozu/flywheel/mixin/light/LightUpdateMixin.java create mode 100644 src/main/java/com/jozufozu/flywheel/mixin/light/NetworkLightUpdateMixin.java create mode 100644 src/main/java/com/jozufozu/flywheel/util/AngleHelper.java create mode 100644 src/main/java/com/jozufozu/flywheel/util/AnimationTickHolder.java create mode 100644 src/main/java/com/jozufozu/flywheel/util/AttribUtil.java create mode 100644 src/main/java/com/jozufozu/flywheel/util/BakedQuadWrapper.java create mode 100644 src/main/java/com/jozufozu/flywheel/util/BufferBuilderReader.java create mode 100644 src/main/java/com/jozufozu/flywheel/util/CodecUtil.java create mode 100644 src/main/java/com/jozufozu/flywheel/util/Pair.java create mode 100644 src/main/java/com/jozufozu/flywheel/util/RenderUtil.java create mode 100644 src/main/java/com/jozufozu/flywheel/util/VirtualEmptyModelData.java create mode 100644 src/main/java/com/jozufozu/flywheel/util/WeakHashSet.java create mode 100644 src/main/java/com/jozufozu/flywheel/util/WorldAttached.java create mode 100644 src/main/resources/META-INF/accesstransformer.cfg create mode 100644 src/main/resources/META-INF/mods.toml create mode 100644 src/main/resources/assets/flywheel/flywheel/programs/model.json create mode 100644 src/main/resources/assets/flywheel/flywheel/programs/oriented.json create mode 100644 src/main/resources/assets/flywheel/flywheel/shaders/block.frag create mode 100644 src/main/resources/assets/flywheel/flywheel/shaders/context/crumbling/builtin.frag create mode 100644 src/main/resources/assets/flywheel/flywheel/shaders/context/crumbling/builtin.vert create mode 100644 src/main/resources/assets/flywheel/flywheel/shaders/context/world/builtin.frag create mode 100644 src/main/resources/assets/flywheel/flywheel/shaders/context/world/builtin.vert create mode 100644 src/main/resources/assets/flywheel/flywheel/shaders/context/world/fog.glsl create mode 100644 src/main/resources/assets/flywheel/flywheel/shaders/core/color.glsl create mode 100644 src/main/resources/assets/flywheel/flywheel/shaders/core/diffuse.glsl create mode 100644 src/main/resources/assets/flywheel/flywheel/shaders/core/lightutil.glsl create mode 100644 src/main/resources/assets/flywheel/flywheel/shaders/core/matutils.glsl create mode 100644 src/main/resources/assets/flywheel/flywheel/shaders/core/quaternion.glsl create mode 100644 src/main/resources/assets/flywheel/flywheel/shaders/data/blockfragment.glsl create mode 100644 src/main/resources/assets/flywheel/flywheel/shaders/data/modelvertex.glsl create mode 100644 src/main/resources/assets/flywheel/flywheel/shaders/model.vert create mode 100644 src/main/resources/assets/flywheel/flywheel/shaders/oriented.vert create mode 100644 src/main/resources/assets/flywheel/flywheel/shaders/smooth_oriented.glsl create mode 100644 src/main/resources/assets/flywheel/flywheel/shaders/template/instanced/instanced.frag create mode 100644 src/main/resources/assets/flywheel/flywheel/shaders/template/instanced/instanced.vert create mode 100644 src/main/resources/assets/flywheel/flywheel/shaders/template/meshlet/meshlet.glsl create mode 100644 src/main/resources/assets/flywheel/flywheel/shaders/template/model/model.frag create mode 100644 src/main/resources/assets/flywheel/flywheel/shaders/template/model/model.vert create mode 100644 src/main/resources/flywheel.mixins.json create mode 100644 src/main/resources/pack.mcmeta 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 0000000000000000000000000000000000000000..7a3265ee94c0ab25cf079ac8ccdf87f41d455d42 GIT binary patch literal 54708 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

_x$+l{{cH8$W#CT literal 0 HcmV?d00001 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." + } +}