diff --git a/dependencies.gradle b/dependencies.gradle index 8b937f6184..45ccc88420 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -7,9 +7,9 @@ dependencies { api("com.github.GTNewHorizons:AE2FluidCraft-Rework:1.5.95-gtnh:dev") api("com.github.GTNewHorizons:Applied-Energistics-2-Unofficial:rv3-beta-999-GTNH:dev") - implementation("com.github.GTNewHorizons:GTNHLib:0.11.43:dev") {transitive = false} + implementation("com.github.GTNewHorizons:GTNHLib:0.11.54:dev") {transitive = false} - compileOnly("com.github.GTNewHorizons:Angelica:2.1.52:api") {transitive = false} + compileOnly("com.github.GTNewHorizons:Angelica:2.2.36:api") {transitive = false} compileOnly("com.github.GTNewHorizons:Avaritiaddons:1.9.4-GTNH:dev") {transitive = false} compileOnly("com.github.GTNewHorizons:BloodMagic:1.9.4:dev") {transitive = false} compileOnly("com.github.GTNewHorizons:BuildCraft:7.1.61:dev") {transitive = false} @@ -55,4 +55,5 @@ dependencies { testImplementation("org.mockito:mockito-all:1.10.19") testImplementation("org.scalatest:scalatest_2.11:2.2.6") testImplementation("junit:junit:4.13.2") + testRuntimeOnly("it.unimi.dsi:fastutil:8.5.18") } diff --git a/src/main/java/li/cil/oc/api/driver/item/UpgradeRenderer.java b/src/main/java/li/cil/oc/api/driver/item/UpgradeRenderer.java index 6610f3504b..71055cee32 100644 --- a/src/main/java/li/cil/oc/api/driver/item/UpgradeRenderer.java +++ b/src/main/java/li/cil/oc/api/driver/item/UpgradeRenderer.java @@ -41,7 +41,8 @@ public interface UpgradeRenderer { * * @param stack the item stack of the upgrade to render. * @param robot the robot the upgrade is rendered on. - * @param availableMountPoints the mount points available for rendering in. + * @param availableMountPoints the mount points available for rendering in; + * reused between calls, do not keep a reference. * @return the mount point to reserve for the upgrade. */ String computePreferredMountPoint(ItemStack stack, Robot robot, Set availableMountPoints); diff --git a/src/main/java/li/cil/oc/client/renderer/glsl/DrawUniforms.java b/src/main/java/li/cil/oc/client/renderer/glsl/DrawUniforms.java new file mode 100644 index 0000000000..f089af12f0 --- /dev/null +++ b/src/main/java/li/cil/oc/client/renderer/glsl/DrawUniforms.java @@ -0,0 +1,69 @@ +package li.cil.oc.client.renderer.glsl; + +import java.nio.FloatBuffer; + +import org.joml.Matrix3f; +import org.joml.Matrix4f; +import org.joml.Matrix4fc; +import org.lwjgl.BufferUtils; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL20; + +final class DrawUniforms { + + private static final int LIGHTS = 2; + + private static final Matrix4f projection = new Matrix4f(); + private static final Matrix4f modelView = new Matrix4f(); + private static final Matrix4f mvp = new Matrix4f(); + private static final Matrix3f normal = new Matrix3f(); + + private static final FloatBuffer scratch = BufferUtils.createFloatBuffer(16); + private static final FloatBuffer lights = BufferUtils.createFloatBuffer(LIGHTS * 3); + + private DrawUniforms() {} + + static void uploadMVP(int location, Matrix4fc model) { + scratch.clear(); + GL11.glGetFloat(GL11.GL_PROJECTION_MATRIX, scratch); + projection.set(scratch); + scratch.clear(); + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, scratch); + modelView.set(scratch) + .mul(model); + upload(location, projection.mul(modelView, mvp)); + } + + static void uploadLit(int mvpLocation, int normalLocation, int lightDirLocation, int lightDiffuseLocation, + Matrix4fc model) { + uploadMVP(mvpLocation, model); + + scratch.clear(); + modelView.normal(normal) + .get(scratch); + scratch.limit(9); + GL20.glUniformMatrix3(normalLocation, false, scratch); + + uploadLight(lightDirLocation, GL11.GL_POSITION); + uploadLight(lightDiffuseLocation, GL11.GL_DIFFUSE); + } + + private static void uploadLight(int location, int parameter) { + lights.clear(); + for (int i = 0; i < LIGHTS; i++) { + scratch.clear(); + GL11.glGetLight(GL11.GL_LIGHT0 + i, parameter, scratch); + lights.put(scratch.get(0)) + .put(scratch.get(1)) + .put(scratch.get(2)); + } + lights.flip(); + GL20.glUniform3(location, lights); + } + + static void upload(int location, Matrix4fc matrix) { + scratch.clear(); + matrix.get(scratch); + GL20.glUniformMatrix4(location, false, scratch); + } +} diff --git a/src/main/java/li/cil/oc/client/renderer/glsl/GlslDialect.java b/src/main/java/li/cil/oc/client/renderer/glsl/GlslDialect.java new file mode 100644 index 0000000000..f236ba763d --- /dev/null +++ b/src/main/java/li/cil/oc/client/renderer/glsl/GlslDialect.java @@ -0,0 +1,43 @@ +package li.cil.oc.client.renderer.glsl; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.lwjgl.opengl.ContextCapabilities; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL32; +import org.lwjgl.opengl.GLContext; + +// GLSL330 sources must stay free of compat builtins and of angelica_*/iris_* uniform names +public enum GlslDialect { + + GLSL120("#version 120\n"), + GLSL330("#version 330 core\n" + "#define OC_CORE\n"); + + static final Logger LOG = LogManager.getLogger("OpenComputers"); + + private static GlslDialect preferred; + + private final String preamble; + + GlslDialect(String preamble) { + this.preamble = preamble; + } + + public String source(String defines, String prelude, String body) { + return preamble + defines + prelude + body; + } + + public static void init() { + if (preferred != null) return; + final ContextCapabilities capabilities = GLContext.getCapabilities(); + final boolean coreContext = capabilities.OpenGL32 + && (GL11.glGetInteger(GL32.GL_CONTEXT_PROFILE_MASK) & GL32.GL_CONTEXT_CORE_PROFILE_BIT) != 0; + preferred = coreContext ? GLSL330 : GLSL120; + LOG.info("Preferred GLSL dialect: {}", preferred); + } + + public static GlslDialect preferred() { + if (preferred == null) throw new IllegalStateException("GlslDialect.init() has not run"); + return preferred; + } +} diff --git a/src/main/java/li/cil/oc/client/renderer/glsl/Program.java b/src/main/java/li/cil/oc/client/renderer/glsl/Program.java new file mode 100644 index 0000000000..29ef08619b --- /dev/null +++ b/src/main/java/li/cil/oc/client/renderer/glsl/Program.java @@ -0,0 +1,73 @@ +package li.cil.oc.client.renderer.glsl; + +import java.util.Arrays; + +import org.joml.Matrix4fc; +import org.lwjgl.opengl.GL20; + +public final class Program { + + private final ProgramRecipe recipe; + private final GlslDialect dialect; + + private final int[] slots; + + private final int transformLocation; + private final int normalLocation; + private final int lightDirLocation; + private final int lightDiffuseLocation; + + private int program; + + Program(ProgramRecipe recipe, GlslDialect dialect, int program, int[] slots, int transformLocation, + int normalLocation, int lightDirLocation, int lightDiffuseLocation) { + this.recipe = recipe; + this.dialect = dialect; + this.program = program; + this.slots = slots; + this.transformLocation = transformLocation; + this.normalLocation = normalLocation; + this.lightDirLocation = lightDirLocation; + this.lightDiffuseLocation = lightDiffuseLocation; + } + + static Program invalid(ProgramRecipe recipe) { + final int[] slots = new int[recipe.uniformCount()]; + Arrays.fill(slots, -1); + return new Program(recipe, GlslDialect.preferred(), 0, slots, -1, -1, -1, -1); + } + + public boolean isValid() { + return program != 0; + } + + public void use() { + if (program != 0) GL20.glUseProgram(program); + } + + public int loc(Uniform uniform) { + if (uniform.owner != recipe) { + throw new IllegalArgumentException(recipe.id() + ": " + uniform + " belongs to another shader"); + } + return slots[uniform.slot]; + } + + public void uploadModel(Matrix4fc model) { + if (program == 0) return; + + if (dialect != GlslDialect.GLSL330) { + DrawUniforms.upload(transformLocation, model); + } else if (normalLocation < 0) { + DrawUniforms.uploadMVP(transformLocation, model); + } else { + DrawUniforms.uploadLit(transformLocation, normalLocation, lightDirLocation, lightDiffuseLocation, model); + } + } + + public void release() { + if (program != 0) { + GL20.glDeleteProgram(program); + program = 0; + } + } +} diff --git a/src/main/java/li/cil/oc/client/renderer/glsl/ProgramRecipe.java b/src/main/java/li/cil/oc/client/renderer/glsl/ProgramRecipe.java new file mode 100644 index 0000000000..ba2811c590 --- /dev/null +++ b/src/main/java/li/cil/oc/client/renderer/glsl/ProgramRecipe.java @@ -0,0 +1,244 @@ +package li.cil.oc.client.renderer.glsl; + +import static li.cil.oc.client.renderer.glsl.GlslDialect.LOG; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import net.minecraft.client.Minecraft; +import net.minecraft.util.ResourceLocation; + +import org.apache.commons.io.IOUtils; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL20; + +import it.unimi.dsi.fastutil.objects.Object2FloatMap; +import it.unimi.dsi.fastutil.objects.Object2FloatOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; + +public final class ProgramRecipe { + + private final String domain; + private final String name; + private final List uniforms = new ArrayList<>(); + private final Object2IntOpenHashMap samplers = new Object2IntOpenHashMap<>(); + private final Object2FloatOpenHashMap constants = new Object2FloatOpenHashMap<>(); + private final StringBuilder defines = new StringBuilder(); + + private boolean tinted; + private boolean lit; + + private ProgramRecipe(String domain, String name) { + this.domain = domain; + this.name = name; + } + + public static ProgramRecipe of(String domain, String name) { + return new ProgramRecipe(domain, name); + } + + public ProgramRecipe required(String... names) { + for (String uniform : names) { + if (uniforms.contains(uniform)) { + throw new IllegalArgumentException(id() + ": " + uniform + " is declared twice"); + } + } + Collections.addAll(uniforms, names); + return this; + } + + public ProgramRecipe sampler(String uniform, int unit) { + samplers.put(uniform, unit); + return required(uniform); + } + + public ProgramRecipe constant(String uniform, float value) { + constants.put(uniform, value); + return required(uniform); + } + + public ProgramRecipe define(String macro, float value) { + defines.append("#define ") + .append(macro) + .append(' ') + .append(value) + .append('\n'); + return this; + } + + public ProgramRecipe tinted() { + this.tinted = true; + return this; + } + + public ProgramRecipe lights() { + this.tinted = true; + this.lit = true; + return this; + } + + public Uniform uniform(String name) { + final int slot = uniforms.indexOf(name); + if (slot < 0) { + throw new IllegalArgumentException( + id() + ": " + name + " was not declared; declared: " + String.join(", ", uniforms)); + } + return new Uniform(this, name, slot); + } + + public String name() { + return name; + } + + public List expectedUniforms(GlslDialect dialect) { + final boolean core = dialect == GlslDialect.GLSL330; + final List expected = new ArrayList<>(uniforms); + expected.add(core ? "u_MVP" : "u_ModelMatrix"); + if (lit && core) { + expected.add("u_NormalMatrix"); + expected.add("u_LightDir"); + expected.add("u_LightDiffuse"); + } + return expected; + } + + public String assemble(GlslDialect dialect, String prelude, String body) { + return dialect.source((tinted ? "#define OC_TINTED\n" : "") + (lit ? "#define OC_LIT\n" : "") + defines, prelude, body); + } + + public Program bake() { + final GlslDialect dialect = GlslDialect.preferred(); + final int program = link(dialect); + final Program handle = program == 0 ? null : resolve(dialect, program); + if (handle == null) { + if (program != 0) GL20.glDeleteProgram(program); + LOG.error("Shader {} [{}]: unavailable, falling back to fixed-function rendering", id(), dialect); + return Program.invalid(this); + } + uploadConstants(handle); + LOG.info("Shader {} [{}]: linked", id(), dialect); + return handle; + } + + private int link(GlslDialect dialect) { + final String vertexBody = read("shaders/" + name + ".vert"); + final String fragmentBody = read("shaders/" + name + ".frag"); + final String vertexPrelude = read("shaders/standalone.vert"); + final String fragmentPrelude = read("shaders/standalone.frag"); + if (vertexBody == null || fragmentBody == null || vertexPrelude == null || fragmentPrelude == null) return 0; + + final int vertex = compile(dialect, GL20.GL_VERTEX_SHADER, assemble(dialect, vertexPrelude, vertexBody)); + final int fragment = compile( + dialect, + GL20.GL_FRAGMENT_SHADER, + assemble(dialect, fragmentPrelude, fragmentBody)); + int program = 0; + if (vertex != 0 && fragment != 0) { + program = GL20.glCreateProgram(); + GL20.glAttachShader(program, vertex); + GL20.glAttachShader(program, fragment); + GL20.glLinkProgram(program); + if (GL20.glGetProgrami(program, GL20.GL_LINK_STATUS) == GL11.GL_FALSE) { + LOG.error( + "Shader {} [{}]: link failed: {}", + id(), + dialect, + GL20.glGetProgramInfoLog(program, GL20.glGetProgrami(program, GL20.GL_INFO_LOG_LENGTH))); + GL20.glDeleteProgram(program); + program = 0; + } + } + if (vertex != 0) GL20.glDeleteShader(vertex); + if (fragment != 0) GL20.glDeleteShader(fragment); + return program; + } + + private int compile(GlslDialect dialect, int type, String source) { + final int shader = GL20.glCreateShader(type); + GL20.glShaderSource(shader, source); + GL20.glCompileShader(shader); + if (GL20.glGetShaderi(shader, GL20.GL_COMPILE_STATUS) == GL11.GL_FALSE) { + LOG.error( + "Shader {} [{}]: {} stage did not compile: {}", + id(), + dialect, + type == GL20.GL_VERTEX_SHADER ? "vertex" : "fragment", + GL20.glGetShaderInfoLog(shader, GL20.glGetShaderi(shader, GL20.GL_INFO_LOG_LENGTH))); + GL20.glDeleteShader(shader); + return 0; + } + return shader; + } + + private String read(String path) { + final ResourceLocation location = new ResourceLocation(domain, path); + try (InputStream in = Minecraft.getMinecraft() + .getResourceManager() + .getResource(location) + .getInputStream()) { + return IOUtils.toString(in, StandardCharsets.UTF_8); + } catch (IOException e) { + LOG.error("Shader {}: could not read {}", id(), location, e); + return null; + } + } + + private Program resolve(GlslDialect dialect, int program) { + final List expected = expectedUniforms(dialect); + final int[] locations = new int[expected.size()]; + boolean ok = true; + for (int i = 0; i < locations.length; i++) { + locations[i] = locate(dialect, program, expected.get(i)); + ok &= locations[i] >= 0; + } + if (!ok) return null; + + final int declared = uniforms.size(); + final int[] slots = new int[declared]; + System.arraycopy(locations, 0, slots, 0, declared); + final boolean lighting = locations.length > declared + 1; + return new Program( + this, + dialect, + program, + slots, + locations[declared], + lighting ? locations[declared + 1] : -1, + lighting ? locations[declared + 2] : -1, + lighting ? locations[declared + 3] : -1); + } + + private int locate(GlslDialect dialect, int program, String uniform) { + final int location = GL20.glGetUniformLocation(program, uniform); + if (location < 0) { + LOG.error("Shader {} [{}]: uniform {} is missing or unused", id(), dialect, uniform); + } + return location; + } + + private void uploadConstants(Program handle) { + if (samplers.isEmpty() && constants.isEmpty()) return; + final int previous = GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM); + handle.use(); + for (Object2IntMap.Entry sampler : samplers.object2IntEntrySet()) { + GL20.glUniform1i(handle.loc(uniform(sampler.getKey())), sampler.getIntValue()); + } + for (Object2FloatMap.Entry constant : constants.object2FloatEntrySet()) { + GL20.glUniform1f(handle.loc(uniform(constant.getKey())), constant.getFloatValue()); + } + GL20.glUseProgram(previous); + } + + String id() { + return domain + ":" + name; + } + + int uniformCount() { + return uniforms.size(); + } +} diff --git a/src/main/java/li/cil/oc/client/renderer/glsl/Uniform.java b/src/main/java/li/cil/oc/client/renderer/glsl/Uniform.java new file mode 100644 index 0000000000..e8c5890eb7 --- /dev/null +++ b/src/main/java/li/cil/oc/client/renderer/glsl/Uniform.java @@ -0,0 +1,19 @@ +package li.cil.oc.client.renderer.glsl; + +public final class Uniform { + + final ProgramRecipe owner; + final String name; + final int slot; + + Uniform(ProgramRecipe owner, String name, int slot) { + this.owner = owner; + this.name = name; + this.slot = slot; + } + + @Override + public String toString() { + return owner.id() + ":" + name; + } +} diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 25ed201a8e..285157efa3 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -11,12 +11,14 @@ opencomputers { # Client side settings, presentation and performance related stuff. client { + # Whether to render through Angelica's batched tile entity mesh cache when + # Angelica provides it. Disable to always use the built-in renderers. + angelicaBatching: true + # The distance at which to start fading out the text on screens. This is # purely cosmetic, to avoid text disappearing instantly when moving too # far away from a screen. This should have no measurable impact on - # performance. Note that this needs OpenGL 1.4 to work, otherwise text - # will always just instantly disappear when moving away from the screen - # displaying it. + # performance. screenTextFadeStartDistance: 15.0 # The maximum distance at which to render text on screens. Rendering text @@ -34,10 +36,6 @@ opencomputers { # Wikipedia.) textLinearFiltering: false - # If you prefer the text on the screens to be aliased (you know, *not* - # anti-aliased / smoothed) turn this option off. - textAntiAlias: true - # Render robots' names as a label above them when near them robotLabels: true @@ -45,12 +43,6 @@ opencomputers { # running noise. Disable sounds by setting this to zero. soundVolume: 1.0 - # This is the scaling of the individual chars rendered on screens. This - # is set to slightly overscale per default, to avoid gaps between fully - # filled chars to appear (i.e. the block symbol that is used for cursor - # blinking for example) on less accurate hardware. - fontCharScale: 1.01 - # The maximum render distance of a hologram projected by a highest tier # hologram projector when at maximum scale. Render distance is scaled # down with the actual scale of the hologram. @@ -72,20 +64,6 @@ opencomputers { # Defaults to white, feel free to make it some other color, tho! monochromeColor: 0xFFFFFF - # Which font renderer to use. Defaults to `hexfont` if invalid. - # Possible values: - # - hexfont: the (since 1.3.2) default font renderer. Font in .hex format - # capable of rendering many unicode glyphs. - # The used font data can be swapped out using resource packs, - # but is harder to work with, since it involves binary data. - # - texture: the old, font-texture based font renderer that was used - # in OC versions prior to 1.3.2. This will allow overriding - # the font texture as before. Keep in mind that this renderer - # is slightly less efficient than the new one, and more - # importantly, can only render code page 437 (as opposed to... - # a *lot* of unicode). - fontRenderer=hexfont - # The sample rate used for generating beeps of computers' internal # speakers. Use custom values at your own responsibility here; if it # breaks OC you'll get no support. Some potentially reasonable diff --git a/src/main/resources/assets/opencomputers/shaders/lit.frag b/src/main/resources/assets/opencomputers/shaders/lit.frag new file mode 100644 index 0000000000..9c567ede60 --- /dev/null +++ b/src/main/resources/assets/opencomputers/shaders/lit.frag @@ -0,0 +1,11 @@ +angelica_in vec2 v_UV; +angelica_in vec2 v_Light; +angelica_in vec4 v_Color; +angelica_in float v_Depth; + +void main() { + vec4 color = angelica_sampleTexture(v_UV) * v_Color; + if (!angelica_alphaTest(color.a)) discard; + color.rgb *= angelica_sampleLightmap(v_Light).rgb; + angelica_FragColor = vec4(angelica_fog(color.rgb, v_Depth), color.a); +} diff --git a/src/main/resources/assets/opencomputers/shaders/lit.vert b/src/main/resources/assets/opencomputers/shaders/lit.vert new file mode 100644 index 0000000000..6f1c9ce3b7 --- /dev/null +++ b/src/main/resources/assets/opencomputers/shaders/lit.vert @@ -0,0 +1,17 @@ +angelica_out vec2 v_UV; +angelica_out vec2 v_Light; +angelica_out vec4 v_Color; +angelica_out float v_Depth; + +void main() { + gl_Position = angelica_clipPosition(); + vec3 normal = angelica_eyeNormal(); + float d0 = max(dot(normal, normalize(angelica_lightDirection(0))), 0.0); + float d1 = max(dot(normal, normalize(angelica_lightDirection(1))), 0.0); + vec3 light = min(vec3(0.4) + angelica_lightDiffuse(0) * d0 + angelica_lightDiffuse(1) * d1, 1.0); + vec4 color = angelica_color(); + v_Color = vec4(color.rgb * light, color.a); + v_UV = angelica_uv(); + v_Light = angelica_lightmapUv(); + v_Depth = angelica_fogCoord(); +} diff --git a/src/main/resources/assets/opencomputers/shaders/standalone.frag b/src/main/resources/assets/opencomputers/shaders/standalone.frag new file mode 100644 index 0000000000..0dae5986ed --- /dev/null +++ b/src/main/resources/assets/opencomputers/shaders/standalone.frag @@ -0,0 +1,45 @@ +#ifdef OC_CORE +#define angelica_in in +#define OC_TEXTURE texture +layout(location = 0) out vec4 angelica_FragColor; +#else +#define angelica_in varying +#define OC_TEXTURE texture2D +#define angelica_FragColor gl_FragColor +#endif + +uniform sampler2D u_Texture; +// Must match the encoding Fog.upload sends: x 0 off, 1 linear, 2 exp, 3 exp2; y start; z end; w density. +uniform vec4 u_Fog; +uniform vec3 u_FogColor; +uniform float u_AlphaRef; + +vec4 angelica_sampleTexture(vec2 uv) { + return OC_TEXTURE(u_Texture, uv); +} + +#ifdef OC_LIT +uniform sampler2D u_Lightmap; + +vec4 angelica_sampleLightmap(vec2 uv) { + return OC_TEXTURE(u_Lightmap, uv); +} +#endif + +vec3 angelica_fog(vec3 color, float fogCoord) { + float fog = 1.0; + if (u_Fog.x > 2.5) { + float d = u_Fog.w * fogCoord; + fog = exp(-d * d); + } else if (u_Fog.x > 1.5) { + fog = exp(-u_Fog.w * fogCoord); + } else if (u_Fog.x > 0.5) { + fog = clamp((u_Fog.z - fogCoord) / (u_Fog.z - u_Fog.y), 0.0, 1.0); + } + return mix(u_FogColor, color, fog); +} + +// Negated <= keeps NaN alpha like the alpha test it replaces; a plain > would discard it. +bool angelica_alphaTest(float alpha) { + return !(alpha <= u_AlphaRef); +} diff --git a/src/main/resources/assets/opencomputers/shaders/standalone.vert b/src/main/resources/assets/opencomputers/shaders/standalone.vert new file mode 100644 index 0000000000..66815c0fcf --- /dev/null +++ b/src/main/resources/assets/opencomputers/shaders/standalone.vert @@ -0,0 +1,91 @@ +#ifdef OC_CORE +#define angelica_out out +layout(location = 0) in vec3 a_Position; +layout(location = 2) in vec2 a_UV; +uniform mat4 u_MVP; + +vec4 angelica_clipPosition() { + return u_MVP * vec4(a_Position, 1.0); +} + +vec2 angelica_uv() { + return a_UV; +} + +#ifdef OC_LIT +layout(location = 1) in vec4 a_Color; +layout(location = 4) in vec3 a_Normal; +uniform mat3 u_NormalMatrix; +uniform vec3 u_LightDir[2]; +uniform vec3 u_LightDiffuse[2]; +#define OC_COLOR a_Color + +vec3 angelica_eyeNormal() { + return normalize(u_NormalMatrix * a_Normal); +} + +vec3 angelica_lightDirection(int i) { + return u_LightDir[i]; +} + +vec3 angelica_lightDiffuse(int i) { + return u_LightDiffuse[i]; +} +#endif +#else +#define angelica_out varying +uniform mat4 u_ModelMatrix; + +vec4 angelica_clipPosition() { + return gl_ModelViewProjectionMatrix * u_ModelMatrix * gl_Vertex; +} + +vec2 angelica_uv() { + return gl_MultiTexCoord0.xy; +} + +#ifdef OC_LIT +#define OC_COLOR gl_Color + +vec3 angelica_eyeNormal() { + return normalize(gl_NormalMatrix * (mat3(u_ModelMatrix) * gl_Normal)); +} + +vec3 angelica_lightDirection(int i) { + return gl_LightSource[i].position.xyz; +} + +vec3 angelica_lightDiffuse(int i) { + return gl_LightSource[i].diffuse.rgb; +} +#endif +#endif + +#ifdef OC_TINTED +uniform vec4 u_Color; +#ifdef OC_LIT +// 0 for a mesh without a color element, whose color input is then undefined. +uniform float u_VertexColor; + +vec4 angelica_color() { + return mix(vec4(1.0), OC_COLOR, u_VertexColor) * u_Color; +} +#else +vec4 angelica_color() { + return u_Color; +} +#endif +#endif + +#ifdef OC_LIT +uniform vec2 u_LightCoord; + +vec2 angelica_lightmapUv() { + return (u_LightCoord + 8.0) / 256.0; +} +#endif + +// Clip-space w, read back from gl_Position: a body assigns gl_Position before calling this. +float angelica_fogCoord() { + return gl_Position.w; +} diff --git a/src/main/resources/assets/opencomputers/shaders/text.frag b/src/main/resources/assets/opencomputers/shaders/text.frag new file mode 100644 index 0000000000..6ce5eb5aa4 --- /dev/null +++ b/src/main/resources/assets/opencomputers/shaders/text.frag @@ -0,0 +1,50 @@ +uniform vec4 u_Cells; +uniform float u_Fade; +uniform float u_Smooth; +uniform sampler2D u_Glyphs; +uniform vec2 u_GlyphScale; + +angelica_in vec2 v_Cell; +angelica_in float v_Depth; + +const vec2 INV = 1.0 / vec2(OC_SHEET_WIDTH, OC_SHEET_HEIGHT); + +float glyph(vec2 slotOrigin, float right, vec2 pixel) { + vec2 px = vec2(right * 8.0 + clamp(pixel.x, 0.0, 7.0), clamp(pixel.y, 0.0, 15.0)) + 0.5; + return OC_TEXTURE(u_Glyphs, (slotOrigin + px) * u_GlyphScale).a; +} + +void main() { + vec2 cell = min(floor(v_Cell), u_Cells.zw - 1.0); + vec2 f = clamp(v_Cell - cell, 0.0, 0.9999); + float index = u_Cells.x + cell.y * u_Cells.z + cell.x; + float row = floor((index + 0.5) / OC_STRIDE); + vec2 base = vec2((index - row * OC_STRIDE) * 2.0, row); + vec4 a = angelica_sampleTexture((base + vec2(0.5, 0.5)) * INV); + vec4 b = angelica_sampleTexture((base + vec2(1.5, 0.5)) * INV); + float lo = floor(a.r * 255.0 + 0.5); + float hi = floor(a.g * 255.0 + 0.5); + float right = step(OC_RIGHT_HALF, hi); + float slot = lo + (hi - right * OC_RIGHT_HALF) * 256.0; + vec2 slotOrigin = vec2(mod(slot, OC_GLYPH_COLUMNS), floor(slot / OC_GLYPH_COLUMNS)) * OC_GLYPH_SIZE; + vec2 gp = f * vec2(8.0, 16.0); + + float cover = glyph(slotOrigin, right, floor(gp)); + if (u_Smooth > 0.5 && max(fwidth(v_Cell.x) * 8.0, fwidth(v_Cell.y) * 16.0) > 1.0) { + vec2 p = gp - 0.5; + vec2 i = floor(p); + vec2 w = p - i; + cover = mix( + mix(glyph(slotOrigin, right, i), glyph(slotOrigin, right, i + vec2(1.0, 0.0)), w.x), + mix(glyph(slotOrigin, right, i + vec2(0.0, 1.0)), glyph(slotOrigin, right, i + vec2(1.0, 1.0)), w.x), + w.y); + } + + vec3 fg = vec3(a.b, a.a, b.r); + vec3 bg = b.gba; + bool hasBackground = bg != vec3(0.0); + float alpha = hasBackground ? 1.0 : cover; + if (!angelica_alphaTest(alpha)) discard; + vec3 color = hasBackground ? mix(bg, fg, cover) : fg; + angelica_FragColor = vec4(angelica_fog(color, v_Depth), alpha * u_Fade); +} diff --git a/src/main/resources/assets/opencomputers/shaders/text.vert b/src/main/resources/assets/opencomputers/shaders/text.vert new file mode 100644 index 0000000000..5d600d3828 --- /dev/null +++ b/src/main/resources/assets/opencomputers/shaders/text.vert @@ -0,0 +1,10 @@ +uniform vec4 u_Cells; + +angelica_out vec2 v_Cell; +angelica_out float v_Depth; + +void main() { + gl_Position = angelica_clipPosition(); + v_Cell = angelica_uv() * u_Cells.zw; + v_Depth = angelica_fogCoord(); +} diff --git a/src/main/resources/assets/opencomputers/shaders/unlit.frag b/src/main/resources/assets/opencomputers/shaders/unlit.frag new file mode 100644 index 0000000000..477078c2db --- /dev/null +++ b/src/main/resources/assets/opencomputers/shaders/unlit.frag @@ -0,0 +1,9 @@ +angelica_in vec2 v_UV; +angelica_in vec4 v_Color; +angelica_in float v_Depth; + +void main() { + vec4 color = angelica_sampleTexture(v_UV) * v_Color; + if (!angelica_alphaTest(color.a)) discard; + angelica_FragColor = vec4(angelica_fog(color.rgb, v_Depth), color.a); +} diff --git a/src/main/resources/assets/opencomputers/shaders/unlit.vert b/src/main/resources/assets/opencomputers/shaders/unlit.vert new file mode 100644 index 0000000000..4975d2866e --- /dev/null +++ b/src/main/resources/assets/opencomputers/shaders/unlit.vert @@ -0,0 +1,10 @@ +angelica_out vec2 v_UV; +angelica_out vec4 v_Color; +angelica_out float v_Depth; + +void main() { + gl_Position = angelica_clipPosition(); + v_UV = angelica_uv(); + v_Color = angelica_color(); + v_Depth = angelica_fogCoord(); +} diff --git a/src/main/resources/assets/opencomputers/textures/font/chars.png b/src/main/resources/assets/opencomputers/textures/font/chars.png deleted file mode 100644 index 53d4543c63..0000000000 Binary files a/src/main/resources/assets/opencomputers/textures/font/chars.png and /dev/null differ diff --git a/src/main/resources/assets/opencomputers/textures/font/chars.txt b/src/main/resources/assets/opencomputers/textures/font/chars.txt deleted file mode 100644 index f76fba393a..0000000000 --- a/src/main/resources/assets/opencomputers/textures/font/chars.txt +++ /dev/null @@ -1,2 +0,0 @@ -☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼ !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~⌂ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ -10 18 \ No newline at end of file diff --git a/src/main/resources/assets/opencomputers/textures/font/chars_aliased.png b/src/main/resources/assets/opencomputers/textures/font/chars_aliased.png deleted file mode 100644 index a7acec6f45..0000000000 Binary files a/src/main/resources/assets/opencomputers/textures/font/chars_aliased.png and /dev/null differ diff --git a/src/main/scala/li/cil/oc/OpenComputers.scala b/src/main/scala/li/cil/oc/OpenComputers.scala index 33f1905bfc..4d8455ad5a 100644 --- a/src/main/scala/li/cil/oc/OpenComputers.scala +++ b/src/main/scala/li/cil/oc/OpenComputers.scala @@ -16,7 +16,7 @@ import org.apache.logging.log4j.Logger @Mod(modid = OpenComputers.ID, name = OpenComputers.Name, version = OpenComputers.Version, - modLanguage = "scala", useMetadata = true /*@MCVERSIONDEP@*/, dependencies = "required-after:gtnhlib@[0.6.9,);") + modLanguage = "scala", useMetadata = true /*@MCVERSIONDEP@*/, dependencies = "required-after:gtnhlib@[0.9.9,);") object OpenComputers { final val ID = "OpenComputers" diff --git a/src/main/scala/li/cil/oc/Settings.scala b/src/main/scala/li/cil/oc/Settings.scala index f4feeda6a0..6369ba9ba4 100644 --- a/src/main/scala/li/cil/oc/Settings.scala +++ b/src/main/scala/li/cil/oc/Settings.scala @@ -29,18 +29,16 @@ import scala.util.matching.Regex class Settings(val config: Config) { // ----------------------------------------------------------------------- // // client + val angelicaBatching = config.getBoolean("client.angelicaBatching") val screenTextFadeStartDistance = config.getDouble("client.screenTextFadeStartDistance") val maxScreenTextRenderDistance = config.getDouble("client.maxScreenTextRenderDistance") val textLinearFiltering = config.getBoolean("client.textLinearFiltering") - val textAntiAlias = config.getBoolean("client.textAntiAlias") val robotLabels = config.getBoolean("client.robotLabels") val soundVolume = config.getDouble("client.soundVolume").toFloat max 0 min 2 - val fontCharScale = config.getDouble("client.fontCharScale") max 0.5 min 2 val hologramFadeStartDistance = config.getDouble("client.hologramFadeStartDistance") max 0 val hologramRenderDistance = config.getDouble("client.hologramRenderDistance") max 0 val hologramFlickerFrequency = config.getDouble("client.hologramFlickerFrequency") max 0 val monochromeColor = Integer.decode(config.getString("client.monochromeColor")) - val fontRenderer = config.getString("client.fontRenderer") val beepSampleRate = config.getInt("client.beepSampleRate") val beepAmplitude = config.getInt("client.beepVolume") max 0 min Byte.MaxValue val beepRadius = config.getDouble("client.beepRadius").toFloat max 1 min 32 @@ -328,6 +326,7 @@ class Settings(val config: Config) { OpenComputers.log.warn("Bad number of hologram max scales, ignoring.") Array(3.0, 4.0) } + val hologramMaxScale = math.max(hologramMaxScaleByTier(0), hologramMaxScaleByTier(1)) val hologramMaxTranslationByTier = Array(config.getDoubleList("hologram.maxTranslation"): _*) match { case Array(tier1, tier2) => Array((tier1: Double) max 0.0, (tier2: Double) max 0.0) diff --git a/src/main/scala/li/cil/oc/client/PacketHandler.scala b/src/main/scala/li/cil/oc/client/PacketHandler.scala index 1ffd855d7d..149c04b3d8 100644 --- a/src/main/scala/li/cil/oc/client/PacketHandler.scala +++ b/src/main/scala/li/cil/oc/client/PacketHandler.scala @@ -180,7 +180,9 @@ object PacketHandler extends CommonPacketHandler { def onDisassemblerActiveChange(p: PacketParser) = p.readTileEntity[Disassembler]() match { - case Some(t) => t.isActive = p.readBoolean() + case Some(t) => + t.isActive = p.readBoolean() + t.world.markBlockForUpdate(t.x, t.y, t.z) case _ => // Invalid packet. } diff --git a/src/main/scala/li/cil/oc/client/Proxy.scala b/src/main/scala/li/cil/oc/client/Proxy.scala index 2feaca4600..90c70a539e 100644 --- a/src/main/scala/li/cil/oc/client/Proxy.scala +++ b/src/main/scala/li/cil/oc/client/Proxy.scala @@ -11,12 +11,15 @@ import li.cil.oc.OpenComputers import li.cil.oc.Settings import li.cil.oc.api import li.cil.oc.client +import li.cil.oc.client.renderer.FrameStart import li.cil.oc.client.renderer.HighlightRenderer import li.cil.oc.client.renderer.MFUTargetRenderer import li.cil.oc.client.renderer.PetRenderer +import li.cil.oc.client.renderer.TextBufferRenderCache import li.cil.oc.client.renderer.WirelessNetworkDebugRenderer import li.cil.oc.client.renderer.block.BlockRenderer import li.cil.oc.client.renderer.entity.DroneRenderer +import li.cil.oc.client.renderer.glsl.GlslDialect import li.cil.oc.client.renderer.item.ItemRenderer import li.cil.oc.client.renderer.tileentity._ import li.cil.oc.common.component.TextBuffer @@ -27,6 +30,10 @@ import li.cil.oc.common.init.Items import li.cil.oc.common.tileentity import li.cil.oc.common.{Proxy => CommonProxy} import li.cil.oc.util.Audio +import net.minecraft.client.Minecraft +import net.minecraft.client.resources.IReloadableResourceManager +import net.minecraft.client.resources.IResourceManager +import net.minecraft.client.resources.IResourceManagerReloadListener import net.minecraftforge.client.MinecraftForgeClient import net.minecraftforge.common.MinecraftForge import org.lwjgl.opengl.GLContext @@ -47,33 +54,33 @@ private[oc] class Proxy extends CommonProxy { OpenComputers.channel.register(client.PacketHandler) + GlslDialect.init() + Settings.blockRenderId = RenderingRegistry.getNextAvailableRenderId RenderingRegistry.registerBlockHandler(BlockRenderer) RenderingRegistry.registerEntityRenderingHandler(classOf[Drone], DroneRenderer) - ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Adapter], AdapterRenderer) - ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Assembler], AssemblerRenderer) - ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Case], CaseRenderer) - ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Charger], ChargerRenderer) - ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Disassembler], DisassemblerRenderer) + ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Assembler], MeshBackend.active.overlayRenderer(Overlays.Assembler)) + ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Case], MeshBackend.active.overlayRenderer(Overlays.Case)) + ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Charger], MeshBackend.active.overlayRenderer(Overlays.Charger)) + ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Geolyzer], MeshBackend.active.overlayRenderer(Overlays.Geolyzer)) + ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Microcontroller], MeshBackend.active.overlayRenderer(Overlays.Microcontroller)) + ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Raid], MeshBackend.active.overlayRenderer(Overlays.Raid)) ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.DiskDrive], DiskDriveRenderer) - ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Geolyzer], GeolyzerRenderer) if (GLContext.getCapabilities.OpenGL15) ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Hologram], HologramRenderer) else ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Hologram], HologramRendererFallback) - ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Microcontroller], MicrocontrollerRenderer) - ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.NetSplitter], NetSplitterRenderer) - ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.PowerDistributor], PowerDistributorRenderer) + ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.PowerDistributor], MeshBackend.active.overlayRenderer(Overlays.PowerDistributor)) ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Printer], PrinterRenderer) - ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Raid], RaidRenderer) ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Rack], RackRenderer) - ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Switch], SwitchRenderer) - ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.AccessPoint], SwitchRenderer) - ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Relay], SwitchRenderer) + val switch = MeshBackend.active.overlayRenderer(Overlays.Switch) + ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Switch], switch) + ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.AccessPoint], switch) + ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Relay], switch) ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.RobotProxy], RobotRenderer) ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Screen], ScreenRenderer) - ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Transposer], TransposerRenderer) + ClientRegistry.bindTileEntitySpecialRenderer(classOf[tileentity.Transposer], MeshBackend.active.overlayRenderer(Overlays.Transposer)) MinecraftForgeClient.registerItemRenderer(Items.get(Constants.ItemName.Floppy).createItemStack(1).getItem, ItemRenderer) MinecraftForgeClient.registerItemRenderer(Items.get(Constants.BlockName.Cable).createItemStack(1).getItem, ItemRenderer) @@ -94,7 +101,14 @@ private[oc] class Proxy extends CommonProxy { NetworkRegistry.INSTANCE.registerGuiHandler(OpenComputers, GuiHandler) FMLCommonHandler.instance.bus.register(Audio) - FMLCommonHandler.instance.bus.register(HologramRenderer) + HologramMesh.active.init() + FMLCommonHandler.instance.bus.register(TextBufferRenderCache) + FMLCommonHandler.instance.bus.register(FrameStart) + Minecraft.getMinecraft.getResourceManager.asInstanceOf[IReloadableResourceManager].registerReloadListener(new IResourceManagerReloadListener { + override def onResourceManagerReload(manager: IResourceManager) { + FrameStart.reloadPending = true + } + }) FMLCommonHandler.instance.bus.register(PetRenderer) FMLCommonHandler.instance.bus.register(Sound) } diff --git a/src/main/scala/li/cil/oc/client/Textures.scala b/src/main/scala/li/cil/oc/client/Textures.scala index a2f24e076b..29c019b00e 100644 --- a/src/main/scala/li/cil/oc/client/Textures.scala +++ b/src/main/scala/li/cil/oc/client/Textures.scala @@ -7,9 +7,6 @@ import net.minecraft.util.IIcon import net.minecraft.util.ResourceLocation object Textures { - val fontAntiAliased = new ResourceLocation(Settings.resourceDomain, "textures/font/chars.png") - val fontAliased = new ResourceLocation(Settings.resourceDomain, "textures/font/chars_aliased.png") - val guiBackground = new ResourceLocation(Settings.resourceDomain, "textures/gui/background.png") val guiBar = new ResourceLocation(Settings.resourceDomain, "textures/gui/bar.png") val guiBorders = new ResourceLocation(Settings.resourceDomain, "textures/gui/borders.png") @@ -134,9 +131,6 @@ object Textures { @SideOnly(Side.CLIENT) def init(tm: TextureManager) { - tm.bindTexture(fontAntiAliased) - tm.bindTexture(fontAliased) - tm.bindTexture(guiBackground) tm.bindTexture(guiBar) tm.bindTexture(guiBorders) diff --git a/src/main/scala/li/cil/oc/client/gui/Drone.scala b/src/main/scala/li/cil/oc/client/gui/Drone.scala index a7e3f97554..eb9c1502b9 100644 --- a/src/main/scala/li/cil/oc/client/gui/Drone.scala +++ b/src/main/scala/li/cil/oc/client/gui/Drone.scala @@ -4,7 +4,7 @@ import li.cil.oc.Localization import li.cil.oc.client.Textures import li.cil.oc.client.gui.widget.ProgressBar import li.cil.oc.client.renderer.TextBufferRenderCache -import li.cil.oc.client.renderer.font.TextBufferRenderData +import li.cil.oc.client.renderer.text.TextBufferRenderData import li.cil.oc.client.{PacketSender => ClientPacketSender} import li.cil.oc.common.container import li.cil.oc.common.entity @@ -84,7 +84,7 @@ class Drone(playerInventory: InventoryPlayer, val drone: entity.Drone) extends D GL11.glPopAttrib() } - override protected def changeSize(w: Double, h: Double, recompile: Boolean) = 2.0 + override protected def changeSize(w: Double, h: Double) = 2.0 override protected def drawSecondaryForegroundLayer(mouseX: Int, mouseY: Int) { drawBufferLayer() diff --git a/src/main/scala/li/cil/oc/client/gui/Robot.scala b/src/main/scala/li/cil/oc/client/gui/Robot.scala index db058a5a0f..d63caee297 100644 --- a/src/main/scala/li/cil/oc/client/gui/Robot.scala +++ b/src/main/scala/li/cil/oc/client/gui/Robot.scala @@ -107,7 +107,7 @@ class Robot(playerInventory: InventoryPlayer, val robot: tileentity.Robot) exten GL11.glPushMatrix() GL11.glTranslatef(-3, -3, 0) GL11.glColor4f(1, 1, 1, 1) - BufferRenderer.drawBackground() + BufferRenderer.drawBackground(bufferRenderWidth.toInt, bufferRenderHeight.toInt, forRobot = true) GL11.glPopMatrix() RenderState.makeItBlend() val scaleX = bufferRenderWidth / buffer.renderWidth @@ -247,14 +247,11 @@ class Robot(playerInventory: InventoryPlayer, val robot: tileentity.Robot) exten } } - override protected def changeSize(w: Double, h: Double, recompile: Boolean) = { + override protected def changeSize(w: Double, h: Double) = { val bw = w * TextBufferRenderCache.renderer.charRenderWidth val bh = h * TextBufferRenderCache.renderer.charRenderHeight val scaleX = math.min(bufferRenderWidth / bw, 1) val scaleY = math.min(bufferRenderHeight / bh, 1) - if (recompile) { - BufferRenderer.compileBackground(bufferRenderWidth.toInt, bufferRenderHeight.toInt, forRobot = true) - } math.min(scaleX, scaleY) } diff --git a/src/main/scala/li/cil/oc/client/gui/Screen.scala b/src/main/scala/li/cil/oc/client/gui/Screen.scala index a568f68dc5..b017d4e9e6 100644 --- a/src/main/scala/li/cil/oc/client/gui/Screen.scala +++ b/src/main/scala/li/cil/oc/client/gui/Screen.scala @@ -20,6 +20,8 @@ class Screen(val buffer: api.internal.TextBuffer, val hasMouse: Boolean, val has private var x, y = 0 + private var innerWidth, innerHeight = 0 + private var mx, my = -1 override def handleMouseInput() { @@ -97,7 +99,7 @@ class Screen(val buffer: api.internal.TextBuffer, val hasMouse: Boolean, val has override def drawBuffer() { GL11.glTranslatef(x, y, 0) - BufferRenderer.drawBackground() + BufferRenderer.drawBackground(innerWidth, innerHeight) if (hasPower()) { GL11.glTranslatef(bufferMargin, bufferMargin, 0) GL11.glScaled(scale, scale, 1) @@ -106,19 +108,16 @@ class Screen(val buffer: api.internal.TextBuffer, val hasMouse: Boolean, val has } } - override protected def changeSize(w: Double, h: Double, recompile: Boolean) = { + override protected def changeSize(w: Double, h: Double) = { val bw = buffer.renderWidth val bh = buffer.renderHeight val scaleX = math.min(width / (bw + bufferMargin * 2.0), 1) val scaleY = math.min(height / (bh + bufferMargin * 2.0), 1) val scale = math.min(scaleX, scaleY) - val innerWidth = (bw * scale).toInt - val innerHeight = (bh * scale).toInt + innerWidth = (bw * scale).toInt + innerHeight = (bh * scale).toInt x = (width - (innerWidth + bufferMargin * 2)) / 2 y = (height - (innerHeight + bufferMargin * 2)) / 2 - if (recompile) { - BufferRenderer.compileBackground(innerWidth, innerHeight) - } scale } } diff --git a/src/main/scala/li/cil/oc/client/gui/traits/DisplayBuffer.scala b/src/main/scala/li/cil/oc/client/gui/traits/DisplayBuffer.scala index f8a0ea1a63..878dd86234 100644 --- a/src/main/scala/li/cil/oc/client/gui/traits/DisplayBuffer.scala +++ b/src/main/scala/li/cil/oc/client/gui/traits/DisplayBuffer.scala @@ -15,36 +15,27 @@ trait DisplayBuffer extends GuiScreen { protected def bufferRows: Int - protected var guiSizeChanged = false - - protected var currentWidth, currentHeight = -1 - protected var scale = 0.0 override def initGui() = { super.initGui() BufferRenderer.init(Minecraft.getMinecraft.renderEngine) - guiSizeChanged = true } protected def drawBufferLayer() { - val oldWidth = currentWidth - val oldHeight = currentHeight - currentWidth = bufferColumns - currentHeight = bufferRows - scale = changeSize(currentWidth, currentHeight, guiSizeChanged || oldWidth != currentWidth || oldHeight != currentHeight) + scale = changeSize(bufferColumns, bufferRows) - RenderState.checkError(getClass.getName + ".drawBufferLayer: entering (aka: wasntme)") + RenderState.checkError(this, ".drawBufferLayer: entering (aka: wasntme)") GL11.glPushMatrix() RenderState.disableLighting() drawBuffer() GL11.glPopMatrix() - RenderState.checkError(getClass.getName + ".drawBufferLayer: buffer layer") + RenderState.checkError(this, ".drawBufferLayer: buffer layer") } protected def drawBuffer() - protected def changeSize(w: Double, h: Double, recompile: Boolean): Double + protected def changeSize(w: Double, h: Double): Double } diff --git a/src/main/scala/li/cil/oc/client/gui/traits/InputBuffer.scala b/src/main/scala/li/cil/oc/client/gui/traits/InputBuffer.scala index 53f11264f1..59d0c1fcdd 100644 --- a/src/main/scala/li/cil/oc/client/gui/traits/InputBuffer.scala +++ b/src/main/scala/li/cil/oc/client/gui/traits/InputBuffer.scala @@ -59,7 +59,7 @@ trait InputBuffer extends DisplayBuffer { t.draw() GL11.glEnable(GL11.GL_DEPTH_TEST) - RenderState.checkError(getClass.getName + ".drawBufferLayer: keyboard icon") + RenderState.checkError(this, ".drawBufferLayer: keyboard icon") } } diff --git a/src/main/scala/li/cil/oc/client/renderer/Blink.scala b/src/main/scala/li/cil/oc/client/renderer/Blink.scala new file mode 100644 index 0000000000..88b9d088b1 --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/Blink.scala @@ -0,0 +1,15 @@ +package li.cil.oc.client.renderer + +import java.util.Random + +object Blink { + final val ActivityMillis = 400L + + def activity(lastAccess: Long, rand: Random): Boolean = System.currentTimeMillis() - lastAccess < ActivityMillis && rand.nextDouble() > 0.1 + + def shouldShowErrorLight(hash: Int): Boolean = { + val time = System.currentTimeMillis + hash + val timeSlice = time / 500 + timeSlice % 2 == 0 + } +} diff --git a/src/main/scala/li/cil/oc/client/renderer/CapturedMesh.scala b/src/main/scala/li/cil/oc/client/renderer/CapturedMesh.scala new file mode 100644 index 0000000000..633e9b4d49 --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/CapturedMesh.scala @@ -0,0 +1,119 @@ +package li.cil.oc.client.renderer + +import java.util + +import com.gtnewhorizon.gtnhlib.client.renderer.DirectTessellator +import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager +import com.gtnewhorizon.gtnhlib.client.renderer.vao.IVertexArrayObject +import com.gtnewhorizon.gtnhlib.client.renderer.vao.IndexBuffer +import com.gtnewhorizon.gtnhlib.client.renderer.vao.VAOManager +import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat +import net.minecraft.client.Minecraft +import net.minecraft.client.renderer.Tessellator +import net.minecraft.util.ResourceLocation +import org.lwjgl.opengl.GL11 + +object CapturedMesh { + def build(format: VertexFormat, texture: ResourceLocation)(emit: Tessellator => Unit): CapturedMesh = { + val mesh = new CapturedMesh + try emit(mesh.bucket(format, texture)) + finally mesh.closeBucket() + mesh + } + + private final class Slot(val format: VertexFormat) { + val ebo = new IndexBuffer() + + val vao: IVertexArrayObject = VAOManager.createMutableVAO(format, GL11.GL_TRIANGLES, ebo) + + var texture: ResourceLocation = _ + } +} + +final class CapturedMesh { + private val slots = new util.ArrayList[CapturedMesh.Slot] + + private var used = 0 + + private var capturing: DirectTessellator = _ + + private var capturingFormat: VertexFormat = _ + + private var capturingTexture: ResourceLocation = _ + + def bucket(format: VertexFormat, texture: ResourceLocation): Tessellator = { + closeBucket() + val t = TessellatorManager.startCapturingDirect(format) + t.startDrawingQuads() + t.draw() + capturing = t + capturingFormat = format + capturingTexture = texture + t + } + + def closeBucket() { + if (capturing != null) { + val t = capturing + capturing = null + try if (!t.isEmpty) { + val slot = nextSlot(capturingFormat) + t.allocateToVBO(slot.vao, slot.ebo) + slot.texture = capturingTexture + } + finally TessellatorManager.stopCapturingDirect() + capturingFormat = null + capturingTexture = null + } + } + + def reset() { + closeBucket() + used = 0 + } + + def delete() { + closeBucket() + var i = 0 + while (i < slots.size) { + slots.get(i).vao.delete() + i += 1 + } + slots.clear() + used = 0 + } + + def draw() { + val textureManager = Minecraft.getMinecraft.getTextureManager + var i = 0 + while (i < used) { + val slot = slots.get(i) + if (slot.texture == null) GL11.glDisable(GL11.GL_TEXTURE_2D) + else { + GL11.glEnable(GL11.GL_TEXTURE_2D) + textureManager.bindTexture(slot.texture) + } + slot.vao.render() + i += 1 + } + } + + def drawGeometry() { + var i = 0 + while (i < used) { + slots.get(i).vao.render() + i += 1 + } + } + + private def nextSlot(format: VertexFormat): CapturedMesh.Slot = { + var i = used + while (i < slots.size && (slots.get(i).format ne format)) i += 1 + if (i == slots.size) slots.add(new CapturedMesh.Slot(format)) + val slot = slots.get(i) + slots.set(i, slots.get(used)) + slots.set(used, slot) + used += 1 + slot + } +} diff --git a/src/main/scala/li/cil/oc/client/renderer/DebugBox.scala b/src/main/scala/li/cil/oc/client/renderer/DebugBox.scala new file mode 100644 index 0000000000..eade9f21dc --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/DebugBox.scala @@ -0,0 +1,59 @@ +package li.cil.oc.client.renderer + +import net.minecraft.client.renderer.Tessellator + +object DebugBox { + private val boxFaceOrder = Array(0, 2, 1, 3, 4, 5) + + def drawBox(minX: Double, minY: Double, minZ: Double, maxX: Double, maxY: Double, maxZ: Double) { + val t = Tessellator.instance + t.startDrawingQuads() + var i = 0 + while (i < boxFaceOrder.length) { + emitBoxFace(t, minX, minY, minZ, maxX, maxY, maxZ, boxFaceOrder(i)) + i += 1 + } + t.draw() + } + + def drawBoxFace(minX: Double, minY: Double, minZ: Double, maxX: Double, maxY: Double, maxZ: Double, side: Int) { + val t = Tessellator.instance + t.startDrawingQuads() + emitBoxFace(t, minX, minY, minZ, maxX, maxY, maxZ, side) + t.draw() + } + + private def emitBoxFace(t: Tessellator, minX: Double, minY: Double, minZ: Double, maxX: Double, maxY: Double, maxZ: Double, side: Int): Unit = side match { + case 0 => + t.addVertex(minX, minY, minZ) + t.addVertex(minX, minY, maxZ) + t.addVertex(maxX, minY, maxZ) + t.addVertex(maxX, minY, minZ) + case 1 => + t.addVertex(maxX, maxY, minZ) + t.addVertex(maxX, maxY, maxZ) + t.addVertex(minX, maxY, maxZ) + t.addVertex(minX, maxY, minZ) + case 2 => + t.addVertex(minX, minY, minZ) + t.addVertex(maxX, minY, minZ) + t.addVertex(maxX, maxY, minZ) + t.addVertex(minX, maxY, minZ) + case 3 => + t.addVertex(maxX, maxY, maxZ) + t.addVertex(maxX, minY, maxZ) + t.addVertex(minX, minY, maxZ) + t.addVertex(minX, maxY, maxZ) + case 4 => + t.addVertex(minX, minY, minZ) + t.addVertex(minX, maxY, minZ) + t.addVertex(minX, maxY, maxZ) + t.addVertex(minX, minY, maxZ) + case 5 => + t.addVertex(maxX, minY, minZ) + t.addVertex(maxX, minY, maxZ) + t.addVertex(maxX, maxY, maxZ) + t.addVertex(maxX, maxY, minZ) + case _ => + } +} diff --git a/src/main/scala/li/cil/oc/client/renderer/FrameStart.scala b/src/main/scala/li/cil/oc/client/renderer/FrameStart.scala new file mode 100644 index 0000000000..944d96cfad --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/FrameStart.scala @@ -0,0 +1,28 @@ +package li.cil.oc.client.renderer + +import cpw.mods.fml.common.eventhandler.SubscribeEvent +import cpw.mods.fml.common.gameevent.TickEvent +import cpw.mods.fml.common.gameevent.TickEvent.RenderTickEvent +import li.cil.oc.client.renderer.shading.Fog +import li.cil.oc.client.renderer.shading.Programs +import li.cil.oc.client.renderer.shading.ShaderPack +import li.cil.oc.client.renderer.text.ScreenText + +object FrameStart { + var reloadPending = true + + @SubscribeEvent + def onRenderTick(e: RenderTickEvent) { + if (e.phase == TickEvent.Phase.START) { + Fog.beginFrame() + ShaderPack.beginFrame() + if (reloadPending) { + reloadPending = false + TextBufferRenderCache.releaseAll() + Programs.reload() + ScreenText.reload(TextBufferRenderCache.renderer) + } + TextBufferRenderCache.beginFrame() + } + } +} diff --git a/src/main/scala/li/cil/oc/client/renderer/MFUTargetRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/MFUTargetRenderer.scala index 470a7b35ff..c1b46bab30 100644 --- a/src/main/scala/li/cil/oc/client/renderer/MFUTargetRenderer.scala +++ b/src/main/scala/li/cil/oc/client/renderer/MFUTargetRenderer.scala @@ -35,7 +35,7 @@ object MFUTargetRenderer { val py = player.lastTickPosY + (player.posY - player.lastTickPosY) * e.partialTicks val pz = player.lastTickPosZ + (player.posZ - player.lastTickPosZ) * e.partialTicks - RenderState.checkError(getClass.getName + ".onRenderWorldLastEvent: entering (aka: wasntme)") + RenderState.checkError(this, ".onRenderWorldLastEvent: entering (aka: wasntme)") GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) GL11.glPushMatrix() @@ -52,104 +52,16 @@ object MFUTargetRenderer { ((color >> 0) & 0xFF) / 255f, 0.25f) GL11.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_LINE) - drawBox(bounds.minX, bounds.minY, bounds.minZ, bounds.maxX, bounds.maxY, bounds.maxZ) + DebugBox.drawBox(bounds.minX, bounds.minY, bounds.minZ, bounds.maxX, bounds.maxY, bounds.maxZ) GL11.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_FILL) - drawFace(bounds.minX, bounds.minY, bounds.minZ, bounds.maxX, bounds.maxY, bounds.maxZ, side) + DebugBox.drawBoxFace(bounds.minX, bounds.minY, bounds.minZ, bounds.maxX, bounds.maxY, bounds.maxZ, side) GL11.glPopMatrix() GL11.glPopAttrib() - RenderState.checkError(getClass.getName + ".onRenderWorldLastEvent: leaving") + RenderState.checkError(this, ".onRenderWorldLastEvent: leaving") } case _ => // Nothing } } - - private def drawBox(minX: Double, minY: Double, minZ: Double, maxX: Double, maxY: Double, maxZ: Double) { - GL11.glBegin(GL11.GL_QUADS) - GL11.glVertex3d(minX, minY, minZ) - GL11.glVertex3d(minX, minY, maxZ) - GL11.glVertex3d(maxX, minY, maxZ) - GL11.glVertex3d(maxX, minY, minZ) - GL11.glEnd() - GL11.glBegin(GL11.GL_QUADS) - GL11.glVertex3d(minX, minY, minZ) - GL11.glVertex3d(maxX, minY, minZ) - GL11.glVertex3d(maxX, maxY, minZ) - GL11.glVertex3d(minX, maxY, minZ) - GL11.glEnd() - GL11.glBegin(GL11.GL_QUADS) - GL11.glVertex3d(maxX, maxY, minZ) - GL11.glVertex3d(maxX, maxY, maxZ) - GL11.glVertex3d(minX, maxY, maxZ) - GL11.glVertex3d(minX, maxY, minZ) - GL11.glEnd() - GL11.glBegin(GL11.GL_QUADS) - GL11.glVertex3d(maxX, maxY, maxZ) - GL11.glVertex3d(maxX, minY, maxZ) - GL11.glVertex3d(minX, minY, maxZ) - GL11.glVertex3d(minX, maxY, maxZ) - GL11.glEnd() - GL11.glBegin(GL11.GL_QUADS) - GL11.glVertex3d(minX, minY, minZ) - GL11.glVertex3d(minX, maxY, minZ) - GL11.glVertex3d(minX, maxY, maxZ) - GL11.glVertex3d(minX, minY, maxZ) - GL11.glEnd() - GL11.glBegin(GL11.GL_QUADS) - GL11.glVertex3d(maxX, minY, minZ) - GL11.glVertex3d(maxX, minY, maxZ) - GL11.glVertex3d(maxX, maxY, maxZ) - GL11.glVertex3d(maxX, maxY, minZ) - GL11.glEnd() - } - - private def drawFace(minX: Double, minY: Double, minZ: Double, maxX: Double, maxY: Double, maxZ: Double, side: Int): Unit = { - side match { - case 0 => // Down - GL11.glBegin(GL11.GL_QUADS) - GL11.glVertex3d(minX, minY, minZ) - GL11.glVertex3d(minX, minY, maxZ) - GL11.glVertex3d(maxX, minY, maxZ) - GL11.glVertex3d(maxX, minY, minZ) - GL11.glEnd() - case 1 => // Up - GL11.glBegin(GL11.GL_QUADS) - GL11.glVertex3d(maxX, maxY, minZ) - GL11.glVertex3d(maxX, maxY, maxZ) - GL11.glVertex3d(minX, maxY, maxZ) - GL11.glVertex3d(minX, maxY, minZ) - GL11.glEnd() - case 2 => // North - GL11.glBegin(GL11.GL_QUADS) - GL11.glVertex3d(minX, minY, minZ) - GL11.glVertex3d(maxX, minY, minZ) - GL11.glVertex3d(maxX, maxY, minZ) - GL11.glVertex3d(minX, maxY, minZ) - GL11.glEnd() - case 3 => // South - GL11.glBegin(GL11.GL_QUADS) - GL11.glVertex3d(maxX, maxY, maxZ) - GL11.glVertex3d(maxX, minY, maxZ) - GL11.glVertex3d(minX, minY, maxZ) - GL11.glVertex3d(minX, maxY, maxZ) - GL11.glEnd() - case 4 => // East - GL11.glBegin(GL11.GL_QUADS) - GL11.glVertex3d(minX, minY, minZ) - GL11.glVertex3d(minX, maxY, minZ) - GL11.glVertex3d(minX, maxY, maxZ) - GL11.glVertex3d(minX, minY, maxZ) - GL11.glEnd() - case 5 => // West - GL11.glBegin(GL11.GL_QUADS) - GL11.glVertex3d(maxX, minY, minZ) - GL11.glVertex3d(maxX, minY, maxZ) - GL11.glVertex3d(maxX, maxY, maxZ) - GL11.glVertex3d(maxX, maxY, minZ) - GL11.glEnd() - case _ => // WTF? - } - } - } diff --git a/src/main/scala/li/cil/oc/client/renderer/TextBufferRenderCache.scala b/src/main/scala/li/cil/oc/client/renderer/TextBufferRenderCache.scala index 21aed3e670..3533c7c4c9 100644 --- a/src/main/scala/li/cil/oc/client/renderer/TextBufferRenderCache.scala +++ b/src/main/scala/li/cil/oc/client/renderer/TextBufferRenderCache.scala @@ -1,32 +1,97 @@ package li.cil.oc.client.renderer -import li.cil.oc.Settings -import li.cil.oc.client.renderer.font.TextBufferRenderData -import li.cil.oc.util.RenderState +import java.util + +import cpw.mods.fml.common.eventhandler.SubscribeEvent +import cpw.mods.fml.common.gameevent.TickEvent +import cpw.mods.fml.common.gameevent.TickEvent.ClientTickEvent +import li.cil.oc.client.renderer.font.DynamicFontRenderer +import li.cil.oc.client.renderer.shading.Programs +import li.cil.oc.client.renderer.text.GlyphMeshText +import li.cil.oc.client.renderer.text.ScreenText +import li.cil.oc.client.renderer.text.TextBufferRenderData object TextBufferRenderCache { - val renderer = - if (Settings.get.fontRenderer == "texture") new font.StaticFontRenderer() - else new font.DynamicFontRenderer() + val renderer = new DynamicFontRenderer() + + var inWorld = false + + var worldFade = 1f + + private final val UnusedTicks = 40 + + private val owners = new util.ArrayList[TextBufferRenderData] - // ----------------------------------------------------------------------- // - // Rendering - // ----------------------------------------------------------------------- // + private var tick = 0 def render(buffer: TextBufferRenderData) { - RenderState.checkError(getClass.getName + ".render: entering (aka: wasntme)") + buffer.consumeDirty() + if (!buffer.tracked) { + buffer.tracked = true + owners.add(buffer) + } + buffer.lastUsedTick = tick + + val drawn = + if (Programs.fixedFunction(inWorld)) ScreenText.drawPacked(renderer, buffer, worldFade) + else ScreenText.render(renderer, buffer, if (inWorld) worldFade else 1f, inWorld) + if (drawn) dropMesh(buffer) + else GlyphMeshText.render(buffer, renderer, inWorld, worldFade) + } - if (buffer.dirty) { - for (line <- buffer.data.buffer) { - renderer.generateChars(line) + def beginFrame() { + ScreenText.beginFrame() + val packs = Programs.fixedFunction(inWorld = true) + var i = 0 + while (i < owners.size) { + val owner = owners.get(i) + if (!packs) ScreenText.releasePackTarget(owner) + else if (owner.drawnInPack) { + owner.drawnInPack = false + ScreenText.renderPackTarget(renderer, owner) } - RenderState.checkError(getClass.getName + ".render: generateChars") - buffer.dirty = false + i += 1 + } + ScreenText.finishPackTargets() + } + + private def dropMesh(buffer: TextBufferRenderData) { + if (buffer.mesh != null) { + buffer.mesh.delete() + buffer.mesh = null } + } - // Keep this uncached: display lists do not replay Tessellator vertex state reliably under Angelica. - renderer.drawBuffer(buffer.data, buffer.viewport._1, buffer.viewport._2) + private def release(buffer: TextBufferRenderData) { + ScreenText.release(buffer) + dropMesh(buffer) + buffer.tracked = false + } - RenderState.checkError(getClass.getName + ".render: leaving") + def releaseAll() { + var i = 0 + while (i < owners.size) { + release(owners.get(i)) + i += 1 + } + owners.clear() + ScreenText.finishPackTargets() + } + + @SubscribeEvent + def onTick(e: ClientTickEvent) { + if (e.phase == TickEvent.Phase.END) { + tick += 1 + var i = owners.size - 1 + while (i >= 0) { + val owner = owners.get(i) + if (tick - owner.lastUsedTick > UnusedTicks) { + release(owner) + owners.remove(i) + } + i -= 1 + } + ScreenText.finishPackTargets() + } } } diff --git a/src/main/scala/li/cil/oc/client/renderer/UnitQuad.scala b/src/main/scala/li/cil/oc/client/renderer/UnitQuad.scala new file mode 100644 index 0000000000..68b90bae55 --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/UnitQuad.scala @@ -0,0 +1,12 @@ +package li.cil.oc.client.renderer + +import net.minecraft.client.renderer.Tessellator + +object UnitQuad { + def emitFrontQuad(t: Tessellator) { + t.addVertexWithUV(0, 1, 0, 0, 1) + t.addVertexWithUV(1, 1, 0, 1, 1) + t.addVertexWithUV(1, 0, 0, 1, 0) + t.addVertexWithUV(0, 0, 0, 0, 0) + } +} diff --git a/src/main/scala/li/cil/oc/client/renderer/WirelessNetworkDebugRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/WirelessNetworkDebugRenderer.scala index a92bccabc0..a8c54dc7d2 100644 --- a/src/main/scala/li/cil/oc/client/renderer/WirelessNetworkDebugRenderer.scala +++ b/src/main/scala/li/cil/oc/client/renderer/WirelessNetworkDebugRenderer.scala @@ -16,7 +16,7 @@ object WirelessNetworkDebugRenderer { @SubscribeEvent def onRenderWorldLastEvent(e: RenderWorldLastEvent) { if (Settings.rTreeDebugRenderer) { - RenderState.checkError(getClass.getName + ".onRenderWorldLastEvent: entering (aka: wasntme)") + RenderState.checkError(this, ".onRenderWorldLastEvent: entering (aka: wasntme)") val world = ObfuscationReflectionHelper.getPrivateValue(classOf[net.minecraft.client.renderer.RenderGlobal], e.context, "theWorld", "field_72769_h", "r").asInstanceOf[World] WirelessNetwork.dimensions.get(world.provider.dimensionId) match { @@ -36,45 +36,6 @@ object WirelessNetworkDebugRenderer { GL11.glDisable(GL11.GL_DEPTH_TEST) GL11.glDisable(GL11.GL_CULL_FACE) - def drawBox(minX: Double, minY: Double, minZ: Double, maxX: Double, maxY: Double, maxZ: Double) { - GL11.glBegin(GL11.GL_QUADS) - GL11.glVertex3d(minX, minY, minZ) - GL11.glVertex3d(minX, minY, maxZ) - GL11.glVertex3d(maxX, minY, maxZ) - GL11.glVertex3d(maxX, minY, minZ) - GL11.glEnd() - GL11.glBegin(GL11.GL_QUADS) - GL11.glVertex3d(minX, minY, minZ) - GL11.glVertex3d(maxX, minY, minZ) - GL11.glVertex3d(maxX, maxY, minZ) - GL11.glVertex3d(minX, maxY, minZ) - GL11.glEnd() - GL11.glBegin(GL11.GL_QUADS) - GL11.glVertex3d(maxX, maxY, minZ) - GL11.glVertex3d(maxX, maxY, maxZ) - GL11.glVertex3d(minX, maxY, maxZ) - GL11.glVertex3d(minX, maxY, minZ) - GL11.glEnd() - GL11.glBegin(GL11.GL_QUADS) - GL11.glVertex3d(maxX, maxY, maxZ) - GL11.glVertex3d(maxX, minY, maxZ) - GL11.glVertex3d(minX, minY, maxZ) - GL11.glVertex3d(minX, maxY, maxZ) - GL11.glEnd() - GL11.glBegin(GL11.GL_QUADS) - GL11.glVertex3d(minX, minY, minZ) - GL11.glVertex3d(minX, maxY, minZ) - GL11.glVertex3d(minX, maxY, maxZ) - GL11.glVertex3d(minX, minY, maxZ) - GL11.glEnd() - GL11.glBegin(GL11.GL_QUADS) - GL11.glVertex3d(maxX, minY, minZ) - GL11.glVertex3d(maxX, minY, maxZ) - GL11.glVertex3d(maxX, maxY, maxZ) - GL11.glVertex3d(maxX, maxY, minZ) - GL11.glEnd() - } - GL11.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_LINE) for (((min, max), level) <- tree.allBounds) { val (minX, minY, minZ) = min @@ -86,7 +47,7 @@ object WirelessNetworkDebugRenderer { ((color >> 0) & 0xFF) / 255f, 0.25f) val size = 0.5 - level * 0.05 - drawBox(minX - size, minY - size, minZ - size, maxX + size, maxY + size, maxZ + size) + DebugBox.drawBox(minX - size, minY - size, minZ - size, maxX + size, maxY + size, maxZ + size) } GL11.glPolygonMode(GL11.GL_FRONT_AND_BACK, GL11.GL_FILL) @@ -95,7 +56,7 @@ object WirelessNetworkDebugRenderer { case _ => } - RenderState.checkError(getClass.getName + ".onRenderWorldLastEvent: leaving") + RenderState.checkError(this, ".onRenderWorldLastEvent: leaving") } } diff --git a/src/main/scala/li/cil/oc/client/renderer/block/BlockRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/block/BlockRenderer.scala index 7582f0d51d..4e06eeeefc 100644 --- a/src/main/scala/li/cil/oc/client/renderer/block/BlockRenderer.scala +++ b/src/main/scala/li/cil/oc/client/renderer/block/BlockRenderer.scala @@ -3,6 +3,8 @@ package li.cil.oc.client.renderer.block import com.gtnewhorizons.angelica.api.ThreadSafeISBRH import cpw.mods.fml.client.registry.ISimpleBlockRenderingHandler import li.cil.oc.Settings +import li.cil.oc.client.Textures +import li.cil.oc.client.renderer.tileentity.CubeFaces import li.cil.oc.client.renderer.tileentity.RobotRenderer import li.cil.oc.common import net.minecraft.block.Block @@ -10,6 +12,7 @@ import net.minecraft.client.renderer.RenderBlocks import net.minecraft.client.renderer.Tessellator import net.minecraft.util.IIcon import net.minecraft.world.IBlockAccess +import net.minecraftforge.client.ForgeHooksClient import net.minecraftforge.common.util.ForgeDirection import org.lwjgl.opengl.GL11 @@ -86,7 +89,22 @@ object BlockRenderer extends ISimpleBlockRenderingHandler { override def renderWorldBlock(world: IBlockAccess, x: Int, y: Int, z: Int, block: Block, modelId: Int, realRenderer: RenderBlocks) = { val renderer = patchedRenderer(realRenderer, block) + val overlayPass = !renderer.hasOverrideBlockTexture && ForgeHooksClient.getWorldRenderPass == 1 world.getTileEntity(x, y, z) match { + case adapter: common.tileentity.Adapter => + if (overlayPass) renderOpenSides(x, y, z, Textures.Adapter.iconOn, CubeFaces.openSides(adapter)) + else renderer.renderStandardBlock(block, x, y, z) + case disassembler: common.tileentity.Disassembler => + if (!overlayPass) renderer.renderStandardBlock(block, x, y, z) + else if (disassembler.isActive) { + val t = beginOverlay() + CubeFaces.emitInflatedTop(t, Textures.Disassembler.iconTopOn, x, y, z) + CubeFaces.emitInflatedSides(t, Textures.Disassembler.iconSideOn, CubeFaces.Walls, x, y, z) + true + } + else false + case splitter: common.tileentity.NetSplitter if overlayPass => + renderOpenSides(x, y, z, Textures.NetSplitter.iconOn, CubeFaces.openSides(splitter)) case assembler: common.tileentity.Assembler => Assembler.render(assembler.block, assembler.getBlockMetadata, x, y, z, renderer) @@ -130,6 +148,20 @@ object BlockRenderer extends ISimpleBlockRenderingHandler { } } + private def beginOverlay(): Tessellator = { + val t = Tessellator.instance + t.setBrightness(0xF000F0) + t.setColorOpaque_F(1, 1, 1) + t + } + + private def renderOpenSides(x: Int, y: Int, z: Int, icon: IIcon, mask: Int): Boolean = + if (mask == 0) false + else { + CubeFaces.emitInflatedSides(beginOverlay(), icon, mask, x, y, z) + true + } + private def needsFlipping(block: Block) = block.isInstanceOf[common.block.Hologram] || block.isInstanceOf[common.block.Printer] || diff --git a/src/main/scala/li/cil/oc/client/renderer/font/DynamicFontRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/font/DynamicFontRenderer.scala index fef5ca1715..b48055f8d8 100644 --- a/src/main/scala/li/cil/oc/client/renderer/font/DynamicFontRenderer.scala +++ b/src/main/scala/li/cil/oc/client/renderer/font/DynamicFontRenderer.scala @@ -1,35 +1,158 @@ package li.cil.oc.client.renderer.font +import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat import gnu.trove.map.hash.TIntObjectHashMap import li.cil.oc.Settings +import li.cil.oc.client.renderer.CapturedMesh import li.cil.oc.client.renderer.font.DynamicFontRenderer.CharTexture +import li.cil.oc.util.ExtendedUnicodeHelper import li.cil.oc.util.FontUtils +import li.cil.oc.util.PackedColor import li.cil.oc.util.RenderState +import li.cil.oc.util.TextBuffer import net.minecraft.client.Minecraft import net.minecraft.client.renderer.Tessellator +import net.minecraft.client.renderer.texture.ITextureObject import net.minecraft.client.resources.IReloadableResourceManager import net.minecraft.client.resources.IResourceManager import net.minecraft.client.resources.IResourceManagerReloadListener +import net.minecraft.util.ResourceLocation import org.lwjgl.BufferUtils import org.lwjgl.opengl._ import scala.collection.mutable -/** - * Font renderer that dynamically generates lookup textures by rendering a font - * to it. It's pretty broken right now, and font rendering looks crappy as hell. - */ -class DynamicFontRenderer extends TextureFontRenderer with IResourceManagerReloadListener { - private val glyphProvider: IGlyphProvider = Settings.get.fontRenderer match { - case _ => new FontParserHex() +class DynamicFontRenderer extends IResourceManagerReloadListener { + private final val basicChars = """☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼ !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~⌂ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■""" + + def charRenderWidth = charWidth / 2 + + def charRenderHeight = charHeight / 2 + + def generateChars(chars: Array[Char]) { + for (char <- chars) { + generateChar(char) + } + } + + def generateChars(chars: Array[Int]) { + for (char <- chars) { + generateChar(char) + } + } + + def emitGeometry(mesh: CapturedMesh, buffer: TextBuffer, viewportWidth: Int, viewportHeight: Int) { + val format = buffer.format + val rows = viewportHeight min buffer.height + + // Background first. We try to merge adjacent backgrounds of the same + // color to reduce the number of quads we have to draw. + val t = mesh.bucket(DefaultVertexFormat.POSITION_COLOR, null) + var y = 0 + while (y < rows) { + val color = buffer.color(y) + var cbg = 0x000000 + var x = 0 + var width = 0 + var c = 0 + while (c < color.length && x + width < viewportWidth) { + val col = PackedColor.unpackBackground(color(c), format) + if (col != cbg) { + drawQuad(t, cbg, x, y, width) + cbg = col + x += width + width = 0 + } + width = width + 1 + c += 1 + } + drawQuad(t, cbg, x, y, width) + y += 1 + } + + RenderState.checkError(this, ".emitGeometry: background") + + var i = 0 + while (i < textureCount) { + val pt = mesh.bucket(DefaultVertexFormat.POSITION_TEXTURE_COLOR, textureLocation(i)) + setActivePage(i) + y = 0 + while (y < rows) { + val line = buffer.buffer(y) + val color = buffer.color(y) + val ty = y * charHeight + var cfg = -1 + var tx = 0f + var n = 0 + while (n < viewportWidth) { + val ch = line(n) + val col = PackedColor.unpackForeground(color(n), format) + // Check if color changed. + if (col != cfg) { + cfg = col + pt.setColorOpaque_I(cfg) + } + // Don't render whitespace. + if (ch != ' ') { + drawChar(pt, tx, ty, ch) + } + tx += charWidth + n += 1 + } + y += 1 + } + i += 1 + } + + RenderState.checkError(this, ".emitGeometry: foreground") + } + + def drawString(s: String, x: Int, y: Int, color: Int): Unit = { + val sLength = ExtendedUnicodeHelper.length(s) + val t = Tessellator.instance + + GL11.glPushMatrix() + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) + + GL11.glTranslatef(x, y, 0) + GL11.glScalef(0.5f, 0.5f, 1) + GL11.glDepthMask(false) + GL11.glEnable(GL11.GL_TEXTURE_2D) + + for (i <- 0 until textureCount) { + bindTexture(i) + t.startDrawingQuads() + t.setColorOpaque_I(color) + var tx = 0f + var cx = 0 + for (n <- 0 until sLength) { + val ch = s.codePointAt(cx) + // Don't render whitespace. + if (ch != ' ') { + drawChar(t, tx, 0, ch) + } + tx += charWidth + cx = s.offsetByCodePoints(cx, 1) + } + t.draw() + } + + GL11.glPopAttrib() + GL11.glPopMatrix() } + val glyphProvider: IGlyphProvider = new FontParserHex() + private val textures = mutable.ArrayBuffer.empty[CharTexture] private val charMap = new TIntObjectHashMap[DynamicFontRenderer.CharIcon]() private var activeTexture: CharTexture = _ + private var generation = 0 + + private val pageLocations = mutable.ArrayBuffer.empty[ResourceLocation] + initialize() Minecraft.getMinecraft.getResourceManager match { @@ -43,6 +166,7 @@ class DynamicFontRenderer extends TextureFontRenderer with IResourceManagerReloa } textures.clear() charMap.clear() + generation += 1 textures += new DynamicFontRenderer.CharTexture(this) activeTexture = textures.head generateChars(basicChars.toCharArray) @@ -53,29 +177,57 @@ class DynamicFontRenderer extends TextureFontRenderer with IResourceManagerReloa initialize() } - override protected def charWidth = glyphProvider.getGlyphWidth + private[renderer] def charWidth = glyphProvider.getGlyphWidth - override protected def charHeight = glyphProvider.getGlyphHeight + private[renderer] def charHeight = glyphProvider.getGlyphHeight - override protected def textureCount = textures.length + private def textureCount = textures.length - override protected def bindTexture(index: Int) { + private def bindTexture(index: Int) { activeTexture = textures(index) activeTexture.bind() - RenderState.checkError(getClass.getName + ".bindTexture") + RenderState.checkError(this, ".bindTexture") } - override protected def generateChar(char: Int) { + def cacheGeneration = generation + + private def textureLocation(page: Int): ResourceLocation = { + while (pageLocations.length <= page) { + val index = pageLocations.length + val location = new ResourceLocation(Settings.resourceDomain, "dynamicfont/" + index) + Minecraft.getMinecraft.getTextureManager.loadTexture(location, new DynamicFontRenderer.PageTexture(this, index)) + pageLocations += location + } + pageLocations(page) + } + + private def generateChar(char: Int) { getOrCreate(char) } - override protected def drawChar(tx: Float, ty: Float, char: Int) { + private def setActivePage(page: Int) { + activeTexture = textures(page) + } + + private def drawChar(t: Tessellator, tx: Float, ty: Float, char: Int) { val icon = charMap.get(char) if (icon != null && icon.texture == activeTexture) { - icon.draw(tx, ty) + icon.draw(t, tx, ty) } } + private def drawQuad(t: Tessellator, color: Int, x: Int, y: Int, width: Int) = if (color != 0 && width > 0) { + val x0 = x * charWidth + val x1 = (x + width) * charWidth + val y0 = y * charHeight + val y1 = (y + 1) * charHeight + t.setColorOpaque_I(color) + t.addVertex(x0, y1, 0) + t.addVertex(x1, y1, 0) + t.addVertex(x1, y0, 0) + t.addVertex(x0, y0, 0) + } + private def getOrCreate(char: Int): DynamicFontRenderer.CharIcon = { if (charMap.containsKey(char)) charMap.get(char) else { @@ -86,7 +238,7 @@ class DynamicFontRenderer extends TextureFontRenderer with IResourceManagerReloa } private def createCharIcon(char: Int): DynamicFontRenderer.CharIcon = { - if (FontUtils.wcwidth(char) < 1 || glyphProvider.getGlyph(char) == null) { + if (FontUtils.wcwidth(char) < 1 || !glyphProvider.hasGlyph(char)) { if (char == '?') null else getOrCreate('?') } @@ -103,8 +255,14 @@ class DynamicFontRenderer extends TextureFontRenderer with IResourceManagerReloa object DynamicFontRenderer { private val size = 256 + class PageTexture(owner: DynamicFontRenderer, index: Int) extends ITextureObject { + def loadTexture(manager: IResourceManager): Unit = {} + + def getGlTextureId: Int = if (index < owner.textures.length) owner.textures(index).id else 0 + } + class CharTexture(val owner: DynamicFontRenderer) { - private val id = GL11.glGenTextures() + private[font] val id = GL11.glGenTextures() GL11.glBindTexture(GL11.GL_TEXTURE_2D, id) if (Settings.get.textLinearFiltering) { GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR) @@ -114,7 +272,7 @@ object DynamicFontRenderer { GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST) GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, size, size, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, BufferUtils.createByteBuffer(size * size * 4)) - RenderState.checkError(getClass.getName + ".: create texture") + RenderState.checkError(this, ".: create texture") // Some padding to avoid bleeding. private val cellWidth = owner.charWidth + 2 @@ -159,8 +317,7 @@ object DynamicFontRenderer { } class CharIcon(val texture: CharTexture, val w: Int, val h: Int, val u1: Double, val v1: Double, val u2: Double, val v2: Double) { - def draw(tx: Float, ty: Float) { - val t = Tessellator.instance + def draw(t: Tessellator, tx: Float, ty: Float) { t.addVertexWithUV(tx, ty + h, 0, u1, v2) t.addVertexWithUV(tx + w, ty + h, 0, u2, v2) t.addVertexWithUV(tx + w, ty, 0, u2, v1) diff --git a/src/main/scala/li/cil/oc/client/renderer/font/FontParserHex.java b/src/main/scala/li/cil/oc/client/renderer/font/FontParserHex.java index b097efe640..31d6bb0985 100644 --- a/src/main/scala/li/cil/oc/client/renderer/font/FontParserHex.java +++ b/src/main/scala/li/cil/oc/client/renderer/font/FontParserHex.java @@ -23,6 +23,8 @@ public class FontParserHex implements IGlyphProvider { private final TIntObjectMap glyphs = new TIntObjectHashMap<>(); + private ByteBuffer scratch = BufferUtils.createByteBuffer(32 * 8 * 4); + private static int hex2int(char c) { if (c >= '0' && c <= '9') { return c - '0'; @@ -90,14 +92,22 @@ public void initialize() { } } + @Override + public boolean hasGlyph(int charCode) { + final byte[] glyph = glyphs.get(charCode); + return glyph != null && glyph.length > 0; + } + @Override public ByteBuffer getGlyph(int charCode) { - if (!glyphs.containsKey(charCode)) - return null; final byte[] glyph = glyphs.get(charCode); if (glyph == null || glyph.length == 0) return null; - final ByteBuffer buffer = BufferUtils.createByteBuffer(glyph.length * getGlyphWidth() * 4); + final int size = glyph.length * getGlyphWidth() * 4; + if (scratch.capacity() < size) + scratch = BufferUtils.createByteBuffer(size); + final ByteBuffer buffer = scratch; + buffer.clear(); for (byte aGlyph : glyph) { int c = ((int) aGlyph) & 0xFF; // Grab all bits by grabbing the leftmost one then shifting. @@ -108,7 +118,7 @@ public ByteBuffer getGlyph(int charCode) { c <<= 1; } } - buffer.rewind(); + buffer.flip(); return buffer; } diff --git a/src/main/scala/li/cil/oc/client/renderer/font/IGlyphProvider.java b/src/main/scala/li/cil/oc/client/renderer/font/IGlyphProvider.java index 328ee895f6..7d0719c359 100644 --- a/src/main/scala/li/cil/oc/client/renderer/font/IGlyphProvider.java +++ b/src/main/scala/li/cil/oc/client/renderer/font/IGlyphProvider.java @@ -17,10 +17,8 @@ public interface IGlyphProvider { /** * Get a byte array of RGBA data describing the specified char. *
- * This is only called once for each char per resource reload cycle (i.e. - * it may called multiple times, but only if {@link #initialize()} was - * called in-between). This means implementations may be relatively - * inefficient (be reasonable) in generating the RGBA data. + * The returned buffer may be reused by the next call, so consume it before + * calling again. *
* The returned buffer is expected to be of a format so that it can be * directly passed on to glTexSubImage2D, meaning a byte array @@ -34,6 +32,8 @@ public interface IGlyphProvider { */ ByteBuffer getGlyph(int charCode); + boolean hasGlyph(int charCode); + /** * Get the single-width glyph width for this provider, in pixels. *
diff --git a/src/main/scala/li/cil/oc/client/renderer/font/StaticFontRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/font/StaticFontRenderer.scala deleted file mode 100644 index 6185bb59fb..0000000000 --- a/src/main/scala/li/cil/oc/client/renderer/font/StaticFontRenderer.scala +++ /dev/null @@ -1,70 +0,0 @@ -package li.cil.oc.client.renderer.font - -import com.google.common.base.Charsets -import li.cil.oc.OpenComputers -import li.cil.oc.Settings -import li.cil.oc.client.Textures -import net.minecraft.client.Minecraft -import net.minecraft.client.renderer.Tessellator -import net.minecraft.util.ResourceLocation - -import scala.io.Source - -/** - * Font renderer using a user specified texture file, meaning the list of - * supported characters is fixed. But at least this one works. - */ -class StaticFontRenderer extends TextureFontRenderer { - protected val (chars, charWidth, charHeight) = try { - val lines = Source.fromInputStream(Minecraft.getMinecraft.getResourceManager.getResource(new ResourceLocation(Settings.resourceDomain, "textures/font/chars.txt")).getInputStream)(Charsets.UTF_8).getLines() - val chars = lines.next() - val (w, h) = if (lines.hasNext) { - val size = lines.next().split(" ", 2) - (size(0).toInt, size(1).toInt) - } else (10, 18) - (chars, w, h) - } - catch { - case t: Throwable => - OpenComputers.log.warn("Failed reading font metadata, using defaults.", t) - (basicChars, 10, 18) - } - - private val cols = 256 / charWidth - private val uStep = charWidth / 256.0 - private val uSize = uStep - private val vStep = (charHeight + 1) / 256.0 - private val vSize = charHeight / 256.0 - private val s = Settings.get.fontCharScale - private val dw = charWidth * s - charWidth - private val dh = charHeight * s - charHeight - - override protected def textureCount = 1 - - override protected def bindTexture(index: Int) { - if (Settings.get.textAntiAlias) { - Minecraft.getMinecraft.getTextureManager.bindTexture(Textures.fontAntiAliased) - } - else { - Minecraft.getMinecraft.getTextureManager.bindTexture(Textures.fontAliased) - } - } - - override protected def drawChar(tx: Float, ty: Float, char: Int) { - val index = 1 + (chars.indexOf(char) match { - case -1 => chars.indexOf('?') - case i => i - }) - val x = (index - 1) % cols - val y = (index - 1) / cols - val u = x * uStep - val v = y * vStep - val t = Tessellator.instance - t.addVertexWithUV(tx - dw, ty + charHeight * s, 0, u, v + vSize) - t.addVertexWithUV(tx + charWidth * s, ty + charHeight * s, 0, u + uSize, v + vSize) - t.addVertexWithUV(tx + charWidth * s, ty - dh, 0, u + uSize, v) - t.addVertexWithUV(tx - dw, ty - dh, 0, u, v) - } - - override protected def generateChar(char: Int) {} -} diff --git a/src/main/scala/li/cil/oc/client/renderer/font/TextBufferRenderData.scala b/src/main/scala/li/cil/oc/client/renderer/font/TextBufferRenderData.scala deleted file mode 100644 index 210b27cf4c..0000000000 --- a/src/main/scala/li/cil/oc/client/renderer/font/TextBufferRenderData.scala +++ /dev/null @@ -1,13 +0,0 @@ -package li.cil.oc.client.renderer.font - -import li.cil.oc.util.TextBuffer - -trait TextBufferRenderData { - def dirty: Boolean - - def dirty_=(value: Boolean): Unit - - def data: TextBuffer - - def viewport: (Int, Int) -} diff --git a/src/main/scala/li/cil/oc/client/renderer/font/TextureFontRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/font/TextureFontRenderer.scala deleted file mode 100644 index a04d10c2e1..0000000000 --- a/src/main/scala/li/cil/oc/client/renderer/font/TextureFontRenderer.scala +++ /dev/null @@ -1,181 +0,0 @@ -package li.cil.oc.client.renderer.font - -import li.cil.oc.Settings -import li.cil.oc.util.{ExtendedUnicodeHelper, PackedColor, RenderState, TextBuffer} -import net.minecraft.client.renderer.Tessellator -import org.lwjgl.opengl.GL11 - -/** - * Base class for texture based font rendering. - * - * Provides common logic for the static one (using an existing texture) and the - * dynamic one (generating textures on the fly from a font). - */ -abstract class TextureFontRenderer { - protected final val basicChars = """☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼ !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~⌂ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■""" - - def charRenderWidth = charWidth / 2 - - def charRenderHeight = charHeight / 2 - - /** - * If drawString() is called inside display lists this should be called - * beforehand, outside the display list, to ensure no characters have to - * be generated inside the draw call. - */ - def generateChars(chars: Array[Char]) { - for (char <- chars) { - generateChar(char) - } - } - - def generateChars(chars: Array[Int]) { - for (char <- chars) { - generateChar(char) - } - } - - def drawBuffer(buffer: TextBuffer, viewportWidth: Int, viewportHeight: Int) { - val format = buffer.format - val t = Tessellator.instance - - GL11.glPushMatrix() - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) - - GL11.glScalef(0.5f, 0.5f, 1) - - GL11.glDepthMask(false) - GL11.glDisable(GL11.GL_TEXTURE_2D) - - RenderState.checkError(getClass.getName + ".drawBuffer: configure state") - - // Background first. We try to merge adjacent backgrounds of the same - // color to reduce the number of quads we have to draw. - t.startDrawingQuads() - for (y <- 0 until (viewportHeight min buffer.height)) { - val color = buffer.color(y) - var cbg = 0x000000 - var x = 0 - var width = 0 - var c = 0 - while (c < color.length && x + width < viewportWidth) { - val col = PackedColor.unpackBackground(color(c), format) - if (col != cbg) { - drawQuad(cbg, x, y, width) - cbg = col - x += width - width = 0 - } - width = width + 1 - c += 1 - } - drawQuad(cbg, x, y, width) - } - t.draw() - - RenderState.checkError(getClass.getName + ".drawBuffer: background") - - GL11.glEnable(GL11.GL_TEXTURE_2D) - - if (Settings.get.textLinearFiltering) { - GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR) - } - - // Foreground second. We only have to flush when the color changes, so - // unless every char has a different color this should be quite efficient. - for (y <- 0 until (viewportHeight min buffer.height)) { - val line = buffer.buffer(y) - val color = buffer.color(y) - val ty = y * charHeight - for (i <- 0 until textureCount) { - bindTexture(i) - t.startDrawingQuads() - var cfg = -1 - var tx = 0f - for (n <- 0 until viewportWidth) { - val ch = line(n) - val col = PackedColor.unpackForeground(color(n), format) - // Check if color changed. - if (col != cfg) { - cfg = col - t.setColorOpaque_I(cfg) - } - // Don't render whitespace. - if (ch != ' ') { - drawChar(tx, ty, ch) - } - tx += charWidth - } - t.draw() - } - } - - RenderState.checkError(getClass.getName + ".drawBuffer: foreground") - - GL11.glPopAttrib() - GL11.glPopMatrix() - - RenderState.checkError(getClass.getName + ".drawBuffer: leaving") - } - - def drawString(s: String, x: Int, y: Int): Unit = drawString(s, x, y, 0xFFFFFF) - - def drawString(s: String, x: Int, y: Int, color: Int): Unit = { - val sLength = ExtendedUnicodeHelper.length(s) - val t = Tessellator.instance - - GL11.glPushMatrix() - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) - - GL11.glTranslatef(x, y, 0) - GL11.glScalef(0.5f, 0.5f, 1) - GL11.glDepthMask(false) - GL11.glEnable(GL11.GL_TEXTURE_2D) - - for (i <- 0 until textureCount) { - bindTexture(i) - t.startDrawingQuads() - t.setColorOpaque_I(color) - var tx = 0f - var cx = 0 - for (n <- 0 until sLength) { - val ch = s.codePointAt(cx) - // Don't render whitespace. - if (ch != ' ') { - drawChar(tx, 0, ch) - } - tx += charWidth - cx = s.offsetByCodePoints(cx, 1) - } - t.draw() - } - - GL11.glPopAttrib() - GL11.glPopMatrix() - } - - protected def charWidth: Int - - protected def charHeight: Int - - protected def textureCount: Int - - protected def bindTexture(index: Int): Unit - - protected def generateChar(char: Int): Unit - - protected def drawChar(tx: Float, ty: Float, char: Int): Unit - - private def drawQuad(color: Int, x: Int, y: Int, width: Int) = if (color != 0 && width > 0) { - val t = Tessellator.instance - val x0 = x * charWidth - val x1 = (x + width) * charWidth - val y0 = y * charHeight - val y1 = (y + 1) * charHeight - t.setColorOpaque_I(color) - t.addVertex(x0, y1, 0) - t.addVertex(x1, y1, 0) - t.addVertex(x1, y0, 0) - t.addVertex(x0, y0, 0) - } -} diff --git a/src/main/scala/li/cil/oc/client/renderer/gui/BufferRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/gui/BufferRenderer.scala index ba3dfabfca..cf1eab692e 100644 --- a/src/main/scala/li/cil/oc/client/renderer/gui/BufferRenderer.scala +++ b/src/main/scala/li/cil/oc/client/renderer/gui/BufferRenderer.scala @@ -1,9 +1,12 @@ package li.cil.oc.client.renderer.gui +import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat import li.cil.oc.api import li.cil.oc.client.Textures -import li.cil.oc.util.RenderState -import net.minecraft.client.renderer.GLAllocation +import li.cil.oc.client.renderer.CapturedMesh +import li.cil.oc.client.renderer.shading.Blend +import li.cil.oc.client.renderer.shading.MeshDraw +import net.minecraft.client.renderer.Tessellator import net.minecraft.client.renderer.texture.TextureManager import org.lwjgl.opengl.GL11 @@ -14,78 +17,72 @@ object BufferRenderer { private var textureManager: Option[TextureManager] = None - private var displayLists = 0 + private val background = new CapturedMesh - def init(tm: TextureManager) = this.synchronized(if (textureManager.isEmpty) { - RenderState.checkError(getClass.getName + ".displayLists: entering (aka: wasntme)") + private var backgroundWidth = -1 - textureManager = Some(tm) - displayLists = GLAllocation.generateDisplayLists(2) + private var backgroundHeight = -1 + + private var backgroundForRobot = false - RenderState.checkError(getClass.getName + ".displayLists: leaving") + def init(tm: TextureManager) = this.synchronized(if (textureManager.isEmpty) { + textureManager = Some(tm) Textures.init(tm) }) - def compileBackground(bufferWidth: Int, bufferHeight: Int, forRobot: Boolean = false) = + def drawBackground(bufferWidth: Int, bufferHeight: Int, forRobot: Boolean = false) = if (textureManager.isDefined) { - RenderState.checkError(getClass.getName + ".compileBackground: entering (aka: wasntme)") - val innerWidth = innerMargin * 2 + bufferWidth val innerHeight = innerMargin * 2 + bufferHeight - GL11.glNewList(displayLists, GL11.GL_COMPILE) - - textureManager.get.bindTexture(Textures.guiBorders) - - GL11.glBegin(GL11.GL_QUADS) - - val margin = if (forRobot) 2 else 7 - val (c0, c1, c2, c3) = if (forRobot) (5, 7, 9, 11) else (0, 7, 9, 16) - - // Top border (left corner, middle bar, right corner). - drawBorder( - 0, 0, margin, margin, - c0, c0, c1, c1) - drawBorder( - margin, 0, innerWidth, margin, - c1 + 0.25, c0, c2 - 0.25, c1) - drawBorder( - margin + innerWidth, 0, margin, margin, - c2, c0, c3, c1) - - // Middle area (left bar, screen background, right bar). - drawBorder( - 0, margin, margin, innerHeight, - c0, c1 + 0.25, c1, c2 - 0.25) - drawBorder( - margin, margin, innerWidth, innerHeight, - c1 + 0.25, c1 + 0.25, c2 - 0.25, c2 - 0.25) - drawBorder( - margin + innerWidth, margin, margin, innerHeight, - c2, c1 + 0.25, c3, c2 - 0.25) - - // Bottom border (left corner, middle bar, right corner). - drawBorder( - 0, margin + innerHeight, margin, margin, - c0, c2, c1, c3) - drawBorder( - margin, margin + innerHeight, innerWidth, margin, - c1 + 0.25, c2, c2 - 0.25, c3) - drawBorder( - margin + innerWidth, margin + innerHeight, margin, margin, - c2, c2, c3, c3) - - GL11.glEnd() - - GL11.glEndList() - - RenderState.checkError(getClass.getName + ".compileBackground: leaving") + if (bufferWidth != backgroundWidth || bufferHeight != backgroundHeight || forRobot != backgroundForRobot) { + background.reset() + try emitBackground(background.bucket(DefaultVertexFormat.POSITION_TEXTURE, Textures.guiBorders), innerWidth, innerHeight, forRobot) + finally background.closeBucket() + backgroundWidth = bufferWidth + backgroundHeight = bufferHeight + backgroundForRobot = forRobot + } + MeshDraw.drawUnlit(background, MeshDraw.Identity, Blend.Alpha, 1, 1, 1, 1, inWorld = false) } - def drawBackground() = - if (textureManager.isDefined) { - GL11.glCallList(displayLists) - } + private def emitBackground(t: Tessellator, innerWidth: Int, innerHeight: Int, forRobot: Boolean) { + val margin = if (forRobot) 2 else 7 + val (c0, c1, c2, c3) = if (forRobot) (5, 7, 9, 11) else (0, 7, 9, 16) + + // Top border (left corner, middle bar, right corner). + drawBorder(t, + 0, 0, margin, margin, + c0, c0, c1, c1) + drawBorder(t, + margin, 0, innerWidth, margin, + c1 + 0.25, c0, c2 - 0.25, c1) + drawBorder(t, + margin + innerWidth, 0, margin, margin, + c2, c0, c3, c1) + + // Middle area (left bar, screen background, right bar). + drawBorder(t, + 0, margin, margin, innerHeight, + c0, c1 + 0.25, c1, c2 - 0.25) + drawBorder(t, + margin, margin, innerWidth, innerHeight, + c1 + 0.25, c1 + 0.25, c2 - 0.25, c2 - 0.25) + drawBorder(t, + margin + innerWidth, margin, margin, innerHeight, + c2, c1 + 0.25, c3, c2 - 0.25) + + // Bottom border (left corner, middle bar, right corner). + drawBorder(t, + 0, margin + innerHeight, margin, margin, + c0, c2, c1, c3) + drawBorder(t, + margin, margin + innerHeight, innerWidth, margin, + c1 + 0.25, c2, c2 - 0.25, c3) + drawBorder(t, + margin + innerWidth, margin + innerHeight, margin, margin, + c2, c2, c3, c3) + } def drawText(screen: api.internal.TextBuffer) = if (textureManager.isDefined) { @@ -97,18 +94,14 @@ object BufferRenderer { } else false - private def drawBorder(x: Double, y: Double, w: Double, h: Double, u1: Double, v1: Double, u2: Double, v2: Double) = { + private def drawBorder(t: Tessellator, x: Double, y: Double, w: Double, h: Double, u1: Double, v1: Double, u2: Double, v2: Double) = { val u1d = u1 / 16.0 val u2d = u2 / 16.0 val v1d = v1 / 16.0 val v2d = v2 / 16.0 - GL11.glTexCoord2d(u1d, v2d) - GL11.glVertex3d(x, y + h, 0) - GL11.glTexCoord2d(u2d, v2d) - GL11.glVertex3d(x + w, y + h, 0) - GL11.glTexCoord2d(u2d, v1d) - GL11.glVertex3d(x + w, y, 0) - GL11.glTexCoord2d(u1d, v1d) - GL11.glVertex3d(x, y, 0) + t.addVertexWithUV(x, y + h, 0, u1d, v2d) + t.addVertexWithUV(x + w, y + h, 0, u2d, v2d) + t.addVertexWithUV(x + w, y, 0, u2d, v1d) + t.addVertexWithUV(x, y, 0, u1d, v1d) } } diff --git a/src/main/scala/li/cil/oc/client/renderer/item/ItemRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/item/ItemRenderer.scala index 99c358e49e..0ea7715e0a 100644 --- a/src/main/scala/li/cil/oc/client/renderer/item/ItemRenderer.scala +++ b/src/main/scala/li/cil/oc/client/renderer/item/ItemRenderer.scala @@ -59,7 +59,7 @@ object ItemRenderer extends IItemRenderer { else helper == ItemRendererHelper.EQUIPPED_BLOCK override def renderItem(renderType: ItemRenderType, stack: ItemStack, data: AnyRef*) { - RenderState.checkError(getClass.getName + ".renderItem: entering (aka: wasntme)") + RenderState.checkError(this, ".renderItem: entering (aka: wasntme)") val mc = Minecraft.getMinecraft val tm = mc.getTextureManager @@ -89,7 +89,7 @@ object ItemRenderer extends IItemRenderer { } GL11.glPopAttrib() - RenderState.checkError("ItemRenderer.renderItem: floppy") + RenderState.checkError(this, ".renderItem: floppy") } else if (descriptor == drone) { @@ -117,7 +117,7 @@ object ItemRenderer extends IItemRenderer { GL11.glPopMatrix() GL11.glPopAttrib() - RenderState.checkError("ItemRenderer.renderItem: drone") + RenderState.checkError(this, ".renderItem: drone") } else if (descriptor == print) { @@ -145,7 +145,7 @@ object ItemRenderer extends IItemRenderer { GL11.glPopMatrix() GL11.glPopAttrib() - RenderState.checkError("ItemRenderer.renderItem: print") + RenderState.checkError(this, ".renderItem: print") } else if (descriptor == cable) { @@ -167,10 +167,10 @@ object ItemRenderer extends IItemRenderer { GL11.glPopMatrix() GL11.glPopAttrib() - RenderState.checkError("ItemRenderer.renderItem: print") + RenderState.checkError(this, ".renderItem: print") } - RenderState.checkError("ItemRenderer.renderItem: leaving") + RenderState.checkError(this, ".renderItem: leaving") } private def drawShape(shape: PrintData.Shape) { @@ -189,76 +189,52 @@ object ItemRenderer extends IItemRenderer { GL11.glColor3ub(r, g, b) }) - GL11.glBegin(GL11.GL_QUADS) - GL11.glDisable(GL11.GL_CULL_FACE) + val t = Tessellator.instance + t.startDrawingQuads() // Front. // GL11.glNormal3f(0, 0, 1) - GL11.glTexCoord2f(texture.getInterpolatedU(bounds.minX * 16), texture.getInterpolatedV(16 - bounds.minY * 16)) - GL11.glVertex3d(bounds.minX, bounds.minY, bounds.maxZ) - GL11.glTexCoord2f(texture.getInterpolatedU(bounds.maxX * 16), texture.getInterpolatedV(16 - bounds.minY * 16)) - GL11.glVertex3d(bounds.maxX, bounds.minY, bounds.maxZ) - GL11.glTexCoord2f(texture.getInterpolatedU(bounds.maxX * 16), texture.getInterpolatedV(16 - bounds.maxY * 16)) - GL11.glVertex3d(bounds.maxX, bounds.maxY, bounds.maxZ) - GL11.glTexCoord2f(texture.getInterpolatedU(bounds.minX * 16), texture.getInterpolatedV(16 - bounds.maxY * 16)) - GL11.glVertex3d(bounds.minX, bounds.maxY, bounds.maxZ) + t.addVertexWithUV(bounds.minX, bounds.minY, bounds.maxZ, texture.getInterpolatedU(bounds.minX * 16), texture.getInterpolatedV(16 - bounds.minY * 16)) + t.addVertexWithUV(bounds.maxX, bounds.minY, bounds.maxZ, texture.getInterpolatedU(bounds.maxX * 16), texture.getInterpolatedV(16 - bounds.minY * 16)) + t.addVertexWithUV(bounds.maxX, bounds.maxY, bounds.maxZ, texture.getInterpolatedU(bounds.maxX * 16), texture.getInterpolatedV(16 - bounds.maxY * 16)) + t.addVertexWithUV(bounds.minX, bounds.maxY, bounds.maxZ, texture.getInterpolatedU(bounds.minX * 16), texture.getInterpolatedV(16 - bounds.maxY * 16)) // Back. // GL11.glNormal3f(0, 0, -1) - GL11.glTexCoord2f(texture.getInterpolatedU(bounds.maxX * 16), texture.getInterpolatedV(16 - bounds.minY * 16)) - GL11.glVertex3d(bounds.maxX, bounds.minY, bounds.minZ) - GL11.glTexCoord2f(texture.getInterpolatedU(bounds.minX * 16), texture.getInterpolatedV(16 - bounds.minY * 16)) - GL11.glVertex3d(bounds.minX, bounds.minY, bounds.minZ) - GL11.glTexCoord2f(texture.getInterpolatedU(bounds.minX * 16), texture.getInterpolatedV(16 - bounds.maxY * 16)) - GL11.glVertex3d(bounds.minX, bounds.maxY, bounds.minZ) - GL11.glTexCoord2f(texture.getInterpolatedU(bounds.maxX * 16), texture.getInterpolatedV(16 - bounds.maxY * 16)) - GL11.glVertex3d(bounds.maxX, bounds.maxY, bounds.minZ) + t.addVertexWithUV(bounds.maxX, bounds.minY, bounds.minZ, texture.getInterpolatedU(bounds.maxX * 16), texture.getInterpolatedV(16 - bounds.minY * 16)) + t.addVertexWithUV(bounds.minX, bounds.minY, bounds.minZ, texture.getInterpolatedU(bounds.minX * 16), texture.getInterpolatedV(16 - bounds.minY * 16)) + t.addVertexWithUV(bounds.minX, bounds.maxY, bounds.minZ, texture.getInterpolatedU(bounds.minX * 16), texture.getInterpolatedV(16 - bounds.maxY * 16)) + t.addVertexWithUV(bounds.maxX, bounds.maxY, bounds.minZ, texture.getInterpolatedU(bounds.maxX * 16), texture.getInterpolatedV(16 - bounds.maxY * 16)) // Top. // GL11.glNormal3f(0, 1, 0) - GL11.glTexCoord2f(texture.getInterpolatedU(bounds.maxX * 16), texture.getInterpolatedV(bounds.maxZ * 16)) - GL11.glVertex3d(bounds.maxX, bounds.maxY, bounds.maxZ) - GL11.glTexCoord2f(texture.getInterpolatedU(bounds.maxX * 16), texture.getInterpolatedV(bounds.minZ * 16)) - GL11.glVertex3d(bounds.maxX, bounds.maxY, bounds.minZ) - GL11.glTexCoord2f(texture.getInterpolatedU(bounds.minX * 16), texture.getInterpolatedV(bounds.minZ * 16)) - GL11.glVertex3d(bounds.minX, bounds.maxY, bounds.minZ) - GL11.glTexCoord2f(texture.getInterpolatedU(bounds.minX * 16), texture.getInterpolatedV(bounds.maxZ * 16)) - GL11.glVertex3d(bounds.minX, bounds.maxY, bounds.maxZ) + t.addVertexWithUV(bounds.maxX, bounds.maxY, bounds.maxZ, texture.getInterpolatedU(bounds.maxX * 16), texture.getInterpolatedV(bounds.maxZ * 16)) + t.addVertexWithUV(bounds.maxX, bounds.maxY, bounds.minZ, texture.getInterpolatedU(bounds.maxX * 16), texture.getInterpolatedV(bounds.minZ * 16)) + t.addVertexWithUV(bounds.minX, bounds.maxY, bounds.minZ, texture.getInterpolatedU(bounds.minX * 16), texture.getInterpolatedV(bounds.minZ * 16)) + t.addVertexWithUV(bounds.minX, bounds.maxY, bounds.maxZ, texture.getInterpolatedU(bounds.minX * 16), texture.getInterpolatedV(bounds.maxZ * 16)) // Bottom. // GL11.glNormal3f(0, -1, 0) - GL11.glTexCoord2f(texture.getInterpolatedU(bounds.minX * 16), texture.getInterpolatedV(bounds.maxZ * 16)) - GL11.glVertex3d(bounds.minX, bounds.minY, bounds.maxZ) - GL11.glTexCoord2f(texture.getInterpolatedU(bounds.minX * 16), texture.getInterpolatedV(bounds.minZ * 16)) - GL11.glVertex3d(bounds.minX, bounds.minY, bounds.minZ) - GL11.glTexCoord2f(texture.getInterpolatedU(bounds.maxX * 16), texture.getInterpolatedV(bounds.minZ * 16)) - GL11.glVertex3d(bounds.maxX, bounds.minY, bounds.minZ) - GL11.glTexCoord2f(texture.getInterpolatedU(bounds.maxX * 16), texture.getInterpolatedV(bounds.maxZ * 16)) - GL11.glVertex3d(bounds.maxX, bounds.minY, bounds.maxZ) + t.addVertexWithUV(bounds.minX, bounds.minY, bounds.maxZ, texture.getInterpolatedU(bounds.minX * 16), texture.getInterpolatedV(bounds.maxZ * 16)) + t.addVertexWithUV(bounds.minX, bounds.minY, bounds.minZ, texture.getInterpolatedU(bounds.minX * 16), texture.getInterpolatedV(bounds.minZ * 16)) + t.addVertexWithUV(bounds.maxX, bounds.minY, bounds.minZ, texture.getInterpolatedU(bounds.maxX * 16), texture.getInterpolatedV(bounds.minZ * 16)) + t.addVertexWithUV(bounds.maxX, bounds.minY, bounds.maxZ, texture.getInterpolatedU(bounds.maxX * 16), texture.getInterpolatedV(bounds.maxZ * 16)) // Left. // GL11.glNormal3f(1, 0, 0) - GL11.glTexCoord2f(texture.getInterpolatedU(bounds.maxZ * 16), texture.getInterpolatedV(16 - bounds.maxY * 16)) - GL11.glVertex3d(bounds.maxX, bounds.maxY, bounds.maxZ) - GL11.glTexCoord2f(texture.getInterpolatedU(bounds.maxZ * 16), texture.getInterpolatedV(16 - bounds.minY * 16)) - GL11.glVertex3d(bounds.maxX, bounds.minY, bounds.maxZ) - GL11.glTexCoord2f(texture.getInterpolatedU(bounds.minZ * 16), texture.getInterpolatedV(16 - bounds.minY * 16)) - GL11.glVertex3d(bounds.maxX, bounds.minY, bounds.minZ) - GL11.glTexCoord2f(texture.getInterpolatedU(bounds.minZ * 16), texture.getInterpolatedV(16 - bounds.maxY * 16)) - GL11.glVertex3d(bounds.maxX, bounds.maxY, bounds.minZ) + t.addVertexWithUV(bounds.maxX, bounds.maxY, bounds.maxZ, texture.getInterpolatedU(bounds.maxZ * 16), texture.getInterpolatedV(16 - bounds.maxY * 16)) + t.addVertexWithUV(bounds.maxX, bounds.minY, bounds.maxZ, texture.getInterpolatedU(bounds.maxZ * 16), texture.getInterpolatedV(16 - bounds.minY * 16)) + t.addVertexWithUV(bounds.maxX, bounds.minY, bounds.minZ, texture.getInterpolatedU(bounds.minZ * 16), texture.getInterpolatedV(16 - bounds.minY * 16)) + t.addVertexWithUV(bounds.maxX, bounds.maxY, bounds.minZ, texture.getInterpolatedU(bounds.minZ * 16), texture.getInterpolatedV(16 - bounds.maxY * 16)) // Right. // GL11.glNormal3f(-1, 0, 0) - GL11.glTexCoord2f(texture.getInterpolatedU(bounds.maxZ * 16), texture.getInterpolatedV(16 - bounds.minY * 16)) - GL11.glVertex3d(bounds.minX, bounds.minY, bounds.maxZ) - GL11.glTexCoord2f(texture.getInterpolatedU(bounds.maxZ * 16), texture.getInterpolatedV(16 - bounds.maxY * 16)) - GL11.glVertex3d(bounds.minX, bounds.maxY, bounds.maxZ) - GL11.glTexCoord2f(texture.getInterpolatedU(bounds.minZ * 16), texture.getInterpolatedV(16 - bounds.maxY * 16)) - GL11.glVertex3d(bounds.minX, bounds.maxY, bounds.minZ) - GL11.glTexCoord2f(texture.getInterpolatedU(bounds.minZ * 16), texture.getInterpolatedV(16 - bounds.minY * 16)) - GL11.glVertex3d(bounds.minX, bounds.minY, bounds.minZ) - - GL11.glEnd() + t.addVertexWithUV(bounds.minX, bounds.minY, bounds.maxZ, texture.getInterpolatedU(bounds.maxZ * 16), texture.getInterpolatedV(16 - bounds.minY * 16)) + t.addVertexWithUV(bounds.minX, bounds.maxY, bounds.maxZ, texture.getInterpolatedU(bounds.maxZ * 16), texture.getInterpolatedV(16 - bounds.maxY * 16)) + t.addVertexWithUV(bounds.minX, bounds.maxY, bounds.minZ, texture.getInterpolatedU(bounds.minZ * 16), texture.getInterpolatedV(16 - bounds.maxY * 16)) + t.addVertexWithUV(bounds.minX, bounds.minY, bounds.minZ, texture.getInterpolatedU(bounds.minZ * 16), texture.getInterpolatedV(16 - bounds.minY * 16)) + + t.draw() GL11.glEnable(GL11.GL_CULL_FACE) GL11.glColor3f(1, 1, 1) diff --git a/src/main/scala/li/cil/oc/client/renderer/item/UpgradeRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/item/UpgradeRenderer.scala index 1b44fa01e7..d5b2ee4dff 100644 --- a/src/main/scala/li/cil/oc/client/renderer/item/UpgradeRenderer.scala +++ b/src/main/scala/li/cil/oc/client/renderer/item/UpgradeRenderer.scala @@ -1,13 +1,15 @@ package li.cil.oc.client.renderer.item +import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat import li.cil.oc.Constants import li.cil.oc.api import li.cil.oc.api.driver.item.UpgradeRenderer.MountPointName import li.cil.oc.api.event.RobotRenderEvent.MountPoint import li.cil.oc.client.Textures +import li.cil.oc.client.renderer.CapturedMesh import li.cil.oc.integration.opencomputers.Item import li.cil.oc.util.RenderState -import net.minecraft.client.Minecraft +import net.minecraft.client.renderer.Tessellator import net.minecraft.item.ItemStack import net.minecraft.util.AxisAlignedBB import org.lwjgl.opengl.GL11 @@ -38,90 +40,73 @@ object UpgradeRenderer { val descriptor = api.Items.get(stack) if (descriptor == api.Items.get(Constants.ItemName.CraftingUpgrade)) { - Minecraft.getMinecraft.getTextureManager.bindTexture(Textures.upgradeCrafting) - drawSimpleBlock(mountPoint) + drawSimpleBlock(mountPoint, Crafting) - RenderState.checkError(getClass.getName + ".renderItem: crafting upgrade") + RenderState.checkError(this, ".renderItem: crafting upgrade") } else if (descriptor == api.Items.get(Constants.ItemName.GeneratorUpgrade)) { - Minecraft.getMinecraft.getTextureManager.bindTexture(Textures.upgradeGenerator) - drawSimpleBlock(mountPoint, if (Item.dataTag(stack).getInteger("remainingTicks") > 0) 0.5f else 0) + drawSimpleBlock(mountPoint, if (Item.dataTag(stack).getInteger("remainingTicks") > 0) GeneratorOn else GeneratorOff) - RenderState.checkError(getClass.getName + ".renderItem: generator upgrade") + RenderState.checkError(this, ".renderItem: generator upgrade") } else if (descriptor == api.Items.get(Constants.ItemName.InventoryUpgrade)) { - Minecraft.getMinecraft.getTextureManager.bindTexture(Textures.upgradeInventory) - drawSimpleBlock(mountPoint) + drawSimpleBlock(mountPoint, Inventory) - RenderState.checkError(getClass.getName + ".renderItem: inventory upgrade") + RenderState.checkError(this, ".renderItem: inventory upgrade") } } + private final val Crafting = 0 + private final val GeneratorOff = 1 + private final val GeneratorOn = 2 + private final val Inventory = 3 + + private val textures = Array(Textures.upgradeCrafting, Textures.upgradeGenerator, Textures.upgradeGenerator, Textures.upgradeInventory) + + private val frontOffsets = Array(0f, 0f, 0.5f, 0f) + + private val meshes = new Array[CapturedMesh](textures.length) + private val bounds = AxisAlignedBB.getBoundingBox(-0.1, -0.1, -0.1, 0.1, 0.1, 0.1) - private def drawSimpleBlock(mountPoint: MountPoint, frontOffset: Float = 0) { + private def drawSimpleBlock(mountPoint: MountPoint, variant: Int) { GL11.glRotatef(mountPoint.rotation.getW, mountPoint.rotation.getX, mountPoint.rotation.getY, mountPoint.rotation.getZ) GL11.glTranslatef(mountPoint.offset.getX, mountPoint.offset.getY, mountPoint.offset.getZ) - GL11.glBegin(GL11.GL_QUADS) - - // Front. - // GL11.glNormal3f(0, 0, 1) - GL11.glTexCoord2f(frontOffset, 0.5f) - GL11.glVertex3d(bounds.minX, bounds.minY, bounds.maxZ) - GL11.glTexCoord2f(frontOffset + 0.5f, 0.5f) - GL11.glVertex3d(bounds.maxX, bounds.minY, bounds.maxZ) - GL11.glTexCoord2f(frontOffset + 0.5f, 0) - GL11.glVertex3d(bounds.maxX, bounds.maxY, bounds.maxZ) - GL11.glTexCoord2f(frontOffset, 0) - GL11.glVertex3d(bounds.minX, bounds.maxY, bounds.maxZ) - - // Top. - // GL11.glNormal3f(0, 1, 0) - GL11.glTexCoord2f(1, 0.5f) - GL11.glVertex3d(bounds.maxX, bounds.maxY, bounds.maxZ) - GL11.glTexCoord2f(1, 1) - GL11.glVertex3d(bounds.maxX, bounds.maxY, bounds.minZ) - GL11.glTexCoord2f(0.5f, 1) - GL11.glVertex3d(bounds.minX, bounds.maxY, bounds.minZ) - GL11.glTexCoord2f(0.5f, 0.5f) - GL11.glVertex3d(bounds.minX, bounds.maxY, bounds.maxZ) - - // Bottom. - // GL11.glNormal3f(0, -1, 0) - GL11.glTexCoord2f(0.5f, 0.5f) - GL11.glVertex3d(bounds.minX, bounds.minY, bounds.maxZ) - GL11.glTexCoord2f(0.5f, 1) - GL11.glVertex3d(bounds.minX, bounds.minY, bounds.minZ) - GL11.glTexCoord2f(1, 1) - GL11.glVertex3d(bounds.maxX, bounds.minY, bounds.minZ) - GL11.glTexCoord2f(1, 0.5f) - GL11.glVertex3d(bounds.maxX, bounds.minY, bounds.maxZ) - - // Left. - // GL11.glNormal3f(1, 0, 0) - GL11.glTexCoord2f(0, 0.5f) - GL11.glVertex3d(bounds.maxX, bounds.maxY, bounds.maxZ) - GL11.glTexCoord2f(0, 1) - GL11.glVertex3d(bounds.maxX, bounds.minY, bounds.maxZ) - GL11.glTexCoord2f(0.5f, 1) - GL11.glVertex3d(bounds.maxX, bounds.minY, bounds.minZ) - GL11.glTexCoord2f(0.5f, 0.5f) - GL11.glVertex3d(bounds.maxX, bounds.maxY, bounds.minZ) - - // Right. - // GL11.glNormal3f(-1, 0, 0) - GL11.glTexCoord2f(0, 1) - GL11.glVertex3d(bounds.minX, bounds.minY, bounds.maxZ) - GL11.glTexCoord2f(0, 0.5f) - GL11.glVertex3d(bounds.minX, bounds.maxY, bounds.maxZ) - GL11.glTexCoord2f(0.5f, 0.5f) - GL11.glVertex3d(bounds.minX, bounds.maxY, bounds.minZ) - GL11.glTexCoord2f(0.5f, 1) - GL11.glVertex3d(bounds.minX, bounds.minY, bounds.minZ) - - GL11.glEnd() + var mesh = meshes(variant) + if (mesh == null) { + mesh = CapturedMesh.build(DefaultVertexFormat.POSITION_TEXTURE, textures(variant))(emitSimpleBlock(_, frontOffsets(variant))) + meshes(variant) = mesh + } + mesh.draw() + } + + private def emitSimpleBlock(t: Tessellator, frontOffset: Float) { + t.addVertexWithUV(bounds.minX, bounds.minY, bounds.maxZ, frontOffset, 0.5f) + t.addVertexWithUV(bounds.maxX, bounds.minY, bounds.maxZ, frontOffset + 0.5f, 0.5f) + t.addVertexWithUV(bounds.maxX, bounds.maxY, bounds.maxZ, frontOffset + 0.5f, 0) + t.addVertexWithUV(bounds.minX, bounds.maxY, bounds.maxZ, frontOffset, 0) + + t.addVertexWithUV(bounds.maxX, bounds.maxY, bounds.maxZ, 1, 0.5f) + t.addVertexWithUV(bounds.maxX, bounds.maxY, bounds.minZ, 1, 1) + t.addVertexWithUV(bounds.minX, bounds.maxY, bounds.minZ, 0.5f, 1) + t.addVertexWithUV(bounds.minX, bounds.maxY, bounds.maxZ, 0.5f, 0.5f) + + t.addVertexWithUV(bounds.minX, bounds.minY, bounds.maxZ, 0.5f, 0.5f) + t.addVertexWithUV(bounds.minX, bounds.minY, bounds.minZ, 0.5f, 1) + t.addVertexWithUV(bounds.maxX, bounds.minY, bounds.minZ, 1, 1) + t.addVertexWithUV(bounds.maxX, bounds.minY, bounds.maxZ, 1, 0.5f) + + t.addVertexWithUV(bounds.maxX, bounds.maxY, bounds.maxZ, 0, 0.5f) + t.addVertexWithUV(bounds.maxX, bounds.minY, bounds.maxZ, 0, 1) + t.addVertexWithUV(bounds.maxX, bounds.minY, bounds.minZ, 0.5f, 1) + t.addVertexWithUV(bounds.maxX, bounds.maxY, bounds.minZ, 0.5f, 0.5f) + + t.addVertexWithUV(bounds.minX, bounds.minY, bounds.maxZ, 0, 1) + t.addVertexWithUV(bounds.minX, bounds.maxY, bounds.maxZ, 0, 0.5f) + t.addVertexWithUV(bounds.minX, bounds.maxY, bounds.minZ, 0.5f, 0.5f) + t.addVertexWithUV(bounds.minX, bounds.minY, bounds.minZ, 0.5f, 1) } } diff --git a/src/main/scala/li/cil/oc/client/renderer/markdown/Document.scala b/src/main/scala/li/cil/oc/client/renderer/markdown/Document.scala index efd1481299..32980a9396 100644 --- a/src/main/scala/li/cil/oc/client/renderer/markdown/Document.scala +++ b/src/main/scala/li/cil/oc/client/renderer/markdown/Document.scala @@ -4,6 +4,7 @@ import li.cil.oc.api import li.cil.oc.client.renderer.markdown.segment.InteractiveSegment import li.cil.oc.client.renderer.markdown.segment.Segment import net.minecraft.client.Minecraft +import net.minecraft.client.renderer.Tessellator import net.minecraft.client.gui.FontRenderer import org.lwjgl.opengl.GL11 @@ -86,16 +87,17 @@ object Document { GL11.glPushMatrix() GL11.glTranslatef(0, 0, 300) - GL11.glBegin(GL11.GL_QUADS) - GL11.glVertex2f(0, y) - GL11.glVertex2f(mc.displayWidth, y) - GL11.glVertex2f(mc.displayWidth, 0) - GL11.glVertex2f(0, 0) - GL11.glVertex2f(0, mc.displayHeight) - GL11.glVertex2f(mc.displayWidth, mc.displayHeight) - GL11.glVertex2f(mc.displayWidth, y + maxHeight) - GL11.glVertex2f(0, y + maxHeight) - GL11.glEnd() + val t = Tessellator.instance + t.startDrawingQuads() + t.addVertex(0, y, 0) + t.addVertex(mc.displayWidth, y, 0) + t.addVertex(mc.displayWidth, 0, 0) + t.addVertex(0, 0, 0) + t.addVertex(0, mc.displayHeight, 0) + t.addVertex(mc.displayWidth, mc.displayHeight, 0) + t.addVertex(mc.displayWidth, y + maxHeight, 0) + t.addVertex(0, y + maxHeight, 0) + t.draw() GL11.glPopMatrix() GL11.glColorMask(true, true, true, true) diff --git a/src/main/scala/li/cil/oc/client/renderer/markdown/segment/RenderSegment.scala b/src/main/scala/li/cil/oc/client/renderer/markdown/segment/RenderSegment.scala index d635e98d59..e4e5853740 100644 --- a/src/main/scala/li/cil/oc/client/renderer/markdown/segment/RenderSegment.scala +++ b/src/main/scala/li/cil/oc/client/renderer/markdown/segment/RenderSegment.scala @@ -5,6 +5,7 @@ import li.cil.oc.api.manual.InteractiveImageRenderer import li.cil.oc.client.renderer.markdown.Document import li.cil.oc.client.renderer.markdown.MarkupFormat import net.minecraft.client.gui.FontRenderer +import net.minecraft.client.renderer.Tessellator import org.lwjgl.opengl.GL11 private[markdown] class RenderSegment(val parent: Segment, val title: String, val imageRenderer: ImageRenderer) extends InteractiveSegment { @@ -53,12 +54,13 @@ private[markdown] class RenderSegment(val parent: Segment, val title: String, va if (hovered.isDefined) { GL11.glColor4f(1, 1, 1, 0.15f) GL11.glDisable(GL11.GL_TEXTURE_2D) - GL11.glBegin(GL11.GL_QUADS) - GL11.glVertex2f(0, 0) - GL11.glVertex2f(0, imageRenderer.getHeight) - GL11.glVertex2f(imageRenderer.getWidth, imageRenderer.getHeight) - GL11.glVertex2f(imageRenderer.getWidth, 0) - GL11.glEnd() + val t = Tessellator.instance + t.startDrawingQuads() + t.addVertex(0, 0, 0) + t.addVertex(0, imageRenderer.getHeight, 0) + t.addVertex(imageRenderer.getWidth, imageRenderer.getHeight, 0) + t.addVertex(imageRenderer.getWidth, 0, 0) + t.draw() GL11.glEnable(GL11.GL_TEXTURE_2D) } diff --git a/src/main/scala/li/cil/oc/client/renderer/markdown/segment/render/TextureImageRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/markdown/segment/render/TextureImageRenderer.scala index 77b1e0705f..a0da1fd808 100644 --- a/src/main/scala/li/cil/oc/client/renderer/markdown/segment/render/TextureImageRenderer.scala +++ b/src/main/scala/li/cil/oc/client/renderer/markdown/segment/render/TextureImageRenderer.scala @@ -5,6 +5,7 @@ import javax.imageio.ImageIO import li.cil.oc.api.manual.ImageRenderer import net.minecraft.client.Minecraft +import net.minecraft.client.renderer.Tessellator import net.minecraft.client.renderer.texture.AbstractTexture import net.minecraft.client.renderer.texture.TextureUtil import net.minecraft.client.resources.IResourceManager @@ -33,16 +34,13 @@ class TextureImageRenderer(val location: ResourceLocation) extends ImageRenderer override def render(mouseX: Int, mouseY: Int): Unit = { Minecraft.getMinecraft.getTextureManager.bindTexture(location) GL11.glColor4f(1, 1, 1, 1) - GL11.glBegin(GL11.GL_QUADS) - GL11.glTexCoord2f(0, 0) - GL11.glVertex2f(0, 0) - GL11.glTexCoord2f(0, 1) - GL11.glVertex2f(0, texture.height) - GL11.glTexCoord2f(1, 1) - GL11.glVertex2f(texture.width, texture.height) - GL11.glTexCoord2f(1, 0) - GL11.glVertex2f(texture.width, 0) - GL11.glEnd() + val t = Tessellator.instance + t.startDrawingQuads() + t.addVertexWithUV(0, 0, 0, 0, 0) + t.addVertexWithUV(0, texture.height, 0, 0, 1) + t.addVertexWithUV(texture.width, texture.height, 0, 1, 1) + t.addVertexWithUV(texture.width, 0, 0, 1, 0) + t.draw() } private class ImageTexture(val location: ResourceLocation) extends AbstractTexture { diff --git a/src/main/scala/li/cil/oc/client/renderer/shading/Blend.scala b/src/main/scala/li/cil/oc/client/renderer/shading/Blend.scala new file mode 100644 index 0000000000..dc9dad9eec --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/shading/Blend.scala @@ -0,0 +1,18 @@ +package li.cil.oc.client.renderer.shading + +import org.lwjgl.opengl.GL11 + +object Blend { + final val Opaque = 0 + final val Alpha = 1 + final val Additive = 2 + final val Count = 3 + + def apply(mode: Int) { + if (mode == Opaque) GL11.glDisable(GL11.GL_BLEND) + else { + GL11.glEnable(GL11.GL_BLEND) + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, if (mode == Additive) GL11.GL_ONE else GL11.GL_ONE_MINUS_SRC_ALPHA) + } + } +} diff --git a/src/main/scala/li/cil/oc/client/renderer/shading/FixedFunction.scala b/src/main/scala/li/cil/oc/client/renderer/shading/FixedFunction.scala new file mode 100644 index 0000000000..c051f2510c --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/shading/FixedFunction.scala @@ -0,0 +1,64 @@ +package li.cil.oc.client.renderer.shading + +import li.cil.oc.client.renderer.CapturedMesh +import li.cil.oc.util.RenderState +import net.minecraft.client.renderer.OpenGlHelper +import org.joml.Matrix4fc +import org.lwjgl.BufferUtils +import org.lwjgl.opengl.GL11 +import org.lwjgl.opengl.GL14 + +object FixedFunction { + private val matrix = BufferUtils.createFloatBuffer(16) + + private var lightX = 0f + + private var lightY = 0f + + def multMatrix(model: Matrix4fc) { + matrix.clear() + model.get(matrix) + GL11.glMultMatrix(matrix) + } + + def constantAlphaBlend(alpha: Float) { + GL14.glBlendColor(0, 0, 0, alpha) + GL11.glBlendFunc(GL11.GL_CONSTANT_ALPHA, GL11.GL_ONE) + } + + def drawUnlit(mesh: CapturedMesh, model: Matrix4fc, blendMode: Int, red: Float, green: Float, blue: Float, alpha: Float) { + begin(model, blendMode, red, green, blue, alpha) + mesh.draw() + end() + } + + def drawPremultiplied(mesh: CapturedMesh, texture: Int, model: Matrix4fc, alpha: Float) { + begin(model, Blend.Alpha, alpha, alpha, alpha, alpha) + GL11.glBlendFunc(GL11.GL_ONE, if (alpha < 1) GL11.GL_ONE else GL11.GL_ONE_MINUS_SRC_ALPHA) + GL11.glDepthMask(false) + RenderState.saveTexture0() + GL11.glEnable(GL11.GL_TEXTURE_2D) + GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture) + mesh.drawGeometry() + RenderState.restoreTexture0() + end() + } + + private def begin(model: Matrix4fc, blendMode: Int, red: Float, green: Float, blue: Float, alpha: Float) { + lightX = OpenGlHelper.lastBrightnessX + lightY = OpenGlHelper.lastBrightnessY + GL11.glPushAttrib(RenderState.DrawAttribBits) + GL11.glPushMatrix() + multMatrix(model) + OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 0xF0, 0xF0) + RenderState.disableLighting() + Blend(blendMode) + GL11.glColor4f(red, green, blue, alpha) + } + + private def end() { + OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, lightX, lightY) + GL11.glPopMatrix() + GL11.glPopAttrib() + } +} diff --git a/src/main/scala/li/cil/oc/client/renderer/shading/Fog.scala b/src/main/scala/li/cil/oc/client/renderer/shading/Fog.scala new file mode 100644 index 0000000000..99933ec3a1 --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/shading/Fog.scala @@ -0,0 +1,51 @@ +package li.cil.oc.client.renderer.shading + +import li.cil.oc.client.renderer.glsl.Program +import li.cil.oc.client.renderer.glsl.Uniform +import org.lwjgl.BufferUtils +import org.lwjgl.opengl.GL11 +import org.lwjgl.opengl.GL20 + +object Fog { + private val color = BufferUtils.createFloatBuffer(16) + + private var mode = 0f + + private var start = 0f + + private var end = 0f + + private var density = 0f + + private var stale = true + + def beginFrame() { + stale = true + } + + def upload(shader: Program, fog: Uniform, fogColor: Uniform, inWorld: Boolean) { + if (inWorld) { + if (stale) read() + GL20.glUniform4f(shader.loc(fog), mode, start, end, density) + GL20.glUniform3f(shader.loc(fogColor), color.get(0), color.get(1), color.get(2)) + } + else GL20.glUniform4f(shader.loc(fog), 0, 0, 0, 0) + } + + private def read() { + stale = false + if (!GL11.glIsEnabled(GL11.GL_FOG)) mode = 0 + else { + mode = GL11.glGetInteger(GL11.GL_FOG_MODE) match { + case GL11.GL_LINEAR => 1 + case GL11.GL_EXP => 2 + case _ => 3 + } + start = GL11.glGetFloat(GL11.GL_FOG_START) + end = GL11.glGetFloat(GL11.GL_FOG_END) + density = GL11.glGetFloat(GL11.GL_FOG_DENSITY) + color.clear() + GL11.glGetFloat(GL11.GL_FOG_COLOR, color) + } + } +} diff --git a/src/main/scala/li/cil/oc/client/renderer/shading/MeshDraw.scala b/src/main/scala/li/cil/oc/client/renderer/shading/MeshDraw.scala new file mode 100644 index 0000000000..51c0838541 --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/shading/MeshDraw.scala @@ -0,0 +1,44 @@ +package li.cil.oc.client.renderer.shading + +import li.cil.oc.client.renderer.CapturedMesh +import li.cil.oc.util.RenderState +import net.minecraft.client.renderer.OpenGlHelper +import org.joml.Matrix4f +import org.joml.Matrix4fc +import org.lwjgl.opengl.GL11 +import org.lwjgl.opengl.GL20 + +object MeshDraw { + val Identity: Matrix4fc = new Matrix4f() + + // Leaves the lit program bound; the caller restores the previous program. + def useLit(alpha: Float, vertexColor: Boolean): Boolean = { + val lit = Programs.lit + if (!Programs.usable(lit)) return false + lit.use() + GL20.glUniform4f(lit.loc(Programs.litColor), 1, 1, 1, alpha) + GL20.glUniform2f(lit.loc(Programs.litLightCoord), OpenGlHelper.lastBrightnessX, OpenGlHelper.lastBrightnessY) + GL20.glUniform1f(lit.loc(Programs.litVertexColor), if (vertexColor) 1 else 0) + Fog.upload(lit, Programs.litFog, Programs.litFogColor, inWorld = true) + lit.uploadModel(Identity) + true + } + + def drawUnlit(mesh: CapturedMesh, model: Matrix4fc, blendMode: Int, red: Float, green: Float, blue: Float, alpha: Float, inWorld: Boolean) { + val unlit = Programs.unlit + if (Programs.fixedFunction(inWorld) || !Programs.usable(unlit)) { + FixedFunction.drawUnlit(mesh, model, blendMode, red, green, blue, alpha) + return + } + val program = GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM) + GL11.glPushAttrib(RenderState.DrawAttribBits) + Blend(blendMode) + unlit.use() + GL20.glUniform4f(unlit.loc(Programs.unlitColor), red, green, blue, alpha) + Fog.upload(unlit, Programs.unlitFog, Programs.unlitFogColor, inWorld) + unlit.uploadModel(model) + mesh.draw() + GL11.glPopAttrib() + GL20.glUseProgram(program) + } +} diff --git a/src/main/scala/li/cil/oc/client/renderer/shading/Programs.scala b/src/main/scala/li/cil/oc/client/renderer/shading/Programs.scala new file mode 100644 index 0000000000..1a1e1ae7c5 --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/shading/Programs.scala @@ -0,0 +1,96 @@ +package li.cil.oc.client.renderer.shading + +import li.cil.oc.OpenComputers +import li.cil.oc.Settings +import li.cil.oc.client.renderer.glsl.Program +import li.cil.oc.client.renderer.glsl.ProgramRecipe +import li.cil.oc.client.renderer.text.GlyphAtlas +import li.cil.oc.client.renderer.text.ScreenSheet +import li.cil.oc.client.renderer.text.TextCells +import org.lwjgl.opengl.GLContext + +object Programs { + final val AlphaCutoff = 0.1f + + private val unlitRecipe = ProgramRecipe.of(Settings.resourceDomain, "unlit"). + required("u_Color", "u_Fog", "u_FogColor"). + sampler("u_Texture", 0). + constant("u_AlphaRef", AlphaCutoff). + tinted() + + private[shading] val unlitColor = unlitRecipe.uniform("u_Color") + + private[shading] val unlitFog = unlitRecipe.uniform("u_Fog") + + private[shading] val unlitFogColor = unlitRecipe.uniform("u_FogColor") + + private[shading] var unlit: Program = _ + + private val litRecipe = ProgramRecipe.of(Settings.resourceDomain, "lit"). + required("u_Color", "u_LightCoord", "u_VertexColor", "u_Fog", "u_FogColor"). + sampler("u_Texture", 0). + sampler("u_Lightmap", 1). + constant("u_AlphaRef", AlphaCutoff). + lights() + + private[shading] val litColor = litRecipe.uniform("u_Color") + + private[shading] val litLightCoord = litRecipe.uniform("u_LightCoord") + + private[shading] val litVertexColor = litRecipe.uniform("u_VertexColor") + + private[shading] val litFog = litRecipe.uniform("u_Fog") + + private[shading] val litFogColor = litRecipe.uniform("u_FogColor") + + private[shading] var lit: Program = _ + + private val textRecipe = ProgramRecipe.of(Settings.resourceDomain, "text"). + required("u_Cells", "u_Fade", "u_Smooth", "u_GlyphScale", "u_Fog", "u_FogColor"). + sampler("u_Texture", 0). + sampler("u_Glyphs", 2). + constant("u_AlphaRef", 0). + define("OC_SHEET_WIDTH", ScreenSheet.Width). + define("OC_SHEET_HEIGHT", ScreenSheet.Height). + define("OC_STRIDE", ScreenSheet.Stride). + define("OC_GLYPH_COLUMNS", GlyphAtlas.Columns). + define("OC_GLYPH_SIZE", GlyphAtlas.GlyphSize). + define("OC_RIGHT_HALF", TextCells.RightHalf) + + private[renderer] val textCells = textRecipe.uniform("u_Cells") + + private[renderer] val textFade = textRecipe.uniform("u_Fade") + + private[renderer] val textSmooth = textRecipe.uniform("u_Smooth") + + private[renderer] val textGlyphScale = textRecipe.uniform("u_GlyphScale") + + private[renderer] val textFog = textRecipe.uniform("u_Fog") + + private[renderer] val textFogColor = textRecipe.uniform("u_FogColor") + + private[renderer] var text: Program = _ + + val recipes: Seq[ProgramRecipe] = Seq(unlitRecipe, litRecipe, textRecipe) + + def reload() { + if (unlit != null) unlit.release() + if (lit != null) lit.release() + if (text != null) text.release() + if (!GLContext.getCapabilities.OpenGL20) { + unlit = null + lit = null + text = null + OpenComputers.log.warn("OpenGL 2.0 is unavailable; shaders are disabled and rendering falls back to fixed-function.") + return + } + unlit = unlitRecipe.bake() + lit = litRecipe.bake() + text = textRecipe.bake() + } + + def usable(program: Program): Boolean = program != null && program.isValid + + // Binding one of our programs while a shader pack is active writes into its gbuffers and shadow map. + def fixedFunction(inWorld: Boolean): Boolean = inWorld && ShaderPack.inUse +} diff --git a/src/main/scala/li/cil/oc/client/renderer/shading/ShaderPack.scala b/src/main/scala/li/cil/oc/client/renderer/shading/ShaderPack.scala new file mode 100644 index 0000000000..691a603999 --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/shading/ShaderPack.scala @@ -0,0 +1,32 @@ +package li.cil.oc.client.renderer.shading + +import java.lang.reflect.Method + +object ShaderPack { + private val api: AnyRef = try { + Class.forName("net.irisshaders.iris.api.v0.IrisApi").getMethod("getInstance").invoke(null) + } + catch { + case _: Throwable => null + } + + private val inUseMethod: Method = if (api == null) null else api.getClass.getMethod("isShaderPackInUse") + + private val NoArgs = new Array[AnyRef](0) + + private var stale = true + + private var active = false + + def beginFrame() { + stale = true + } + + def inUse: Boolean = { + if (stale) { + stale = false + active = inUseMethod != null && inUseMethod.invoke(api, NoArgs: _*).asInstanceOf[Boolean] + } + active + } +} diff --git a/src/main/scala/li/cil/oc/client/renderer/text/DirtyRows.scala b/src/main/scala/li/cil/oc/client/renderer/text/DirtyRows.scala new file mode 100644 index 0000000000..5ecd3e5094 --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/text/DirtyRows.scala @@ -0,0 +1,34 @@ +package li.cil.oc.client.renderer.text + +final class DirtyRows { + var from = 0 + + var to = Int.MaxValue + + def isEmpty: Boolean = from >= to + + def mark(first: Int, end: Int) { + val start = math.max(first, 0) + if (end > start) { + if (start < from) from = start + if (end > to) to = end + } + } + + def markAll(): Unit = mark(0, Int.MaxValue) + + def clear() { + from = Int.MaxValue + to = 0 + } + + def set(row: Int, s: String, vertical: Boolean): Unit = + if (vertical && row < 0) mark(0, s.length) + else mark(row, row + (if (vertical) s.length else 1)) + + def fill(row: Int, h: Int): Unit = mark(row, row + h) + + def copy(row: Int, h: Int, ty: Int): Unit = mark(row + ty, math.max(row + ty + h, 1)) + + def bitBlt(row: Int, h: Int): Unit = mark(row - 1, row - 1 + h) +} diff --git a/src/main/scala/li/cil/oc/client/renderer/text/GlyphAtlas.scala b/src/main/scala/li/cil/oc/client/renderer/text/GlyphAtlas.scala new file mode 100644 index 0000000000..e469a4a65f --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/text/GlyphAtlas.scala @@ -0,0 +1,94 @@ +package li.cil.oc.client.renderer.text + +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap + +trait GlyphStore { + def store(texture: Int, char: Int, x: Int, y: Int): Boolean + + def clear(texture: Int, x: Int, y: Int, width: Int): Unit +} + +object GlyphAtlas { + final val Width = 2048 + final val GlyphSize = 16 + final val Columns = Width / GlyphSize + final val InitialHeight = 256 + final val MaxSlots = 1 << 15 + final val ResetCooldown = 100 +} + +final class GlyphAtlas(textures: SheetTextures, store: GlyphStore, maxHeight: Int) extends GlyphSlots { + import GlyphAtlas._ + + private final val Unmapped = -1 + + private val slots = { + val map = new Int2IntOpenHashMap(256, 0.5f) + map.defaultReturnValue(Unmapped) + map + } + + private val heightLimit = math.min(maxHeight, MaxSlots / Columns * GlyphSize) + + var height: Int = math.min(InitialHeight, heightLimit) + + var texture: Int = textures.create(Width, height) + + var generation = 0 + + private var count = 1 + + private var resetPending = false + + private var cooldown = 0 + + store.clear(texture, 0, 0, GlyphSize) + slotOf('?') + + override def slotOf(char: Int): Int = { + if (char == ' ') return 0 + var slot = slots.get(char) + if (slot != Unmapped) return slot + if (count == height / GlyphSize * Columns && !grow()) { + resetPending = true + slot = slots.get('?') + return if (slot == Unmapped) 0 else slot + } + if (store.store(texture, char, (count % Columns) * GlyphSize, (count / Columns) * GlyphSize)) { + slot = count + count += 1 + } + else slot = if (char == '?') 0 else slotOf('?') + slots.put(char, slot) + slot + } + + def beginFrame() { + if (cooldown > 0) cooldown -= 1 + else if (resetPending) { + reset() + cooldown = ResetCooldown + } + } + + def reset() { + slots.clear() + count = 1 + generation += 1 + resetPending = false + slotOf('?') + } + + def dispose(): Unit = textures.delete(texture) + + private def grow(): Boolean = { + if (height >= heightLimit) return false + val next = math.min(height * 2, heightLimit) + val grown = textures.create(Width, next) + textures.copy(texture, grown, Width, height) + textures.delete(texture) + texture = grown + height = next + true + } +} diff --git a/src/main/scala/li/cil/oc/client/renderer/text/GlyphMeshText.scala b/src/main/scala/li/cil/oc/client/renderer/text/GlyphMeshText.scala new file mode 100644 index 0000000000..7de83324fa --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/text/GlyphMeshText.scala @@ -0,0 +1,50 @@ +package li.cil.oc.client.renderer.text + +import li.cil.oc.client.renderer.CapturedMesh +import li.cil.oc.client.renderer.font.DynamicFontRenderer +import li.cil.oc.client.renderer.shading.Blend +import li.cil.oc.client.renderer.shading.FixedFunction +import li.cil.oc.util.RenderState +import net.minecraft.client.renderer.OpenGlHelper +import org.lwjgl.opengl.GL11 + +object GlyphMeshText { + def render(buffer: TextBufferRenderData, renderer: DynamicFontRenderer, inWorld: Boolean, worldFade: Float) { + var mesh = buffer.mesh + if (mesh == null) { + mesh = new CapturedMesh + buffer.mesh = mesh + buffer.meshVersion = -1 + } + if (buffer.meshVersion != buffer.version || buffer.cacheGeneration != renderer.cacheGeneration) { + val lines = buffer.data.buffer + var i = 0 + while (i < lines.length) { + renderer.generateChars(lines(i)) + i += 1 + } + mesh.reset() + try renderer.emitGeometry(mesh, buffer.data, buffer.viewport._1, buffer.viewport._2) + finally mesh.closeBucket() + buffer.meshVersion = buffer.version + buffer.cacheGeneration = renderer.cacheGeneration + } + + val lightX = OpenGlHelper.lastBrightnessX + val lightY = OpenGlHelper.lastBrightnessY + GL11.glPushAttrib(RenderState.DrawAttribBits) + if (inWorld) { + OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 0xFF, 0xFF) + RenderState.disableLighting() + } + Blend(Blend.Alpha) + if (inWorld && worldFade < 1) FixedFunction.constantAlphaBlend(worldFade) + if (inWorld) GL11.glDepthMask(false) + GL11.glPushMatrix() + GL11.glScalef(0.5f, 0.5f, 1) + mesh.draw() + GL11.glPopMatrix() + if (inWorld) OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, lightX, lightY) + GL11.glPopAttrib() + } +} diff --git a/src/main/scala/li/cil/oc/client/renderer/text/ScreenCellUpdate.scala b/src/main/scala/li/cil/oc/client/renderer/text/ScreenCellUpdate.scala new file mode 100644 index 0000000000..9a66de54a7 --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/text/ScreenCellUpdate.scala @@ -0,0 +1,39 @@ +package li.cil.oc.client.renderer.text + +import java.nio.ByteBuffer + +import li.cil.oc.util.TextBuffer + +trait CellUploader extends CellRects { + def bind(cells: ScreenCells): Unit +} + +object ScreenCellUpdate { + def acquire(pool: SheetPool, buffer: TextBufferRenderData, size: Int): ScreenCells = { + var cells = buffer.cells + if (cells == null) { + cells = new ScreenCells + pool.place(cells, size) + buffer.cells = cells + } + else if (cells.size != size) pool.resize(cells, size) + cells + } + + def update(uploader: CellUploader, atlas: GlyphAtlas, out: ByteBuffer, data: TextBuffer, dirty: DirtyRows, cells: ScreenCells, columns: Int, lines: Int) { + if (cells.width != columns || cells.height != lines || cells.generation != atlas.generation) { + cells.width = columns + cells.height = lines + cells.generation = atlas.generation + dirty.mark(0, lines) + } + val from = math.max(dirty.from, 0) + val to = math.min(dirty.to, lines) + if (from < to) { + TextCells.encode(out, data, columns, from, to, atlas) + uploader.bind(cells) + ScreenSheet.cellRects(cells.start + from * columns, cells.start + to * columns, uploader) + } + dirty.clear() + } +} diff --git a/src/main/scala/li/cil/oc/client/renderer/text/ScreenSheet.scala b/src/main/scala/li/cil/oc/client/renderer/text/ScreenSheet.scala new file mode 100644 index 0000000000..b956784e22 --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/text/ScreenSheet.scala @@ -0,0 +1,180 @@ +package li.cil.oc.client.renderer.text + +import java.util + +import it.unimi.dsi.fastutil.ints.IntArrayList + +trait SheetTextures { + def create(width: Int, height: Int): Int + + def delete(texture: Int): Unit + + def copy(from: Int, to: Int, width: Int, height: Int): Unit +} + +trait CellRects { + def rect(column: Int, row: Int, columns: Int, rows: Int, firstCell: Int): Unit +} + +final class ScreenCells { + var sheet: ScreenSheet = _ + + var start = 0 + + var size = 0 + + var width = 0 + + var height = 0 + + var generation = -1 +} + +object ScreenSheet { + final val Width = 2048 + final val Height = 512 + final val Stride = Width / 2 + final val Cells = Stride * Height + + def cellRects(firstCell: Int, endCell: Int, out: CellRects) { + var cell = firstCell + val column = cell % Stride + if (column != 0) { + val count = math.min(Stride - column, endCell - cell) + out.rect(column, cell / Stride, count, 1, cell - firstCell) + cell += count + } + val fullRows = (endCell - cell) / Stride + if (fullRows > 0) { + out.rect(0, cell / Stride, Stride, fullRows, cell - firstCell) + cell += fullRows * Stride + } + if (cell < endCell) out.rect(0, cell / Stride, endCell - cell, 1, cell - firstCell) + } +} + +final class ScreenSheet(val texture: Int) { + private val starts = IntArrayList.wrap(Array(0)) + + private val lengths = IntArrayList.wrap(Array(ScreenSheet.Cells)) + + var screens = 0 + + def freeCells: Int = { + var total = 0 + var i = 0 + while (i < lengths.size) { + total += lengths.getInt(i) + i += 1 + } + total + } + + def alloc(size: Int): Int = { + var best = -1 + var bestLength = Int.MaxValue + var i = 0 + while (i < starts.size) { + val length = lengths.getInt(i) + if (length >= size && length < bestLength) { + best = i + bestLength = length + if (length == size) i = starts.size + } + i += 1 + } + if (best < 0) return -1 + val start = starts.getInt(best) + if (bestLength == size) { + starts.removeInt(best) + lengths.removeInt(best) + } + else { + starts.set(best, start + size) + lengths.set(best, bestLength - size) + } + screens += 1 + start + } + + def free(start: Int, size: Int) { + screens -= 1 + var low = 0 + var high = starts.size + while (low < high) { + val mid = (low + high) >>> 1 + if (starts.getInt(mid) < start) low = mid + 1 else high = mid + } + val joinsPrevious = low > 0 && starts.getInt(low - 1) + lengths.getInt(low - 1) == start + val joinsNext = low < starts.size && start + size == starts.getInt(low) + if (joinsPrevious && joinsNext) { + lengths.set(low - 1, lengths.getInt(low - 1) + size + lengths.getInt(low)) + starts.removeInt(low) + lengths.removeInt(low) + } + else if (joinsPrevious) lengths.set(low - 1, lengths.getInt(low - 1) + size) + else if (joinsNext) { + starts.set(low, start) + lengths.set(low, lengths.getInt(low) + size) + } + else { + starts.add(low, start) + lengths.add(low, size) + } + } +} + +final class SheetPool(textures: SheetTextures) { + private val sheets = new util.ArrayList[ScreenSheet] + + def place(cells: ScreenCells, size: Int): Unit = place(cells, size, null) + + def resize(cells: ScreenCells, size: Int) { + val sheet = cells.sheet + sheet.free(cells.start, cells.size) + val start = sheet.alloc(size) + if (start >= 0) assign(cells, sheet, start, size) + else { + place(cells, size, sheet) + dropIfEmpty(sheet) + } + } + + def release(cells: ScreenCells) { + val sheet = cells.sheet + sheet.free(cells.start, cells.size) + cells.sheet = null + dropIfEmpty(sheet) + } + + private def place(cells: ScreenCells, size: Int, skip: ScreenSheet) { + var i = 0 + while (i < sheets.size) { + val sheet = sheets.get(i) + if (sheet ne skip) { + val start = sheet.alloc(size) + if (start >= 0) { + assign(cells, sheet, start, size) + return + } + } + i += 1 + } + val sheet = new ScreenSheet(textures.create(ScreenSheet.Width, ScreenSheet.Height)) + sheets.add(sheet) + assign(cells, sheet, sheet.alloc(size), size) + } + + private def assign(cells: ScreenCells, sheet: ScreenSheet, start: Int, size: Int) { + cells.sheet = sheet + cells.start = start + cells.size = size + cells.width = 0 + cells.height = 0 + } + + private def dropIfEmpty(sheet: ScreenSheet): Unit = if (sheet.screens == 0) { + textures.delete(sheet.texture) + sheets.remove(sheet) + } +} diff --git a/src/main/scala/li/cil/oc/client/renderer/text/ScreenText.scala b/src/main/scala/li/cil/oc/client/renderer/text/ScreenText.scala new file mode 100644 index 0000000000..9acdc8302e --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/text/ScreenText.scala @@ -0,0 +1,293 @@ +package li.cil.oc.client.renderer.text + +import java.nio.ByteBuffer + +import com.gtnewhorizon.gtnhlib.client.opengl.GLCaps +import com.gtnewhorizon.gtnhlib.client.renderer.postprocessing.CustomFramebuffer +import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat +import li.cil.oc.Settings +import li.cil.oc.client.renderer.CapturedMesh +import li.cil.oc.client.renderer.UnitQuad +import li.cil.oc.client.renderer.font.DynamicFontRenderer +import li.cil.oc.client.renderer.font.IGlyphProvider +import li.cil.oc.client.renderer.shading.Blend +import li.cil.oc.client.renderer.shading.FixedFunction +import li.cil.oc.client.renderer.shading.Fog +import li.cil.oc.client.renderer.shading.Programs +import li.cil.oc.util.RenderState +import net.minecraft.client.renderer.OpenGlHelper +import org.joml.Matrix4f +import org.joml.Matrix4fc +import org.lwjgl.BufferUtils +import org.lwjgl.opengl.GL11 +import org.lwjgl.opengl.GL13 +import org.lwjgl.opengl.GL20 +import org.lwjgl.opengl.GL30 + +final class PackBudget(limit: Long) { + var used = 0L + + var epoch = 0 + + def resize(previous: Long, needed: Long): Boolean = + if (used - previous + needed > limit) false + else { + used += needed - previous + if (needed < previous) epoch += 1 + true + } + + def release(bytes: Long) { + used -= bytes + epoch += 1 + } +} + +object ScreenText { + private var quad: CapturedMesh = _ + + private var provider: IGlyphProvider = _ + + private var atlas: GlyphAtlas = _ + + private var available = false + + private val budget = new PackBudget(256L << 20) + + private object Glyphs extends GlyphStore { + override def store(texture: Int, char: Int, x: Int, y: Int): Boolean = { + val glyph = provider.getGlyph(char) + if (glyph == null) return false + val height = provider.getGlyphHeight + val width = glyph.remaining / (height * 4) + GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture) + GL11.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, x, y, width, height, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, glyph) + if (width < GlyphAtlas.GlyphSize) clear(texture, x + width, y, GlyphAtlas.GlyphSize - width) + true + } + + override def clear(texture: Int, x: Int, y: Int, width: Int) { + GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture) + GL11.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, x, y, width, GlyphAtlas.GlyphSize, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, blankGlyph) + } + } + + private object Textures extends SheetTextures { + override def create(width: Int, height: Int): Int = { + val texture = GL11.glGenTextures() + GL11.glBindTexture(GL11.GL_TEXTURE_2D, texture) + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST) + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST) + GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA8, width, height, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, null.asInstanceOf[ByteBuffer]) + texture + } + + override def delete(texture: Int): Unit = GL11.glDeleteTextures(texture) + + override def copy(from: Int, to: Int, width: Int, height: Int) { + val pixels = BufferUtils.createByteBuffer(width * height * 4) + GL11.glBindTexture(GL11.GL_TEXTURE_2D, from) + GL11.glGetTexImage(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixels) + GL11.glBindTexture(GL11.GL_TEXTURE_2D, to) + GL11.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, width, height, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, pixels) + } + } + + private val blankGlyph = BufferUtils.createByteBuffer(GlyphAtlas.GlyphSize * GlyphAtlas.GlyphSize * 4) + + private val pool = new SheetPool(Textures) + + private var staging = BufferUtils.createByteBuffer(80 * 25 * TextCells.BytesPerCell) + + private object Upload extends CellUploader { + override def bind(cells: ScreenCells): Unit = GL11.glBindTexture(GL11.GL_TEXTURE_2D, cells.sheet.texture) + + override def rect(column: Int, row: Int, columns: Int, rows: Int, firstCell: Int) { + staging.position(firstCell * TextCells.BytesPerCell) + GL11.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, column * 2, row, columns * 2, rows, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, staging) + } + } + + private val model = new Matrix4f() + + private val smoothing = if (Settings.get.textLinearFiltering) 1f else 0f + + private val packFilter = if (Settings.get.textLinearFiltering) CustomFramebuffer.TEXTURE_LINEAR else 0 + + private var framebufferSaved = false + + private var savedFramebuffer = 0 + + private val savedViewport = BufferUtils.createIntBuffer(16) + + def reload(renderer: DynamicFontRenderer) { + provider = renderer.glyphProvider + val shader = Programs.text + available = Programs.usable(shader) && GL11.glGetInteger(GL20.GL_MAX_TEXTURE_IMAGE_UNITS) >= 3 + if (atlas != null) atlas.dispose() + atlas = null + if (!available) return + val program = GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM) + shader.use() + GL20.glUniform1f(shader.loc(Programs.textSmooth), smoothing) + GL20.glUseProgram(program) + if (quad == null) quad = CapturedMesh.build(DefaultVertexFormat.POSITION_TEXTURE, null)(UnitQuad.emitFrontQuad) + RenderState.saveTexture0() + atlas = new GlyphAtlas(Textures, Glyphs, GL11.glGetInteger(GL11.GL_MAX_TEXTURE_SIZE)) + RenderState.restoreTexture0() + } + + def beginFrame(): Unit = if (atlas != null) atlas.beginFrame() + + def release(buffer: TextBufferRenderData) { + releaseCells(buffer) + releasePackTarget(buffer) + buffer.drawnInPack = false + buffer.packRejectedAt = -1 + } + + def render(renderer: DynamicFontRenderer, buffer: TextBufferRenderData, fade: Float, inWorld: Boolean): Boolean = { + if (!available) return false + val viewport = buffer.viewport + val columns = columnsOf(buffer, viewport) + val lines = linesOf(buffer, viewport, columns) + if (columns * lines == 0) return true + RenderState.saveTexture0() + val cells = prepare(buffer, columns, lines) + draw(cells, columns, lines, model.scaling(columns * renderer.charRenderWidth, lines * renderer.charRenderHeight, 1), fade, inWorld, + if (fade < 1) Blend.Additive else Blend.Alpha) + RenderState.restoreTexture0() + true + } + + def drawPacked(renderer: DynamicFontRenderer, buffer: TextBufferRenderData, fade: Float): Boolean = { + if (!available || !GLCaps.fboSupported() || buffer.packRejectedAt == budget.epoch) return false + buffer.drawnInPack = true + val target = buffer.packTarget + if (target != null) { + FixedFunction.drawPremultiplied(quad, target.framebufferTexture, + model.scaling(buffer.packColumns * renderer.charRenderWidth, buffer.packLines * renderer.charRenderHeight, 1), fade) + } + true + } + + def renderPackTarget(renderer: DynamicFontRenderer, buffer: TextBufferRenderData) { + if (!available) return + val viewport = buffer.viewport + val columns = columnsOf(buffer, viewport) + val lines = linesOf(buffer, viewport, columns) + if (columns * lines == 0) return + val width = columns * renderer.charWidth + val height = lines * renderer.charHeight + var target = buffer.packTarget + if (target == null || target.framebufferWidth != width || target.framebufferHeight != height) { + val previous = if (target == null) 0L else targetBytes(target) + if (!budget.resize(previous, width.toLong * height * 4)) { + releasePackTarget(buffer) + releaseCells(buffer) + buffer.packRejectedAt = budget.epoch + return + } + saveFramebuffer() + if (target == null) { + target = new CustomFramebuffer(packFilter) + buffer.packTarget = target + } + target.createBindFramebuffer(width, height) + buffer.packVersion = -1 + } + RenderState.saveTexture0() + val cells = prepare(buffer, columns, lines) + if (buffer.packStale(columns, lines, atlas.generation)) { + saveFramebuffer() + target.clearBindFramebuffer(true) + GL11.glMatrixMode(GL11.GL_PROJECTION) + GL11.glPushMatrix() + GL11.glLoadIdentity() + GL11.glOrtho(0, width, 0, height, -1, 1) + GL11.glMatrixMode(GL11.GL_MODELVIEW) + GL11.glPushMatrix() + GL11.glLoadIdentity() + GL11.glPushAttrib(GL11.GL_ENABLE_BIT) + GL11.glDisable(GL11.GL_CULL_FACE) + draw(cells, columns, lines, model.scaling(width, height, 1), 1, inWorld = false, Blend.Opaque) + GL11.glPopAttrib() + GL11.glPopMatrix() + GL11.glMatrixMode(GL11.GL_PROJECTION) + GL11.glPopMatrix() + GL11.glMatrixMode(GL11.GL_MODELVIEW) + buffer.packVersion = buffer.version + buffer.packColumns = columns + buffer.packLines = lines + buffer.packGeneration = atlas.generation + } + RenderState.restoreTexture0() + } + + def finishPackTargets(): Unit = if (framebufferSaved) { + GLCaps.FBO.glBindFramebuffer(GL30.GL_FRAMEBUFFER, savedFramebuffer) + GL11.glViewport(savedViewport.get(0), savedViewport.get(1), savedViewport.get(2), savedViewport.get(3)) + framebufferSaved = false + } + + def releasePackTarget(buffer: TextBufferRenderData) { + val target = buffer.packTarget + if (target != null) { + saveFramebuffer() + budget.release(targetBytes(target)) + target.checkDeleteFramebuffer() + buffer.packTarget = null + } + } + + private def releaseCells(buffer: TextBufferRenderData) { + if (buffer.cells != null) { + pool.release(buffer.cells) + buffer.cells = null + } + } + + private def columnsOf(buffer: TextBufferRenderData, viewport: (Int, Int)) = math.min(viewport._1, buffer.data.width) + + private def linesOf(buffer: TextBufferRenderData, viewport: (Int, Int), columns: Int) = + math.min(math.min(viewport._2, buffer.data.height), ScreenSheet.Cells / math.max(columns, 1)) + + private def targetBytes(target: CustomFramebuffer) = target.framebufferWidth.toLong * target.framebufferHeight * 4 + + private def prepare(buffer: TextBufferRenderData, columns: Int, lines: Int): ScreenCells = { + val size = columns * lines + if (staging.capacity < size * TextCells.BytesPerCell) staging = BufferUtils.createByteBuffer(size * TextCells.BytesPerCell) + val cells = ScreenCellUpdate.acquire(pool, buffer, size) + ScreenCellUpdate.update(Upload, atlas, staging, buffer.data, buffer.rows, cells, columns, lines) + cells + } + + private def saveFramebuffer(): Unit = if (!framebufferSaved) { + savedFramebuffer = GL11.glGetInteger(GL30.GL_FRAMEBUFFER_BINDING) + savedViewport.clear() + GL11.glGetInteger(GL11.GL_VIEWPORT, savedViewport) + framebufferSaved = true + } + + private def draw(cells: ScreenCells, columns: Int, lines: Int, transform: Matrix4fc, fade: Float, inWorld: Boolean, blendMode: Int) { + val shader = Programs.text + val program = GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM) + GL11.glPushAttrib(RenderState.DrawAttribBits) + Blend(blendMode) + GL11.glDepthMask(false) + shader.use() + GL20.glUniform4f(shader.loc(Programs.textCells), cells.start, 0, columns, lines) + GL20.glUniform1f(shader.loc(Programs.textFade), fade) + GL20.glUniform2f(shader.loc(Programs.textGlyphScale), 1f / GlyphAtlas.Width, 1f / atlas.height) + Fog.upload(shader, Programs.textFog, Programs.textFogColor, inWorld) + shader.uploadModel(transform) + GL11.glBindTexture(GL11.GL_TEXTURE_2D, cells.sheet.texture) + // Never unbind the atlas: under Angelica, changing a non-zero unit's binding flushes and ends TESR batching. + OpenGlHelper.setActiveTexture(GL13.GL_TEXTURE2) + GL11.glBindTexture(GL11.GL_TEXTURE_2D, atlas.texture) + OpenGlHelper.setActiveTexture(OpenGlHelper.defaultTexUnit) + quad.drawGeometry() + GL11.glPopAttrib() + GL20.glUseProgram(program) + } +} diff --git a/src/main/scala/li/cil/oc/client/renderer/text/TextBufferRenderData.scala b/src/main/scala/li/cil/oc/client/renderer/text/TextBufferRenderData.scala new file mode 100644 index 0000000000..24c230e8be --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/text/TextBufferRenderData.scala @@ -0,0 +1,56 @@ +package li.cil.oc.client.renderer.text + +import com.gtnewhorizon.gtnhlib.client.renderer.postprocessing.CustomFramebuffer +import li.cil.oc.client.renderer.CapturedMesh +import li.cil.oc.util.TextBuffer + +trait TextBufferRenderData { + def dirty: Boolean + + def dirty_=(value: Boolean): Unit + + var cacheGeneration: Int = -1 + + var version: Int = 0 + + var mesh: CapturedMesh = _ + + var meshVersion: Int = -1 + + var cells: ScreenCells = _ + + var lastUsedTick: Int = 0 + + var tracked = false + + var packTarget: CustomFramebuffer = _ + + var packVersion: Int = -1 + + var packColumns: Int = 0 + + var packLines: Int = 0 + + var packGeneration: Int = -1 + + var drawnInPack = false + + var packRejectedAt: Int = -1 + + val rows = new DirtyRows + + def data: TextBuffer + + def viewport: (Int, Int) + + def packStale(columns: Int, lines: Int, generation: Int): Boolean = + packVersion != version || packColumns != columns || packLines != lines || packGeneration != generation + + def consumeDirty() { + if (dirty) { + if (rows.isEmpty) rows.markAll() + version += 1 + dirty = false + } + } +} diff --git a/src/main/scala/li/cil/oc/client/renderer/text/TextCells.scala b/src/main/scala/li/cil/oc/client/renderer/text/TextCells.scala new file mode 100644 index 0000000000..209acd04c4 --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/text/TextCells.scala @@ -0,0 +1,52 @@ +package li.cil.oc.client.renderer.text + +import java.nio.ByteBuffer + +import li.cil.oc.util.FontUtils +import li.cil.oc.util.PackedColor +import li.cil.oc.util.TextBuffer + +trait GlyphSlots { + def slotOf(char: Int): Int +} + +object TextCells { + final val RightHalf = 0x80 + + final val BytesPerCell = 8 + + def encode(out: ByteBuffer, data: TextBuffer, columns: Int, from: Int, to: Int, glyphs: GlyphSlots) { + val format = data.format + out.clear() + var y = from + while (y < to) { + val chars = data.buffer(y) + val colors = data.color(y) + var x = 0 + while (x < columns) { + val char = chars(x) + val slot = glyphs.slotOf(char) + val foreground = PackedColor.unpackForeground(colors(x), format) + putCell(out, slot, 0, foreground, PackedColor.unpackBackground(colors(x), format)) + if (slot != 0 && FontUtils.wcwidth(char) == 2 && x + 1 < columns) { + x += 1 + putCell(out, slot, RightHalf, foreground, PackedColor.unpackBackground(colors(x), format)) + } + x += 1 + } + y += 1 + } + out.flip() + } + + private def putCell(out: ByteBuffer, slot: Int, flags: Int, foreground: Int, background: Int) { + out.put(slot.toByte) + out.put(((slot >> 8) | flags).toByte) + out.put((foreground >> 16).toByte) + out.put((foreground >> 8).toByte) + out.put(foreground.toByte) + out.put((background >> 16).toByte) + out.put((background >> 8).toByte) + out.put(background.toByte) + } +} diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/AdapterRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/AdapterRenderer.scala deleted file mode 100644 index 4e9b3784ca..0000000000 --- a/src/main/scala/li/cil/oc/client/renderer/tileentity/AdapterRenderer.scala +++ /dev/null @@ -1,88 +0,0 @@ -package li.cil.oc.client.renderer.tileentity - -import li.cil.oc.client.Textures -import li.cil.oc.common.tileentity -import li.cil.oc.util.RenderState -import net.minecraft.client.renderer.Tessellator -import net.minecraft.client.renderer.texture.TextureMap -import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer -import net.minecraft.tileentity.TileEntity -import net.minecraftforge.common.util.ForgeDirection -import org.lwjgl.opengl.GL11 - -object AdapterRenderer extends TileEntitySpecialRenderer { - override def renderTileEntityAt(tileEntity: TileEntity, x: Double, y: Double, z: Double, f: Float) { - RenderState.checkError(getClass.getName + ".renderTileEntityAt: entering (aka: wasntme)") - - val adapter = tileEntity.asInstanceOf[tileentity.Adapter] - if (adapter.openSides.contains(true)) { - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) - - RenderState.disableLighting() - RenderState.makeItBlend() - - GL11.glPushMatrix() - - GL11.glTranslated(x + 0.5, y + 0.5, z + 0.5) - GL11.glScaled(1.0025, -1.0025, 1.0025) - GL11.glTranslatef(-0.5f, -0.5f, -0.5f) - - bindTexture(TextureMap.locationBlocksTexture) - val t = Tessellator.instance - t.startDrawingQuads() - - val sideActivity = Textures.Adapter.iconOn - - if (adapter.isSideOpen(ForgeDirection.DOWN)) { - t.addVertexWithUV(0, 1, 0, sideActivity.getMaxU, sideActivity.getMinV) - t.addVertexWithUV(1, 1, 0, sideActivity.getMinU, sideActivity.getMinV) - t.addVertexWithUV(1, 1, 1, sideActivity.getMinU, sideActivity.getMaxV) - t.addVertexWithUV(0, 1, 1, sideActivity.getMaxU, sideActivity.getMaxV) - } - - if (adapter.isSideOpen(ForgeDirection.UP)) { - t.addVertexWithUV(0, 0, 0, sideActivity.getMaxU, sideActivity.getMaxV) - t.addVertexWithUV(0, 0, 1, sideActivity.getMaxU, sideActivity.getMinV) - t.addVertexWithUV(1, 0, 1, sideActivity.getMinU, sideActivity.getMinV) - t.addVertexWithUV(1, 0, 0, sideActivity.getMinU, sideActivity.getMaxV) - } - - if (adapter.isSideOpen(ForgeDirection.NORTH)) { - t.addVertexWithUV(1, 1, 0, sideActivity.getMinU, sideActivity.getMaxV) - t.addVertexWithUV(0, 1, 0, sideActivity.getMaxU, sideActivity.getMaxV) - t.addVertexWithUV(0, 0, 0, sideActivity.getMaxU, sideActivity.getMinV) - t.addVertexWithUV(1, 0, 0, sideActivity.getMinU, sideActivity.getMinV) - } - - if (adapter.isSideOpen(ForgeDirection.SOUTH)) { - t.addVertexWithUV(0, 1, 1, sideActivity.getMinU, sideActivity.getMaxV) - t.addVertexWithUV(1, 1, 1, sideActivity.getMaxU, sideActivity.getMaxV) - t.addVertexWithUV(1, 0, 1, sideActivity.getMaxU, sideActivity.getMinV) - t.addVertexWithUV(0, 0, 1, sideActivity.getMinU, sideActivity.getMinV) - } - - if (adapter.isSideOpen(ForgeDirection.WEST)) { - t.addVertexWithUV(0, 1, 0, sideActivity.getMinU, sideActivity.getMaxV) - t.addVertexWithUV(0, 1, 1, sideActivity.getMaxU, sideActivity.getMaxV) - t.addVertexWithUV(0, 0, 1, sideActivity.getMaxU, sideActivity.getMinV) - t.addVertexWithUV(0, 0, 0, sideActivity.getMinU, sideActivity.getMinV) - } - - if (adapter.isSideOpen(ForgeDirection.EAST)) { - t.addVertexWithUV(1, 1, 1, sideActivity.getMinU, sideActivity.getMaxV) - t.addVertexWithUV(1, 1, 0, sideActivity.getMaxU, sideActivity.getMaxV) - t.addVertexWithUV(1, 0, 0, sideActivity.getMaxU, sideActivity.getMinV) - t.addVertexWithUV(1, 0, 1, sideActivity.getMinU, sideActivity.getMinV) - } - - t.draw() - - RenderState.enableLighting() - - GL11.glPopMatrix() - GL11.glPopAttrib() - } - - RenderState.checkError(getClass.getName + ".renderTileEntityAt: leaving") - } -} diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/AssemblerRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/AssemblerRenderer.scala deleted file mode 100644 index 24c4da08df..0000000000 --- a/src/main/scala/li/cil/oc/client/renderer/tileentity/AssemblerRenderer.scala +++ /dev/null @@ -1,73 +0,0 @@ -package li.cil.oc.client.renderer.tileentity - -import li.cil.oc.client.Textures -import li.cil.oc.common.tileentity.Assembler -import li.cil.oc.util.RenderState -import net.minecraft.client.renderer.Tessellator -import net.minecraft.client.renderer.texture.TextureMap -import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer -import net.minecraft.tileentity.TileEntity -import org.lwjgl.opengl.GL11 - -object AssemblerRenderer extends TileEntitySpecialRenderer { - override def renderTileEntityAt(tileEntity: TileEntity, x: Double, y: Double, z: Double, f: Float) { - RenderState.checkError(getClass.getName + ".renderTileEntityAt: entering (aka: wasntme)") - - val assembler = tileEntity.asInstanceOf[Assembler] - - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) - - RenderState.disableLighting() - RenderState.makeItBlend() - RenderState.setBlendAlpha(1) - - GL11.glPushMatrix() - GL11.glTranslated(x + 0.5, y + 0.5, z + 0.5) - - bindTexture(TextureMap.locationBlocksTexture) - val t = Tessellator.instance - t.startDrawingQuads() - - { - val icon = Textures.Assembler.iconTopOn - t.addVertexWithUV(-0.5, 0.55, 0.5, icon.getMinU, icon.getMaxV) - t.addVertexWithUV(0.5, 0.55, 0.5, icon.getMaxU, icon.getMaxV) - t.addVertexWithUV(0.5, 0.55, -0.5, icon.getMaxU, icon.getMinV) - t.addVertexWithUV(-0.5, 0.55, -0.5, icon.getMinU, icon.getMinV) - } - - t.draw() - - val indent = 6 / 16f + 0.005 - for (i <- 0 until 4) { - t.startDrawingQuads() - - if (assembler.isAssembling) { - val icon = Textures.Assembler.iconSideAssembling - t.addVertexWithUV(indent, 0.5, -indent, icon.getInterpolatedU((0.5 - indent) * 16), icon.getMaxV) - t.addVertexWithUV(indent, 0.5, indent, icon.getInterpolatedU((0.5 + indent) * 16), icon.getMaxV) - t.addVertexWithUV(indent, -0.5, indent, icon.getInterpolatedU((0.5 + indent) * 16), icon.getMinV) - t.addVertexWithUV(indent, -0.5, -indent, icon.getInterpolatedU((0.5 - indent) * 16), icon.getMinV) - } - - { - val icon = Textures.Assembler.iconSideOn - t.addVertexWithUV(0.5005, 0.5, -0.5, icon.getMinU, icon.getMaxV) - t.addVertexWithUV(0.5005, 0.5, 0.5, icon.getMaxU, icon.getMaxV) - t.addVertexWithUV(0.5005, -0.5, 0.5, icon.getMaxU, icon.getMinV) - t.addVertexWithUV(0.5005, -0.5, -0.5, icon.getMinU, icon.getMinV) - } - - t.draw() - - GL11.glRotatef(90, 0, 1, 0) - } - - RenderState.enableLighting() - - GL11.glPopMatrix() - GL11.glPopAttrib() - - RenderState.checkError(getClass.getName + ".renderTileEntityAt: leaving") - } -} diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/BlockTransforms.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/BlockTransforms.scala new file mode 100644 index 0000000000..9d60b6caf5 --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/tileentity/BlockTransforms.scala @@ -0,0 +1,45 @@ +package li.cil.oc.client.renderer.tileentity + +import net.minecraftforge.common.util.ForgeDirection +import org.joml.Matrix4f +import org.joml.Matrix4fc +import org.lwjgl.opengl.GL11 + +object BlockTransforms { + val Inflated: Matrix4fc = new Matrix4f().translate(0.5f, 0.5f, 0.5f).scale(1.0025f, -1.0025f, 1.0025f).translate(-0.5f, -0.5f, -0.5f) + + val Centered: Matrix4fc = new Matrix4f().translate(0.5f, 0.5f, 0.5f) + + def yawDegrees(yaw: ForgeDirection): Int = yaw match { + case ForgeDirection.WEST => -90 + case ForgeDirection.NORTH => 180 + case ForgeDirection.EAST => 90 + case _ => 0 + } + + def rotateYaw(yaw: ForgeDirection) { + val degrees = yawDegrees(yaw) + if (degrees != 0) GL11.glRotatef(degrees, 0, 1, 0) + } + + def rotatePitch(pitch: ForgeDirection) { + pitch match { + case ForgeDirection.DOWN => GL11.glRotatef(90, 1, 0, 0) + case ForgeDirection.UP => GL11.glRotatef(-90, 1, 0, 0) + case _ => + } + } +} + +final class FrontTransforms(zFront: Float) { + private val byYaw = { + val table = new Array[Matrix4fc](ForgeDirection.values.length) + for (yaw <- ForgeDirection.values) { + table(yaw.ordinal) = new Matrix4f().translate(0.5f, 0.5f, 0.5f).rotateY(math.toRadians(BlockTransforms.yawDegrees(yaw)).toFloat). + translate(-0.5f, 0.5f, zFront).scale(1, -1, 1) + } + table + } + + def apply(yaw: ForgeDirection): Matrix4fc = byYaw(yaw.ordinal) +} diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/CaseRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/CaseRenderer.scala deleted file mode 100644 index e578b083fa..0000000000 --- a/src/main/scala/li/cil/oc/client/renderer/tileentity/CaseRenderer.scala +++ /dev/null @@ -1,66 +0,0 @@ -package li.cil.oc.client.renderer.tileentity - -import li.cil.oc.client.Textures -import li.cil.oc.common.tileentity.Case -import li.cil.oc.util.RenderState -import net.minecraft.client.renderer.Tessellator -import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer -import net.minecraft.tileentity.TileEntity -import net.minecraft.util.ResourceLocation -import net.minecraftforge.common.util.ForgeDirection -import org.lwjgl.opengl.GL11 - -object CaseRenderer extends TileEntitySpecialRenderer { - override def renderTileEntityAt(tileEntity: TileEntity, x: Double, y: Double, z: Double, f: Float) { - RenderState.checkError(getClass.getName + ".renderTileEntityAt: entering (aka: wasntme)") - - val computer = tileEntity.asInstanceOf[Case] - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) - - RenderState.disableLighting() - RenderState.makeItBlend() - RenderState.setBlendAlpha(1) - - GL11.glPushMatrix() - - GL11.glTranslated(x + 0.5, y + 0.5, z + 0.5) - - computer.yaw match { - case ForgeDirection.WEST => GL11.glRotatef(-90, 0, 1, 0) - case ForgeDirection.NORTH => GL11.glRotatef(180, 0, 1, 0) - case ForgeDirection.EAST => GL11.glRotatef(90, 0, 1, 0) - case _ => // No yaw. - } - - GL11.glTranslated(-0.5, 0.5, 0.505) - GL11.glScalef(1, -1, 1) - - if (computer.isRunning) { - renderFrontOverlay(Textures.blockCaseFrontOn) - if (System.currentTimeMillis() - computer.lastFileSystemAccess < 400 && computer.world.rand.nextDouble() > 0.1) { - renderFrontOverlay(Textures.blockCaseFrontActivity) - } - } - else if (computer.hasErrored && RenderUtil.shouldShowErrorLight(computer.hashCode)) { - renderFrontOverlay(Textures.blockCaseFrontError) - } - - RenderState.enableLighting() - - GL11.glPopMatrix() - GL11.glPopAttrib() - - RenderState.checkError(getClass.getName + ".renderTileEntityAt: leaving") - } - - private def renderFrontOverlay(texture: ResourceLocation): Unit = { - bindTexture(texture) - val t = Tessellator.instance - t.startDrawingQuads() - t.addVertexWithUV(0, 1, 0, 0, 1) - t.addVertexWithUV(1, 1, 0, 1, 1) - t.addVertexWithUV(1, 0, 0, 1, 0) - t.addVertexWithUV(0, 0, 0, 0, 0) - t.draw() - } -} \ No newline at end of file diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/ChargerRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/ChargerRenderer.scala deleted file mode 100644 index 1acf82104e..0000000000 --- a/src/main/scala/li/cil/oc/client/renderer/tileentity/ChargerRenderer.scala +++ /dev/null @@ -1,79 +0,0 @@ -package li.cil.oc.client.renderer.tileentity - -import li.cil.oc.client.Textures -import li.cil.oc.common.tileentity.Charger -import li.cil.oc.util.RenderState -import net.minecraft.client.renderer.Tessellator -import net.minecraft.client.renderer.texture.TextureMap -import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer -import net.minecraft.tileentity.TileEntity -import net.minecraftforge.common.util.ForgeDirection -import org.lwjgl.opengl.GL11 - -object ChargerRenderer extends TileEntitySpecialRenderer { - override def renderTileEntityAt(tileEntity: TileEntity, x: Double, y: Double, z: Double, f: Float) { - RenderState.checkError(getClass.getName + ".renderTileEntityAt: entering (aka: wasntme)") - - val charger = tileEntity.asInstanceOf[Charger] - if (charger.chargeSpeed > 0) { - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) - - RenderState.disableLighting() - RenderState.makeItBlend() - RenderState.setBlendAlpha(1) - - GL11.glPushMatrix() - - GL11.glTranslated(x + 0.5, y + 0.5, z + 0.5) - - charger.yaw match { - case ForgeDirection.WEST => GL11.glRotatef(-90, 0, 1, 0) - case ForgeDirection.NORTH => GL11.glRotatef(180, 0, 1, 0) - case ForgeDirection.EAST => GL11.glRotatef(90, 0, 1, 0) - case _ => // No yaw. - } - - GL11.glTranslatef(-0.5f, 0.5f, 0.5f) - GL11.glScalef(1, -1, 1) - - val t = Tessellator.instance - - val frontIcon = Textures.Charger.iconFrontCharging - bindTexture(TextureMap.locationBlocksTexture) - t.startDrawingQuads() - - val inverse = 1 - charger.chargeSpeed - t.addVertexWithUV(0, 1, 0.005, frontIcon.getMinU, frontIcon.getMaxV) - t.addVertexWithUV(1, 1, 0.005, frontIcon.getMaxU, frontIcon.getMaxV) - t.addVertexWithUV(1, inverse, 0.005, frontIcon.getMaxU, frontIcon.getInterpolatedV(inverse * 16)) - t.addVertexWithUV(0, inverse, 0.005, frontIcon.getMinU, frontIcon.getInterpolatedV(inverse * 16)) - - if (charger.hasPower) { - val sideIcon = Textures.Charger.iconSideCharging - t.addVertexWithUV(-0.005, 1, -1, sideIcon.getMinU, sideIcon.getMaxV) - t.addVertexWithUV(-0.005, 1, 0, sideIcon.getMaxU, sideIcon.getMaxV) - t.addVertexWithUV(-0.005, 0, 0, sideIcon.getMaxU, sideIcon.getMinV) - t.addVertexWithUV(-0.005, 0, -1, sideIcon.getMinU, sideIcon.getMinV) - - t.addVertexWithUV(1, 1, -1.005, sideIcon.getMinU, sideIcon.getMaxV) - t.addVertexWithUV(0, 1, -1.005, sideIcon.getMaxU, sideIcon.getMaxV) - t.addVertexWithUV(0, 0, -1.005, sideIcon.getMaxU, sideIcon.getMinV) - t.addVertexWithUV(1, 0, -1.005, sideIcon.getMinU, sideIcon.getMinV) - - t.addVertexWithUV(1.005, 1, 0, sideIcon.getMinU, sideIcon.getMaxV) - t.addVertexWithUV(1.005, 1, -1, sideIcon.getMaxU, sideIcon.getMaxV) - t.addVertexWithUV(1.005, 0, -1, sideIcon.getMaxU, sideIcon.getMinV) - t.addVertexWithUV(1.005, 0, 0, sideIcon.getMinU, sideIcon.getMinV) - } - - t.draw() - - RenderState.enableLighting() - - GL11.glPopMatrix() - GL11.glPopAttrib() - } - - RenderState.checkError(getClass.getName + ".renderTileEntityAt: leaving") - } -} diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/CubeFaces.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/CubeFaces.scala new file mode 100644 index 0000000000..28f724264b --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/tileentity/CubeFaces.scala @@ -0,0 +1,79 @@ +package li.cil.oc.client.renderer.tileentity + +import li.cil.oc.common.tileentity +import net.minecraft.client.renderer.Tessellator +import net.minecraft.util.IIcon +import net.minecraftforge.common.util.ForgeDirection + +object CubeFaces { + final val Walls = ForgeDirection.NORTH.flag | ForgeDirection.SOUTH.flag | ForgeDirection.WEST.flag | ForgeDirection.EAST.flag + + def openSides(te: tileentity.traits.OpenSides): Int = { + val sides = ForgeDirection.VALID_DIRECTIONS + var mask = 0 + var i = 0 + while (i < sides.length) { + if (te.isSideOpen(sides(i))) mask |= sides(i).flag + i += 1 + } + mask + } + + private final val Inflation = 0.00125 + + def emitTop(t: Tessellator, icon: IIcon): Unit = emitTop(t, icon, 0, 0, 0, 1, 1) + + def emitSides(t: Tessellator, icon: IIcon, mask: Int): Unit = emitSides(t, icon, mask, 0, 0, 0, 1, 1, 1) + + def emitInflatedTop(t: Tessellator, icon: IIcon, x: Int, y: Int, z: Int): Unit = + emitTop(t, icon, x - Inflation, y + 1 + Inflation, z - Inflation, x + 1 + Inflation, z + 1 + Inflation) + + def emitInflatedSides(t: Tessellator, icon: IIcon, mask: Int, x: Int, y: Int, z: Int): Unit = + emitSides(t, icon, mask, x - Inflation, y + 1 + Inflation, z - Inflation, x + 1 + Inflation, y - Inflation, z + 1 + Inflation) + + def emitTop(t: Tessellator, icon: IIcon, x0: Double, y0: Double, z0: Double, x1: Double, z1: Double) { + t.addVertexWithUV(x0, y0, z1, icon.getMinU, icon.getMaxV) + t.addVertexWithUV(x1, y0, z1, icon.getMaxU, icon.getMaxV) + t.addVertexWithUV(x1, y0, z0, icon.getMaxU, icon.getMinV) + t.addVertexWithUV(x0, y0, z0, icon.getMinU, icon.getMinV) + } + + def emitSides(t: Tessellator, icon: IIcon, mask: Int, x0: Double, y0: Double, z0: Double, x1: Double, y1: Double, z1: Double) { + if ((mask & ForgeDirection.DOWN.flag) != 0) { + t.addVertexWithUV(x0, y1, z0, icon.getMaxU, icon.getMinV) + t.addVertexWithUV(x1, y1, z0, icon.getMinU, icon.getMinV) + t.addVertexWithUV(x1, y1, z1, icon.getMinU, icon.getMaxV) + t.addVertexWithUV(x0, y1, z1, icon.getMaxU, icon.getMaxV) + } + if ((mask & ForgeDirection.UP.flag) != 0) { + t.addVertexWithUV(x0, y0, z0, icon.getMaxU, icon.getMaxV) + t.addVertexWithUV(x0, y0, z1, icon.getMaxU, icon.getMinV) + t.addVertexWithUV(x1, y0, z1, icon.getMinU, icon.getMinV) + t.addVertexWithUV(x1, y0, z0, icon.getMinU, icon.getMaxV) + } + if ((mask & ForgeDirection.NORTH.flag) != 0) { + t.addVertexWithUV(x1, y1, z0, icon.getMinU, icon.getMaxV) + t.addVertexWithUV(x0, y1, z0, icon.getMaxU, icon.getMaxV) + t.addVertexWithUV(x0, y0, z0, icon.getMaxU, icon.getMinV) + t.addVertexWithUV(x1, y0, z0, icon.getMinU, icon.getMinV) + } + if ((mask & ForgeDirection.SOUTH.flag) != 0) { + t.addVertexWithUV(x0, y1, z1, icon.getMinU, icon.getMaxV) + t.addVertexWithUV(x1, y1, z1, icon.getMaxU, icon.getMaxV) + t.addVertexWithUV(x1, y0, z1, icon.getMaxU, icon.getMinV) + t.addVertexWithUV(x0, y0, z1, icon.getMinU, icon.getMinV) + } + if ((mask & ForgeDirection.WEST.flag) != 0) { + t.addVertexWithUV(x0, y1, z0, icon.getMinU, icon.getMaxV) + t.addVertexWithUV(x0, y1, z1, icon.getMaxU, icon.getMaxV) + t.addVertexWithUV(x0, y0, z1, icon.getMaxU, icon.getMinV) + t.addVertexWithUV(x0, y0, z0, icon.getMinU, icon.getMinV) + } + if ((mask & ForgeDirection.EAST.flag) != 0) { + t.addVertexWithUV(x1, y1, z1, icon.getMinU, icon.getMaxV) + t.addVertexWithUV(x1, y1, z0, icon.getMaxU, icon.getMaxV) + t.addVertexWithUV(x1, y0, z0, icon.getMaxU, icon.getMinV) + t.addVertexWithUV(x1, y0, z1, icon.getMinU, icon.getMinV) + } + } +} diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/DisassemblerRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/DisassemblerRenderer.scala deleted file mode 100644 index 868143d076..0000000000 --- a/src/main/scala/li/cil/oc/client/renderer/tileentity/DisassemblerRenderer.scala +++ /dev/null @@ -1,71 +0,0 @@ -package li.cil.oc.client.renderer.tileentity - -import li.cil.oc.client.Textures -import li.cil.oc.common.tileentity -import li.cil.oc.util.RenderState -import net.minecraft.client.renderer.Tessellator -import net.minecraft.client.renderer.texture.TextureMap -import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer -import net.minecraft.tileentity.TileEntity -import org.lwjgl.opengl.GL11 - -object DisassemblerRenderer extends TileEntitySpecialRenderer { - override def renderTileEntityAt(tileEntity: TileEntity, x: Double, y: Double, z: Double, f: Float) { - RenderState.checkError(getClass.getName + ".renderTileEntityAt: entering (aka: wasntme)") - - val disassembler = tileEntity.asInstanceOf[tileentity.Disassembler] - if (disassembler.isActive) { - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) - - RenderState.disableLighting() - RenderState.makeItBlend() - - GL11.glPushMatrix() - - GL11.glTranslated(x + 0.5, y + 0.5, z + 0.5) - GL11.glScaled(1.0025, -1.0025, 1.0025) - GL11.glTranslatef(-0.5f, -0.5f, -0.5f) - - bindTexture(TextureMap.locationBlocksTexture) - val t = Tessellator.instance - t.startDrawingQuads() - - val topOn = Textures.Disassembler.iconTopOn - t.addVertexWithUV(0, 0, 1, topOn.getMinU, topOn.getMaxV) - t.addVertexWithUV(1, 0, 1, topOn.getMaxU, topOn.getMaxV) - t.addVertexWithUV(1, 0, 0, topOn.getMaxU, topOn.getMinV) - t.addVertexWithUV(0, 0, 0, topOn.getMinU, topOn.getMinV) - - val sideOn = Textures.Disassembler.iconSideOn - t.addVertexWithUV(1, 1, 0, sideOn.getMinU, sideOn.getMaxV) - t.addVertexWithUV(0, 1, 0, sideOn.getMaxU, sideOn.getMaxV) - t.addVertexWithUV(0, 0, 0, sideOn.getMaxU, sideOn.getMinV) - t.addVertexWithUV(1, 0, 0, sideOn.getMinU, sideOn.getMinV) - - t.addVertexWithUV(0, 1, 1, sideOn.getMinU, sideOn.getMaxV) - t.addVertexWithUV(1, 1, 1, sideOn.getMaxU, sideOn.getMaxV) - t.addVertexWithUV(1, 0, 1, sideOn.getMaxU, sideOn.getMinV) - t.addVertexWithUV(0, 0, 1, sideOn.getMinU, sideOn.getMinV) - - t.addVertexWithUV(1, 1, 1, sideOn.getMinU, sideOn.getMaxV) - t.addVertexWithUV(1, 1, 0, sideOn.getMaxU, sideOn.getMaxV) - t.addVertexWithUV(1, 0, 0, sideOn.getMaxU, sideOn.getMinV) - t.addVertexWithUV(1, 0, 1, sideOn.getMinU, sideOn.getMinV) - - t.addVertexWithUV(0, 1, 0, sideOn.getMinU, sideOn.getMaxV) - t.addVertexWithUV(0, 1, 1, sideOn.getMaxU, sideOn.getMaxV) - t.addVertexWithUV(0, 0, 1, sideOn.getMaxU, sideOn.getMinV) - t.addVertexWithUV(0, 0, 0, sideOn.getMinU, sideOn.getMinV) - - t.draw() - - RenderState.enableLighting() - - GL11.glPopMatrix() - GL11.glPopAttrib() - } - - RenderState.checkError(getClass.getName + ".renderTileEntityAt: leaving") - } - -} diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/DiskDriveRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/DiskDriveRenderer.scala index c5d1430e37..198e6c8e4f 100644 --- a/src/main/scala/li/cil/oc/client/renderer/tileentity/DiskDriveRenderer.scala +++ b/src/main/scala/li/cil/oc/client/renderer/tileentity/DiskDriveRenderer.scala @@ -1,39 +1,28 @@ package li.cil.oc.client.renderer.tileentity -import li.cil.oc.client.Textures import li.cil.oc.common.tileentity.DiskDrive import li.cil.oc.util.RenderState import net.minecraft.client.renderer.OpenGlHelper -import net.minecraft.client.renderer.Tessellator import net.minecraft.client.renderer.entity.RenderItem import net.minecraft.client.renderer.entity.RenderManager import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer import net.minecraft.entity.item.EntityItem import net.minecraft.tileentity.TileEntity -import net.minecraftforge.common.util.ForgeDirection import org.lwjgl.opengl.GL11 object DiskDriveRenderer extends TileEntitySpecialRenderer { + private lazy val activity = MeshBackend.active.overlayRenderer(Overlays.DiskDriveActivity) + override def renderTileEntityAt(tileEntity: TileEntity, x: Double, y: Double, z: Double, f: Float) { - RenderState.checkError(getClass.getName + ".renderTileEntityAt: entering (aka: wasntme)") + RenderState.checkError(this, ".renderTileEntityAt: entering (aka: wasntme)") val drive = tileEntity.asInstanceOf[DiskDrive] - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) - - GL11.glPushMatrix() - - GL11.glTranslated(x + 0.5, y + 0.5, z + 0.5) - - drive.yaw match { - case ForgeDirection.WEST => GL11.glRotatef(-90, 0, 1, 0) - case ForgeDirection.NORTH => GL11.glRotatef(180, 0, 1, 0) - case ForgeDirection.EAST => GL11.glRotatef(90, 0, 1, 0) - case _ => // No yaw. - } - drive.items(0) match { case Some(stack) => + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) GL11.glPushMatrix() + GL11.glTranslated(x + 0.5, y + 0.5, z + 0.5) + BlockTransforms.rotateYaw(drive.yaw) GL11.glTranslatef(0, 3.5f / 16, 9 / 16f) GL11.glRotatef(90, -1, 0, 0) @@ -47,32 +36,12 @@ object DiskDriveRenderer extends TileEntitySpecialRenderer { RenderManager.instance.renderEntityWithPosYaw(entity, 0, 0, 0, 0, 0) RenderItem.renderInFrame = false GL11.glPopMatrix() + GL11.glPopAttrib() case _ => } - if (System.currentTimeMillis() - drive.lastAccess < 400 && drive.world.rand.nextDouble() > 0.1) { - GL11.glTranslated(-0.5, 0.5, 0.505) - GL11.glScalef(1, -1, 1) - - RenderState.disableLighting() - RenderState.makeItBlend() - RenderState.setBlendAlpha(1) - - bindTexture(Textures.blockDiskDriveFrontActivity) - val t = Tessellator.instance - t.startDrawingQuads() - t.addVertexWithUV(0, 1, 0, 0, 1) - t.addVertexWithUV(1, 1, 0, 1, 1) - t.addVertexWithUV(1, 0, 0, 1, 0) - t.addVertexWithUV(0, 0, 0, 0, 0) - t.draw() - - RenderState.enableLighting() - } - - GL11.glPopMatrix() - GL11.glPopAttrib() + activity.renderTileEntityAt(tileEntity, x, y, z, f) - RenderState.checkError(getClass.getName + ".renderTileEntityAt: leaving") + RenderState.checkError(this, ".renderTileEntityAt: leaving") } } \ No newline at end of file diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/GeolyzerRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/GeolyzerRenderer.scala deleted file mode 100644 index 9cf53b75dc..0000000000 --- a/src/main/scala/li/cil/oc/client/renderer/tileentity/GeolyzerRenderer.scala +++ /dev/null @@ -1,47 +0,0 @@ -package li.cil.oc.client.renderer.tileentity - -import li.cil.oc.client.Textures -import li.cil.oc.util.RenderState -import net.minecraft.client.renderer.Tessellator -import net.minecraft.client.renderer.texture.TextureMap -import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer -import net.minecraft.tileentity.TileEntity -import org.lwjgl.opengl.GL11 - -object GeolyzerRenderer extends TileEntitySpecialRenderer { - override def renderTileEntityAt(tileEntity: TileEntity, x: Double, y: Double, z: Double, f: Float) { - RenderState.checkError(getClass.getName + ".renderTileEntityAt: entering (aka: wasntme)") - - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) - - RenderState.disableLighting() - RenderState.makeItBlend() - RenderState.setBlendAlpha(1) - - GL11.glPushMatrix() - - GL11.glTranslated(x + 0.5, y + 0.5, z + 0.5) - GL11.glScaled(1.0025, -1.0025, 1.0025) - GL11.glTranslatef(-0.5f, -0.5f, -0.5f) - - bindTexture(TextureMap.locationBlocksTexture) - val t = Tessellator.instance - t.startDrawingQuads() - - val topOn = Textures.Geolyzer.iconTopOn - t.addVertexWithUV(0, 0, 1, topOn.getMinU, topOn.getMaxV) - t.addVertexWithUV(1, 0, 1, topOn.getMaxU, topOn.getMaxV) - t.addVertexWithUV(1, 0, 0, topOn.getMaxU, topOn.getMinV) - t.addVertexWithUV(0, 0, 0, topOn.getMinU, topOn.getMinV) - - t.draw() - - RenderState.enableLighting() - - GL11.glPopMatrix() - GL11.glPopAttrib() - - RenderState.checkError(getClass.getName + ".renderTileEntityAt: leaving") - } - -} diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/HologramFaces.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/HologramFaces.scala new file mode 100644 index 0000000000..af3096e277 --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/tileentity/HologramFaces.scala @@ -0,0 +1,35 @@ +package li.cil.oc.client.renderer.tileentity + +object HologramFaces { + val faceNormals = Array( + 0, 0, 1, + 0, 0, -1, + 1, 0, 0, + -1, 0, 0, + 0, 1, 0, + 0, -1, 0) + + val faceVertices = Array( + 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, + 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, + 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, + 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, + 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1) + + final val FaceCount = 6 + + final val FaceStride = 4 * 5 + + def pack(x: Int, y: Int, z: Int, face: Int, value: Int): Int = x | y << 6 | z << 12 | face << 18 | value << 21 + + def x(packed: Int): Int = packed & 63 + + def y(packed: Int): Int = (packed >> 6) & 63 + + def z(packed: Int): Int = (packed >> 12) & 63 + + def face(packed: Int): Int = (packed >> 18) & 7 + + def value(packed: Int): Int = packed >>> 21 +} diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/HologramFade.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/HologramFade.scala new file mode 100644 index 0000000000..b62881d809 --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/tileentity/HologramFade.scala @@ -0,0 +1,10 @@ +package li.cil.oc.client.renderer.tileentity + +object HologramFade { + final val MaxAlpha = 0.75f + + def alpha(distSq: Double, fadeSq: Double, maxSq: Double): Float = { + if (distSq <= fadeSq || maxSq <= fadeSq) MaxAlpha + else MaxAlpha * math.max(0.0, 1 - (distSq - fadeSq) / (maxSq - fadeSq)).toFloat + } +} diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/HologramMesh.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/HologramMesh.scala new file mode 100644 index 0000000000..a91db6f115 --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/tileentity/HologramMesh.scala @@ -0,0 +1,15 @@ +package li.cil.oc.client.renderer.tileentity + +import li.cil.oc.common.tileentity.Hologram +import li.cil.oc.integration.angelica.AngelicaHologramMesh +import li.cil.oc.integration.angelica.AngelicaTesr + +trait HologramMesh { + def init(): Unit + + def draw(hologram: Hologram, alpha: Float, x: Double, y: Double, z: Double, partialTicks: Float): Unit +} + +object HologramMesh { + val active: HologramMesh = if (AngelicaTesr.MeshReplacement) AngelicaHologramMesh else StandaloneHologramMesh +} diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/HologramRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/HologramRenderer.scala index 35e29f162f..815f2d9972 100644 --- a/src/main/scala/li/cil/oc/client/renderer/tileentity/HologramRenderer.scala +++ b/src/main/scala/li/cil/oc/client/renderer/tileentity/HologramRenderer.scala @@ -1,379 +1,147 @@ package li.cil.oc.client.renderer.tileentity -import java.nio.IntBuffer -import java.util.concurrent.Callable -import java.util.concurrent.TimeUnit +import java.util -import com.google.common.cache.CacheBuilder -import com.google.common.cache.RemovalListener -import com.google.common.cache.RemovalNotification -import cpw.mods.fml.common.eventhandler.SubscribeEvent -import cpw.mods.fml.common.gameevent.TickEvent.ClientTickEvent import li.cil.oc.Settings -import li.cil.oc.client.Textures +import li.cil.oc.client.renderer.shading.FixedFunction import li.cil.oc.common.tileentity.Hologram import li.cil.oc.util.RenderState +import net.minecraft.client.renderer.ActiveRenderInfo +import net.minecraft.client.renderer.Tessellator import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer import net.minecraft.tileentity.TileEntity import net.minecraftforge.common.util.ForgeDirection -import org.lwjgl.BufferUtils -import org.lwjgl.opengl.GL11 -import org.lwjgl.opengl.GL15 +import org.joml.Matrix4f +import org.joml.Vector3f import scala.util.Random -object HologramRenderer extends TileEntitySpecialRenderer with Callable[Int] with RemovalListener[TileEntity, Int] { +object HologramRenderer extends TileEntitySpecialRenderer { private val random = new Random() - /** We cache the VBOs for the projectors we render for performance. */ - private val cache = com.google.common.cache.CacheBuilder.newBuilder(). - expireAfterAccess(5, TimeUnit.SECONDS). - removalListener(this). - asInstanceOf[CacheBuilder[Hologram, Int]]. - build[Hologram, Int]() + private val model = new Matrix4f() + private val inverse = new Matrix4f() + private val camera = new Vector3f() - /** - * Common for all holograms. Holds the vertex positions, texture - * coordinates and normals information. Layout is: u v nx ny nz x y z - * - * WARNING: this optimization only works if all the holograms have the - * same dimensions (in voxels). If we ever need holograms of different - * sizes we could probably just fake that by making the outer layers - * immutable (i.e. always empty). - * - * NOTE: It already takes up 47.25 MiB of video memory and increasing - * hologram size to, for example, 64*64*64 will result in 168 MiB. - */ - private var commonBuffer = 0 + // Blocks; the near plane clips front faces before the camera reaches the box, which would expose culled back faces. + private final val NearPlanePad = 0.1f - /** - * Also common for all holograms. Temporary buffer used to upload - * hologram data to GPU. First half stores colors for each vertex - * (0xAABBGGRR Int, alpha is used for alignment only) and second - * half stores (Int) indices of vertices that should be drawn. - */ - private var dataBuffer: IntBuffer = null - - /** Used to pass the current screen along to call(). */ - private var hologram: Hologram = null - - /** - * Whether initialization failed (e.g. due to an out of memory error) and we - * should render using the fallback renderer instead. - */ - private var failed = false + private var faces = new Array[Int](1 << 14) override def renderTileEntityAt(te: TileEntity, x: Double, y: Double, z: Double, f: Float) { - if (failed) { - HologramRendererFallback.renderTileEntityAt(te, x, y, z, f) - return - } - - RenderState.checkError(getClass.getName + ".renderTileEntityAt: entering (aka: wasntme)") + RenderState.checkError(this, ".renderTileEntityAt: entering (aka: wasntme)") - hologram = te.asInstanceOf[Hologram] + val hologram = te.asInstanceOf[Hologram] if (!hologram.hasPower) return - GL11.glPushClientAttrib(GL11.GL_ALL_CLIENT_ATTRIB_BITS) - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) - RenderState.makeItBlend() - GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE) + val alpha = HologramFade.alpha(x * x + y * y + z * z, hologram.getFadeStartDistanceSquared, hologram.getMaxRenderDistanceSquared) + if (alpha <= 0) return - val playerDistSq = x * x + y * y + z * z - val maxDistSq = hologram.getMaxRenderDistanceSquared - val fadeDistSq = hologram.getFadeStartDistanceSquared - RenderState.setBlendAlpha(0.75f * (if (playerDistSq > fadeDistSq) math.max(0, 1 - ((playerDistSq - fadeDistSq) / (maxDistSq - fadeDistSq)).toFloat) else 1)) + HologramMesh.active.draw(hologram, alpha, x, y, z, f) - GL11.glPushMatrix() - GL11.glTranslated(x + 0.5, y + 0.5, z + 0.5) + RenderState.checkError(this, ".renderTileEntityAt: leaving") + } - hologram.yaw match { - case ForgeDirection.WEST => GL11.glRotatef(-90, 0, 1, 0) - case ForgeDirection.NORTH => GL11.glRotatef(180, 0, 1, 0) - case ForgeDirection.EAST => GL11.glRotatef(90, 0, 1, 0) - case _ => // No yaw. - } + def applyTransform(hologram: Hologram, x: Double, y: Double, z: Double, f: Float): Boolean = { + val m = model.identity() + m.translate((x + 0.5).toFloat, (y + 0.5).toFloat, (z + 0.5).toFloat) + + val yaw = BlockTransforms.yawDegrees(hologram.yaw) + if (yaw != 0) m.rotateY(math.toRadians(yaw).toFloat) hologram.pitch match { - case ForgeDirection.DOWN => GL11.glRotatef(90, 1, 0, 0) - case ForgeDirection.UP => GL11.glRotatef(-90, 1, 0, 0) - case _ => // No pitch. + case ForgeDirection.DOWN => m.rotateX(math.toRadians(90).toFloat) + case ForgeDirection.UP => m.rotateX(math.toRadians(-90).toFloat) + case _ => } - GL11.glRotatef(hologram.rotationAngle, hologram.rotationX, hologram.rotationY, hologram.rotationZ) - GL11.glRotatef(hologram.rotationSpeed * (hologram.getWorldObj.getTotalWorldTime % (360 * 20 - 1) + f) / 20f, hologram.rotationSpeedX, hologram.rotationSpeedY, hologram.rotationSpeedZ) + rotate(m, hologram.rotationAngle, hologram.rotationX, hologram.rotationY, hologram.rotationZ) + rotate(m, hologram.rotationSpeed * (hologram.getWorldObj.getTotalWorldTime % (360 * 20 - 1) + f) / 20f, hologram.rotationSpeedX, hologram.rotationSpeedY, hologram.rotationSpeedZ) - GL11.glScaled(1.001, 1.001, 1.001) // Avoid z-fighting with other blocks. - GL11.glTranslated( - (hologram.translation.xCoord * hologram.width / 16 - 1.5) * hologram.scale, - hologram.translation.yCoord * hologram.height / 16 * hologram.scale, - (hologram.translation.zCoord * hologram.width / 16 - 1.5) * hologram.scale) + m.scale(1.001f) // Avoid z-fighting with other blocks. + m.translate( + ((hologram.translation.xCoord * hologram.width / 16 - 1.5) * hologram.scale).toFloat, + (hologram.translation.yCoord * hologram.height / 16 * hologram.scale).toFloat, + ((hologram.translation.zCoord * hologram.width / 16 - 1.5) * hologram.scale).toFloat) // Do a bit of flickering, because that's what holograms do! if (Settings.get.hologramFlickerFrequency > 0 && random.nextDouble() < Settings.get.hologramFlickerFrequency) { - GL11.glScaled(1 + random.nextGaussian() * 0.01, 1 + random.nextGaussian() * 0.001, 1 + random.nextGaussian() * 0.01) - GL11.glTranslated(random.nextGaussian() * 0.01, random.nextGaussian() * 0.01, random.nextGaussian() * 0.01) + m.scale((1 + random.nextGaussian() * 0.01).toFloat, (1 + random.nextGaussian() * 0.001).toFloat, (1 + random.nextGaussian() * 0.01).toFloat) + m.translate((random.nextGaussian() * 0.01).toFloat, (random.nextGaussian() * 0.01).toFloat, (random.nextGaussian() * 0.01).toFloat) } // After the below scaling, hologram is drawn inside a [0..48]x[0..32]x[0..48] box - GL11.glScaled(hologram.scale / 16f, hologram.scale / 16f, hologram.scale / 16f) - - bindTexture(Textures.blockHologram) + m.scale((hologram.scale / 16f).toFloat) - // Normalize normals (yes, glScale scales them too). - GL11.glEnable(GL11.GL_NORMALIZE) + FixedFunction.multMatrix(m) - val sx = (x + 0.5) * hologram.scale - val sy = -(y + 0.5) * hologram.scale - val sz = (z + 0.5) * hologram.scale - if (sx >= -1.5 && sx <= 1.5 && sz >= -1.5 && sz <= 1.5 && sy >= 0 && sy <= 2) { - // Camera is inside the hologram. - GL11.glDisable(GL11.GL_CULL_FACE) - } - else { - // Camera is outside the hologram. - GL11.glEnable(GL11.GL_CULL_FACE) - GL11.glCullFace(GL11.GL_BACK) - } - - // We do two passes here to avoid weird transparency effects: in the first - // pass we find the front-most fragment, in the second we actually draw it. - // When we don't do this the hologram will look different from different - // angles (because some faces will shine through sometimes and sometimes - // they won't), so a more... consistent look is desirable. - val glBuffer = cache.get(hologram, this) - GL11.glColorMask(false, false, false, false) - GL11.glDepthMask(true) - draw(glBuffer) - GL11.glColorMask(true, true, true, true) - GL11.glDepthFunc(GL11.GL_EQUAL) - draw(glBuffer) - - GL11.glPopMatrix() - GL11.glPopAttrib() - GL11.glPopClientAttrib() - - RenderState.checkError(getClass.getName + ".renderTileEntityAt: leaving") + m.invertAffine(inverse) + camera.set(ActiveRenderInfo.objectX, ActiveRenderInfo.objectY, ActiveRenderInfo.objectZ) + inverse.transformPosition(camera) + val pad = NearPlanePad * 16 / (hologram.scale * 1.001f).toFloat + camera.x >= -pad && camera.x <= hologram.width + pad && camera.y >= -pad && camera.y <= hologram.height + pad && + camera.z >= -pad && camera.z <= hologram.width + pad } - def draw(glBuffer: Int) { - if (initialize()) { - validate(glBuffer) - publish(glBuffer) - } + private def rotate(m: Matrix4f, degrees: Float, ax: Float, ay: Float, az: Float) { + val length = math.sqrt(ax * ax + ay * ay + az * az).toFloat + if (degrees != 0 && length > 0) m.rotate(math.toRadians(degrees).toFloat, ax / length, ay / length, az / length) } - private def initialize(): Boolean = !failed && (try { - // First run only, create structure information. - if (commonBuffer == 0) { - dataBuffer = BufferUtils.createIntBuffer(hologram.width * hologram.width * hologram.height * 6 * 4 * 2) - - commonBuffer = GL15.glGenBuffers() - - val data = BufferUtils.createFloatBuffer(hologram.width * hologram.width * hologram.height * 24 * (2 + 3 + 3)) - def addVertex(x: Int, y: Int, z: Int, u: Int, v: Int, nx: Int, ny: Int, nz: Int) { - data.put(u) - data.put(v) - data.put(nx) - data.put(ny) - data.put(nz) - data.put(x) - data.put(y) - data.put(z) - } - - for (x <- 0 until hologram.width) { - for (z <- 0 until hologram.width) { - for (y <- 0 until hologram.height) { - /* - 0---1 - | N | - 0---3---2---1---0 - | W | U | E | D | - 5---6---7---4---5 - | S | - 5---4 - */ - - // South - addVertex(x + 1, y + 1, z + 1, 0, 0, 0, 0, 1) // 5 - addVertex(x + 0, y + 1, z + 1, 1, 0, 0, 0, 1) // 4 - addVertex(x + 0, y + 0, z + 1, 1, 1, 0, 0, 1) // 7 - addVertex(x + 1, y + 0, z + 1, 0, 1, 0, 0, 1) // 6 - // North - addVertex(x + 1, y + 0, z + 0, 0, 0, 0, 0, -1) // 3 - addVertex(x + 0, y + 0, z + 0, 1, 0, 0, 0, -1) // 2 - addVertex(x + 0, y + 1, z + 0, 1, 1, 0, 0, -1) // 1 - addVertex(x + 1, y + 1, z + 0, 0, 1, 0, 0, -1) // 0 - - // East - addVertex(x + 1, y + 1, z + 1, 1, 0, 1, 0, 0) // 5 - addVertex(x + 1, y + 0, z + 1, 1, 1, 1, 0, 0) // 6 - addVertex(x + 1, y + 0, z + 0, 0, 1, 1, 0, 0) // 3 - addVertex(x + 1, y + 1, z + 0, 0, 0, 1, 0, 0) // 0 - // West - addVertex(x + 0, y + 0, z + 1, 1, 0, -1, 0, 0) // 7 - addVertex(x + 0, y + 1, z + 1, 1, 1, -1, 0, 0) // 4 - addVertex(x + 0, y + 1, z + 0, 0, 1, -1, 0, 0) // 1 - addVertex(x + 0, y + 0, z + 0, 0, 0, -1, 0, 0) // 2 - - // Up - addVertex(x + 1, y + 1, z + 0, 0, 0, 0, 1, 0) // 0 - addVertex(x + 0, y + 1, z + 0, 1, 0, 0, 1, 0) // 1 - addVertex(x + 0, y + 1, z + 1, 1, 1, 0, 1, 0) // 4 - addVertex(x + 1, y + 1, z + 1, 0, 1, 0, 1, 0) // 5 - // Down - addVertex(x + 1, y + 0, z + 1, 0, 0, 0, -1, 0) // 6 - addVertex(x + 0, y + 0, z + 1, 1, 0, 0, -1, 0) // 7 - addVertex(x + 0, y + 0, z + 0, 1, 1, 0, -1, 0) // 2 - addVertex(x + 1, y + 0, z + 0, 0, 1, 0, -1, 0) // 3 - } - } - } - - // Important! OpenGL will start reading from the current buffer position. - data.rewind() - - // This buffer never ever changes, so static is the way to go. - GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, commonBuffer) - GL15.glBufferData(GL15.GL_ARRAY_BUFFER, data, GL15.GL_STATIC_DRAW) - } - true - } - catch { - case oom: OutOfMemoryError => - HologramRendererFallback.text = "Not enough memory" - failed = true - false - }) - - private def validate(glBuffer: Int) { - // Refresh indexes when the hologram's data changed. - if (hologram.needsRendering) { - def value(hx: Int, hy: Int, hz: Int) = if (hx >= 0 && hy >= 0 && hz >= 0 && hx < hologram.width && hy < hologram.height && hz < hologram.width) hologram.getColor(hx, hy, hz) else 0 - - def isSolid(hx: Int, hy: Int, hz: Int) = value(hx, hy, hz) != 0 - - def addFace(index: Int, color: Int) { - dataBuffer.put(index) - dataBuffer.put(index + 1) - dataBuffer.put(index + 2) - dataBuffer.put(index + 3) - - dataBuffer.put(index, color) - dataBuffer.put(index + 1, color) - dataBuffer.put(index + 2, color) - dataBuffer.put(index + 3, color) - - hologram.visibleQuads += 1 - } - - // Copy color information, identify which quads to render and prepare data for glDrawElements - hologram.visibleQuads = 0 - var index = 0 - dataBuffer.position(hologram.width * hologram.width * hologram.height * 6 * 4) - for (hx <- 0 until hologram.width) { - for (hz <- 0 until hologram.width) { - for (hy <- 0 until hologram.height) { - // Do we need to draw at least one face? - if (isSolid(hx, hy, hz)) { - // Yes, get the color of the voxel. - val color = hologram.colors(value(hx, hy, hz) - 1) - - // South - if (!isSolid(hx, hy, hz + 1)) { - addFace(index, color) - } - index += 4 - // North - if (!isSolid(hx, hy, hz - 1)) { - addFace(index, color) - } - index += 4 - - // East - if (!isSolid(hx + 1, hy, hz)) { - addFace(index, color) + private def isSolid(hologram: Hologram, hx: Int, hy: Int, hz: Int) = + hx >= 0 && hy >= 0 && hz >= 0 && hx < hologram.width && hy < hologram.height && hz < hologram.width && hologram.getColor(hx, hy, hz) != 0 + + private def isFaceVisible(hologram: Hologram, hx: Int, hy: Int, hz: Int, face: Int) = + !isSolid(hologram, hx + HologramFaces.faceNormals(face * 3), hy + HologramFaces.faceNormals(face * 3 + 1), hz + HologramFaces.faceNormals(face * 3 + 2)) + + def collectFaces(hologram: Hologram): Int = { + val width = hologram.width + val height = hologram.height + var count = 0 + var hx = 0 + while (hx < width) { + var hz = 0 + while (hz < width) { + var hy = 0 + while (hy < height) { + val value = hologram.getColor(hx, hy, hz) + if (value != 0) { + var face = 0 + while (face < HologramFaces.FaceCount) { + if (isFaceVisible(hologram, hx, hy, hz, face)) { + if (count == faces.length) faces = util.Arrays.copyOf(faces, count * 2) + faces(count) = HologramFaces.pack(hx, hy, hz, face, value) + count += 1 } - index += 4 - // West - if (!isSolid(hx - 1, hy, hz)) { - addFace(index, color) - } - index += 4 - - // Up - if (!isSolid(hx, hy + 1, hz)) { - addFace(index, color) - } - index += 4 - // Down - if (!isSolid(hx, hy - 1, hz)) { - addFace(index, color) - } - index += 4 - } - else { - // No, skip all associated indices. - index += 6 * 4 + face += 1 } } + hy += 1 } + hz += 1 } - - GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, glBuffer) - if (hologram.visibleQuads > 0) { - // Flip the buffer to only fill in as much data as necessary. - dataBuffer.flip() - - // This buffer can be updated quite frequently, so dynamic seems sensible. - GL15.glBufferData(GL15.GL_ARRAY_BUFFER, dataBuffer, GL15.GL_DYNAMIC_DRAW) - } - else { - // Empty hologram. - GL15.glBufferData(GL15.GL_ARRAY_BUFFER, 0L, GL15.GL_DYNAMIC_DRAW) - } - - // Reset for the next operation. - dataBuffer.clear() - - hologram.needsRendering = false + hx += 1 } + count } - private def publish(glBuffer: Int) { - GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, commonBuffer) - GL11.glEnableClientState(GL11.GL_VERTEX_ARRAY) - GL11.glEnableClientState(GL11.GL_TEXTURE_COORD_ARRAY) - GL11.glEnableClientState(GL11.GL_NORMAL_ARRAY) - GL11.glInterleavedArrays(GL11.GL_T2F_N3F_V3F, 0, 0) - - GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, glBuffer) - GL11.glEnableClientState(GL11.GL_COLOR_ARRAY) - GL11.glColorPointer(3, GL11.GL_UNSIGNED_BYTE, 4, 0) - - GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, glBuffer) - GL11.glDrawElements(GL11.GL_QUADS, hologram.visibleQuads * 4, GL11.GL_UNSIGNED_INT, hologram.width * hologram.width * hologram.height * 6 * 4 * 4) - } - - // ----------------------------------------------------------------------- // - // Cache - // ----------------------------------------------------------------------- // - - def call = { - val glBuffer = GL15.glGenBuffers() - - // Force re-indexing. - hologram.needsRendering = true - - glBuffer - } - - def onRemoval(e: RemovalNotification[TileEntity, Int]) { - val glBuffer = e.getValue - GL15.glDeleteBuffers(glBuffer) - dataBuffer.clear() + def emitFaces(t: Tessellator, hologram: Hologram, count: Int) { + var i = 0 + while (i < count) { + val packed = faces(i) + val hx = HologramFaces.x(packed) + val hy = HologramFaces.y(packed) + val hz = HologramFaces.z(packed) + val face = HologramFaces.face(packed) + val color = hologram.colors(HologramFaces.value(packed) - 1) + t.setColorOpaque(color & 0xFF, (color >> 8) & 0xFF, (color >> 16) & 0xFF) + t.setNormal(HologramFaces.faceNormals(face * 3), HologramFaces.faceNormals(face * 3 + 1), HologramFaces.faceNormals(face * 3 + 2)) + var v = face * HologramFaces.FaceStride + val end = v + HologramFaces.FaceStride + while (v < end) { + t.addVertexWithUV(hx + HologramFaces.faceVertices(v), hy + HologramFaces.faceVertices(v + 1), hz + HologramFaces.faceVertices(v + 2), HologramFaces.faceVertices(v + 3), HologramFaces.faceVertices(v + 4)) + v += 5 + } + i += 1 + } } - - @SubscribeEvent - def onTick(e: ClientTickEvent) = cache.cleanUp() } diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/HologramRendererFallback.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/HologramRendererFallback.scala index ecae894dde..d44720f221 100644 --- a/src/main/scala/li/cil/oc/client/renderer/tileentity/HologramRendererFallback.scala +++ b/src/main/scala/li/cil/oc/client/renderer/tileentity/HologramRendererFallback.scala @@ -7,10 +7,10 @@ import net.minecraft.tileentity.TileEntity import org.lwjgl.opengl.GL11 object HologramRendererFallback extends TileEntitySpecialRenderer { - var text = "Requires OpenGL 1.5" + private val text = "Requires OpenGL 1.5" override def renderTileEntityAt(te: TileEntity, x: Double, y: Double, z: Double, f: Float) { - RenderState.checkError(getClass.getName + ".renderTileEntityAt: entering (aka: wasntme)") + RenderState.checkError(this, ".renderTileEntityAt: entering (aka: wasntme)") val fontRenderer = Minecraft.getMinecraft.fontRenderer @@ -23,6 +23,6 @@ object HologramRendererFallback extends TileEntitySpecialRenderer { GL11.glPopMatrix() - RenderState.checkError(getClass.getName + ".renderTileEntityAt: leaving") + RenderState.checkError(this, ".renderTileEntityAt: leaving") } } diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/MeshBackend.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/MeshBackend.scala new file mode 100644 index 0000000000..42a3b39ec6 --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/tileentity/MeshBackend.scala @@ -0,0 +1,19 @@ +package li.cil.oc.client.renderer.tileentity + +import li.cil.oc.integration.angelica.AngelicaMeshBackend +import li.cil.oc.integration.angelica.AngelicaTesr +import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer + +trait MeshBackend { + def overlayRenderer(overlay: Overlay): TileEntitySpecialRenderer + + def drawRobotPiece(top: Boolean): Unit + + def drawRobotLight(step: Int, red: Float, green: Float, blue: Float): Unit + + def drawUpIndicator(): Unit +} + +object MeshBackend { + val active: MeshBackend = if (AngelicaTesr.Batching) AngelicaMeshBackend else StandaloneMeshBackend +} diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/MicrocontrollerRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/MicrocontrollerRenderer.scala deleted file mode 100644 index d54393de0b..0000000000 --- a/src/main/scala/li/cil/oc/client/renderer/tileentity/MicrocontrollerRenderer.scala +++ /dev/null @@ -1,72 +0,0 @@ -package li.cil.oc.client.renderer.tileentity - -import li.cil.oc.client.Textures -import li.cil.oc.common.tileentity.Microcontroller -import li.cil.oc.util.RenderState -import net.minecraft.client.renderer.Tessellator -import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer -import net.minecraft.tileentity.TileEntity -import net.minecraft.util.ResourceLocation -import net.minecraftforge.common.util.ForgeDirection -import org.lwjgl.opengl.GL11 - -object MicrocontrollerRenderer extends TileEntitySpecialRenderer { - override def renderTileEntityAt(tileEntity: TileEntity, x: Double, y: Double, z: Double, f: Float) { - RenderState.checkError(getClass.getName + ".renderTileEntityAt: entering (aka: wasntme)") - - val mcu = tileEntity.asInstanceOf[Microcontroller] - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) - - RenderState.disableLighting() - RenderState.makeItBlend() - RenderState.setBlendAlpha(1) - - GL11.glPushMatrix() - - GL11.glTranslated(x + 0.5, y + 0.5, z + 0.5) - - mcu.yaw match { - case ForgeDirection.WEST => GL11.glRotatef(-90, 0, 1, 0) - case ForgeDirection.NORTH => GL11.glRotatef(180, 0, 1, 0) - case ForgeDirection.EAST => GL11.glRotatef(90, 0, 1, 0) - case _ => // No yaw. - } - - GL11.glTranslated(-0.5, 0.5, 0.505) - GL11.glScalef(1, -1, 1) - - bindTexture(Textures.blockMicrocontrollerFrontLight) - val t = Tessellator.instance - t.startDrawingQuads() - t.addVertexWithUV(0, 1, 0, 0, 1) - t.addVertexWithUV(1, 1, 0, 1, 1) - t.addVertexWithUV(1, 0, 0, 1, 0) - t.addVertexWithUV(0, 0, 0, 0, 0) - t.draw() - - if (mcu.isRunning) { - renderFrontOverlay(Textures.blockMicrocontrollerFrontOn) - } - else if (mcu.hasErrored && RenderUtil.shouldShowErrorLight(mcu.hashCode)) { - renderFrontOverlay(Textures.blockMicrocontrollerFrontError) - } - - RenderState.enableLighting() - - GL11.glPopMatrix() - GL11.glPopAttrib() - - RenderState.checkError(getClass.getName + ".renderTileEntityAt: leaving") - } - - private def renderFrontOverlay(texture: ResourceLocation): Unit = { - bindTexture(texture) - val t = Tessellator.instance - t.startDrawingQuads() - t.addVertexWithUV(0, 1, 0, 0, 1) - t.addVertexWithUV(1, 1, 0, 1, 1) - t.addVertexWithUV(1, 0, 0, 1, 0) - t.addVertexWithUV(0, 0, 0, 0, 0) - t.draw() - } -} \ No newline at end of file diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/NetSplitterRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/NetSplitterRenderer.scala deleted file mode 100644 index b0eeca6ded..0000000000 --- a/src/main/scala/li/cil/oc/client/renderer/tileentity/NetSplitterRenderer.scala +++ /dev/null @@ -1,88 +0,0 @@ -package li.cil.oc.client.renderer.tileentity - -import li.cil.oc.client.Textures -import li.cil.oc.common.tileentity -import li.cil.oc.util.RenderState -import net.minecraft.client.renderer.Tessellator -import net.minecraft.client.renderer.texture.TextureMap -import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer -import net.minecraft.tileentity.TileEntity -import net.minecraftforge.common.util.ForgeDirection -import org.lwjgl.opengl.GL11 - -object NetSplitterRenderer extends TileEntitySpecialRenderer { - override def renderTileEntityAt(tileEntity: TileEntity, x: Double, y: Double, z: Double, f: Float) { - RenderState.checkError(getClass.getName + ".renderTileEntityAt: entering (aka: wasntme)") - - val splitter = tileEntity.asInstanceOf[tileentity.NetSplitter] - if (splitter.openSides.contains(!splitter.isInverted)) { - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) - - RenderState.disableLighting() - RenderState.makeItBlend() - - GL11.glPushMatrix() - - GL11.glTranslated(x + 0.5, y + 0.5, z + 0.5) - GL11.glScaled(1.0025, -1.0025, 1.0025) - GL11.glTranslatef(-0.5f, -0.5f, -0.5f) - - bindTexture(TextureMap.locationBlocksTexture) - val t = Tessellator.instance - t.startDrawingQuads() - - val sideActivity = Textures.NetSplitter.iconOn - - if (splitter.isSideOpen(ForgeDirection.DOWN)) { - t.addVertexWithUV(0, 1, 0, sideActivity.getMaxU, sideActivity.getMinV) - t.addVertexWithUV(1, 1, 0, sideActivity.getMinU, sideActivity.getMinV) - t.addVertexWithUV(1, 1, 1, sideActivity.getMinU, sideActivity.getMaxV) - t.addVertexWithUV(0, 1, 1, sideActivity.getMaxU, sideActivity.getMaxV) - } - - if (splitter.isSideOpen(ForgeDirection.UP)) { - t.addVertexWithUV(0, 0, 0, sideActivity.getMaxU, sideActivity.getMaxV) - t.addVertexWithUV(0, 0, 1, sideActivity.getMaxU, sideActivity.getMinV) - t.addVertexWithUV(1, 0, 1, sideActivity.getMinU, sideActivity.getMinV) - t.addVertexWithUV(1, 0, 0, sideActivity.getMinU, sideActivity.getMaxV) - } - - if (splitter.isSideOpen(ForgeDirection.NORTH)) { - t.addVertexWithUV(1, 1, 0, sideActivity.getMinU, sideActivity.getMaxV) - t.addVertexWithUV(0, 1, 0, sideActivity.getMaxU, sideActivity.getMaxV) - t.addVertexWithUV(0, 0, 0, sideActivity.getMaxU, sideActivity.getMinV) - t.addVertexWithUV(1, 0, 0, sideActivity.getMinU, sideActivity.getMinV) - } - - if (splitter.isSideOpen(ForgeDirection.SOUTH)) { - t.addVertexWithUV(0, 1, 1, sideActivity.getMinU, sideActivity.getMaxV) - t.addVertexWithUV(1, 1, 1, sideActivity.getMaxU, sideActivity.getMaxV) - t.addVertexWithUV(1, 0, 1, sideActivity.getMaxU, sideActivity.getMinV) - t.addVertexWithUV(0, 0, 1, sideActivity.getMinU, sideActivity.getMinV) - } - - if (splitter.isSideOpen(ForgeDirection.WEST)) { - t.addVertexWithUV(0, 1, 0, sideActivity.getMinU, sideActivity.getMaxV) - t.addVertexWithUV(0, 1, 1, sideActivity.getMaxU, sideActivity.getMaxV) - t.addVertexWithUV(0, 0, 1, sideActivity.getMaxU, sideActivity.getMinV) - t.addVertexWithUV(0, 0, 0, sideActivity.getMinU, sideActivity.getMinV) - } - - if (splitter.isSideOpen(ForgeDirection.EAST)) { - t.addVertexWithUV(1, 1, 1, sideActivity.getMinU, sideActivity.getMaxV) - t.addVertexWithUV(1, 1, 0, sideActivity.getMaxU, sideActivity.getMaxV) - t.addVertexWithUV(1, 0, 0, sideActivity.getMaxU, sideActivity.getMinV) - t.addVertexWithUV(1, 0, 1, sideActivity.getMinU, sideActivity.getMinV) - } - - t.draw() - - RenderState.enableLighting() - - GL11.glPopMatrix() - GL11.glPopAttrib() - } - - RenderState.checkError(getClass.getName + ".renderTileEntityAt: leaving") - } -} diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/Overlay.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/Overlay.scala new file mode 100644 index 0000000000..8f26ed5b3e --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/tileentity/Overlay.scala @@ -0,0 +1,21 @@ +package li.cil.oc.client.renderer.tileentity + +import net.minecraft.client.renderer.Tessellator +import net.minecraft.tileentity.TileEntity +import net.minecraft.util.ResourceLocation +import org.joml.Matrix4fc + +trait OverlaySink { + def bucket(texture: ResourceLocation): Tessellator +} + +// animated: modelview, color or lightmap change on most visible frames, so the mesh is streamed instead of retained. +abstract class Overlay(val states: Int, val blendMode: Int, val animated: Boolean = false) { + def state(te: TileEntity): Int = 0 + + def alpha(te: TileEntity): Float = 1f + + def transform(te: TileEntity): Matrix4fc + + def emit(sink: OverlaySink, state: Int): Unit +} diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/OverlayState.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/OverlayState.scala new file mode 100644 index 0000000000..ac66581157 --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/tileentity/OverlayState.scala @@ -0,0 +1,72 @@ +package li.cil.oc.client.renderer.tileentity + +object OverlayState { + final val NoMesh = -1 + + final val ComputerRunning = 1 + final val ComputerActivity = 2 + final val ComputerError = 4 + final val ComputerStates = 8 + + def computer(running: Boolean, activity: Boolean, errorLight: Boolean): Int = + if (running) { + if (activity) ComputerRunning | ComputerActivity else ComputerRunning + } + else if (errorLight) ComputerError + else NoMesh + + final val MicrocontrollerRunning = 1 + final val MicrocontrollerError = 2 + final val MicrocontrollerStates = 3 + + def microcontroller(running: Boolean, errorLight: Boolean): Int = + if (running) MicrocontrollerRunning + else if (errorLight) MicrocontrollerError + else 0 + + final val RaidSlotBits = 3 + final val RaidStates = 1 << 5 + + def raid(missingSlots: Int, activeSlot: Int): Int = { + val active = if (activeSlot >= 0 && activeSlot < RaidSlotBits) activeSlot + 1 else 0 + val packed = (missingSlots & ((1 << RaidSlotBits) - 1)) | active << RaidSlotBits + if (packed == 0) NoMesh else packed + } + + def raidState(presence: Array[Boolean], slots: Int, lastAccess: Long, active: Boolean): Int = { + var missing = 0 + var slot = 0 + while (slot < slots) { + if (!presence(slot)) missing |= 1 << slot + slot += 1 + } + val activeSlot = if (active) (lastAccess % slots).toInt else -1 + raid(missing, activeSlot) + } + + def raidMissingSlots(state: Int): Int = state & ((1 << RaidSlotBits) - 1) + + def raidActiveSlot(state: Int): Int = (state >> RaidSlotBits) - 1 + + def raidActivityVisible(state: Int): Boolean = { + val active = raidActiveSlot(state) + active >= 0 && (raidMissingSlots(state) & (1 << active)) == 0 + } + + final val ChargerLevels = 15 + final val ChargerStates = 1 << 5 + + private final val ChargerLevelMask = 15 + private final val ChargerPowered = 16 + + def charger(chargeSpeed: Double, powered: Boolean): Int = + if (chargeSpeed <= 0) NoMesh + else { + val level = math.max(1, math.min(math.round(chargeSpeed * ChargerLevels).toInt, ChargerLevels)) + if (powered) level | ChargerPowered else level + } + + def chargerSpeed(state: Int): Double = (state & ChargerLevelMask) / ChargerLevels.toDouble + + def chargerPowered(state: Int): Boolean = (state & ChargerPowered) != 0 +} diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/Overlays.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/Overlays.scala new file mode 100644 index 0000000000..b8bebdb095 --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/tileentity/Overlays.scala @@ -0,0 +1,226 @@ +package li.cil.oc.client.renderer.tileentity + +import li.cil.oc.client.Textures +import li.cil.oc.client.renderer.Blink +import li.cil.oc.client.renderer.UnitQuad +import li.cil.oc.client.renderer.shading.Blend +import li.cil.oc.common.tileentity +import net.minecraft.client.renderer.Tessellator +import net.minecraft.client.renderer.texture.TextureMap +import net.minecraft.tileentity.TileEntity +import net.minecraftforge.common.util.ForgeDirection +import org.joml.Matrix4fc + +object Overlays { + private val Atlas = TextureMap.locationBlocksTexture + + private val Front = new FrontTransforms(0.505f) + + private val ChargerFront = new FrontTransforms(0.5f) + + private final val AllSides = CubeFaces.Walls | ForgeDirection.DOWN.flag | ForgeDirection.UP.flag + + val Geolyzer: Overlay = new Overlay(1, Blend.Additive) { + override def transform(te: TileEntity): Matrix4fc = BlockTransforms.Inflated + + override def emit(sink: OverlaySink, state: Int): Unit = CubeFaces.emitTop(sink.bucket(Atlas), Textures.Geolyzer.iconTopOn) + } + + val Case: Overlay = new Overlay(OverlayState.ComputerStates, Blend.Additive) { + override def state(te: TileEntity): Int = { + val computer = te.asInstanceOf[tileentity.Case] + val running = computer.isRunning + OverlayState.computer(running, + running && Blink.activity(computer.lastFileSystemAccess, computer.world.rand), + computer.hasErrored && Blink.shouldShowErrorLight(computer.hashCode)) + } + + override def transform(te: TileEntity): Matrix4fc = Front(te.asInstanceOf[tileentity.Case].yaw) + + override def emit(sink: OverlaySink, state: Int) { + if ((state & OverlayState.ComputerRunning) != 0) UnitQuad.emitFrontQuad(sink.bucket(Textures.blockCaseFrontOn)) + if ((state & OverlayState.ComputerActivity) != 0) UnitQuad.emitFrontQuad(sink.bucket(Textures.blockCaseFrontActivity)) + if ((state & OverlayState.ComputerError) != 0) UnitQuad.emitFrontQuad(sink.bucket(Textures.blockCaseFrontError)) + } + } + + val Microcontroller: Overlay = new Overlay(OverlayState.MicrocontrollerStates, Blend.Additive) { + override def state(te: TileEntity): Int = { + val mcu = te.asInstanceOf[tileentity.Microcontroller] + OverlayState.microcontroller(mcu.isRunning, mcu.hasErrored && Blink.shouldShowErrorLight(mcu.hashCode)) + } + + override def transform(te: TileEntity): Matrix4fc = Front(te.asInstanceOf[tileentity.Microcontroller].yaw) + + override def emit(sink: OverlaySink, state: Int) { + UnitQuad.emitFrontQuad(sink.bucket(Textures.blockMicrocontrollerFrontLight)) + if (state == OverlayState.MicrocontrollerRunning) UnitQuad.emitFrontQuad(sink.bucket(Textures.blockMicrocontrollerFrontOn)) + if (state == OverlayState.MicrocontrollerError) UnitQuad.emitFrontQuad(sink.bucket(Textures.blockMicrocontrollerFrontError)) + } + } + + val Raid: Overlay = new Overlay(OverlayState.RaidStates, Blend.Alpha) { + override def state(te: TileEntity): Int = { + val raid = te.asInstanceOf[tileentity.Raid] + val slots = math.min(raid.getSizeInventory, OverlayState.RaidSlotBits) + val active = slots > 0 && Blink.activity(raid.lastAccess, raid.world.rand) + OverlayState.raidState(raid.presence, slots, raid.lastAccess, active) + } + + override def transform(te: TileEntity): Matrix4fc = Front(te.asInstanceOf[tileentity.Raid].yaw) + + override def emit(sink: OverlaySink, state: Int) { + val missing = OverlayState.raidMissingSlots(state) + val activeSlot = OverlayState.raidActiveSlot(state) + if (missing != 0) { + val t = sink.bucket(Textures.blockRaidFrontError) + var slot = 0 + while (slot < OverlayState.RaidSlotBits) { + if ((missing & (1 << slot)) != 0) emitRaidSlot(t, slot) + slot += 1 + } + } + if (OverlayState.raidActivityVisible(state)) emitRaidSlot(sink.bucket(Textures.blockRaidFrontActivity), activeSlot) + } + } + + val Assembler: Overlay = new Overlay(2, Blend.Additive) { + override def state(te: TileEntity): Int = if (te.asInstanceOf[tileentity.Assembler].isAssembling) 1 else 0 + + override def transform(te: TileEntity): Matrix4fc = BlockTransforms.Centered + + override def emit(sink: OverlaySink, state: Int): Unit = emitAssembler(sink.bucket(Atlas), state != 0) + } + + val Charger: Overlay = new Overlay(OverlayState.ChargerStates, Blend.Additive) { + override def state(te: TileEntity): Int = { + val charger = te.asInstanceOf[tileentity.Charger] + OverlayState.charger(charger.chargeSpeed, charger.hasPower) + } + + override def transform(te: TileEntity): Matrix4fc = ChargerFront(te.asInstanceOf[tileentity.Charger].yaw) + + override def emit(sink: OverlaySink, state: Int): Unit = + emitCharger(sink.bucket(Atlas), OverlayState.chargerSpeed(state), OverlayState.chargerPowered(state)) + } + + val Switch: Overlay = new Overlay(1, Blend.Additive, animated = true) { + override def alpha(te: TileEntity): Float = + 1 - (System.currentTimeMillis() - te.asInstanceOf[tileentity.traits.SwitchLike].lastMessage) / 1000f + + override def transform(te: TileEntity): Matrix4fc = BlockTransforms.Inflated + + override def emit(sink: OverlaySink, state: Int): Unit = CubeFaces.emitSides(sink.bucket(Atlas), Textures.Switch.iconSideActivity, CubeFaces.Walls) + } + + val Transposer: Overlay = new Overlay(1, Blend.Additive, animated = true) { + override def alpha(te: TileEntity): Float = + 1 - (System.currentTimeMillis() - te.asInstanceOf[tileentity.Transposer].lastOperation) / 1000f + + override def transform(te: TileEntity): Matrix4fc = BlockTransforms.Inflated + + override def emit(sink: OverlaySink, state: Int): Unit = CubeFaces.emitSides(sink.bucket(Atlas), Textures.Transposer.iconOn, AllSides) + } + + val PowerDistributor: Overlay = new Overlay(1, Blend.Additive) { + // Not animated: alpha is the glColor, not mesh data. + override def alpha(te: TileEntity): Float = { + val distributor = te.asInstanceOf[tileentity.PowerDistributor] + if (distributor.globalBuffer > 0) (distributor.globalBuffer / distributor.globalBufferSize).toFloat else 0f + } + + override def transform(te: TileEntity): Matrix4fc = BlockTransforms.Inflated + + override def emit(sink: OverlaySink, state: Int) { + val t = sink.bucket(Atlas) + CubeFaces.emitTop(t, Textures.PowerDistributor.iconTopOn) + CubeFaces.emitSides(t, Textures.PowerDistributor.iconSideOn, CubeFaces.Walls) + } + } + + val DiskDriveActivity: Overlay = new Overlay(1, Blend.Additive) { + override def state(te: TileEntity): Int = { + val drive = te.asInstanceOf[tileentity.DiskDrive] + if (Blink.activity(drive.lastAccess, drive.world.rand)) 0 else OverlayState.NoMesh + } + + override def transform(te: TileEntity): Matrix4fc = Front(te.asInstanceOf[tileentity.DiskDrive].yaw) + + override def emit(sink: OverlaySink, state: Int): Unit = UnitQuad.emitFrontQuad(sink.bucket(Textures.blockDiskDriveFrontActivity)) + } + + private final val RaidInset = 2 / 16f + + private final val RaidSlotWidth = 4 / 16f + + private def emitRaidSlot(t: Tessellator, slot: Int) { + val l = RaidInset + slot * RaidSlotWidth + val h = RaidInset + (slot + 1) * RaidSlotWidth + t.addVertexWithUV(l, 1, 0, l, 1) + t.addVertexWithUV(h, 1, 0, h, 1) + t.addVertexWithUV(h, 0, 0, h, 0) + t.addVertexWithUV(l, 0, 0, l, 0) + } + + private def emitQuarterTurned(t: Tessellator, turns: Int, x: Double, y: Double, z: Double, u: Double, v: Double) { + var rx = x + var rz = z + var i = 0 + while (i < turns) { + val nx = rz + rz = -rx + rx = nx + i += 1 + } + t.addVertexWithUV(rx, y, rz, u, v) + } + + private def emitAssembler(t: Tessellator, assembling: Boolean) { + CubeFaces.emitTop(t, Textures.Assembler.iconTopOn, -0.5, 0.55, -0.5, 0.5, 0.5) + + val indent = 6 / 16f + 0.005 + val working = Textures.Assembler.iconSideAssembling + val side = Textures.Assembler.iconSideOn + var turns = 0 + while (turns < 4) { + if (assembling) { + emitQuarterTurned(t, turns, indent, 0.5, -indent, working.getInterpolatedU((0.5 - indent) * 16), working.getMaxV) + emitQuarterTurned(t, turns, indent, 0.5, indent, working.getInterpolatedU((0.5 + indent) * 16), working.getMaxV) + emitQuarterTurned(t, turns, indent, -0.5, indent, working.getInterpolatedU((0.5 + indent) * 16), working.getMinV) + emitQuarterTurned(t, turns, indent, -0.5, -indent, working.getInterpolatedU((0.5 - indent) * 16), working.getMinV) + } + emitQuarterTurned(t, turns, 0.5005, 0.5, -0.5, side.getMinU, side.getMaxV) + emitQuarterTurned(t, turns, 0.5005, 0.5, 0.5, side.getMaxU, side.getMaxV) + emitQuarterTurned(t, turns, 0.5005, -0.5, 0.5, side.getMaxU, side.getMinV) + emitQuarterTurned(t, turns, 0.5005, -0.5, -0.5, side.getMinU, side.getMinV) + turns += 1 + } + } + + private def emitCharger(t: Tessellator, chargeSpeed: Double, hasPower: Boolean) { + val front = Textures.Charger.iconFrontCharging + val inverse = 1 - chargeSpeed + t.addVertexWithUV(0, 1, 0.005, front.getMinU, front.getMaxV) + t.addVertexWithUV(1, 1, 0.005, front.getMaxU, front.getMaxV) + t.addVertexWithUV(1, inverse, 0.005, front.getMaxU, front.getInterpolatedV(inverse * 16)) + t.addVertexWithUV(0, inverse, 0.005, front.getMinU, front.getInterpolatedV(inverse * 16)) + + if (hasPower) { + val side = Textures.Charger.iconSideCharging + t.addVertexWithUV(-0.005, 1, -1, side.getMinU, side.getMaxV) + t.addVertexWithUV(-0.005, 1, 0, side.getMaxU, side.getMaxV) + t.addVertexWithUV(-0.005, 0, 0, side.getMaxU, side.getMinV) + t.addVertexWithUV(-0.005, 0, -1, side.getMinU, side.getMinV) + + t.addVertexWithUV(1, 1, -1.005, side.getMinU, side.getMaxV) + t.addVertexWithUV(0, 1, -1.005, side.getMaxU, side.getMaxV) + t.addVertexWithUV(0, 0, -1.005, side.getMaxU, side.getMinV) + t.addVertexWithUV(1, 0, -1.005, side.getMinU, side.getMinV) + + t.addVertexWithUV(1.005, 1, 0, side.getMinU, side.getMaxV) + t.addVertexWithUV(1.005, 1, -1, side.getMaxU, side.getMaxV) + t.addVertexWithUV(1.005, 0, -1, side.getMaxU, side.getMinV) + t.addVertexWithUV(1.005, 0, 0, side.getMinU, side.getMinV) + } + } +} diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/PowerDistributorRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/PowerDistributorRenderer.scala deleted file mode 100644 index 88c3b1a351..0000000000 --- a/src/main/scala/li/cil/oc/client/renderer/tileentity/PowerDistributorRenderer.scala +++ /dev/null @@ -1,72 +0,0 @@ -package li.cil.oc.client.renderer.tileentity - -import li.cil.oc.client.Textures -import li.cil.oc.common.tileentity -import li.cil.oc.util.RenderState -import net.minecraft.client.renderer.Tessellator -import net.minecraft.client.renderer.texture.TextureMap -import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer -import net.minecraft.tileentity.TileEntity -import org.lwjgl.opengl.GL11 - -object PowerDistributorRenderer extends TileEntitySpecialRenderer { - override def renderTileEntityAt(tileEntity: TileEntity, x: Double, y: Double, z: Double, f: Float) { - RenderState.checkError(getClass.getName + ".renderTileEntityAt: entering (aka: wasntme)") - - val distributor = tileEntity.asInstanceOf[tileentity.PowerDistributor] - if (distributor.globalBuffer > 0) { - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) - - RenderState.disableLighting() - RenderState.makeItBlend() - RenderState.setBlendAlpha((distributor.globalBuffer / distributor.globalBufferSize).toFloat) - - GL11.glPushMatrix() - - GL11.glTranslated(x + 0.5, y + 0.5, z + 0.5) - GL11.glScaled(1.0025, -1.0025, 1.0025) - GL11.glTranslatef(-0.5f, -0.5f, -0.5f) - - bindTexture(TextureMap.locationBlocksTexture) - val t = Tessellator.instance - t.startDrawingQuads() - - val topOn = Textures.PowerDistributor.iconTopOn - t.addVertexWithUV(0, 0, 1, topOn.getMinU, topOn.getMaxV) - t.addVertexWithUV(1, 0, 1, topOn.getMaxU, topOn.getMaxV) - t.addVertexWithUV(1, 0, 0, topOn.getMaxU, topOn.getMinV) - t.addVertexWithUV(0, 0, 0, topOn.getMinU, topOn.getMinV) - - val sideOn = Textures.PowerDistributor.iconSideOn - t.addVertexWithUV(1, 1, 0, sideOn.getMinU, sideOn.getMaxV) - t.addVertexWithUV(0, 1, 0, sideOn.getMaxU, sideOn.getMaxV) - t.addVertexWithUV(0, 0, 0, sideOn.getMaxU, sideOn.getMinV) - t.addVertexWithUV(1, 0, 0, sideOn.getMinU, sideOn.getMinV) - - t.addVertexWithUV(0, 1, 1, sideOn.getMinU, sideOn.getMaxV) - t.addVertexWithUV(1, 1, 1, sideOn.getMaxU, sideOn.getMaxV) - t.addVertexWithUV(1, 0, 1, sideOn.getMaxU, sideOn.getMinV) - t.addVertexWithUV(0, 0, 1, sideOn.getMinU, sideOn.getMinV) - - t.addVertexWithUV(1, 1, 1, sideOn.getMinU, sideOn.getMaxV) - t.addVertexWithUV(1, 1, 0, sideOn.getMaxU, sideOn.getMaxV) - t.addVertexWithUV(1, 0, 0, sideOn.getMaxU, sideOn.getMinV) - t.addVertexWithUV(1, 0, 1, sideOn.getMinU, sideOn.getMinV) - - t.addVertexWithUV(0, 1, 0, sideOn.getMinU, sideOn.getMaxV) - t.addVertexWithUV(0, 1, 1, sideOn.getMaxU, sideOn.getMaxV) - t.addVertexWithUV(0, 0, 1, sideOn.getMaxU, sideOn.getMinV) - t.addVertexWithUV(0, 0, 0, sideOn.getMinU, sideOn.getMinV) - - t.draw() - - RenderState.enableLighting() - - GL11.glPopMatrix() - GL11.glPopAttrib() - } - - RenderState.checkError(getClass.getName + ".renderTileEntityAt: leaving") - } - -} diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/PrinterRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/PrinterRenderer.scala index fb62b431df..d82925217d 100644 --- a/src/main/scala/li/cil/oc/client/renderer/tileentity/PrinterRenderer.scala +++ b/src/main/scala/li/cil/oc/client/renderer/tileentity/PrinterRenderer.scala @@ -12,7 +12,7 @@ import org.lwjgl.opengl.GL11 object PrinterRenderer extends TileEntitySpecialRenderer { override def renderTileEntityAt(tileEntity: TileEntity, x: Double, y: Double, z: Double, f: Float) { - RenderState.checkError(getClass.getName + ".renderTileEntityAt: entering (aka: wasntme)") + RenderState.checkError(this, ".renderTileEntityAt: entering (aka: wasntme)") val printer = tileEntity.asInstanceOf[Printer] if (printer.data.stateOff.size > 0) { @@ -39,6 +39,6 @@ object PrinterRenderer extends TileEntitySpecialRenderer { GL11.glPopAttrib() } - RenderState.checkError(getClass.getName + ".renderTileEntityAt: leaving") + RenderState.checkError(this, ".renderTileEntityAt: leaving") } } \ No newline at end of file diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/RackRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/RackRenderer.scala index d4fe7b3cf2..d6db46f9c9 100644 --- a/src/main/scala/li/cil/oc/client/renderer/tileentity/RackRenderer.scala +++ b/src/main/scala/li/cil/oc/client/renderer/tileentity/RackRenderer.scala @@ -1,40 +1,35 @@ package li.cil.oc.client.renderer.tileentity import li.cil.oc.api.event.RackMountableRenderEvent +import li.cil.oc.client.renderer.shading.FixedFunction import li.cil.oc.common.tileentity.Rack import li.cil.oc.util.RenderState import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer import net.minecraft.tileentity.TileEntity import net.minecraftforge.common.MinecraftForge -import net.minecraftforge.common.util.ForgeDirection import org.lwjgl.opengl.GL11 object RackRenderer extends TileEntitySpecialRenderer { private final val vOffset = 2 / 16f private final val vSize = 3 / 16f + private val Front = new FrontTransforms((0.505 - 1 / 16f).toFloat) + override def renderTileEntityAt(tileEntity: TileEntity, x: Double, y: Double, z: Double, f: Float) = { - RenderState.checkError(getClass.getName + ".renderTileEntityAt: entering (aka: wasntme)") + RenderState.checkError(this, ".renderTileEntityAt: entering (aka: wasntme)") val rack = tileEntity.asInstanceOf[Rack] GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) GL11.glPushMatrix() - GL11.glTranslated(x + 0.5, y + 0.5, z + 0.5) - - rack.yaw match { - case ForgeDirection.WEST => GL11.glRotatef(-90, 0, 1, 0) - case ForgeDirection.NORTH => GL11.glRotatef(180, 0, 1, 0) - case ForgeDirection.EAST => GL11.glRotatef(90, 0, 1, 0) - case _ => // No yaw. - } - - GL11.glTranslated(-0.5, 0.5, 0.505 - 1 / 16f) - GL11.glScalef(1, -1, 1) + GL11.glTranslated(x, y, z) + FixedFunction.multMatrix(Front(rack.yaw)) // Note: we manually sync the rack inventory for this to work. - for (i <- 0 until rack.getSizeInventory) { + val slots = rack.getSizeInventory + var i = 0 + while (i < slots) { if (rack.getStackInSlot(i) != null) { GL11.glPushMatrix() GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) @@ -47,11 +42,12 @@ object RackRenderer extends TileEntitySpecialRenderer { GL11.glPopAttrib() GL11.glPopMatrix() } + i += 1 } GL11.glPopMatrix() GL11.glPopAttrib() - RenderState.checkError(getClass.getName + ".renderTileEntityAt: leaving") + RenderState.checkError(this, ".renderTileEntityAt: leaving") } } diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/RaidRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/RaidRenderer.scala deleted file mode 100644 index c6d1f62ea8..0000000000 --- a/src/main/scala/li/cil/oc/client/renderer/tileentity/RaidRenderer.scala +++ /dev/null @@ -1,70 +0,0 @@ -package li.cil.oc.client.renderer.tileentity - -import li.cil.oc.client.Textures -import li.cil.oc.common.tileentity.Raid -import li.cil.oc.util.RenderState -import net.minecraft.client.renderer.Tessellator -import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer -import net.minecraft.tileentity.TileEntity -import net.minecraftforge.common.util.ForgeDirection -import org.lwjgl.opengl.GL11 - -object RaidRenderer extends TileEntitySpecialRenderer { - override def renderTileEntityAt(tileEntity: TileEntity, x: Double, y: Double, z: Double, f: Float) = { - RenderState.checkError(getClass.getName + ".renderTileEntityAt: entering (aka: wasntme)") - - val raid = tileEntity.asInstanceOf[Raid] - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) - - RenderState.disableLighting() - RenderState.makeItBlend() - - GL11.glPushMatrix() - - GL11.glTranslated(x + 0.5, y + 0.5, z + 0.5) - - raid.yaw match { - case ForgeDirection.WEST => GL11.glRotatef(-90, 0, 1, 0) - case ForgeDirection.NORTH => GL11.glRotatef(180, 0, 1, 0) - case ForgeDirection.EAST => GL11.glRotatef(90, 0, 1, 0) - case _ => // No yaw. - } - - GL11.glTranslated(-0.5, 0.5, 0.505) - GL11.glScalef(1, -1, 1) - - for (slot <- 0 until raid.getSizeInventory) { - - if (!raid.presence(slot)) { - bindTexture(Textures.blockRaidFrontError) - renderSlot(slot) - } - else if (System.currentTimeMillis() - raid.lastAccess < 400 && raid.world.rand.nextDouble() > 0.1 && slot == raid.lastAccess % raid.getSizeInventory) { - bindTexture(Textures.blockRaidFrontActivity) - renderSlot(slot) - } - } - - RenderState.enableLighting() - - GL11.glPopMatrix() - GL11.glPopAttrib() - - RenderState.checkError(getClass.getName + ".renderTileEntityAt: leaving") - } - - private val u1 = 2 / 16f - private val fs = 4 / 16f - - private def renderSlot(slot: Int) { - val l = u1 + slot * fs - val h = u1 + (slot + 1) * fs - val t = Tessellator.instance - t.startDrawingQuads() - t.addVertexWithUV(l, 1, 0, l, 1) - t.addVertexWithUV(h, 1, 0, h, 1) - t.addVertexWithUV(h, 0, 0, h, 0) - t.addVertexWithUV(l, 0, 0, l, 0) - t.draw() - } -} diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/RenderUtil.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/RenderUtil.scala deleted file mode 100644 index 2a422ad4e6..0000000000 --- a/src/main/scala/li/cil/oc/client/renderer/tileentity/RenderUtil.scala +++ /dev/null @@ -1,9 +0,0 @@ -package li.cil.oc.client.renderer.tileentity - -object RenderUtil { - def shouldShowErrorLight(hash: Int): Boolean = { - val time = System.currentTimeMillis + hash - val timeSlice = time / 500 - timeSlice % 2 == 0 - } -} diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/RobotLight.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/RobotLight.scala new file mode 100644 index 0000000000..ca174d9a60 --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/tileentity/RobotLight.scala @@ -0,0 +1,7 @@ +package li.cil.oc.client.renderer.tileentity + +object RobotLight { + final val Steps = 16 + + def step(offset: Double): Int = math.min(((offset - math.floor(offset)) * Steps).toInt, Steps - 1) +} diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/RobotRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/RobotRenderer.scala index 589e5f6270..dba5b898fd 100644 --- a/src/main/scala/li/cil/oc/client/renderer/tileentity/RobotRenderer.scala +++ b/src/main/scala/li/cil/oc/client/renderer/tileentity/RobotRenderer.scala @@ -1,18 +1,20 @@ package li.cil.oc.client.renderer.tileentity import com.google.common.base.Strings +import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat +import java.util import li.cil.oc.OpenComputers import li.cil.oc.Settings import li.cil.oc.api.driver.item.UpgradeRenderer import li.cil.oc.api.driver.item.UpgradeRenderer.MountPointName import li.cil.oc.api.event.RobotRenderEvent import li.cil.oc.client.Textures +import li.cil.oc.client.renderer.CapturedMesh import li.cil.oc.common.EventHandler import li.cil.oc.common.tileentity import li.cil.oc.util.RenderState import net.minecraft.block.Block import net.minecraft.client.Minecraft -import net.minecraft.client.renderer.GLAllocation import net.minecraft.client.renderer.RenderBlocks import net.minecraft.client.renderer.Tessellator import net.minecraft.client.renderer.entity.RenderManager @@ -23,120 +25,163 @@ import net.minecraft.item.ItemBlock import net.minecraft.item.ItemStack import net.minecraft.tileentity.TileEntity import net.minecraft.util.EnumChatFormatting -import net.minecraft.util.Vec3 import net.minecraftforge.client.IItemRenderer.ItemRenderType._ import net.minecraftforge.client.IItemRenderer.ItemRendererHelper._ import net.minecraftforge.client.MinecraftForgeClient import net.minecraftforge.common.MinecraftForge -import net.minecraftforge.common.util.ForgeDirection import org.lwjgl.opengl.GL11 import org.lwjgl.opengl.GL12 -import scala.collection.convert.WrapAsJava._ -import scala.collection.mutable - object RobotRenderer extends TileEntitySpecialRenderer { - private val displayList = GLAllocation.generateDisplayLists(2) - - private val mountPoints = new Array[RobotRenderEvent.MountPoint](7) - - private val slotNameMapping = Map( - UpgradeRenderer.MountPointName.TopLeft -> 0, - UpgradeRenderer.MountPointName.TopRight -> 1, - UpgradeRenderer.MountPointName.TopBack -> 2, - UpgradeRenderer.MountPointName.BottomLeft -> 3, - UpgradeRenderer.MountPointName.BottomRight -> 4, - UpgradeRenderer.MountPointName.BottomBack -> 5, - UpgradeRenderer.MountPointName.BottomFront -> 6 + private val mountPointNames = Array( + UpgradeRenderer.MountPointName.TopLeft, + UpgradeRenderer.MountPointName.TopRight, + UpgradeRenderer.MountPointName.TopBack, + UpgradeRenderer.MountPointName.BottomLeft, + UpgradeRenderer.MountPointName.BottomRight, + UpgradeRenderer.MountPointName.BottomBack, + UpgradeRenderer.MountPointName.BottomFront ) - for ((name, index) <- slotNameMapping) { - mountPoints(index) = new RobotRenderEvent.MountPoint(name) + private val slotIndices = new util.HashMap[String, Integer]() + for (index <- mountPointNames.indices) { + slotIndices.put(mountPointNames(index), index) + } + + private final class RenderSlots { + val mountPoints = new Array[RobotRenderEvent.MountPoint](mountPointNames.length) + for (index <- mountPointNames.indices) { + mountPoints(index) = new RobotRenderEvent.MountPoint(mountPointNames(index)) + } + val availableSlots = new util.HashSet[String]() + val mountStacks = new Array[ItemStack](mountPoints.length) + val mountRenderers = new Array[UpgradeRenderer](mountPoints.length) + var wildcardStacks = new Array[ItemStack](4) + var wildcardRenderers = new Array[UpgradeRenderer](4) + var wildcardCount = 0 + + def clear() { + util.Arrays.fill(mountStacks.asInstanceOf[Array[AnyRef]], null) + util.Arrays.fill(mountRenderers.asInstanceOf[Array[AnyRef]], null) + util.Arrays.fill(wildcardStacks.asInstanceOf[Array[AnyRef]], 0, wildcardCount, null) + util.Arrays.fill(wildcardRenderers.asInstanceOf[Array[AnyRef]], 0, wildcardCount, null) + wildcardCount = 0 + } + } + + // Render thread only. Upgrade renderers and event handlers may render another robot, so state is per nesting depth. + private val slotPool = new util.ArrayList[RenderSlots]() + private var depth = 0 + + private def enter(): RenderSlots = { + if (depth == slotPool.size) slotPool.add(new RenderSlots) + val slots = slotPool.get(depth) + depth += 1 + slots + } + + private def exit() { + depth -= 1 } private val gap = 1.0f / 28.0f private val gt = 0.5f + gap private val gb = 0.5f - gap - private def normal(v: Vec3) { - val n = v.normalize() - GL11.glNormal3f(n.xCoord.toFloat, n.yCoord.toFloat, n.zCoord.toFloat) - } + private val size = 0.4f + private val l = 0.5f - size + private val h = 0.5f + size - def compileList() { - val t = Tessellator.instance + private def setNormal(t: Tessellator, x: Float, y: Float, z: Float) { + val il = 1f / math.sqrt(x * x + y * y + z * z).toFloat + t.setNormal(x * il, y * il, z * il) + } - val size = 0.4f - val l = 0.5f - size - val h = 0.5f + size + // Degenerate quad: the captured chassis mesh is drawn as GL_QUADS. + private def fanQuad(t: Tessellator, apexY: Float, apexU: Float, + nx: Float, ny: Float, nz: Float, + x1: Float, y1: Float, z1: Float, u1: Float, v1: Float, + x2: Float, y2: Float, z2: Float, u2: Float, v2: Float) { + setNormal(t, nx, ny, nz) + t.addVertexWithUV(0.5f, apexY, 0.5f, apexU, 0.25) + t.addVertexWithUV(x1, y1, z1, u1, v1) + t.addVertexWithUV(x2, y2, z2, u2, v2) + t.addVertexWithUV(x2, y2, z2, u2, v2) + } - GL11.glNewList(displayList, GL11.GL_COMPILE) - - // This is _all_ deprecated old GL stuff... https://stackoverflow.com/a/13149580 - GL11.glBegin(GL11.GL_TRIANGLE_FAN) - GL11.glTexCoord2f(0.25f, 0.25f) - GL11.glVertex3f(0.5f, 1, 0.5f) - GL11.glTexCoord2f(0, 0.5f) - GL11.glVertex3f(l, gt, h) - // normal(Vec3.createVectorHelper(0, 0.2, 1)) - GL11.glTexCoord2f(0.5f, 0.5f) - GL11.glVertex3f(h, gt, h) - // normal(Vec3.createVectorHelper(1, 0.2, 0)) - GL11.glTexCoord2f(0.5f, 0) - GL11.glVertex3f(h, gt, l) - // normal(Vec3.createVectorHelper(0, 0.2, -1)) - GL11.glTexCoord2f(0, 0) - GL11.glVertex3f(l, gt, l) - // normal(Vec3.createVectorHelper(-1, 0.2, 0)) - GL11.glTexCoord2f(0, 0.5f) - GL11.glVertex3f(l, gt, h) - GL11.glEnd() - - t.startDrawingQuads() + private def emitTopPiece(t: Tessellator) { + fanQuad(t, 1, 0.25f, 0, 0.2f, 1, l, gt, h, 0, 0.5f, h, gt, h, 0.5f, 0.5f) + fanQuad(t, 1, 0.25f, 1, 0.2f, 0, h, gt, h, 0.5f, 0.5f, h, gt, l, 0.5f, 0) + fanQuad(t, 1, 0.25f, 0, 0.2f, -1, h, gt, l, 0.5f, 0, l, gt, l, 0, 0) + fanQuad(t, 1, 0.25f, -1, 0.2f, 0, l, gt, l, 0, 0, l, gt, h, 0, 0.5f) t.setNormal(0, -1, 0) t.addVertexWithUV(l, gt, h, 0, 1) t.addVertexWithUV(l, gt, l, 0, 0.5) t.addVertexWithUV(h, gt, l, 0.5, 0.5) t.addVertexWithUV(h, gt, h, 0.5, 1) - t.draw() - - GL11.glEndList() - - GL11.glNewList(displayList + 1, GL11.GL_COMPILE) - - GL11.glBegin(GL11.GL_TRIANGLE_FAN) - GL11.glTexCoord2f(0.75f, 0.25f) - GL11.glVertex3f(0.5f, 0.03f, 0.5f) - GL11.glTexCoord2f(0.5f, 0) - GL11.glVertex3f(l, gb, l) - // normal(Vec3.createVectorHelper(0, -0.2, 1)) - GL11.glTexCoord2f(1, 0) - GL11.glVertex3f(h, gb, l) - // normal(Vec3.createVectorHelper(1, -0.2, 0)) - GL11.glTexCoord2f(1, 0.5f) - GL11.glVertex3f(h, gb, h) - // normal(Vec3.createVectorHelper(0, -0.2, -1)) - GL11.glTexCoord2f(0.5f, 0.5f) - GL11.glVertex3f(l, gb, h) - // normal(Vec3.createVectorHelper(-1, -0.2, 0)) - GL11.glTexCoord2f(0.5f, 0) - GL11.glVertex3f(l, gb, l) - GL11.glEnd() - - t.startDrawingQuads() + } + + private def emitBottomPiece(t: Tessellator) { + fanQuad(t, 0.03f, 0.75f, 0, -0.2f, -1, l, gb, l, 0.5f, 0, h, gb, l, 1, 0) + fanQuad(t, 0.03f, 0.75f, 1, -0.2f, 0, h, gb, l, 1, 0, h, gb, h, 1, 0.5f) + fanQuad(t, 0.03f, 0.75f, 0, -0.2f, 1, h, gb, h, 1, 0.5f, l, gb, h, 0.5f, 0.5f) + fanQuad(t, 0.03f, 0.75f, -1, -0.2f, 0, l, gb, h, 0.5f, 0.5f, l, gb, l, 0.5f, 0) t.setNormal(0, 1, 0) t.addVertexWithUV(l, gb, l, 0, 0.5) t.addVertexWithUV(l, gb, h, 0, 1) t.addVertexWithUV(h, gb, h, 0.5, 1) t.addVertexWithUV(h, gb, l, 0.5, 0.5) - t.draw() + } - GL11.glEndList() + private val pieces = new Array[CapturedMesh](2) + + private[tileentity] def pieceMesh(top: Boolean): CapturedMesh = { + var mesh = pieces(if (top) 1 else 0) + if (mesh == null) { + mesh = CapturedMesh.build(DefaultVertexFormat.POSITION_TEXTURE_NORMAL, Textures.blockRobot)(emitPiece(_, top)) + pieces(if (top) 1 else 0) = mesh + } + mesh } - compileList() + private def drawPiece(top: Boolean, robot: tileentity.Robot): Unit = + if (robot == null) pieceMesh(top).draw() + else MeshBackend.active.drawRobotPiece(top) + + def emitPiece(t: Tessellator, top: Boolean): Unit = if (top) emitTopPiece(t) else emitBottomPiece(t) + + def emitLight(t: Tessellator, step: Int) { + val size = 0.3f + val l = 0.5f - size + val h = 0.5f + size + val vStep = 1.0f / 32.0f + val u0 = 0.5f + val u1 = 1f + val v0 = 0.5f + step * vStep + val v1 = v0 + vStep + + t.addVertexWithUV(l, gt, l, u0, v0) + t.addVertexWithUV(l, gb, l, u0, v1) + t.addVertexWithUV(l, gb, h, u1, v1) + t.addVertexWithUV(l, gt, h, u1, v0) + + t.addVertexWithUV(l, gt, h, u0, v0) + t.addVertexWithUV(l, gb, h, u0, v1) + t.addVertexWithUV(h, gb, h, u1, v1) + t.addVertexWithUV(h, gt, h, u1, v0) + + t.addVertexWithUV(h, gt, h, u0, v0) + t.addVertexWithUV(h, gb, h, u0, v1) + t.addVertexWithUV(h, gb, l, u1, v1) + t.addVertexWithUV(h, gt, l, u1, v0) + + t.addVertexWithUV(h, gt, l, u0, v0) + t.addVertexWithUV(h, gb, l, u0, v1) + t.addVertexWithUV(l, gb, l, u1, v1) + t.addVertexWithUV(l, gt, l, u1, v0) + } - def resetMountPoints(running: Boolean) { + private def resetMountPoints(mountPoints: Array[RobotRenderEvent.MountPoint], running: Boolean) { val offset = if (running) 0 else -0.06f // Left top. @@ -204,88 +249,87 @@ object RobotRenderer extends TileEntitySpecialRenderer { } def renderChassis(robot: tileentity.Robot = null, offset: Double = 0, isRunningOverride: Boolean = false) { - val isRunning = if (robot == null) isRunningOverride else robot.isRunning - - val size = 0.3f - val l = 0.5f - size - val h = 0.5f + size - val vStep = 1.0f / 32.0f - - val offsetV = ((offset - offset.toInt) * 16).toInt * vStep - val (u0, u1, v0, v1) = { - if (isRunning) - (0.5f, 1f, 0.5f + offsetV, 0.5f + vStep + offsetV) - else - (0.25f - vStep, 0.25f + vStep, 0.75f - vStep, 0.75f + vStep) - } + val slots = enter() + try renderChassis(slots, robot, offset, isRunningOverride) + finally exit() + } - resetMountPoints(robot != null && robot.isRunning) - val event = new RobotRenderEvent(robot, mountPoints) + private def renderChassis(slots: RenderSlots, robot: tileentity.Robot, offset: Double, isRunningOverride: Boolean) { + val isRunning = if (robot == null) isRunningOverride else robot.isRunning + resetMountPoints(slots.mountPoints, robot != null && robot.isRunning) + val event = new RobotRenderEvent(robot, slots.mountPoints) MinecraftForge.EVENT_BUS.post(event) if (!event.isCanceled) { bindTexture(Textures.blockRobot) + if (MinecraftForgeClient.getRenderPass > 0) { + if (!isRunning) GL11.glTranslatef(0, -4 * gap, 0) + return + } if (!isRunning) { GL11.glTranslatef(0, -2 * gap, 0) } - GL11.glCallList(displayList + 1) + + drawPiece(top = false, robot) if (!isRunning) { GL11.glTranslatef(0, -2 * gap, 0) } - if (MinecraftForgeClient.getRenderPass > 0) return - - GL11.glCallList(displayList) + drawPiece(top = true, robot) GL11.glColor3f(1, 1, 1) if (isRunning) { - RenderState.disableLighting() - - // Additive blending for the light. - GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE) - // Light color. val lightColor = if (robot != null && robot.info != null) robot.info.lightColor else 0xF23030 - val r = ((lightColor >>> 16) & 0xFF).toByte - val g = ((lightColor >>> 8) & 0xFF).toByte - val b = ((lightColor >>> 0) & 0xFF).toByte - GL11.glColor3ub(r, g, b) - - val t = Tessellator.instance - t.startDrawingQuads() - t.addVertexWithUV(l, gt, l, u0, v0) - t.addVertexWithUV(l, gb, l, u0, v1) - t.addVertexWithUV(l, gb, h, u1, v1) - t.addVertexWithUV(l, gt, h, u1, v0) - - t.addVertexWithUV(l, gt, h, u0, v0) - t.addVertexWithUV(l, gb, h, u0, v1) - t.addVertexWithUV(h, gb, h, u1, v1) - t.addVertexWithUV(h, gt, h, u1, v0) - - t.addVertexWithUV(h, gt, h, u0, v0) - t.addVertexWithUV(h, gb, h, u0, v1) - t.addVertexWithUV(h, gb, l, u1, v1) - t.addVertexWithUV(h, gt, l, u1, v0) - - t.addVertexWithUV(h, gt, l, u0, v0) - t.addVertexWithUV(h, gb, l, u0, v1) - t.addVertexWithUV(l, gb, l, u1, v1) - t.addVertexWithUV(l, gt, l, u1, v0) - t.draw() - - RenderState.enableLighting() + val red = ((lightColor >>> 16) & 0xFF) / 255f + val green = ((lightColor >>> 8) & 0xFF) / 255f + val blue = (lightColor & 0xFF) / 255f + val step = RobotLight.step(offset) + + if (robot != null) MeshBackend.active.drawRobotLight(step, red, green, blue) + else { + RenderState.disableLighting() + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE) + GL11.glColor3f(red, green, blue) + val t = Tessellator.instance + t.startDrawingQuads() + emitLight(t, step) + t.draw() + RenderState.enableLighting() + } } } } + private def hasUpgradeRenderer(robot: tileentity.Robot): Boolean = { + val size = robot.getSizeInventory + hasUpgradeRenderer(robot, size - robot.componentCount, size) || + hasUpgradeRenderer(robot, 1, robot.info.containers.length + 1) + } + + private def hasUpgradeRenderer(robot: tileentity.Robot, from: Int, until: Int): Boolean = { + var slot = from + while (slot < until) { + val stack = robot.getStackInSlot(slot) + if (stack != null && stack.getItem.isInstanceOf[UpgradeRenderer]) return true + slot += 1 + } + false + } + override def renderTileEntityAt(entity: TileEntity, x: Double, y: Double, z: Double, f: Float) { - RenderState.checkError(getClass.getName + ".renderTileEntityAt: entering (aka: wasntme)") + val slots = enter() + try renderRobot(slots, entity, x, y, z, f) + finally exit() + } + + private def renderRobot(slots: RenderSlots, entity: TileEntity, x: Double, y: Double, z: Double, f: Float) { + RenderState.checkError(this, ".renderTileEntityAt: entering (aka: wasntme)") val proxy = entity.asInstanceOf[tileentity.RobotProxy] val robot = proxy.robot val worldTime = entity.getWorldObj.getTotalWorldTime + f GL11.glPushMatrix() - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) + GL11.glPushAttrib(RenderState.DrawAttribBits) GL11.glTranslated(x + 0.5, y + 0.5, z + 0.5) // If the move started while we were rendering and we have a reference to @@ -321,17 +365,12 @@ object RobotRenderer extends TileEntitySpecialRenderer { GL11.glRotated(90 * remaining, 0, robot.turnAxis, 0) } - robot.yaw match { - case ForgeDirection.WEST => GL11.glRotatef(-90, 0, 1, 0) - case ForgeDirection.NORTH => GL11.glRotatef(180, 0, 1, 0) - case ForgeDirection.EAST => GL11.glRotatef(90, 0, 1, 0) - case _ => // No yaw. - } + BlockTransforms.rotateYaw(robot.yaw) GL11.glTranslatef(-0.5f, -0.5f, -0.5f) val offset = timeJitter + worldTime / 20.0 - renderChassis(robot, offset) + renderChassis(slots, robot, offset, isRunningOverride = false) if (!robot.renderingErrored && x * x + y * y + z * z < 24 * 24) { Option(robot.getStackInSlot(0)) match { @@ -435,42 +474,78 @@ object RobotRenderer extends TileEntitySpecialRenderer { case _ => } - if (MinecraftForgeClient.getRenderPass == 0) { - lazy val availableSlots = slotNameMapping.keys.to[mutable.Set] - lazy val wildcardRenderers = mutable.Buffer.empty[(ItemStack, UpgradeRenderer)] - lazy val slotMapping = Array.fill(mountPoints.length)(null: (ItemStack, UpgradeRenderer)) - - val renderers = (robot.componentSlots ++ robot.containerSlots).map(robot.getStackInSlot). - collect { case stack if stack != null && stack.getItem.isInstanceOf[UpgradeRenderer] => (stack, stack.getItem.asInstanceOf[UpgradeRenderer]) } - - for ((stack, renderer) <- renderers) { - val preferredSlot = renderer.computePreferredMountPoint(stack, robot, availableSlots) - if (availableSlots.remove(preferredSlot)) { - slotMapping(slotNameMapping(preferredSlot)) = (stack, renderer) - } - else if (preferredSlot == MountPointName.Any) { - wildcardRenderers += ((stack, renderer)) - } + if (MinecraftForgeClient.getRenderPass == 0 && hasUpgradeRenderer(robot)) { + slots.availableSlots.clear() + var nameIndex = 0 + while (nameIndex < mountPointNames.length) { + slots.availableSlots.add(mountPointNames(nameIndex)) + nameIndex += 1 } + try { + val size = robot.getSizeInventory + var slot = size - robot.componentCount + var phase = 0 + var last = size + while (phase < 2) { + while (slot < last) { + val stack = robot.getStackInSlot(slot) + if (stack != null && stack.getItem.isInstanceOf[UpgradeRenderer]) { + val renderer = stack.getItem.asInstanceOf[UpgradeRenderer] + val preferredSlot = renderer.computePreferredMountPoint(stack, robot, slots.availableSlots) + if (slots.availableSlots.remove(preferredSlot)) { + val index = slotIndices.get(preferredSlot).intValue + slots.mountStacks(index) = stack + slots.mountRenderers(index) = renderer + } + else if (preferredSlot == MountPointName.Any) { + if (slots.wildcardCount == slots.wildcardStacks.length) { + slots.wildcardStacks = util.Arrays.copyOf(slots.wildcardStacks, slots.wildcardCount * 2) + slots.wildcardRenderers = util.Arrays.copyOf(slots.wildcardRenderers, slots.wildcardCount * 2) + } + slots.wildcardStacks(slots.wildcardCount) = stack + slots.wildcardRenderers(slots.wildcardCount) = renderer + slots.wildcardCount += 1 + } + } + slot += 1 + } + phase += 1 + slot = 1 + last = robot.info.containers.length + 1 + } - var firstEmpty = slotMapping.indexOf(null) - for (entry <- wildcardRenderers if firstEmpty >= 0) { - slotMapping(firstEmpty) = entry - firstEmpty = slotMapping.indexOf(null) - } + var firstEmpty = 0 + var wildcard = 0 + while (wildcard < slots.wildcardCount) { + while (firstEmpty < slots.mountStacks.length && slots.mountStacks(firstEmpty) != null) firstEmpty += 1 + if (firstEmpty >= slots.mountStacks.length) wildcard = slots.wildcardCount + else { + slots.mountStacks(firstEmpty) = slots.wildcardStacks(wildcard) + slots.mountRenderers(firstEmpty) = slots.wildcardRenderers(wildcard) + wildcard += 1 + } + } - for ((info, mountPoint) <- (slotMapping, mountPoints).zipped if info != null) try { - val (stack, renderer) = info - GL11.glPushMatrix() - GL11.glTranslatef(0.5f, 0.5f, 0.5f) - renderer.render(stack, mountPoint, robot, f) - GL11.glPopMatrix() - } - catch { - case e: Throwable => - OpenComputers.log.warn("Failed rendering equipped upgrade.", e) - robot.renderingErrored = true + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) + var mount = 0 + while (mount < slots.mountStacks.length) { + val stack = slots.mountStacks(mount) + if (stack != null) try { + GL11.glPushMatrix() + GL11.glTranslatef(0.5f, 0.5f, 0.5f) + slots.mountRenderers(mount).render(stack, slots.mountPoints(mount), robot, f) + GL11.glPopMatrix() + } + catch { + case e: Throwable => + OpenComputers.log.warn("Failed rendering equipped upgrade.", e) + robot.renderingErrored = true + } + mount += 1 + } + GL11.glPopAttrib() } + finally slots.clear() } } GL11.glPopMatrix() @@ -508,7 +583,7 @@ object RobotRenderer extends TileEntitySpecialRenderer { t.draw GL11.glEnable(GL11.GL_TEXTURE_2D) // For the font. - f.drawString((if (EventHandler.isItTime) EnumChatFormatting.OBFUSCATED.toString else "") + name, -halfWidth, 0, 0xFFFFFFFF) + f.drawString(if (EventHandler.isItTime) EnumChatFormatting.OBFUSCATED.toString + name else name, -halfWidth, 0, 0xFFFFFFFF) GL11.glDepthMask(true) GL11.glEnable(GL11.GL_LIGHTING) @@ -520,6 +595,6 @@ object RobotRenderer extends TileEntitySpecialRenderer { GL11.glPopMatrix() GL11.glPopAttrib() - RenderState.checkError(getClass.getName + ".renderTileEntityAt: leaving") + RenderState.checkError(this, ".renderTileEntityAt: leaving") } } diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/ScreenRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/ScreenRenderer.scala index efc9856d20..5beef41fdd 100644 --- a/src/main/scala/li/cil/oc/client/renderer/tileentity/ScreenRenderer.scala +++ b/src/main/scala/li/cil/oc/client/renderer/tileentity/ScreenRenderer.scala @@ -3,19 +3,15 @@ package li.cil.oc.client.renderer.tileentity import li.cil.oc.Constants import li.cil.oc.Settings import li.cil.oc.api -import li.cil.oc.client.Textures +import li.cil.oc.client.renderer.TextBufferRenderCache import li.cil.oc.common.tileentity.Screen import li.cil.oc.integration.util.Wrench import li.cil.oc.util.RenderState import net.minecraft.client.Minecraft -import net.minecraft.client.renderer.OpenGlHelper -import net.minecraft.client.renderer.Tessellator import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer import net.minecraft.tileentity.TileEntity import net.minecraftforge.common.util.ForgeDirection import org.lwjgl.opengl.GL11 -import org.lwjgl.opengl.GL14 -import org.lwjgl.opengl.GLContext object ScreenRenderer extends TileEntitySpecialRenderer { private val maxRenderDistanceSq = Settings.get.maxScreenTextRenderDistance * Settings.get.maxScreenTextRenderDistance @@ -31,14 +27,12 @@ object ScreenRenderer extends TileEntitySpecialRenderer { api.Items.get(Constants.BlockName.ScreenTier2), api.Items.get(Constants.BlockName.ScreenTier3)) - private val canUseBlendColor = GLContext.getCapabilities.OpenGL14 - // ----------------------------------------------------------------------- // // Rendering // ----------------------------------------------------------------------- // override def renderTileEntityAt(t: TileEntity, x: Double, y: Double, z: Double, f: Float) { - RenderState.checkError(getClass.getName + ".renderTileEntityAt: entering (aka: wasntme)") + RenderState.checkError(this, ".renderTileEntityAt: entering (aka: wasntme)") screen = t.asInstanceOf[Screen] if (!screen.isOrigin) { @@ -57,61 +51,43 @@ object ScreenRenderer extends TileEntitySpecialRenderer { return } - RenderState.checkError(getClass.getName + ".renderTileEntityAt: checks") - - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) - - OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 0xFF, 0xFF) - RenderState.disableLighting() - RenderState.makeItBlend() + RenderState.checkError(this, ".renderTileEntityAt: checks") GL11.glPushMatrix() - GL11.glTranslated(x + 0.5, y + 0.5, z + 0.5) - RenderState.checkError(getClass.getName + ".renderTileEntityAt: setup") - - drawOverlay() - - RenderState.checkError(getClass.getName + ".renderTileEntityAt: overlay") - - if (distance > fadeDistanceSq) { - val alpha = math.max(0, 1 - ((distance - fadeDistanceSq) * fadeRatio).toFloat) - if (canUseBlendColor) { - GL14.glBlendColor(0, 0, 0, alpha) - GL11.glBlendFunc(GL11.GL_CONSTANT_ALPHA, GL11.GL_ONE) - } + if (showsUpIndicator) { + GL11.glPushMatrix() + transform() + GL11.glTranslatef(screen.width / 2f - 0.5f, screen.height / 2f - 0.5f, 0.05f) + MeshBackend.active.drawUpIndicator() + GL11.glPopMatrix() } - RenderState.checkError(getClass.getName + ".renderTileEntityAt: fade") - if (screen.buffer.isRenderingEnabled) { + TextBufferRenderCache.worldFade = fadeAlpha(distance) + TextBufferRenderCache.inWorld = true val profiler = Minecraft.getMinecraft.mcProfiler profiler.startSection("opencomputers:screen_text") - draw() - profiler.endSection() + try draw() + finally { + TextBufferRenderCache.inWorld = false + profiler.endSection() + } } - RenderState.enableLighting() - GL11.glPopMatrix() - GL11.glPopAttrib() - RenderState.checkError(getClass.getName + ".renderTileEntityAt: leaving") + RenderState.checkError(this, ".renderTileEntityAt: leaving") } + private def fadeAlpha(distance: Double): Float = + if (distance > fadeDistanceSq) math.max(0, 1 - ((distance - fadeDistanceSq) * fadeRatio).toFloat) + else 1f + private def transform() { - screen.yaw match { - case ForgeDirection.WEST => GL11.glRotatef(-90, 0, 1, 0) - case ForgeDirection.NORTH => GL11.glRotatef(180, 0, 1, 0) - case ForgeDirection.EAST => GL11.glRotatef(90, 0, 1, 0) - case _ => // No yaw. - } - screen.pitch match { - case ForgeDirection.DOWN => GL11.glRotatef(90, 1, 0, 0) - case ForgeDirection.UP => GL11.glRotatef(-90, 1, 0, 0) - case _ => // No pitch. - } + BlockTransforms.rotateYaw(screen.yaw) + BlockTransforms.rotatePitch(screen.pitch) // Fit area to screen (bottom left = bottom left). GL11.glTranslatef(-0.5f, -0.5f, 0.5f) @@ -121,31 +97,14 @@ object ScreenRenderer extends TileEntitySpecialRenderer { GL11.glScalef(1, -1, 1) } - private def drawOverlay() = if (screen.facing == ForgeDirection.UP || screen.facing == ForgeDirection.DOWN) { - // Show up vector overlay when holding same screen block. - val stack = Minecraft.getMinecraft.thePlayer.getHeldItem - if (stack != null) { - if (Wrench.holdsApplicableWrench(Minecraft.getMinecraft.thePlayer, screen.position) || screens.contains(api.Items.get(stack))) { - GL11.glPushMatrix() - transform() - bindTexture(Textures.blockScreenUpIndicator) - GL11.glDepthMask(false) - GL11.glTranslatef(screen.width / 2f - 0.5f, screen.height / 2f - 0.5f, 0.05f) - val t = Tessellator.instance - t.startDrawingQuads() - t.addVertexWithUV(0, 1, 0, 0, 1) - t.addVertexWithUV(1, 1, 0, 1, 1) - t.addVertexWithUV(1, 0, 0, 1, 0) - t.addVertexWithUV(0, 0, 0, 0, 0) - t.draw() - GL11.glDepthMask(true) - GL11.glPopMatrix() - } - } + private def showsUpIndicator: Boolean = (screen.facing == ForgeDirection.UP || screen.facing == ForgeDirection.DOWN) && { + val player = Minecraft.getMinecraft.thePlayer + val stack = player.getHeldItem + stack != null && (Wrench.holdsApplicableWrench(player, screen.position) || screens.contains(api.Items.get(stack))) } private def draw() { - RenderState.checkError(getClass.getName + ".draw: entering (aka: wasntme)") + RenderState.checkError(this, ".draw: entering (aka: wasntme)") val sx = screen.width val sy = screen.height @@ -184,12 +143,12 @@ object ScreenRenderer extends TileEntitySpecialRenderer { // Slightly offset the text so it doesn't clip into the screen. GL11.glTranslated(0, 0, 0.01) - RenderState.checkError(getClass.getName + ".draw: setup") + RenderState.checkError(this, ".draw: setup") // Render the actual text. screen.buffer.renderText() - RenderState.checkError(getClass.getName + ".draw: text") + RenderState.checkError(this, ".draw: text") } private def playerDistanceSq() = { diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/StandaloneHologramMesh.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/StandaloneHologramMesh.scala new file mode 100644 index 0000000000..c1e86b0063 --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/tileentity/StandaloneHologramMesh.scala @@ -0,0 +1,84 @@ +package li.cil.oc.client.renderer.tileentity + +import java.util.concurrent.TimeUnit + +import com.google.common.cache.CacheBuilder +import com.google.common.cache.RemovalListener +import com.google.common.cache.RemovalNotification +import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat +import cpw.mods.fml.common.FMLCommonHandler +import cpw.mods.fml.common.eventhandler.SubscribeEvent +import cpw.mods.fml.common.gameevent.TickEvent.ClientTickEvent +import li.cil.oc.client.Textures +import li.cil.oc.client.renderer.CapturedMesh +import li.cil.oc.client.renderer.shading.FixedFunction +import li.cil.oc.client.renderer.shading.MeshDraw +import li.cil.oc.client.renderer.shading.Programs +import li.cil.oc.common.tileentity.Hologram +import li.cil.oc.util.RenderState +import org.lwjgl.opengl.GL11 +import org.lwjgl.opengl.GL20 + +object StandaloneHologramMesh extends HologramMesh with RemovalListener[Hologram, CapturedMesh] { + private val cache = CacheBuilder.newBuilder(). + expireAfterAccess(5, TimeUnit.SECONDS). + removalListener(this). + build[Hologram, CapturedMesh]() + + override def init(): Unit = FMLCommonHandler.instance.bus.register(this) + + override def draw(hologram: Hologram, alpha: Float, x: Double, y: Double, z: Double, partialTicks: Float) { + var mesh = cache.getIfPresent(hologram) + if (mesh == null) { + mesh = new CapturedMesh + cache.put(hologram, mesh) + hologram.needsRendering = true + } + if (hologram.needsRendering) { + hologram.needsRendering = false + mesh.reset() + val count = HologramRenderer.collectFaces(hologram) + try HologramRenderer.emitFaces(mesh.bucket(DefaultVertexFormat.POSITION_TEXTURE_COLOR_NORMAL, Textures.blockHologram), hologram, count) + finally mesh.closeBucket() + } + + val program = GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM) + GL11.glPushAttrib(RenderState.DrawAttribBits) + GL11.glPushMatrix() + val inside = HologramRenderer.applyTransform(hologram, x, y, z, partialTicks) + + GL11.glEnable(GL11.GL_BLEND) + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE) + + // Seen from inside, the back faces are the only ones in view. + if (inside) GL11.glDisable(GL11.GL_CULL_FACE) + else GL11.glEnable(GL11.GL_CULL_FACE) + + if (Programs.fixedFunction(inWorld = true) || !MeshDraw.useLit(alpha, vertexColor = true)) { + GL11.glPushAttrib(RenderState.DrawAttribBits) + GL11.glEnable(GL11.GL_NORMALIZE) + FixedFunction.constantAlphaBlend(alpha) + drawPasses(mesh) + GL11.glPopAttrib() + } + else drawPasses(mesh) + + GL11.glPopMatrix() + GL11.glPopAttrib() + GL20.glUseProgram(program) + } + + private def drawPasses(mesh: CapturedMesh) { + GL11.glColorMask(false, false, false, false) + GL11.glDepthMask(true) + mesh.draw() + GL11.glColorMask(true, true, true, true) + GL11.glDepthFunc(GL11.GL_EQUAL) + mesh.draw() + } + + override def onRemoval(e: RemovalNotification[Hologram, CapturedMesh]): Unit = e.getValue.delete() + + @SubscribeEvent + def onTick(e: ClientTickEvent) = cache.cleanUp() +} diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/StandaloneMeshBackend.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/StandaloneMeshBackend.scala new file mode 100644 index 0000000000..2ca5f9a75e --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/tileentity/StandaloneMeshBackend.scala @@ -0,0 +1,63 @@ +package li.cil.oc.client.renderer.tileentity + +import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat +import cpw.mods.fml.common.eventhandler.SubscribeEvent +import li.cil.oc.client.Textures +import li.cil.oc.client.renderer.CapturedMesh +import li.cil.oc.client.renderer.UnitQuad +import li.cil.oc.client.renderer.shading.Blend +import li.cil.oc.client.renderer.shading.MeshDraw +import li.cil.oc.client.renderer.shading.Programs +import li.cil.oc.util.RenderState +import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer +import net.minecraftforge.client.event.TextureStitchEvent +import net.minecraftforge.common.MinecraftForge +import org.lwjgl.opengl.GL11 +import org.lwjgl.opengl.GL20 + +object StandaloneMeshBackend extends MeshBackend { + MinecraftForge.EVENT_BUS.register(this) + + var atlasGeneration = 0 + + @SubscribeEvent + def onTextureStitch(e: TextureStitchEvent.Post) { + atlasGeneration += 1 + } + + private lazy val upIndicator = + CapturedMesh.build(DefaultVertexFormat.POSITION_TEXTURE, Textures.blockScreenUpIndicator)(UnitQuad.emitFrontQuad) + + private val lightMeshes = new Array[CapturedMesh](RobotLight.Steps) + + override def overlayRenderer(overlay: Overlay): TileEntitySpecialRenderer = new StandaloneOverlayRenderer(overlay) + + override def drawRobotPiece(top: Boolean) { + val mesh = RobotRenderer.pieceMesh(top) + if (Programs.fixedFunction(inWorld = true)) mesh.draw() + else { + val program = GL11.glGetInteger(GL20.GL_CURRENT_PROGRAM) + GL11.glPushAttrib(RenderState.DrawAttribBits) + MeshDraw.useLit(1f, vertexColor = false) + mesh.draw() + GL11.glPopAttrib() + GL20.glUseProgram(program) + } + } + + override def drawRobotLight(step: Int, red: Float, green: Float, blue: Float) { + var mesh = lightMeshes(step) + if (mesh == null) { + mesh = CapturedMesh.build(DefaultVertexFormat.POSITION_TEXTURE, Textures.blockRobot)(RobotRenderer.emitLight(_, step)) + lightMeshes(step) = mesh + } + MeshDraw.drawUnlit(mesh, MeshDraw.Identity, Blend.Opaque, red, green, blue, 1, inWorld = true) + } + + override def drawUpIndicator() { + GL11.glPushAttrib(GL11.GL_DEPTH_BUFFER_BIT) + GL11.glDepthMask(false) + MeshDraw.drawUnlit(upIndicator, MeshDraw.Identity, Blend.Alpha, 1, 1, 1, 1, inWorld = true) + GL11.glPopAttrib() + } +} diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/StandaloneOverlayRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/StandaloneOverlayRenderer.scala new file mode 100644 index 0000000000..8e41e26b6e --- /dev/null +++ b/src/main/scala/li/cil/oc/client/renderer/tileentity/StandaloneOverlayRenderer.scala @@ -0,0 +1,61 @@ +package li.cil.oc.client.renderer.tileentity + +import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat +import li.cil.oc.client.renderer.CapturedMesh +import li.cil.oc.client.renderer.shading.MeshDraw +import net.minecraft.client.renderer.Tessellator +import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer +import net.minecraft.tileentity.TileEntity +import net.minecraft.util.ResourceLocation +import org.joml.Matrix4f + +object StandaloneOverlayRenderer { + private val model = new Matrix4f() +} + +final class StandaloneOverlayRenderer(overlay: Overlay) extends TileEntitySpecialRenderer { + private val meshes = new Array[CapturedMesh](overlay.states) + + private var atlasGeneration = -1 + + private object Sink extends OverlaySink { + var mesh: CapturedMesh = _ + + override def bucket(texture: ResourceLocation): Tessellator = mesh.bucket(DefaultVertexFormat.POSITION_TEXTURE, texture) + } + + override def renderTileEntityAt(te: TileEntity, x: Double, y: Double, z: Double, f: Float) { + val alpha = overlay.alpha(te) + if (alpha <= 0) return + val state = overlay.state(te) + if (state == OverlayState.NoMesh) return + MeshDraw.drawUnlit(mesh(state), StandaloneOverlayRenderer.model.translation(x.toFloat, y.toFloat, z.toFloat).mul(overlay.transform(te)), + overlay.blendMode, 1, 1, 1, alpha, inWorld = true) + } + + private def mesh(state: Int): CapturedMesh = { + if (atlasGeneration != StandaloneMeshBackend.atlasGeneration) { + atlasGeneration = StandaloneMeshBackend.atlasGeneration + var i = 0 + while (i < meshes.length) { + if (meshes(i) != null) { + meshes(i).delete() + meshes(i) = null + } + i += 1 + } + } + var mesh = meshes(state) + if (mesh == null) { + mesh = new CapturedMesh + Sink.mesh = mesh + try overlay.emit(Sink, state) + finally { + mesh.closeBucket() + Sink.mesh = null + } + meshes(state) = mesh + } + mesh + } +} diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/SwitchRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/SwitchRenderer.scala deleted file mode 100644 index b2fbe27b41..0000000000 --- a/src/main/scala/li/cil/oc/client/renderer/tileentity/SwitchRenderer.scala +++ /dev/null @@ -1,66 +0,0 @@ -package li.cil.oc.client.renderer.tileentity - -import li.cil.oc.client.Textures -import li.cil.oc.common.tileentity -import li.cil.oc.util.RenderState -import net.minecraft.client.renderer.Tessellator -import net.minecraft.client.renderer.texture.TextureMap -import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer -import net.minecraft.tileentity.TileEntity -import org.lwjgl.opengl.GL11 - -object SwitchRenderer extends TileEntitySpecialRenderer { - override def renderTileEntityAt(tileEntity: TileEntity, x: Double, y: Double, z: Double, f: Float) { - RenderState.checkError(getClass.getName + ".renderTileEntityAt: entering (aka: wasntme)") - - val switch = tileEntity.asInstanceOf[tileentity.traits.SwitchLike] - val activity = math.max(0, 1 - (System.currentTimeMillis() - switch.lastMessage) / 1000.0) - if (activity > 0) { - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) - - RenderState.disableLighting() - RenderState.makeItBlend() - RenderState.setBlendAlpha(activity.toFloat) - - GL11.glPushMatrix() - - GL11.glTranslated(x + 0.5, y + 0.5, z + 0.5) - GL11.glScaled(1.0025, -1.0025, 1.0025) - GL11.glTranslatef(-0.5f, -0.5f, -0.5f) - - bindTexture(TextureMap.locationBlocksTexture) - val t = Tessellator.instance - t.startDrawingQuads() - - val sideActivity = Textures.Switch.iconSideActivity - t.addVertexWithUV(1, 1, 0, sideActivity.getMinU, sideActivity.getMaxV) - t.addVertexWithUV(0, 1, 0, sideActivity.getMaxU, sideActivity.getMaxV) - t.addVertexWithUV(0, 0, 0, sideActivity.getMaxU, sideActivity.getMinV) - t.addVertexWithUV(1, 0, 0, sideActivity.getMinU, sideActivity.getMinV) - - t.addVertexWithUV(0, 1, 1, sideActivity.getMinU, sideActivity.getMaxV) - t.addVertexWithUV(1, 1, 1, sideActivity.getMaxU, sideActivity.getMaxV) - t.addVertexWithUV(1, 0, 1, sideActivity.getMaxU, sideActivity.getMinV) - t.addVertexWithUV(0, 0, 1, sideActivity.getMinU, sideActivity.getMinV) - - t.addVertexWithUV(1, 1, 1, sideActivity.getMinU, sideActivity.getMaxV) - t.addVertexWithUV(1, 1, 0, sideActivity.getMaxU, sideActivity.getMaxV) - t.addVertexWithUV(1, 0, 0, sideActivity.getMaxU, sideActivity.getMinV) - t.addVertexWithUV(1, 0, 1, sideActivity.getMinU, sideActivity.getMinV) - - t.addVertexWithUV(0, 1, 0, sideActivity.getMinU, sideActivity.getMaxV) - t.addVertexWithUV(0, 1, 1, sideActivity.getMaxU, sideActivity.getMaxV) - t.addVertexWithUV(0, 0, 1, sideActivity.getMaxU, sideActivity.getMinV) - t.addVertexWithUV(0, 0, 0, sideActivity.getMinU, sideActivity.getMinV) - - t.draw() - - RenderState.enableLighting() - - GL11.glPopMatrix() - GL11.glPopAttrib() - } - - RenderState.checkError(getClass.getName + ".renderTileEntityAt: leaving") - } -} diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/TransposerRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/TransposerRenderer.scala deleted file mode 100644 index ccd146088f..0000000000 --- a/src/main/scala/li/cil/oc/client/renderer/tileentity/TransposerRenderer.scala +++ /dev/null @@ -1,77 +0,0 @@ -package li.cil.oc.client.renderer.tileentity - -import li.cil.oc.client.Textures -import li.cil.oc.common.tileentity -import li.cil.oc.util.RenderState -import net.minecraft.client.renderer.Tessellator -import net.minecraft.client.renderer.texture.TextureMap -import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer -import net.minecraft.tileentity.TileEntity -import org.lwjgl.opengl.GL11 - -object TransposerRenderer extends TileEntitySpecialRenderer { - override def renderTileEntityAt(tileEntity: TileEntity, x: Double, y: Double, z: Double, f: Float) { - RenderState.checkError(getClass.getName + ".renderTileEntityAt: entering (aka: wasntme)") - - val transposer = tileEntity.asInstanceOf[tileentity.Transposer] - val activity = math.max(0, 1 - (System.currentTimeMillis() - transposer.lastOperation) / 1000.0) - if (activity > 0) { - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS) - - RenderState.disableLighting() - RenderState.makeItBlend() - RenderState.setBlendAlpha(activity.toFloat) - - GL11.glPushMatrix() - - GL11.glTranslated(x + 0.5, y + 0.5, z + 0.5) - GL11.glScaled(1.0025, -1.0025, 1.0025) - GL11.glTranslatef(-0.5f, -0.5f, -0.5f) - - bindTexture(TextureMap.locationBlocksTexture) - val t = Tessellator.instance - t.startDrawingQuads() - - val icon = Textures.Transposer.iconOn - - t.addVertexWithUV(0, 1, 0, icon.getMaxU, icon.getMinV) - t.addVertexWithUV(1, 1, 0, icon.getMinU, icon.getMinV) - t.addVertexWithUV(1, 1, 1, icon.getMinU, icon.getMaxV) - t.addVertexWithUV(0, 1, 1, icon.getMaxU, icon.getMaxV) - - t.addVertexWithUV(0, 0, 0, icon.getMaxU, icon.getMaxV) - t.addVertexWithUV(0, 0, 1, icon.getMaxU, icon.getMinV) - t.addVertexWithUV(1, 0, 1, icon.getMinU, icon.getMinV) - t.addVertexWithUV(1, 0, 0, icon.getMinU, icon.getMaxV) - - t.addVertexWithUV(1, 1, 0, icon.getMinU, icon.getMaxV) - t.addVertexWithUV(0, 1, 0, icon.getMaxU, icon.getMaxV) - t.addVertexWithUV(0, 0, 0, icon.getMaxU, icon.getMinV) - t.addVertexWithUV(1, 0, 0, icon.getMinU, icon.getMinV) - - t.addVertexWithUV(0, 1, 1, icon.getMinU, icon.getMaxV) - t.addVertexWithUV(1, 1, 1, icon.getMaxU, icon.getMaxV) - t.addVertexWithUV(1, 0, 1, icon.getMaxU, icon.getMinV) - t.addVertexWithUV(0, 0, 1, icon.getMinU, icon.getMinV) - - t.addVertexWithUV(0, 1, 0, icon.getMinU, icon.getMaxV) - t.addVertexWithUV(0, 1, 1, icon.getMaxU, icon.getMaxV) - t.addVertexWithUV(0, 0, 1, icon.getMaxU, icon.getMinV) - t.addVertexWithUV(0, 0, 0, icon.getMinU, icon.getMinV) - - t.addVertexWithUV(1, 1, 1, icon.getMinU, icon.getMaxV) - t.addVertexWithUV(1, 1, 0, icon.getMaxU, icon.getMaxV) - t.addVertexWithUV(1, 0, 0, icon.getMaxU, icon.getMinV) - t.addVertexWithUV(1, 0, 1, icon.getMinU, icon.getMinV) - - t.draw() - - RenderState.enableLighting() - - GL11.glPopMatrix() - GL11.glPopAttrib() - } - - RenderState.checkError(getClass.getName + ".renderTileEntityAt: leaving") - } -} diff --git a/src/main/scala/li/cil/oc/common/EventHandler.scala b/src/main/scala/li/cil/oc/common/EventHandler.scala index 4d1a5cd259..0f8268ac37 100644 --- a/src/main/scala/li/cil/oc/common/EventHandler.scala +++ b/src/main/scala/li/cil/oc/common/EventHandler.scala @@ -362,11 +362,22 @@ object EventHandler { (month == Calendar.DECEMBER && dayOfMonth == 14) } + // Next check time shifted left by one, with the cached result in the low bit; one field so threads never see a torn pair. + @volatile private var isItTimeState = 0L + + private final val IsItTimeInterval = 60000L + def isItTime = { - val now = Calendar.getInstance() - val month = now.get(Calendar.MONTH) - val dayOfMonth = now.get(Calendar.DAY_OF_MONTH) - month == Calendar.APRIL && dayOfMonth == 1 + val millis = System.currentTimeMillis() + val state = isItTimeState + val nextCheck = state >> 1 + if (millis >= nextCheck || millis < nextCheck - IsItTimeInterval) { + val now = Calendar.getInstance() + val result = now.get(Calendar.MONTH) == Calendar.APRIL && now.get(Calendar.DAY_OF_MONTH) == 1 + isItTimeState = ((millis + IsItTimeInterval) << 1) | (if (result) 1L else 0L) + result + } + else (state & 1L) != 0L } private def recraft(e: ItemCraftedEvent, item: ItemInfo, callback: ItemStack => Option[ItemStack]): Boolean = { diff --git a/src/main/scala/li/cil/oc/common/block/Adapter.scala b/src/main/scala/li/cil/oc/common/block/Adapter.scala index 673c3ac5f4..09f6c9bd00 100644 --- a/src/main/scala/li/cil/oc/common/block/Adapter.scala +++ b/src/main/scala/li/cil/oc/common/block/Adapter.scala @@ -39,6 +39,10 @@ class Adapter extends SimpleBlock with traits.GUI { override def createTileEntity(world: World, metadata: Int) = new tileentity.Adapter() + override def getRenderBlockPass = 1 + + override def canRenderInPass(pass: Int) = pass == 0 || pass == 1 + // ----------------------------------------------------------------------- // override def onNeighborBlockChange(world: World, x: Int, y: Int, z: Int, block: Block) = diff --git a/src/main/scala/li/cil/oc/common/block/Disassembler.scala b/src/main/scala/li/cil/oc/common/block/Disassembler.scala index aecf9fb00e..ecdcd94bae 100644 --- a/src/main/scala/li/cil/oc/common/block/Disassembler.scala +++ b/src/main/scala/li/cil/oc/common/block/Disassembler.scala @@ -41,4 +41,8 @@ class Disassembler extends SimpleBlock with traits.PowerAcceptor with traits.Sta override def hasTileEntity(metadata: Int) = true override def createTileEntity(world: World, metadata: Int) = new tileentity.Disassembler() + + override def getRenderBlockPass = 1 + + override def canRenderInPass(pass: Int) = pass == 0 || pass == 1 } diff --git a/src/main/scala/li/cil/oc/common/block/NetSplitter.scala b/src/main/scala/li/cil/oc/common/block/NetSplitter.scala index 6095927532..5fa399ae6f 100644 --- a/src/main/scala/li/cil/oc/common/block/NetSplitter.scala +++ b/src/main/scala/li/cil/oc/common/block/NetSplitter.scala @@ -35,6 +35,10 @@ class NetSplitter extends RedstoneAware { override def createTileEntity(world: World, metadata: Int) = new tileentity.NetSplitter() + override def getRenderBlockPass = 1 + + override def canRenderInPass(pass: Int) = pass == 0 || pass == 1 + // ----------------------------------------------------------------------- // override def onBlockActivated(world: World, x: Int, y: Int, z: Int, player: EntityPlayer, side: ForgeDirection, hitX: Float, hitY: Float, hitZ: Float) = { diff --git a/src/main/scala/li/cil/oc/common/component/TextBuffer.scala b/src/main/scala/li/cil/oc/common/component/TextBuffer.scala index 40a9edec68..2e3774f059 100644 --- a/src/main/scala/li/cil/oc/common/component/TextBuffer.scala +++ b/src/main/scala/li/cil/oc/common/component/TextBuffer.scala @@ -18,7 +18,7 @@ import li.cil.oc.api.network.EnvironmentHost import li.cil.oc.api.network._ import li.cil.oc.api.prefab import li.cil.oc.client.renderer.TextBufferRenderCache -import li.cil.oc.client.renderer.font.TextBufferRenderData +import li.cil.oc.client.renderer.text.TextBufferRenderData import li.cil.oc.client.{ComponentTracker => ClientComponentTracker} import li.cil.oc.client.{PacketSender => ClientPacketSender} import li.cil.oc.common._ @@ -634,13 +634,17 @@ object TextBuffer { wasDirty } - override def onBufferColorChange() { - markDirty() + override def markDirty() { + renderer.rows.markAll() + dirty = true } + override def onBufferColorChange() {} + override def onBufferCopy(col: Int, row: Int, w: Int, h: Int, tx: Int, ty: Int) { super.onBufferCopy(col, row, w, h, tx, ty) - markDirty() + renderer.rows.copy(row, h, ty) + dirty = true } override def onBufferDepthChange(depth: api.internal.TextBuffer.ColorDepth) { @@ -649,7 +653,8 @@ object TextBuffer { override def onBufferFill(col: Int, row: Int, w: Int, h: Int, c: Int) { super.onBufferFill(col, row, w, h, c) - markDirty() + renderer.rows.fill(row, h) + dirty = true } override def onBufferPaletteChange(index: Int) { @@ -668,12 +673,32 @@ object TextBuffer { override def onBufferSet(col: Int, row: Int, s: String, vertical: Boolean) { super.onBufferSet(col, row, s, vertical) - markDirty() + renderer.rows.set(row, s, vertical) + dirty = true } override def onBufferBitBlt(col: Int, row: Int, w: Int, h: Int, ram: component.GpuTextBuffer, fromCol: Int, fromRow: Int): Unit = { super.onBufferBitBlt(col, row, w, h, ram, fromCol, fromRow) - markDirty() + renderer.rows.bitBlt(row, h) + dirty = true + } + + override def onBufferRawSetText(col: Int, row: Int, text: Array[Array[Int]]) { + super.onBufferRawSetText(col, row, text) + renderer.rows.fill(row, text.length) + dirty = true + } + + override def onBufferRawSetBackground(col: Int, row: Int, color: Array[Array[Int]]) { + super.onBufferRawSetBackground(col, row, color) + renderer.rows.fill(row, color.length) + dirty = true + } + + override def onBufferRawSetForeground(col: Int, row: Int, color: Array[Array[Int]]) { + super.onBufferRawSetForeground(col, row, color) + renderer.rows.fill(row, color.length) + dirty = true } override def onBufferRamInit(ram: component.GpuTextBuffer): Unit = { diff --git a/src/main/scala/li/cil/oc/common/event/ExperienceUpgradeHandler.scala b/src/main/scala/li/cil/oc/common/event/ExperienceUpgradeHandler.scala index 272f5f48a7..7634eb81b7 100644 --- a/src/main/scala/li/cil/oc/common/event/ExperienceUpgradeHandler.scala +++ b/src/main/scala/li/cil/oc/common/event/ExperienceUpgradeHandler.scala @@ -69,12 +69,15 @@ object ExperienceUpgradeHandler { val level = e.agent match { case robot: Robot => var acc = 0 - for (index <- 0 until robot.getSizeInventory) { + val size = robot.getSizeInventory + var index = 0 + while (index < size) { robot.getComponentInSlot(index) match { case upgrade: component.UpgradeExperience => acc += upgrade.level case _ => } + index += 1 } acc case _ => 0 diff --git a/src/main/scala/li/cil/oc/common/event/RackMountableRenderHandler.scala b/src/main/scala/li/cil/oc/common/event/RackMountableRenderHandler.scala index 1c6c9b21f5..6b54658d83 100644 --- a/src/main/scala/li/cil/oc/common/event/RackMountableRenderHandler.scala +++ b/src/main/scala/li/cil/oc/common/event/RackMountableRenderHandler.scala @@ -5,7 +5,7 @@ import li.cil.oc.Constants import li.cil.oc.api import li.cil.oc.api.event.RackMountableRenderEvent import li.cil.oc.client.Textures -import li.cil.oc.client.renderer.tileentity.RenderUtil +import li.cil.oc.client.renderer.Blink import li.cil.oc.util.BlockPosition import li.cil.oc.util.ExtendedWorld._ import li.cil.oc.util.RenderState @@ -55,7 +55,7 @@ object RackMountableRenderHandler { } } - if (System.currentTimeMillis() - e.data.getLong("lastAccess") < 400 && e.rack.world.rand.nextDouble() > 0.1) { + if (Blink.activity(e.data.getLong("lastAccess"), e.rack.world.rand)) { RenderState.disableLighting() RenderState.makeItBlend() @@ -72,10 +72,10 @@ object RackMountableRenderHandler { if (e.data.getBoolean("isRunning")) { e.renderOverlay(Textures.blockRackServerOn) } - if (e.data.getBoolean("hasErrored") && RenderUtil.shouldShowErrorLight(e.rack.hashCode * (e.mountable + 1))) { + if (e.data.getBoolean("hasErrored") && Blink.shouldShowErrorLight(e.rack.hashCode * (e.mountable + 1))) { e.renderOverlay(Textures.blockRackServerError) } - if (System.currentTimeMillis() - e.data.getLong("lastFileSystemAccess") < 400 && e.rack.world.rand.nextDouble() > 0.1) { + if (Blink.activity(e.data.getLong("lastFileSystemAccess"), e.rack.world.rand)) { e.renderOverlay(Textures.blockRackServerActivity) } if ((System.currentTimeMillis() - e.data.getLong("lastNetworkActivity") < 300 && System.currentTimeMillis() % 200 > 100) && e.data.getBoolean("isRunning")) { diff --git a/src/main/scala/li/cil/oc/common/tileentity/Disassembler.scala b/src/main/scala/li/cil/oc/common/tileentity/Disassembler.scala index c6802fa97a..9101c41924 100644 --- a/src/main/scala/li/cil/oc/common/tileentity/Disassembler.scala +++ b/src/main/scala/li/cil/oc/common/tileentity/Disassembler.scala @@ -163,6 +163,7 @@ class Disassembler extends traits.Environment with traits.PowerAcceptor with tra override def readFromNBTForClient(nbt: NBTTagCompound) { super.readFromNBTForClient(nbt) isActive = nbt.getBoolean("isActive") + if (world != null) world.markBlockForUpdate(x, y, z) } override def writeToNBTForClient(nbt: NBTTagCompound) { diff --git a/src/main/scala/li/cil/oc/common/tileentity/Hologram.scala b/src/main/scala/li/cil/oc/common/tileentity/Hologram.scala index f3df5b053c..f817496edf 100644 --- a/src/main/scala/li/cil/oc/common/tileentity/Hologram.scala +++ b/src/main/scala/li/cil/oc/common/tileentity/Hologram.scala @@ -66,10 +66,6 @@ class Hologram(var tier: Int) extends traits.Environment with SidedEnvironment w // Whether we need to recompile our display list. var needsRendering = false - // Store it here for convenience, this is the number of visible voxel faces - // as determined in the last VBO index update. See HologramRenderer. - var visibleQuads = 0 - // What parts of the hologram changed and need an update packet. var dirty = mutable.Set.empty[Short] @@ -438,9 +434,9 @@ class Hologram(var tier: Int) extends traits.Environment with SidedEnvironment w override def shouldRenderInPass(pass: Int) = pass == 1 - override def getMaxRenderDistanceSquared = scale / Settings.get.hologramMaxScaleByTier.max * Settings.get.hologramRenderDistance * Settings.get.hologramRenderDistance + override def getMaxRenderDistanceSquared = scale / Settings.get.hologramMaxScale * Settings.get.hologramRenderDistance * Settings.get.hologramRenderDistance - def getFadeStartDistanceSquared = scale / Settings.get.hologramMaxScaleByTier.max * Settings.get.hologramFadeStartDistance * Settings.get.hologramFadeStartDistance + def getFadeStartDistanceSquared = scale / Settings.get.hologramMaxScale * Settings.get.hologramFadeStartDistance * Settings.get.hologramFadeStartDistance private final val Sqrt2 = Math.sqrt(2) override def getRenderBoundingBox = { diff --git a/src/main/scala/li/cil/oc/common/tileentity/Screen.scala b/src/main/scala/li/cil/oc/common/tileentity/Screen.scala index e6f8939afc..b2abf236ae 100644 --- a/src/main/scala/li/cil/oc/common/tileentity/Screen.scala +++ b/src/main/scala/li/cil/oc/common/tileentity/Screen.scala @@ -312,7 +312,7 @@ class Screen(var tier: Int) extends traits.TextBuffer with SidedEnvironment with @SideOnly(Side.CLIENT) override def getRenderBoundingBox = - if ((width == 1 && height == 1) || !isOrigin) super.getRenderBoundingBox + if (!isOrigin) super.getRenderBoundingBox else cachedBounds match { case Some(bounds) => bounds case _ => diff --git a/src/main/scala/li/cil/oc/common/tileentity/traits/OpenSides.scala b/src/main/scala/li/cil/oc/common/tileentity/traits/OpenSides.scala index bc41268a02..f95cab6bcc 100644 --- a/src/main/scala/li/cil/oc/common/tileentity/traits/OpenSides.scala +++ b/src/main/scala/li/cil/oc/common/tileentity/traits/OpenSides.scala @@ -40,6 +40,7 @@ trait OpenSides extends TileEntity { override def readFromNBTForClient(nbt: NBTTagCompound) { super.readFromNBTForClient(nbt) openSides = uncompressSides(nbt.getByte(Settings.namespace + "openSides")) + if (world != null) world.markBlockForUpdate(x, y, z) } override def writeToNBTForClient(nbt: NBTTagCompound) { diff --git a/src/main/scala/li/cil/oc/integration/Mods.scala b/src/main/scala/li/cil/oc/integration/Mods.scala index ac540e4aa7..c691dfc49b 100644 --- a/src/main/scala/li/cil/oc/integration/Mods.scala +++ b/src/main/scala/li/cil/oc/integration/Mods.scala @@ -173,6 +173,7 @@ object Mods { object IDs { final val AgriCraft = "AgriCraft" final val Ae2Fc = "ae2fc" + final val AngelicaTesr = "angelica|tesr" final val AppliedEnergistics2 = "appliedenergistics2" final val AvaritiaAddons = "avaritiaddons" final val BattleGear2 = "battlegear2" diff --git a/src/main/scala/li/cil/oc/integration/angelica/AngelicaHologramMesh.scala b/src/main/scala/li/cil/oc/integration/angelica/AngelicaHologramMesh.scala new file mode 100644 index 0000000000..3c68313846 --- /dev/null +++ b/src/main/scala/li/cil/oc/integration/angelica/AngelicaHologramMesh.scala @@ -0,0 +1,52 @@ +package li.cil.oc.integration.angelica + +import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat +import com.gtnewhorizons.angelica.api.tesr.TesrMaterial +import com.gtnewhorizons.angelica.api.tesr.TesrMeshBuilder +import com.gtnewhorizons.angelica.api.tesr.TesrMeshCache +import com.gtnewhorizons.angelica.api.tesr.TesrMeshSink +import li.cil.oc.client.Textures +import li.cil.oc.client.renderer.tileentity.HologramMesh +import li.cil.oc.client.renderer.tileentity.HologramRenderer +import li.cil.oc.common.tileentity.Hologram +import org.lwjgl.opengl.GL11 + +object AngelicaHologramMesh extends HologramMesh { + override def init() {} + + override def draw(hologram: Hologram, alpha: Float, x: Double, y: Double, z: Double, partialTicks: Float) { + GL11.glPushMatrix() + val inside = HologramRenderer.applyTransform(hologram, x, y, z, partialTicks) + + GL11.glColor4f(1, 1, 1, alpha) + + if (hologram.needsRendering) { + TesrMeshCache.invalidate(hologram) + hologram.needsRendering = false + } + HologramMeshBuilder.hologram = hologram + val cull = GL11.glIsEnabled(GL11.GL_CULL_FACE) + if (inside) GL11.glDisable(GL11.GL_CULL_FACE) + else GL11.glEnable(GL11.GL_CULL_FACE) + TesrMeshCache.renderCached(hologram, HologramMeshBuilder) + if (cull) GL11.glEnable(GL11.GL_CULL_FACE) + else GL11.glDisable(GL11.GL_CULL_FACE) + HologramMeshBuilder.hologram = null + GL11.glColor4f(1, 1, 1, 1) + + GL11.glPopMatrix() + } +} + +private object HologramMeshBuilder extends TesrMeshBuilder { + var hologram: Hologram = _ + + override def `angelica$build`(sink: TesrMeshSink): Unit = { + val count = HologramRenderer.collectFaces(hologram) + HologramRenderer.emitFaces(bucket(sink, AngelicaMaterials.HologramPrepass), hologram, count) + HologramRenderer.emitFaces(bucket(sink, AngelicaMaterials.HologramColor), hologram, count) + } + + private def bucket(sink: TesrMeshSink, material: TesrMaterial) = + sink.`angelica$bucket`(DefaultVertexFormat.POSITION_TEXTURE_COLOR_NORMAL, Textures.blockHologram, material) +} diff --git a/src/main/scala/li/cil/oc/integration/angelica/AngelicaMaterials.scala b/src/main/scala/li/cil/oc/integration/angelica/AngelicaMaterials.scala new file mode 100644 index 0000000000..e10535acff --- /dev/null +++ b/src/main/scala/li/cil/oc/integration/angelica/AngelicaMaterials.scala @@ -0,0 +1,57 @@ +package li.cil.oc.integration.angelica + +import com.gtnewhorizons.angelica.api.tesr.TesrMaterial +import com.gtnewhorizons.angelica.api.tesr.TesrShader +import com.gtnewhorizons.angelica.api.tesr.TesrShaders +import li.cil.oc.Settings +import li.cil.oc.client.renderer.shading.Blend +import li.cil.oc.client.renderer.shading.Programs +import net.minecraft.util.ResourceLocation + +object AngelicaMaterials { + // Requires API version 3+ + private def register(name: String): TesrShader = TesrShaders.register(Settings.resourceDomain + ":" + name, + new ResourceLocation(Settings.resourceDomain, "shaders/" + name + ".vert"), + new ResourceLocation(Settings.resourceDomain, "shaders/" + name + ".frag")) + + private lazy val Unlit = register("unlit") + + private lazy val Lit = register("lit") + + private def unlit(material: TesrMaterial.Builder): TesrMaterial.Builder = + if (AngelicaTesr.SourceShaders) material.shader(Unlit) else material + + private def lit(material: TesrMaterial.Builder): TesrMaterial.Builder = + if (AngelicaTesr.SourceShaders) material.shader(Lit) else material + + private val overlays = { + val table = new Array[TesrMaterial](Blend.Count * 2) + var blend = 0 + while (blend < Blend.Count) { + table(blend * 2) = buildOverlay(blend, animated = false) + table(blend * 2 + 1) = buildOverlay(blend, animated = true) + blend += 1 + } + table + } + + private def buildOverlay(blend: Int, animated: Boolean): TesrMaterial = { + val builder = TesrMaterial.builder().unlit().lightmap(240, 240).cutout(Programs.AlphaCutoff) + if (blend == Blend.Alpha) builder.translucent() + else if (blend == Blend.Additive) builder.additiveAlpha() + if (animated) builder.stream() + unlit(builder).build() + } + + def overlay(blend: Int, animated: Boolean): TesrMaterial = overlays(blend * 2 + (if (animated) 1 else 0)) + + val UpIndicator: TesrMaterial = unlit(TesrMaterial.builder().translucent().unlit().lightmap(240, 240).noDepthWrite().color(1, 1, 1, 1).cutout(Programs.AlphaCutoff)).build() + + val RobotLightMaterial: TesrMaterial = overlay(Blend.Opaque, animated = true) + + val RobotPiece: TesrMaterial = lit(TesrMaterial.builder().cutout(Programs.AlphaCutoff).stream()).build() + + lazy val HologramPrepass: TesrMaterial = lit(TesrMaterial.builder().depthOnly().cutout(Programs.AlphaCutoff).stream()).build() + + lazy val HologramColor: TesrMaterial = lit(TesrMaterial.builder().additiveAlpha().depthEqual().cutout(Programs.AlphaCutoff).stream()).build() +} diff --git a/src/main/scala/li/cil/oc/integration/angelica/AngelicaMeshBackend.scala b/src/main/scala/li/cil/oc/integration/angelica/AngelicaMeshBackend.scala new file mode 100644 index 0000000000..deb43667b6 --- /dev/null +++ b/src/main/scala/li/cil/oc/integration/angelica/AngelicaMeshBackend.scala @@ -0,0 +1,57 @@ +package li.cil.oc.integration.angelica + +import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat +import com.gtnewhorizons.angelica.api.tesr.TesrMeshBuilder +import com.gtnewhorizons.angelica.api.tesr.TesrMeshCache +import com.gtnewhorizons.angelica.api.tesr.TesrMeshSink +import li.cil.oc.client.Textures +import li.cil.oc.client.renderer.UnitQuad +import li.cil.oc.client.renderer.tileentity.MeshBackend +import li.cil.oc.client.renderer.tileentity.Overlay +import li.cil.oc.client.renderer.tileentity.RobotLight +import li.cil.oc.client.renderer.tileentity.RobotRenderer +import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer +import org.lwjgl.opengl.GL11 + +object AngelicaMeshBackend extends MeshBackend { + private val UpIndicatorKey = new Object + + private val RobotLightKeys = Array.fill[AnyRef](RobotLight.Steps)(new AnyRef) + + private val RobotPieceKeys = Array.fill[AnyRef](2)(new AnyRef) + + override def overlayRenderer(overlay: Overlay): TileEntitySpecialRenderer = new AngelicaOverlayRenderer(overlay) + + override def drawRobotPiece(top: Boolean) { + RobotPieceBuilder.top = top + TesrMeshCache.renderCached(RobotPieceKeys(if (top) 1 else 0), RobotPieceBuilder) + } + + override def drawRobotLight(step: Int, red: Float, green: Float, blue: Float) { + RobotLightBuilder.step = step + GL11.glColor4f(red, green, blue, 1) + TesrMeshCache.renderCached(RobotLightKeys(step), RobotLightBuilder) + GL11.glColor4f(1, 1, 1, 1) + } + + override def drawUpIndicator(): Unit = TesrMeshCache.renderCached(UpIndicatorKey, UpIndicatorBuilder) +} + +private object UpIndicatorBuilder extends TesrMeshBuilder { + override def `angelica$build`(sink: TesrMeshSink): Unit = + UnitQuad.emitFrontQuad(sink.`angelica$bucket`(DefaultVertexFormat.POSITION_TEXTURE, Textures.blockScreenUpIndicator, AngelicaMaterials.UpIndicator)) +} + +private object RobotLightBuilder extends TesrMeshBuilder { + var step = 0 + + override def `angelica$build`(sink: TesrMeshSink): Unit = + RobotRenderer.emitLight(sink.`angelica$bucket`(DefaultVertexFormat.POSITION_TEXTURE, Textures.blockRobot, AngelicaMaterials.RobotLightMaterial), step) +} + +private object RobotPieceBuilder extends TesrMeshBuilder { + var top = false + + override def `angelica$build`(sink: TesrMeshSink): Unit = + RobotRenderer.emitPiece(sink.`angelica$bucket`(DefaultVertexFormat.POSITION_TEXTURE_NORMAL, Textures.blockRobot, AngelicaMaterials.RobotPiece), top) +} diff --git a/src/main/scala/li/cil/oc/integration/angelica/AngelicaOverlayRenderer.scala b/src/main/scala/li/cil/oc/integration/angelica/AngelicaOverlayRenderer.scala new file mode 100644 index 0000000000..2d9fb28bf5 --- /dev/null +++ b/src/main/scala/li/cil/oc/integration/angelica/AngelicaOverlayRenderer.scala @@ -0,0 +1,58 @@ +package li.cil.oc.integration.angelica + +import com.gtnewhorizon.gtnhlib.client.renderer.vertex.DefaultVertexFormat +import com.gtnewhorizons.angelica.api.tesr.TesrMaterial +import com.gtnewhorizons.angelica.api.tesr.TesrMeshBuilder +import com.gtnewhorizons.angelica.api.tesr.TesrMeshCache +import com.gtnewhorizons.angelica.api.tesr.TesrMeshSink +import li.cil.oc.client.renderer.shading.FixedFunction +import li.cil.oc.client.renderer.tileentity.Overlay +import li.cil.oc.client.renderer.tileentity.OverlaySink +import li.cil.oc.client.renderer.tileentity.OverlayState +import net.minecraft.client.renderer.Tessellator +import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer +import net.minecraft.tileentity.TileEntity +import net.minecraft.util.ResourceLocation +import org.lwjgl.opengl.GL11 + +object AngelicaOverlayRenderer { + private object Sink extends OverlaySink { + var sink: TesrMeshSink = _ + + var material: TesrMaterial = _ + + override def bucket(texture: ResourceLocation): Tessellator = + sink.`angelica$bucket`(DefaultVertexFormat.POSITION_TEXTURE, texture, material) + } +} + +final class AngelicaOverlayRenderer(overlay: Overlay) extends TileEntitySpecialRenderer with TesrMeshBuilder { + import AngelicaOverlayRenderer.Sink + + private val keys = Array.fill[AnyRef](overlay.states)(new AnyRef) + + private val overlayMaterial = AngelicaMaterials.overlay(overlay.blendMode, overlay.animated) + + private var state = 0 + + override def `angelica$build`(sink: TesrMeshSink) { + Sink.sink = sink + Sink.material = overlayMaterial + try overlay.emit(Sink, state) + finally Sink.sink = null + } + + override def renderTileEntityAt(te: TileEntity, x: Double, y: Double, z: Double, f: Float) { + val alpha = overlay.alpha(te) + if (alpha <= 0) return + state = overlay.state(te) + if (state == OverlayState.NoMesh) return + GL11.glPushMatrix() + GL11.glTranslated(x, y, z) + FixedFunction.multMatrix(overlay.transform(te)) + GL11.glColor4f(1, 1, 1, alpha) + TesrMeshCache.renderCached(keys(state), this) + GL11.glColor4f(1, 1, 1, 1) + GL11.glPopMatrix() + } +} diff --git a/src/main/scala/li/cil/oc/integration/angelica/AngelicaTesr.scala b/src/main/scala/li/cil/oc/integration/angelica/AngelicaTesr.scala new file mode 100644 index 0000000000..4d6eb74fda --- /dev/null +++ b/src/main/scala/li/cil/oc/integration/angelica/AngelicaTesr.scala @@ -0,0 +1,26 @@ +package li.cil.oc.integration.angelica + +import cpw.mods.fml.common.ModAPIManager +import li.cil.oc.Settings +import li.cil.oc.integration.Mods + +object AngelicaTesr { + val apiVersion: Int = if (Settings.get.angelicaBatching) installedVersion(Mods.IDs.AngelicaTesr) else 0 + + val Batching: Boolean = apiVersion >= 1 + + val MeshReplacement: Boolean = apiVersion >= 3 + + val SourceShaders: Boolean = apiVersion >= 3 + + private def installedVersion(id: String): Int = { + val apis = ModAPIManager.INSTANCE.getAPIList.iterator + while (apis.hasNext) { + val api = apis.next() + if (api.getModId == id) return try api.getVersion.toInt catch { + case _: NumberFormatException => 0 + } + } + 0 + } +} diff --git a/src/main/scala/li/cil/oc/util/RenderState.scala b/src/main/scala/li/cil/oc/util/RenderState.scala index 3767df61c8..6050e068b4 100644 --- a/src/main/scala/li/cil/oc/util/RenderState.scala +++ b/src/main/scala/li/cil/oc/util/RenderState.scala @@ -3,28 +3,39 @@ package li.cil.oc.util import li.cil.oc.OpenComputers import li.cil.oc.Settings import net.minecraft.client.Minecraft +import net.minecraft.client.renderer.OpenGlHelper import net.minecraft.client.renderer.RenderHelper import org.lwjgl.opengl._ import org.lwjgl.util.glu.GLU object RenderState { - val arb = GLContext.getCapabilities.GL_ARB_multitexture && !GLContext.getCapabilities.OpenGL13 - - def checkError(where: String) { - if (Settings.get.logOpenGLErrors) { - val error = GL11.glGetError - if (error != 0) { - OpenComputers.log.warn("GL ERROR @ " + where + ": " + GLU.gluErrorString(error)) - } - } + final val DrawAttribBits = GL11.GL_ENABLE_BIT | GL11.GL_LIGHTING_BIT | GL11.GL_COLOR_BUFFER_BIT | GL11.GL_CURRENT_BIT | GL11.GL_DEPTH_BUFFER_BIT + + private var savedUnit = 0 + + private var savedTexture = 0 + + def saveTexture0() { + savedUnit = GL11.glGetInteger(GL13.GL_ACTIVE_TEXTURE) + OpenGlHelper.setActiveTexture(OpenGlHelper.defaultTexUnit) + savedTexture = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D) + } + + def restoreTexture0() { + OpenGlHelper.setActiveTexture(OpenGlHelper.defaultTexUnit) + GL11.glBindTexture(GL11.GL_TEXTURE_2D, savedTexture) + OpenGlHelper.setActiveTexture(savedUnit) + } + + def checkError(owner: AnyRef, stage: String) { + if (Settings.get.logOpenGLErrors) logError(owner.getClass.getName + stage) } - def compilingDisplayList = { - if (GL11.glGetInteger(GL11.GL_LIST_INDEX) != 0) { - val mode = GL11.glGetInteger(GL11.GL_LIST_MODE) - mode == GL11.GL_COMPILE || mode == GL11.GL_COMPILE_AND_EXECUTE + private def logError(where: String) { + val error = GL11.glGetError + if (error != 0) { + OpenComputers.log.warn("GL ERROR @ " + where + ": " + GLU.gluErrorString(error)) } - else false } def disableLighting() { @@ -41,9 +52,4 @@ object RenderState { GL11.glEnable(GL11.GL_BLEND) GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA) } - - def setBlendAlpha(alpha: Float) = { - GL11.glColor4f(1, 1, 1, alpha) - GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE) - } } diff --git a/src/test/scala/HologramFacesTest.scala b/src/test/scala/HologramFacesTest.scala new file mode 100644 index 0000000000..451b684bf4 --- /dev/null +++ b/src/test/scala/HologramFacesTest.scala @@ -0,0 +1,18 @@ +import li.cil.oc.client.renderer.tileentity.HologramFaces +import org.junit.runner.RunWith +import org.scalatest.FunSpec +import org.scalatest.Matchers.be +import org.scalatest.Matchers.convertToAnyShouldWrapper +import org.scalatest.junit.JUnitRunner + +@RunWith(classOf[JUnitRunner]) +class HologramFacesTest extends FunSpec { + describe("Hologram face packing") { + it("round-trips every field at its extents") { + for (x <- Seq(0, 47); y <- Seq(0, 31); z <- Seq(0, 47); face <- 0 until HologramFaces.FaceCount; value <- 1 to 3) { + val packed = HologramFaces.pack(x, y, z, face, value) + (HologramFaces.x(packed), HologramFaces.y(packed), HologramFaces.z(packed), HologramFaces.face(packed), HologramFaces.value(packed)) should be((x, y, z, face, value)) + } + } + } +} diff --git a/src/test/scala/HologramFadeTest.scala b/src/test/scala/HologramFadeTest.scala new file mode 100644 index 0000000000..56a9386f13 --- /dev/null +++ b/src/test/scala/HologramFadeTest.scala @@ -0,0 +1,18 @@ +import li.cil.oc.client.renderer.tileentity.HologramFade +import org.junit.runner.RunWith +import org.scalatest.FunSpec +import org.scalatest.Matchers.be +import org.scalatest.Matchers.convertToAnyShouldWrapper +import org.scalatest.junit.JUnitRunner + +@RunWith(classOf[JUnitRunner]) +class HologramFadeTest extends FunSpec { + describe("Hologram fade") { + it("stays at 0.75 without NaN when the fade start is not below the maximum") { + HologramFade.alpha(100, 4096, 4096) should be(0.75f) + HologramFade.alpha(5000, 4096, 4096) should be(0.75f) + HologramFade.alpha(5000, 6000, 4096) should be(0.75f) + HologramFade.alpha(0, 0, 0) should be(0.75f) + } + } +} diff --git a/src/test/scala/OverlayStateTest.scala b/src/test/scala/OverlayStateTest.scala new file mode 100644 index 0000000000..16861622be --- /dev/null +++ b/src/test/scala/OverlayStateTest.scala @@ -0,0 +1,58 @@ +import li.cil.oc.client.renderer.tileentity.OverlayState +import org.junit.runner.RunWith +import org.scalatest.FunSpec +import org.scalatest.Matchers.be +import org.scalatest.Matchers.convertToAnyShouldWrapper +import org.scalatest.junit.JUnitRunner + +@RunWith(classOf[JUnitRunner]) +class OverlayStateTest extends FunSpec { + describe("Overlay state encoders") { + it("keeps every encoder inside its key table") { + val charger = OverlayState.charger(100, powered = true) + val cases = + (for (running <- Seq(false, true); activity <- Seq(false, true); error <- Seq(false, true)) + yield (s"computer($running, $activity, $error)", OverlayState.computer(running, activity, error), OverlayState.ComputerStates)) ++ + (for (missing <- Seq(-1, 0, 5, 255); active <- Seq(-9, -1, 0, 2, 3, 100)) + yield (s"raid($missing, $active)", OverlayState.raid(missing, active), OverlayState.RaidStates)) ++ + Seq(("charger(100, true)", charger, OverlayState.ChargerStates)) ++ + (for (running <- Seq(false, true); error <- Seq(false, true)) + yield (s"microcontroller($running, $error)", OverlayState.microcontroller(running, error), OverlayState.MicrocontrollerStates)) + for ((name, state, bound) <- cases) { + withClue(s"$name = $state, bound $bound: ") { + (state == OverlayState.NoMesh || (state >= 0 && state < bound)) should be(true) + } + } + OverlayState.chargerSpeed(charger) should be(1.0) + } + } + + describe("Raid overlay state") { + it("round-trips every missing mask and active slot") { + for (missing <- 0 until 8; active <- -1 until 3 if missing != 0 || active >= 0) { + val state = OverlayState.raid(missing, active) + (state >= 0 && state < OverlayState.RaidStates) should be(true) + OverlayState.raidMissingSlots(state) should be(missing) + OverlayState.raidActiveSlot(state) should be(active) + } + } + it("suppresses the activity overlay when the active slot is missing") { + val lastAccess = 9999L + val missing = OverlayState.raidState(Array(true, false, true), 3, lastAccess, active = true) + OverlayState.raidActiveSlot(missing) should be((lastAccess % 3).toInt) + OverlayState.raidActivityVisible(missing) should be((lastAccess % 3) != 1) + OverlayState.raidActivityVisible(OverlayState.raid(2, 1)) should be(false) + OverlayState.raidActivityVisible(OverlayState.raid(2, 0)) should be(true) + OverlayState.raidActivityVisible(OverlayState.raid(2, -1)) should be(false) + } + } + + describe("Charger overlay state") { + it("represents each of the fifteen redstone levels exactly") { + for (signal <- 1 to 15) { + val state = OverlayState.charger(signal / 15.0, powered = false) + OverlayState.chargerSpeed(state) should be(signal / 15.0) + } + } + } +} diff --git a/src/test/scala/RobotLightTest.scala b/src/test/scala/RobotLightTest.scala new file mode 100644 index 0000000000..df7e7cd7f5 --- /dev/null +++ b/src/test/scala/RobotLightTest.scala @@ -0,0 +1,19 @@ +import li.cil.oc.client.renderer.tileentity.RobotLight +import org.junit.runner.RunWith +import org.scalatest.FunSpec +import org.scalatest.Matchers.be +import org.scalatest.Matchers.convertToAnyShouldWrapper +import org.scalatest.junit.JUnitRunner + +@RunWith(classOf[JUnitRunner]) +class RobotLightTest extends FunSpec { + describe("Cycle step") { + it("stays inside the step table and follows the fractional part") { + for (offset <- Seq(0.0, 15.5, Int.MaxValue + 0.5, 9e15, -0.25, -7.9375, -1e-17)) { + val step = RobotLight.step(offset) + (step >= 0 && step < 16) should be(true) + } + RobotLight.step(Int.MaxValue + 0.5) should be(8) + } + } +} diff --git a/src/test/scala/ScreenTextTest.scala b/src/test/scala/ScreenTextTest.scala new file mode 100644 index 0000000000..77cd92e5b1 --- /dev/null +++ b/src/test/scala/ScreenTextTest.scala @@ -0,0 +1,436 @@ +import java.nio.ByteBuffer + +import li.cil.oc.client.renderer.text.CellRects +import li.cil.oc.client.renderer.text.CellUploader +import li.cil.oc.client.renderer.text.DirtyRows +import li.cil.oc.client.renderer.text.GlyphAtlas +import li.cil.oc.client.renderer.text.GlyphSlots +import li.cil.oc.client.renderer.text.GlyphStore +import li.cil.oc.client.renderer.text.PackBudget +import li.cil.oc.client.renderer.text.ScreenCellUpdate +import li.cil.oc.client.renderer.text.ScreenCells +import li.cil.oc.client.renderer.text.ScreenSheet +import li.cil.oc.client.renderer.text.SheetPool +import li.cil.oc.client.renderer.text.SheetTextures +import li.cil.oc.client.renderer.text.TextBufferRenderData +import li.cil.oc.client.renderer.text.TextCells +import li.cil.oc.util.FontUtils +import li.cil.oc.util.PackedColor +import li.cil.oc.util.TextBuffer +import org.junit.runner.RunWith +import org.scalatest.FunSpec +import org.scalatest.Matchers.be +import org.scalatest.Matchers.convertToAnyShouldWrapper +import org.scalatest.junit.JUnitRunner + +import scala.collection.mutable +import scala.util.Random + +@RunWith(classOf[JUnitRunner]) +class ScreenTextTest extends FunSpec { + private val Wide = 0x4E2D + + private object CodePointSlots extends GlyphSlots { + override def slotOf(char: Int): Int = if (char == ' ') 0 else char + } + + private class CountingStore extends GlyphStore { + var stored = 0 + + var missing = Set.empty[Int] + + override def store(texture: Int, char: Int, x: Int, y: Int): Boolean = + if (missing(char)) false + else { + stored += 1 + true + } + + override def clear(texture: Int, x: Int, y: Int, width: Int): Unit = {} + } + + private class FakeTextures extends SheetTextures { + private var next = 0 + + val live = mutable.Set.empty[Int] + + var copies = 0 + + override def create(width: Int, height: Int): Int = { + next += 1 + live += next + next + } + + override def delete(texture: Int): Unit = live.remove(texture) should be(true) + + override def copy(from: Int, to: Int, width: Int, height: Int): Unit = copies += 1 + } + + private final class Data extends TextBufferRenderData { + var dirty = false + + override def data: TextBuffer = null + + override def viewport: (Int, Int) = (0, 0) + } + + private def cleared: DirtyRows = { + val rows = new DirtyRows + rows.clear() + rows + } + + private def buffer(width: Int, height: Int) = new TextBuffer(width, height, new PackedColor.SingleBitFormat(0x33FF33)) + + private def cell(out: ByteBuffer, index: Int) = { + val bytes = new Array[Int](TextCells.BytesPerCell) + for (i <- bytes.indices) bytes(i) = out.get(index * TextCells.BytesPerCell + i) & 0xFF + bytes + } + + private def fill(atlas: GlyphAtlas, from: Int, count: Int) { + for (char <- from until from + count) atlas.slotOf(char) + } + + describe("Row encoding") { + it("writes slot, foreground and background for each cell of the requested rows") { + val data = buffer(4, 3) + data.set(0, 1, "ab", vertical = false) + val out = ByteBuffer.allocate(4 * 3 * TextCells.BytesPerCell) + TextCells.encode(out, data, 4, 1, 2, CodePointSlots) + out.remaining should be(4 * TextCells.BytesPerCell) + cell(out, 0) should be(Array('a'.toInt, 0, 0x33, 0xFF, 0x33, 0, 0, 0)) + cell(out, 1) should be(Array('b'.toInt, 0, 0x33, 0xFF, 0x33, 0, 0, 0)) + cell(out, 2).take(2) should be(Array(0, 0)) + } + it("marks the placeholder cell of a wide glyph as its right half and clips it in the last visible column") { + FontUtils.wcwidth(Wide) should be(2) + val data = buffer(4, 1) + data.set(1, 0, new String(Character.toChars(Wide)), vertical = false) + val out = ByteBuffer.allocate(4 * TextCells.BytesPerCell) + TextCells.encode(out, data, 4, 0, 1, CodePointSlots) + cell(out, 1).take(2) should be(Array(Wide & 0xFF, Wide >> 8)) + cell(out, 2).take(2) should be(Array(Wide & 0xFF, (Wide >> 8) | TextCells.RightHalf)) + cell(out, 3).take(2) should be(Array(0, 0)) + val clipped = ByteBuffer.allocate(4 * TextCells.BytesPerCell) + TextCells.encode(clipped, data, 2, 0, 1, CodePointSlots) + clipped.remaining should be(2 * TextCells.BytesPerCell) + cell(clipped, 1).take(2) should be(Array(Wide & 0xFF, Wide >> 8)) + } + it("resolves palette colors on the CPU") { + val format = new PackedColor.MutablePaletteFormat + val data = new TextBuffer(1, 1, format) + format(3) = 0x123456 + format(5) = 0x654321 + data.foreground = PackedColor.Color(3, isPalette = true) + data.background = PackedColor.Color(5, isPalette = true) + data.set(0, 0, "x", vertical = false) + val out = ByteBuffer.allocate(TextCells.BytesPerCell) + TextCells.encode(out, data, 1, 0, 1, CodePointSlots) + cell(out, 0).drop(2) should be(Array(0x12, 0x34, 0x56, 0x65, 0x43, 0x21)) + } + } + + describe("Dirty rows") { + it("marks the one-based destination rows of a bitblt") { + val rows = cleared + rows.bitBlt(1, 4) + (rows.from, rows.to) should be((0, 4)) + } + it("covers every row a buffer operation changes, including clamped negative targets") { + val random = new Random(7) + val width = 20 + val height = 12 + val data = buffer(width, height) + def text(length: Int) = Seq.fill(length)(('a' + random.nextInt(26)).toChar).mkString + for (y <- 0 until height) data.set(0, y, text(width), vertical = false) + for (_ <- 0 until 4000) { + val before = data.buffer.map(_.clone()) + val rows = cleared + val col = random.nextInt(width) + val row = random.nextInt(height + 6) - 3 + random.nextInt(4) match { + case 0 => + val s = text(1 + random.nextInt(6)) + data.set(col, row, s, vertical = false) + rows.set(row, s, vertical = false) + case 1 => + val s = text(1 + random.nextInt(6)) + val written = if (row < 0) s.drop(-row) else s + if (written.nonEmpty) { + data.set(col, math.max(row, 0), written, vertical = true) + rows.set(row, written, vertical = true) + } + case 2 => + val h = 1 + random.nextInt(8) + data.fill(col, row, 1 + random.nextInt(8), h, 'A' + random.nextInt(26)) + rows.fill(row, h) + case _ => + val h = 1 + random.nextInt(8) + val ty = random.nextInt(21) - 10 + data.copy(col, row, 1 + random.nextInt(8), h, random.nextInt(21) - 10, ty) + rows.copy(row, h, ty) + } + for (y <- 0 until height if !before(y).sameElements(data.buffer(y))) + (y >= rows.from && y < rows.to) should be(true) + } + } + it("marks all rows and bumps the version when dirty with no marked rows") { + val d = new Data + d.rows.clear() + d.dirty = true + d.consumeDirty() + (d.rows.from, d.rows.to) should be((0, Int.MaxValue)) + d.version should be(1) + d.dirty should be(false) + } + } + + describe("Glyph atlas") { + it("keeps slot zero for blanks, reserves the question mark and stores each glyph once") { + val store = new CountingStore + store.missing = Set(0x1234) + val atlas = new GlyphAtlas(new FakeTextures, store, 4096) + atlas.slotOf(' ') should be(0) + atlas.slotOf('?') should be(1) + atlas.slotOf('a') should be(2) + atlas.slotOf('a') should be(2) + atlas.slotOf(0x1234) should be(1) + atlas.slotOf(0x1234) should be(1) + store.stored should be(2) + } + it("keeps slot numbers when it grows into a larger texture") { + val textures = new FakeTextures + val atlas = new GlyphAtlas(textures, new CountingStore, 512) + val first = atlas.texture + val initialSlots = GlyphAtlas.InitialHeight / GlyphAtlas.GlyphSize * GlyphAtlas.Columns + fill(atlas, 0x100, initialSlots - 2) + atlas.slotOf(0x100) should be(2) + atlas.height should be(GlyphAtlas.InitialHeight) + atlas.slotOf(0x10000) should be(initialSlots) + atlas.height should be(GlyphAtlas.InitialHeight * 2) + textures.copies should be(1) + textures.live should be(Set(atlas.texture)) + (atlas.texture != first) should be(true) + atlas.slotOf(0x100) should be(2) + } + it("substitutes when full, resets on the next frame, and does not reset again within the cooldown") { + val atlas = new GlyphAtlas(new FakeTextures, new CountingStore, GlyphAtlas.InitialHeight) + val capacity = GlyphAtlas.InitialHeight / GlyphAtlas.GlyphSize * GlyphAtlas.Columns + fill(atlas, 0x100, capacity - 2) + atlas.slotOf(0x10000) should be(1) + atlas.generation should be(0) + atlas.beginFrame() + atlas.generation should be(1) + atlas.slotOf(0x10000) should be(2) + fill(atlas, 0x100, capacity) + for (_ <- 0 until GlyphAtlas.ResetCooldown) atlas.beginFrame() + atlas.generation should be(1) + atlas.beginFrame() + atlas.generation should be(2) + } + } + + describe("Sheet allocation") { + it("merges freed extents with both neighbours, the next one and the previous one") { + val both = new ScreenSheet(1) + Seq(100, 100, 100, ScreenSheet.Cells - 300).map(both.alloc) should be(Seq(0, 100, 200, 300)) + both.alloc(1) should be(-1) + both.free(0, 100) + both.free(200, 100) + both.free(100, 100) + both.alloc(300) should be(0) + val next = new ScreenSheet(2) + Seq(100, 100, ScreenSheet.Cells - 200).map(next.alloc) + next.free(100, 100) + next.free(0, 100) + next.alloc(200) should be(0) + val previous = new ScreenSheet(3) + Seq(100, 100, ScreenSheet.Cells - 200).map(previous.alloc) + previous.free(0, 100) + previous.free(100, 100) + previous.alloc(200) should be(0) + } + it("never overlaps or leaks a sheet under random place, resize and release") { + val random = new Random(11) + val textures = new FakeTextures + val pool = new SheetPool(textures) + val live = mutable.ArrayBuffer.empty[ScreenCells] + def size() = random.nextInt(4) match { + case 0 => 800 + case 1 => 2000 + case 2 => 8000 + case _ => 1 + random.nextInt(ScreenSheet.Cells / 2) + } + for (_ <- 0 until 3000) { + random.nextInt(3) match { + case 0 => + val cells = new ScreenCells + pool.place(cells, size()) + live += cells + case 1 if live.nonEmpty => pool.resize(live(random.nextInt(live.size)), size()) + case _ if live.nonEmpty => pool.release(live.remove(random.nextInt(live.size))) + case _ => + } + val bySheet = live.groupBy(_.sheet) + textures.live should be(bySheet.keys.map(_.texture).toSet) + for ((sheet, cells) <- bySheet) { + sheet.freeCells should be(ScreenSheet.Cells - cells.map(_.size).sum) + val sorted = cells.sortBy(_.start) + for (c <- sorted) (c.start >= 0 && c.start + c.size <= ScreenSheet.Cells) should be(true) + for ((a, b) <- sorted.zip(sorted.drop(1))) (a.start + a.size <= b.start) should be(true) + } + } + live.foreach(pool.release) + textures.live should be(Set.empty) + } + it("fills earlier sheets first so a later sheet drains and is deleted") { + val textures = new FakeTextures + val pool = new SheetPool(textures) + val a, b, c, d = new ScreenCells + pool.place(a, ScreenSheet.Cells / 2) + pool.place(b, ScreenSheet.Cells / 2) + pool.place(c, 100) + (c.sheet eq a.sheet) should be(false) + pool.release(b) + pool.place(d, 100) + (d.sheet eq a.sheet) should be(true) + pool.release(c) + textures.live should be(Set(a.sheet.texture)) + } + } + + describe("Screen cell update") { + final class Uploader(store: CountingStore = null) extends CellUploader { + var binds = 0 + var storedAtBind = -1 + var rects = Vector.empty[(Int, Int)] + var placed = Vector.empty[(Int, Int, Int, Int)] + + override def bind(cells: ScreenCells) { + binds += 1 + if (store != null) storedAtBind = store.stored + } + + override def rect(column: Int, row: Int, columns: Int, rows: Int, firstCell: Int) { + rects :+= ((firstCell, columns * rows)) + placed :+= ((column, row, columns, rows)) + } + + def uploaded: Int = rects.map(_._2).sum + } + + val columns = 4 + val lines = 3 + + def out = ByteBuffer.allocate(columns * lines * TextCells.BytesPerCell) + + final class Screen(atlasHeight: Int = 4096) { + val textures = new FakeTextures + val pool = new SheetPool(textures) + val store = new CountingStore + val atlas = new GlyphAtlas(textures, store, atlasHeight) + val data: TextBuffer = buffer(columns, lines) + val rows: DirtyRows = cleared + val owner = new Data + pool.place(new ScreenCells, ScreenSheet.Stride + 476) + val cells: ScreenCells = ScreenCellUpdate.acquire(pool, owner, columns * lines) + + def update(uploader: Uploader = new Uploader, buf: ByteBuffer = out): Uploader = { + ScreenCellUpdate.update(uploader, atlas, buf, data, rows, cells, columns, lines) + uploader + } + } + + def primed(atlasHeight: Int = 4096) = { + val screen = new Screen(atlasHeight) + screen.update() + screen + } + + it("uploads every row when the cells were just placed or resized") { + val screen = new Screen + val uploader = screen.update() + uploader.uploaded should be(columns * lines) + uploader.rects.head._1 should be(0) + (screen.cells.width, screen.cells.height) should be((columns, lines)) + screen.rows.isEmpty should be(true) + ScreenCellUpdate.acquire(screen.pool, screen.owner, columns * lines + 1) + screen.cells.width should be(0) + } + it("uploads nothing when no rows are dirty and the size is unchanged") { + val uploader = primed().update() + uploader.rects should be(Vector.empty) + uploader.binds should be(0) + } + it("uploads only the dirty rows, binding the sheet after new glyphs are stored") { + val screen = primed() + screen.data.set(0, 1, "ab", vertical = false) + screen.rows.set(1, "ab", vertical = false) + val buf = out + val uploader = screen.update(new Uploader(screen.store), buf) + uploader.storedAtBind should be(3) + uploader.rects should be(Vector((0, columns))) + uploader.placed should be(Vector((480, 1, columns, 1))) + buf.remaining should be(columns * TextCells.BytesPerCell) + screen.rows.isEmpty should be(true) + } + it("clamps dirty rows beyond the viewport") { + val screen = primed() + screen.rows.mark(2, 99) + screen.update().uploaded should be(columns) + } + it("re-uploads every row after the glyph atlas resets") { + val screen = primed(GlyphAtlas.InitialHeight) + fill(screen.atlas, 0x100, GlyphAtlas.InitialHeight / GlyphAtlas.GlyphSize * GlyphAtlas.Columns) + screen.atlas.beginFrame() + screen.update().uploaded should be(columns * lines) + } + } + + describe("Pack target budget") { + it("rejects past the limit and advances the epoch when bytes are freed, so rejected screens retry") { + val budget = new PackBudget(100) + budget.resize(0, 60) should be(true) + val rejectedAt = budget.epoch + budget.resize(0, 60) should be(false) + budget.resize(60, 80) should be(true) + budget.epoch should be(rejectedAt) + budget.release(80) + (budget.epoch != rejectedAt) should be(true) + budget.resize(0, 100) should be(true) + budget.used should be(100) + } + } + + describe("Cell rects") { + final class Grid(rows: Int) extends CellRects { + val source = Array.fill(ScreenSheet.Stride * rows)(-1) + var rects = 0 + + override def rect(column: Int, row: Int, columns: Int, rows: Int, firstCell: Int) { + rects += 1 + (column + columns) should be <= ScreenSheet.Stride + for (y <- 0 until rows; x <- 0 until columns) { + val target = (row + y) * ScreenSheet.Stride + column + x + source(target) should be(-1) + source(target) = firstCell + y * columns + x + } + } + } + + it("maps every cell exactly once in at most three rects across sheet rows") { + for ((start, count, single) <- Seq((0, 1024, true), (100, 200, true), (1000, 50, false), (1023, 1, true), (500, 3000, false), (7, 2053, false), (0, 3072, true))) { + val grid = new Grid(8) + ScreenSheet.cellRects(start, start + count, grid) + withClue(s"($start, $count): ") { + for (cell <- grid.source.indices) + grid.source(cell) should be(if (cell >= start && cell < start + count) cell - start else -1) + grid.rects should be <= 3 + if (single) grid.rects should be(1) + } + } + } + } +} diff --git a/src/test/scala/ShaderSourceTest.scala b/src/test/scala/ShaderSourceTest.scala new file mode 100644 index 0000000000..2bc2252168 --- /dev/null +++ b/src/test/scala/ShaderSourceTest.scala @@ -0,0 +1,158 @@ +import li.cil.oc.client.renderer.glsl.GlslDialect +import li.cil.oc.client.renderer.glsl.ProgramRecipe +import li.cil.oc.client.renderer.shading.Programs +import org.junit.runner.RunWith +import org.scalatest.FunSpec +import org.scalatest.Matchers.be +import org.scalatest.Matchers.convertToAnyShouldWrapper +import org.scalatest.junit.JUnitRunner + +import scala.collection.JavaConverters._ +import scala.collection.mutable +import scala.io.Source + +@RunWith(classOf[JUnitRunner]) +class ShaderSourceTest extends FunSpec { + private val files = Programs.recipes.flatMap(recipe => Seq(recipe.name + ".vert", recipe.name + ".frag")) ++ Seq("standalone.vert", "standalone.frag") + + private val bodies = Seq("lit", "unlit") + + // Must match the names Angelica's prelude defines for a registered body. + private val contract = Seq( + "vert" -> "angelica_out", + "vert" -> "angelica_modelView", + "vert" -> "angelica_eyePosition", + "vert" -> "angelica_clipPosition", + "vert" -> "angelica_eyeNormal", + "vert" -> "angelica_color", + "vert" -> "angelica_uv", + "vert" -> "angelica_lightmapUv", + "vert" -> "angelica_fogCoord", + "vert" -> "angelica_lightDirection", + "vert" -> "angelica_lightDiffuse", + "frag" -> "angelica_in", + "frag" -> "angelica_FragColor", + "frag" -> "angelica_sampleTexture", + "frag" -> "angelica_sampleLightmap", + "frag" -> "angelica_fog", + "frag" -> "angelica_alphaTest") + + private val vertexContract = contract.collect { case ("vert", name) => name }.toSet + + private val fragmentContract = contract.collect { case ("frag", name) => name }.toSet + + // Any of these puts a program on Angelica's fixed-function emulation, by source rewrite or by uniform name. + private val emulated = Seq("gl_Vertex", "gl_Color", "gl_Normal", "gl_MultiTexCoord", "gl_ModelView", "gl_Projection", "gl_NormalMatrix", + "gl_LightSource", "gl_Fog", "gl_FragColor", "gl_TexCoord", "ftransform", "texture2D", "attribute", "varying", "iris_") + + // Angelica renames these as whole words anywhere in a source, comments included, and never renames them back. + private val renamed = Seq("sample", "new", "sampler") + + private case class Program(recipe: ProgramRecipe, dialect: GlslDialect) { + def shader: String = recipe.name + + def vertex: String = preprocess(recipe.assemble(dialect, read("standalone.vert"), read(shader + ".vert"))) + + def fragment: String = preprocess(recipe.assemble(dialect, read("standalone.frag"), read(shader + ".frag"))) + + override def toString = s"$shader ($dialect)" + } + + private val programs = for (recipe <- Programs.recipes; dialect <- GlslDialect.values.toSeq) yield Program(recipe, dialect) + + private def read(name: String): String = { + val stream = getClass.getResourceAsStream("/assets/opencomputers/shaders/" + name) + if (stream == null) fail(name + " is missing") + try Source.fromInputStream(stream, "UTF-8").mkString finally stream.close() + } + + // Handles only #ifdef/#else/#endif, the only conditionals the sources use. + private def preprocess(source: String): String = { + val defined = mutable.Set.empty[String] + var open = List.empty[Boolean] + var taken = List.empty[Boolean] + source.linesIterator.filter { line => + val words = line.trim.split("\\s+") + val live = open.forall(identity) + words(0) match { + case "#ifdef" => + val branch = defined(words(1)) + open ::= branch + taken ::= branch + false + case "#else" => + open = !taken.head :: open.tail + false + case "#endif" => + open = open.tail + taken = taken.tail + false + case "#if" | "#ifndef" | "#elif" => fail("unsupported directive: " + line) + case "#define" if live => + defined += words(1).takeWhile(_ != '(') + true + case _ => live + } + }.mkString("\n") + } + + private def uncommented(source: String) = source.linesIterator.map(_.replaceFirst("//.*", "")).mkString("\n") + + private def angelicaNames(source: String) = "angelica_\\w*".r.findAllIn(source).toSet + + private def uniformNames(source: String) = "(?m)^\\s*uniform\\s+\\w+\\s+(\\w+)".r.findAllMatchIn(source).map(_.group(1)).toSet + + private def defined(source: String) = + "(?m)^(?:#define |\\w+ |layout\\(location = 0\\) out vec4 )(angelica_\\w+)[ (;]".r.findAllMatchIn(source).map(_.group(1)).toSet + + describe("Assembled shader sources") { + for (program <- programs) { + it(s"$program declares exactly the uniforms its recipe requires") { + (uniformNames(program.vertex) ++ uniformNames(program.fragment)) should be(program.recipe.expectedUniforms(program.dialect).asScala.toSet) + } + + it(s"$program defines every contract name its stages use") { + (angelicaNames(read(program.shader + ".vert")) -- defined(program.vertex)) should be(Set.empty) + (angelicaNames(read(program.shader + ".frag")) -- defined(program.fragment)) should be(Set.empty) + } + } + + for (program <- programs if program.dialect == GlslDialect.GLSL330) { + it(s"$program uses nothing Angelica would emulate") { + val vertex = program.vertex + val fragment = program.fragment + for (token <- emulated) withClue(token) { + vertex.contains(token) should be(false) + fragment.contains(token) should be(false) + } + (angelicaNames(vertex) -- vertexContract) should be(Set.empty) + (angelicaNames(fragment) -- fragmentContract) should be(Set.empty) + } + } + } + + describe("Shader bodies") { + for (shader <- bodies; (stage, contract) <- Seq("vert" -> vertexContract, "frag" -> fragmentContract)) { + it(s"$shader.$stage is written against the contract names alone") { + val body = uncommented(read(shader + "." + stage)) + (angelicaNames(body) -- contract) should be(Set.empty) + body.contains("#") should be(false) + "\\b[ua]_\\w*".r.findFirstIn(body) should be(None) + ("\\bgl_\\w*".r.findAllIn(body).toSet - "gl_Position") should be(Set.empty) + for (word <- Seq("uniform", "texture", "texture2D", "varying", "attribute", "layout")) withClue(word) { + ("\\b" + word + "\\b").r.findFirstIn(body) should be(None) + } + "\\bvoid main\\(\\)".r.findAllIn(body).length should be(1) + } + } + } + + describe("Shader source text") { + for (file <- files) { + it(s"$file has no word Angelica renames") { + val source = read(file) + for (word <- renamed) withClue(word) { ("\\b" + word + "\\b").r.findFirstIn(source) should be(None) } + } + } + } +}