/* * Copyright 2016 MovingBlocks * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.terasology.rendering.opengl; import com.google.common.base.Objects; import com.google.common.collect.Sets; import gnu.trove.iterator.TIntIntIterator; import gnu.trove.map.TIntIntMap; import gnu.trove.map.TIntObjectMap; import gnu.trove.map.TObjectIntMap; import gnu.trove.map.hash.TIntIntHashMap; import gnu.trove.map.hash.TIntObjectHashMap; import gnu.trove.map.hash.TObjectIntHashMap; import org.lwjgl.opengl.GL13; import org.lwjgl.opengl.GL20; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.terasology.assets.AssetType; import org.terasology.assets.ResourceUrn; import org.terasology.engine.GameThread; import org.terasology.math.MatrixUtils; import org.terasology.math.geom.Matrix3f; import org.terasology.math.geom.Matrix4f; import org.terasology.registry.CoreRegistry; import org.terasology.rendering.ShaderManager; import org.terasology.rendering.assets.material.BaseMaterial; import org.terasology.rendering.assets.material.MaterialData; import org.terasology.rendering.assets.shader.ShaderParameterMetadata; import org.terasology.rendering.assets.shader.ShaderProgramFeature; import org.terasology.rendering.assets.texture.Texture; import org.terasology.rendering.shader.ShaderParameters; import java.nio.FloatBuffer; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.Map; import java.util.Set; public class GLSLMaterial extends BaseMaterial { private static final Logger logger = LoggerFactory.getLogger(GLSLMaterial.class); private int textureIndex; private TObjectIntMap<String> bindMap = new TObjectIntHashMap<>(); private TIntObjectMap<Texture> textureMap = new TIntObjectHashMap<>(); private GLSLShader shader; private boolean activeFeaturesChanged; private TObjectIntMap<UniformId> uniformLocationMap = new TObjectIntHashMap<>(); private EnumSet<ShaderProgramFeature> activeFeatures = Sets.newEnumSet(Collections.emptyList(), ShaderProgramFeature.class); private int activeFeaturesMask; private final ShaderManager shaderManager; private ShaderParameters shaderParameters; private DisposalAction disposalAction; private MaterialData materialData; public GLSLMaterial(ResourceUrn urn, AssetType<?, MaterialData> assetType, MaterialData data) { super(urn, assetType); disposalAction = new DisposalAction(urn); getDisposalHook().setDisposeAction(disposalAction); this.materialData = data; shaderManager = CoreRegistry.get(ShaderManager.class); reload(data); } public ShaderParameters getShaderParameters() { return shaderParameters; } public void setShaderParameters(ShaderParameters param) { this.shaderParameters = param; param.initialParameters(this); } @Override public void enable() { if (shaderManager.getActiveMaterial() != this || activeFeaturesChanged) { GL13.glActiveTexture(GL13.GL_TEXTURE0); GL20.glUseProgram(getActiveShaderProgramId()); // Make sure the shader manager knows that this program is currently active shaderManager.setActiveMaterial(this); activeFeaturesChanged = false; // Set the shader parameters if available if (shaderParameters != null) { shaderParameters.applyParameters(this); } } } @Override public void bindTextures() { if (isDisposed()) { return; } enable(); for (int slot : textureMap.keys()) { Texture texture = textureMap.get(slot); if (texture.isDisposed()) { logger.error("Attempted to bind disposed texture {}", texture); } else { shaderManager.bindTexture(slot, texture); } } } @Override public boolean isRenderable() { for (Texture texture : textureMap.valueCollection()) { if (!texture.isLoaded()) { return false; } } return true; } @Override public void recompile() { TIntIntIterator it = disposalAction.shaderPrograms.iterator(); while (it.hasNext()) { it.advance(); GL20.glDeleteProgram(it.value()); } disposalAction.shaderPrograms.clear(); uniformLocationMap.clear(); bindMap.clear(); disposalAction.shaderPrograms.put(0, shader.linkShaderProgram(0)); for (Set<ShaderProgramFeature> permutation : Sets.powerSet(shader.getAvailableFeatures())) { int featureMask = ShaderProgramFeature.getBitset(permutation); disposalAction.shaderPrograms.put(featureMask, shader.linkShaderProgram(featureMask)); } if (shaderParameters != null) { shaderParameters.initialParameters(this); } //resolves #966 //Some of the uniforms are not updated constantly between frames //this function will rebind any uniforms that are not bound rebindVariables(materialData); } @Override public final void doReload(MaterialData data) { try { GameThread.synch(() -> { disposalAction.run(); uniformLocationMap.clear(); shader = (GLSLShader) data.getShader(); recompile(); rebindVariables(data); }); } catch (InterruptedException e) { logger.error("Failed to reload {}", getUrn(), e); } } private void rebindVariables(MaterialData data) { for (Map.Entry<String, Texture> entry : data.getTextures().entrySet()) { setTexture(entry.getKey(), entry.getValue()); } for (Map.Entry<String, Float> entry : data.getFloatParams().entrySet()) { setFloat(entry.getKey(), entry.getValue()); } for (Map.Entry<String, Integer> entry : data.getIntegerParams().entrySet()) { setInt(entry.getKey(), entry.getValue()); } for (Map.Entry<String, float[]> entry : data.getFloatArrayParams().entrySet()) { float[] value = entry.getValue(); switch (value.length) { case 1: setFloat(entry.getKey(), value[0]); break; case 2: setFloat2(entry.getKey(), value[0], value[1]); break; case 3: setFloat3(entry.getKey(), value[0], value[1], value[2]); break; case 4: setFloat4(entry.getKey(), value[0], value[1], value[2], value[3]); break; default: logger.error("MaterialData contains float array entry of size > 4"); break; } } } @Override public void setTexture(String desc, Texture texture) { if (isDisposed()) { return; } int texId; if (bindMap.containsKey(desc)) { texId = bindMap.get(desc); } else { // TODO: do this initially, and try and have similar textures in similar slots for all materials. ShaderParameterMetadata metadata = shader.getParameter(desc); if (metadata == null || !metadata.getType().isTexture()) { return; } texId = textureIndex++; // Make sure to bind the texture for all permutations setInt(desc, texId); bindMap.put(desc, texId); } textureMap.put(texId, texture); } @Override public void activateFeature(ShaderProgramFeature feature) { if (shader.getAvailableFeatures().contains(feature)) { activeFeatures.add(feature); activeFeaturesMask = ShaderProgramFeature.getBitset(activeFeatures); activeFeaturesChanged = true; } else { logger.error("Attempt to activate unsupported feature {} for material {} using shader {}", feature, getUrn(), shader.getUrn()); } } @Override public void deactivateFeature(ShaderProgramFeature feature) { if (activeFeatures.remove(feature)) { activeFeaturesMask = ShaderProgramFeature.getBitset(activeFeatures); activeFeaturesChanged = true; } } @Override public void deactivateFeatures(ShaderProgramFeature... features) { Arrays.asList(features).forEach(this::deactivateFeature); } @Override public boolean supportsFeature(ShaderProgramFeature feature) { return shader.getAvailableFeatures().contains(feature); } @Override public void setFloat(String desc, float f, boolean currentOnly) { if (isDisposed()) { return; } if (currentOnly) { enable(); int id = getUniformLocation(getActiveShaderProgramId(), desc); GL20.glUniform1f(id, f); } else { TIntIntIterator it = disposalAction.shaderPrograms.iterator(); while (it.hasNext()) { it.advance(); GL20.glUseProgram(it.value()); int id = getUniformLocation(it.value(), desc); GL20.glUniform1f(id, f); } restoreStateAfterUniformsSet(); } } @Override public void setFloat1(String desc, FloatBuffer buffer, boolean currentOnly) { if (isDisposed()) { return; } if (currentOnly) { enable(); int id = getUniformLocation(getActiveShaderProgramId(), desc); GL20.glUniform1(id, buffer); } else { TIntIntIterator it = disposalAction.shaderPrograms.iterator(); while (it.hasNext()) { it.advance(); GL20.glUseProgram(it.value()); int id = getUniformLocation(it.value(), desc); GL20.glUniform1(id, buffer); } restoreStateAfterUniformsSet(); } } @Override public void setFloat2(String desc, float f1, float f2, boolean currentOnly) { if (isDisposed()) { return; } if (currentOnly) { enable(); int id = getUniformLocation(getActiveShaderProgramId(), desc); GL20.glUniform2f(id, f1, f2); } else { TIntIntIterator it = disposalAction.shaderPrograms.iterator(); while (it.hasNext()) { it.advance(); GL20.glUseProgram(it.value()); int id = getUniformLocation(it.value(), desc); GL20.glUniform2f(id, f1, f2); } restoreStateAfterUniformsSet(); } } @Override public void setFloat2(String desc, FloatBuffer buffer, boolean currentOnly) { if (isDisposed()) { return; } if (currentOnly) { enable(); int id = getUniformLocation(getActiveShaderProgramId(), desc); GL20.glUniform2(id, buffer); } else { TIntIntIterator it = disposalAction.shaderPrograms.iterator(); while (it.hasNext()) { it.advance(); GL20.glUseProgram(it.value()); int id = getUniformLocation(it.value(), desc); GL20.glUniform2(id, buffer); } restoreStateAfterUniformsSet(); } } @Override public void setFloat3(String desc, float f1, float f2, float f3, boolean currentOnly) { if (isDisposed()) { return; } if (currentOnly) { enable(); int id = getUniformLocation(getActiveShaderProgramId(), desc); GL20.glUniform3f(id, f1, f2, f3); } else { TIntIntIterator it = disposalAction.shaderPrograms.iterator(); while (it.hasNext()) { it.advance(); GL20.glUseProgram(it.value()); int id = getUniformLocation(it.value(), desc); GL20.glUniform3f(id, f1, f2, f3); } restoreStateAfterUniformsSet(); } } @Override public void setFloat3(String desc, FloatBuffer buffer, boolean currentOnly) { if (isDisposed()) { return; } if (currentOnly) { enable(); int id = getUniformLocation(getActiveShaderProgramId(), desc); GL20.glUniform3(id, buffer); } else { TIntIntIterator it = disposalAction.shaderPrograms.iterator(); while (it.hasNext()) { it.advance(); GL20.glUseProgram(it.value()); int id = getUniformLocation(it.value(), desc); GL20.glUniform3(id, buffer); } restoreStateAfterUniformsSet(); } } @Override public void setFloat4(String desc, float f1, float f2, float f3, float f4, boolean currentOnly) { if (isDisposed()) { return; } if (currentOnly) { enable(); int id = getUniformLocation(getActiveShaderProgramId(), desc); GL20.glUniform4f(id, f1, f2, f3, f4); } else { TIntIntIterator it = disposalAction.shaderPrograms.iterator(); while (it.hasNext()) { it.advance(); GL20.glUseProgram(it.value()); int id = getUniformLocation(it.value(), desc); GL20.glUniform4f(id, f1, f2, f3, f4); } restoreStateAfterUniformsSet(); } } @Override public void setFloat4(String desc, FloatBuffer buffer, boolean currentOnly) { if (isDisposed()) { return; } if (currentOnly) { enable(); int id = getUniformLocation(getActiveShaderProgramId(), desc); GL20.glUniform4(id, buffer); } else { TIntIntIterator it = disposalAction.shaderPrograms.iterator(); while (it.hasNext()) { it.advance(); GL20.glUseProgram(it.value()); int id = getUniformLocation(it.value(), desc); GL20.glUniform4(id, buffer); } restoreStateAfterUniformsSet(); } } @Override public void setInt(String desc, int i, boolean currentOnly) { if (isDisposed()) { return; } if (currentOnly) { enable(); int id = getUniformLocation(getActiveShaderProgramId(), desc); GL20.glUniform1i(id, i); } else { TIntIntIterator it = disposalAction.shaderPrograms.iterator(); while (it.hasNext()) { it.advance(); GL20.glUseProgram(it.value()); int id = getUniformLocation(it.value(), desc); GL20.glUniform1i(id, i); } restoreStateAfterUniformsSet(); } } @Override public void setBoolean(String desc, boolean value, boolean currentOnly) { if (isDisposed()) { return; } if (currentOnly) { enable(); int id = getUniformLocation(getActiveShaderProgramId(), desc); GL20.glUniform1i(id, value ? 1 : 0); } else { TIntIntIterator it = disposalAction.shaderPrograms.iterator(); while (it.hasNext()) { it.advance(); GL20.glUseProgram(it.value()); int id = getUniformLocation(it.value(), desc); GL20.glUniform1i(id, value ? 1 : 0); } restoreStateAfterUniformsSet(); } } @Override public void setMatrix3(String desc, Matrix3f value, boolean currentOnly) { if (isDisposed()) { return; } if (currentOnly) { enable(); int id = getUniformLocation(getActiveShaderProgramId(), desc); GL20.glUniformMatrix3(id, false, MatrixUtils.matrixToFloatBuffer(value)); } else { TIntIntIterator it = disposalAction.shaderPrograms.iterator(); while (it.hasNext()) { it.advance(); GL20.glUseProgram(it.value()); int id = getUniformLocation(it.value(), desc); GL20.glUniformMatrix3(id, false, MatrixUtils.matrixToFloatBuffer(value)); } restoreStateAfterUniformsSet(); } } @Override public void setMatrix3(String desc, FloatBuffer value, boolean currentOnly) { if (isDisposed()) { return; } if (currentOnly) { enable(); int id = getUniformLocation(getActiveShaderProgramId(), desc); GL20.glUniformMatrix3(id, false, value); } else { TIntIntIterator it = disposalAction.shaderPrograms.iterator(); while (it.hasNext()) { it.advance(); GL20.glUseProgram(it.value()); int id = getUniformLocation(it.value(), desc); GL20.glUniformMatrix3(id, false, value); } restoreStateAfterUniformsSet(); } } @Override public void setMatrix4(String desc, Matrix4f value, boolean currentOnly) { if (isDisposed()) { return; } if (currentOnly) { enable(); int id = getUniformLocation(getActiveShaderProgramId(), desc); GL20.glUniformMatrix4(id, false, MatrixUtils.matrixToFloatBuffer(value)); } else { TIntIntIterator it = disposalAction.shaderPrograms.iterator(); while (it.hasNext()) { it.advance(); GL20.glUseProgram(it.value()); int id = getUniformLocation(it.value(), desc); GL20.glUniformMatrix4(id, false, MatrixUtils.matrixToFloatBuffer(value)); } restoreStateAfterUniformsSet(); } } @Override public void setMatrix4(String desc, FloatBuffer value, boolean currentOnly) { if (isDisposed()) { return; } if (currentOnly) { enable(); int id = getUniformLocation(getActiveShaderProgramId(), desc); GL20.glUniformMatrix4(id, false, value); } else { TIntIntIterator it = disposalAction.shaderPrograms.iterator(); while (it.hasNext()) { it.advance(); GL20.glUseProgram(it.value()); int id = getUniformLocation(it.value(), desc); GL20.glUniformMatrix4(id, false, value); } restoreStateAfterUniformsSet(); } } private int getActiveShaderProgramId() { return disposalAction.shaderPrograms.get(activeFeaturesMask); } private int getUniformLocation(int activeShaderProgramId, String desc) { UniformId id = new UniformId(activeShaderProgramId, desc); if (uniformLocationMap.containsKey(id)) { return uniformLocationMap.get(id); } int loc = GL20.glGetUniformLocation(activeShaderProgramId, desc); uniformLocationMap.put(id, loc); return loc; } private void restoreStateAfterUniformsSet() { if (shaderManager.getActiveMaterial() == this) { GL20.glUseProgram(getActiveShaderProgramId()); } else { enable(); } } private static final class UniformId { private int shaderProgramId; private String name; // made package-private after Jenkins' suggestion UniformId(int shaderProgramId, String name) { this.shaderProgramId = shaderProgramId; this.name = name; } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj instanceof UniformId) { UniformId other = (UniformId) obj; return shaderProgramId == other.shaderProgramId && Objects.equal(name, other.name); } return false; } @Override public int hashCode() { return Objects.hashCode(shaderProgramId, name); } } private static class DisposalAction implements Runnable { private final ResourceUrn urn; private TIntIntMap shaderPrograms = new TIntIntHashMap(); // made package-private after Jenkins' suggestion DisposalAction(ResourceUrn urn) { this.urn = urn; } @Override public void run() { try { GameThread.synch(() -> { logger.debug("Disposing material {}.", urn); TIntIntIterator it = shaderPrograms.iterator(); while (it.hasNext()) { it.advance(); GL20.glDeleteProgram(it.value()); } shaderPrograms.clear(); }); } catch (InterruptedException e) { logger.error("Failed to dispose {}", urn, e); } } } }