package org.newdawn.slick.opengl.shader; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.util.HashMap; import org.lwjgl.BufferUtils; import org.lwjgl.opengl.ARBShaderObjects; import org.lwjgl.opengl.ARBVertexShader; import org.lwjgl.opengl.ContextCapabilities; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL20; import org.lwjgl.opengl.GL32; import org.lwjgl.opengl.GLContext; import org.newdawn.slick.Color; import org.newdawn.slick.SlickException; import org.newdawn.slick.geom.Vector2f; import org.newdawn.slick.util.Log; import org.newdawn.slick.util.ResourceLoader; /** * A simple wrapper utility for creating and reusing shaders and shader programs. * * @author davedes */ public class ShaderProgram { /** The vertex shader type (GL20.GL_VERTEX_SHADER). */ public static final int VERTEX_SHADER = GL20.GL_VERTEX_SHADER; /** The fragment shader type (GL20.GL_FRAGMENT_SHADER). */ public static final int FRAGMENT_SHADER = GL20.GL_FRAGMENT_SHADER; private static boolean strict = true; /** * Returns true if GLSL shaders are supported in hardware on this system. This checks for * the following OpenGL extensions: GL_ARB_shader_objects, GL_ARB_vertex_shader, * GL_ARB_fragment_shader * * @return true if shaders are supported */ public static boolean isSupported() { ContextCapabilities c = GLContext.getCapabilities(); return c.GL_ARB_shader_objects && c.GL_ARB_vertex_shader && c.GL_ARB_fragment_shader; } /** * Whether shader programs are to use "strict" uniform/attribute name * checking. That is, when strict mode is enabled, trying to modify or retrieve uniform/attribute * data by name will fail and throw an IllegalArgumentException if there exists no * 'active' uniforms/attributes by the given name. (In GLSL, declared uniforms might still be * "inactive" if they are not used.) If strict mode is disabled, getting/setting uniform/attribute * data will fail silently if the name is not found. * @param enabled true to enable strict mode */ public static void setStrictMode(boolean enabled) { strict = enabled; } /** * Returns <tt>true</tt> if shader programs are to use "strict" uniform/attribute name * checking. That is, when strict mode is enabled, trying to modify or retrieve uniform/attribute * data by name will fail and throw an IllegalArgumentException if there exists no * 'active' uniforms/attributes by the given name. (In GLSL, declared uniforms might still be * "inactive" if they are not used.) If strict mode is disabled, getting/setting uniform/attribute * data will fail silently if the name is not found. * @return true if strict mode is enabled */ public static boolean isStrictMode() { return strict; } /** * Disables shaders. */ public static void unbindAll() { ARBShaderObjects.glUseProgramObjectARB(0); } /** The OpenGL handle for this shader program object. */ protected int program; /** The log for this program. */ protected String log = ""; /** A map of uniforms by <name, int>. */ protected HashMap<String, Integer> uniforms = new HashMap<String, Integer>(); /** A map of attributes by <name, int>. */ protected HashMap<String, Integer> attributes = new HashMap<String, Integer>(); /** The vertex shader source. */ protected String vertShaderSource; /** The fragment shader source. */ protected String fragShaderSource; /** The OpenGL handle for this program's vertex shader object. */ protected int vert; /** The OpenGL handle for this program's fragment shader object. */ protected int frag; private FloatBuffer buf4; private IntBuffer ibuf4; /** * A convenience method to load a ShaderProgram from two text files. * @param vertFile the location of the vertex shader source * @param fragFile the location of the frag shader source * @return the compiled and linked ShaderProgram * @throws SlickException if there was an issue reading the file, compiling the source, * or linking the program */ public static ShaderProgram loadProgram(String vertFile, String fragFile) throws SlickException { return new ShaderProgram(readFile(vertFile), readFile(fragFile)); } /** * Loads the given input stream into a source code string. * @param ref the location of the text file * @return the resulting source code String * @throws SlickException if there was an issue reading the source */ public static String readFile(String ref) throws SlickException { InputStream in = ResourceLoader.getResourceAsStream(ref); try { return readFile(in); } catch (SlickException e) { throw new SlickException("could not load source file: "+ref); } } /** * Loads the given input stream into a source code string. * @param in the input stream * @return the resulting source code String * @throws SlickException if there was an issue reading the source * @author Nitram */ public static String readFile(InputStream in) throws SlickException { try { final StringBuffer sBuffer = new StringBuffer(); final BufferedReader br = new BufferedReader(new InputStreamReader( in)); final char[] buffer = new char[1024]; int cnt; while ((cnt = br.read(buffer, 0, buffer.length)) > -1) { sBuffer.append(buffer, 0, cnt); } br.close(); return sBuffer.toString(); } catch (IOException e) { throw new SlickException("could not load source file"); } } /** * Creates a new shader program with the given vertex and fragment shader * source code. The given source code is compiled, then the shaders attached * and linked. * * If shaders are not supported on this system (isSupported returns false), * a SlickException will be thrown. * * If one of the shaders does not compile successfully, a SlickException will be thrown. * * If there was a problem in linking the shaders to the program, a SlickException will * be thrown and the program will be deleted. * * @param vertexShaderSource the shader code to compile, attach and link * @param fragShaderSource the frag code to compile, attach and link * @throws SlickException if there was an issue * @throws IllegalArgumentException if there was an issue */ public ShaderProgram(String vertexShaderSource, String fragShaderSource) throws SlickException { if (vertexShaderSource==null || fragShaderSource==null) throw new IllegalArgumentException("shader source must be non-null"); if (!isSupported()) throw new SlickException("no shader support found; driver does not support extension GL_ARB_shader_objects"); this.vertShaderSource = vertexShaderSource; this.fragShaderSource = fragShaderSource; vert = compileShader(VERTEX_SHADER, vertexShaderSource); frag = compileShader(FRAGMENT_SHADER, fragShaderSource); program = createProgram(); try { linkProgram(); } catch (SlickException e) { release(); throw e; } if (log!=null && log.length()!=0) Log.warn("GLSL Info: "+log); } /** * Subclasses may wish to implement this to manually handle program/shader creation, compiling, and linking. * This constructor does nothing; users will need to call compileShader, createProgram and linkProgram manually. * @throws SlickException */ protected ShaderProgram() { } /** * Creates a shader program and returns its OpenGL handle. If the result is zero, an exception will be thrown. * @return the OpenGL handle for the newly created shader program * @throws SlickException if the result is zero */ protected int createProgram() throws SlickException { if (!isSupported()) throw new SlickException("no shader support found; driver does not support extension GL_ARB_shader_objects"); int program = ARBShaderObjects.glCreateProgramObjectARB(); if (program == 0) throw new SlickException("could not create program; check ShaderProgram.isSupported()"); return program; } private String shaderTypeString(int type) { if (type==FRAGMENT_SHADER) return "FRAGMENT_SHADER"; else if (type==VERTEX_SHADER) return "VERTEX_SHADER"; else if (type==GL32.GL_GEOMETRY_SHADER) return "GEOMETRY_SHADER"; else return "shader"; } /** * Compiles a shader from source and returns its handle. If the compilation failed, * a SlickException will be thrown. If the compilation had error, info or warnings messages, * they will be appended to this program's log. * * @param type the type to use in compilation * @param source the source code to compile * @return the resulting ID * @throws SlickException if compilation was unsuccessful */ protected int compileShader(int type, String source) throws SlickException { int shader = ARBShaderObjects.glCreateShaderObjectARB(type); if (shader==0) throw new SlickException("could not create shader object; check ShaderProgram.isSupported()"); ARBShaderObjects.glShaderSourceARB(shader, source); ARBShaderObjects.glCompileShaderARB(shader); int comp = ARBShaderObjects.glGetObjectParameteriARB(shader, GL20.GL_COMPILE_STATUS); int len = ARBShaderObjects.glGetObjectParameteriARB(shader, GL20.GL_INFO_LOG_LENGTH); String t = shaderTypeString(type); String err = ARBShaderObjects.glGetInfoLogARB(shader, len); if (err!=null&&err.length()!=0) log += t+" compile log:\n"+err+"\n"; if (comp==GL11.GL_FALSE) throw new SlickException(log); return shader; } /** * Called to attach vertex and fragment; users may override this for more specific purposes. */ protected void attachShaders() { ARBShaderObjects.glAttachObjectARB(getID(), vert); ARBShaderObjects.glAttachObjectARB(getID(), frag); } /** * Calls attachShaders and links the program. * * @throws SlickException * if this program is invalid (released) or * if the link was unsuccessful */ protected void linkProgram() throws SlickException { if (!valid()) throw new SlickException("trying to link an invalid (i.e. released) program"); uniforms.clear(); attributes.clear(); attachShaders(); ARBShaderObjects.glLinkProgramARB(program); int comp = ARBShaderObjects.glGetObjectParameteriARB(program, GL20.GL_LINK_STATUS); int len = ARBShaderObjects.glGetObjectParameteriARB(program, GL20.GL_INFO_LOG_LENGTH); String err = ARBShaderObjects.glGetInfoLogARB(program, len); if (err!=null&&err.length()!=0) log = err + "\n" + log; if (log!=null) log = log.trim(); if (comp==GL11.GL_FALSE) throw new SlickException(log); fetchUniforms(); fetchAttributes(); } /** * Returns the full log of compiling/linking errors, info, warnings, etc. * @return the full log of this ShaderProgram */ public String getLog() { return log; } /** * Enables this shader for use -- only one shader can be bound at a time. Calling * bind() when another program is bound will simply make this object the active program. * @throw IllegalStateException if this program is invalid */ public void bind() { if (!valid()) throw new IllegalStateException("trying to enable a program that is not valid"); ARBShaderObjects.glUseProgramObjectARB(program); } /** * Unbinds all shaders; this is the equivalent of ShaderProgram.unbindAll(), and only included * for consistency with bind() and the rest of the API (i.e. startUse/endUse). Users do not need to unbind * one shader before binding a new one. */ public void unbind() { ShaderProgram.unbindAll(); } /** * Disables shaders (unbind), then detaches and releases the shaders associated with this program. * This can be called after linking a program in order to free up memory (as the shaders are no longer needed), * however, since it is not a commonly used feature and thus not well tested on all drivers, * it should be used with caution. * Shaders shouldn't be used after being released. */ public void releaseShaders() { unbind(); if (vert!=0) { ARBShaderObjects.glDetachObjectARB(getID(), vert); ARBShaderObjects.glDeleteObjectARB(vert); vert = 0; } if (frag!=0) { ARBShaderObjects.glDetachObjectARB(getID(), frag); ARBShaderObjects.glDeleteObjectARB(frag); frag = 0; } } /** * If this program has not yet been released, this will disable shaders (unbind), * then releases this program and its shaders. To only release * the shaders (not the program itself), call releaseShaders(). * Programs will be considered "invalid" after being released, and should no longer be used. */ public void release() { if (program!=0) { unbind(); releaseShaders(); ARBShaderObjects.glDeleteObjectARB(program); program = 0; } } /** * Returns the OpenGL handle for this program's vertex shader. * @return the vertex ID */ public int getVertexShaderID() { return vert; } /** * Returns the OpenGL handle for this program's fragment shader. * @return the fragment ID */ public int getFragmentShaderID() { return frag; } /** * Returns the source code for the vertex shader. * @return the source code */ public String getVertexShaderSource() { return vertShaderSource; } /** * Returns the source code for the fragment shader. * @return the source code */ public String getFragmentShaderSource() { return fragShaderSource; } /** * Returns the OpenGL handle for this shader program * @return the program ID */ public int getID() { return program; } /** * A shader program is "valid" if it's ID is not zero. Upon * releasing a program, the ID will be set to zero. * * @return whether this program is valid */ public boolean valid() { return program != 0; } private void fetchUniforms() { int len = ARBShaderObjects.glGetObjectParameteriARB(program, GL20.GL_ACTIVE_UNIFORMS); //max length of all uniforms stored in program int strLen = ARBShaderObjects.glGetObjectParameteriARB(program, GL20.GL_ACTIVE_UNIFORM_MAX_LENGTH); for (int i=0; i<len; i++) { String name = ARBShaderObjects.glGetActiveUniformARB(program, i, strLen); int id = ARBShaderObjects.glGetUniformLocationARB(program, name); uniforms.put(name, id); } } private void fetchAttributes() { int len = ARBShaderObjects.glGetObjectParameteriARB(program, GL20.GL_ACTIVE_ATTRIBUTES); //max length of all attributes stored in program int strLen = ARBShaderObjects.glGetObjectParameteriARB(program, GL20.GL_ACTIVE_ATTRIBUTE_MAX_LENGTH); for (int i=0; i<len; i++) { String name = ARBVertexShader.glGetActiveAttribARB(program, i, strLen); int id = ARBVertexShader.glGetAttribLocationARB(program, name); attributes.put(name, id); } } /** * Returns the ID of the given uniform. * @param name the uniform name * @return the ID (location) in the shader program */ public int getUniformID(String name) { Integer locI = uniforms.get(name); int location = locI==null ? -1 : locI.intValue(); if (location!=-1) return location; location = ARBShaderObjects.glGetUniformLocationARB(program, name); if (location == -1 && strict) throw new IllegalArgumentException("no active uniform by name '"+name+"' (disable strict compiling to suppress warnings)"); uniforms.put(name, location); return location; } /** * Returns the ID of the given attribute. * @param name the attribute name * @return the ID (location) in the shader program */ public int getAttributeID(String name) { int location = attributes.get(name); if (location!=-1) return location; location = ARBVertexShader.glGetAttribLocationARB(program, name); if (location == -1 && strict) throw new IllegalArgumentException("no active attribute by name '"+name+"'"); attributes.put(name, location); return location; } /** * Returns the names of all active attributes that were found * when linking the program. * @return an array list of active attribute names */ public String[] getAttributes() { return attributes.keySet().toArray(new String[attributes.size()]); } /** * Returns the names of all active uniforms that were found * when linking the program. * @return an array list of active uniform names */ public String[] getUniformNames() { return uniforms.keySet().toArray(new String[uniforms.size()]); } /** * Enables the vertex array -- in strict mode, if the vertex attribute * is not found (or it's inactive), an IllegalArgumentException will * be thrown. If strict mode is disabled and the vertex attribute * is not found, this method will return <tt>false</tt> otherwise it * will return <tt>true</tt>. * * @param name the name of the vertex attribute to enable * @return false if strict mode is disabled and this attribute couldn't be found */ public boolean enableVertexAttribute(String name) { int id = getAttributeID(name); if (id==-1) return false; ARBVertexShader.glEnableVertexAttribArrayARB(id); return true; } /** * Disables the vertex array -- in strict mode, if the vertex attribute * is not found (or it's inactive), an IllegalArgumentException will * be thrown. If strict mode is disabled and the vertex attribute * is not found, this method will return <tt>false</tt> otherwise it * will return <tt>true</tt>. * * @param name the name of the vertex attribute to disable * @return false if strict mode is disabled and this attribute couldn't be found */ public boolean disableVertexAttribute(String name) { int id = getAttributeID(name); if (id==-1) return false; ARBVertexShader.glDisableVertexAttribArrayARB(id); return true; } // public void setVertexAttribute(String name, int size, int type, boolean normalize, int stride, FloatBuffer buffer) { // ARBVertexShader.glVertexAttrib // } /** * Sets the value of an RGBA vec4 uniform to the given color * @param name the RGBA vec4 uniform * @param color the color to assign */ public void setUniform4f(String name, Color color) { setUniform4f(name, color.r, color.g, color.b, color.a); } /** * Sets the value of a vec2 uniform to the given Vector2f. * @param name the vec2 uniform * @param vec the vector to use */ public void setUniform2f(String name, Vector2f vec) { setUniform2f(name, vec.x, vec.y); } private FloatBuffer uniformf(String name) { if (buf4==null) buf4 = BufferUtils.createFloatBuffer(4); buf4.clear(); getUniform(name, buf4); return buf4; } private IntBuffer uniformi(String name) { //TODO: add setters/getters for ivec2, ivec3, ivec4 if (ibuf4==null) ibuf4 = BufferUtils.createIntBuffer(4); ibuf4.clear(); getUniform(name, ibuf4); return ibuf4; } /** * A convenience method to retrieve an integer/sampler2D uniform. * @param name the uniform name * @return the value */ public int getUniform1i(String name) { return uniformi(name).get(0); } /** * A convenience method to retrieve an ivec2 uniform; * for maximum performance and memory efficiency you * should use getUniform(String, IntBuffer) with a shared * buffer. * @param name the name of the uniform * @return a newly created int[] array with 2 elements; e.g. (x, y) */ public int[] getUniform2i(String name) { IntBuffer buf = uniformi(name); return new int[] { buf.get(0), buf.get(1) }; } /** * A convenience method to retrieve an ivec3 uniform; * for maximum performance and memory efficiency you * should use getUniform(String, IntBuffer) with a shared * buffer. * @param name the name of the uniform * @return a newly created int[] array with 3 elements; e.g. (x, y, z) */ public int[] getUniform3i(String name) { IntBuffer buf = uniformi(name); return new int[] { buf.get(0), buf.get(1), buf.get(2) }; } /** * A convenience method to retrieve an ivec4 uniform; * for maximum performance and memory efficiency you * should use getUniform(String, IntBuffer) with a shared * buffer. * @param name the name of the uniform * @return a newly created int[] array with 2 elements; e.g. (r, g, b, a) */ public int[] getUniform4i(String name) { IntBuffer buf = uniformi(name); return new int[] { buf.get(0), buf.get(1), buf.get(2), buf.get(3) }; } /** * A convenience method to retrieve a float uniform. * @param name the uniform name * @return the value */ public float getUniform1f(String name) { return uniformf(name).get(0); } /** * A convenience method to retrieve a vec2 uniform; * for maximum performance and memory efficiency you * should use getUniform(String, FloatBuffer) with a shared * buffer. * @param name the name of the uniform * @return a newly created float[] array with 2 elements; e.g. (x, y) */ public float[] getUniform2f(String name) { FloatBuffer buf = uniformf(name); return new float[] { buf.get(0), buf.get(1) }; } /** * A convenience method to retrieve a vec3 uniform; * for maximum performance and memory efficiency you * should use getUniform(String, FloatBuffer) with a shared * buffer. * @param name the name of the uniform * @return a newly created float[] array with 3 elements; e.g. (x, y, z) */ public float[] getUniform3f(String name) { FloatBuffer buf = uniformf(name); return new float[] { buf.get(0), buf.get(1), buf.get(2) }; } /** * A convenience method to retrieve a vec4 uniform; * for maximum performance and memory efficiency you * should use getUniform(String, FloatBuffer) with a shared * buffer. * @param name the name of the uniform * @return a newly created float[] array with 4 elements; e.g. (r, g, b, a) */ public float[] getUniform4f(String name) { FloatBuffer buf = uniformf(name); return new float[] { buf.get(0), buf.get(1), buf.get(2), buf.get(3) }; } /** * Retrieves data from a uniform and places it in the given buffer. If * strict mode is enabled, this will throw an IllegalArgumentException * if the given uniform is not 'active' -- i.e. if GLSL determined that * the shader isn't using it. If strict mode is disabled, this method will * return <tt>true</tt> if the uniform was found, and <tt>false</tt> otherwise. * * @param name the name of the uniform * @param buf the buffer to place the data * @return true if the uniform was found, false if there is no active uniform by that name */ public boolean getUniform(String name, FloatBuffer buf) { int id = getUniformID(name); if (id==-1) return false; ARBShaderObjects.glGetUniformARB(program, id, buf); return true; } /** * Retrieves data from a uniform and places it in the given buffer. If * strict mode is enabled, this will throw an IllegalArgumentException * if the given uniform is not 'active' -- i.e. if GLSL determined that * the shader isn't using it. If strict mode is disabled, this method will * return <tt>true</tt> if the uniform was found, and <tt>false</tt> otherwise. * * @param name the name of the uniform * @param buf the buffer to place the data * @return true if the uniform was found, false if there is no active uniform by that name */ public boolean getUniform(String name, IntBuffer buf) { int id = getUniformID(name); if (id==-1) return false; ARBShaderObjects.glGetUniformARB(program, id, buf); return true; } /** * Whether the shader program was linked with the active uniform by the given name. A * uniform might be "inactive" even if it was declared at the top of a shader; * if GLSL finds that a uniform isn't needed (i.e. not used in shader), then * it will not be active. * @param name the name of the uniform * @return true if this shader program could find the active uniform */ public boolean hasUniform(String name) { return uniforms.containsKey(name); } /** * Whether the shader program was linked with the active attribute by the given name. A * attribute might be "inactive" even if it was declared at the top of a shader; * if GLSL finds that a attribute isn't needed (i.e. not used in shader), then * it will not be active. * @param name the name of the attribute * @return true if this shader program could find the active attribute */ public boolean hasAttribute(String name) { return attributes.containsKey(name); } /** * Sets the value of a float uniform. * @param name the uniform by name * @param f the float value */ public void setUniform1f(String name, float f) { int id = getUniformID(name); if (id==-1) return; ARBShaderObjects.glUniform1fARB(id, f); } /** * Sets the value of a sampler2D uniform. * @param name the uniform by name * @param i the integer / active texture (e.g. 0 for TEXTURE0) */ public void setUniform1i(String name, int i) { int id = getUniformID(name); if (id==-1) return; ARBShaderObjects.glUniform1iARB(id, i); } /** * Sets the value of a vec2 uniform. * @param name the uniform by name * @param a vec.x / tex.s * @param b vec.y / tex.t */ public void setUniform2f(String name, float a, float b) { int id = getUniformID(name); if (id==-1) return; ARBShaderObjects.glUniform2fARB(id, a, b); } /** * Sets the value of a vec3 uniform. * @param name the uniform by name * @param a vec.x / color.r / tex.s * @param b vec.y / color.g / tex.t * @param c vec.z / color.b / tex.p */ public void setUniform3f(String name, float a, float b, float c) { int id = getUniformID(name); if (id==-1) return; ARBShaderObjects.glUniform3fARB(id, a, b, c); } /** * Sets the value of a vec4 uniform. * @param name the uniform by name * @param a vec.x / color.r * @param b vec.y / color.g * @param c vec.z / color.b * @param d vec.w / color.a */ public void setUniform4f(String name, float a, float b, float c, float d) { int id = getUniformID(name); if (id==-1) return; ARBShaderObjects.glUniform4fARB(id, a, b, c, d); } /** * Sets the value of a ivec2 uniform. * @param name the uniform by name * @param a vec.x / tex.s * @param b vec.y / tex.t */ public void setUniform2i(String name, int a, int b) { int id = getUniformID(name); if (id==-1) return; ARBShaderObjects.glUniform2iARB(id, a, b); } /** * Sets the value of a ivec3 uniform. * @param name the uniform by name * @param a vec.x / color.r * @param b vec.y / color.g * @param c vec.z / color.b */ public void setUniform3i(String name, int a, int b, int c) { int id = getUniformID(name); if (id==-1) return; ARBShaderObjects.glUniform3iARB(id, a, b, c); } /** * Sets the value of a ivec4 uniform. * @param name the uniform by name * @param a vec.x / color.r * @param b vec.y / color.g * @param c vec.z / color.b * @param d vec.w / color.a */ public void setUniform4i(String name, int a, int b, int c, int d) { int id = getUniformID(name); if (id==-1) return; ARBShaderObjects.glUniform4iARB(id, a, b, c, d); } /** * Sets a uniform matrix2 with the given name and transpose. * @param name the name to use * @param transpose whether to transpose the matrix * @param buf the buffer representing the matrix2 */ public void setMatrix2(String name, boolean transpose, FloatBuffer buf) { int id = getUniformID(name); if (id==-1) return; ARBShaderObjects.glUniformMatrix2ARB(id, transpose, buf); } /** * Sets a uniform matrix3 with the given name and transpose. * @param name the name to use * @param transpose whether to transpose the matrix * @param buf the buffer representing the matrix3 */ public void setMatrix3(String name, boolean transpose, FloatBuffer buf) { int id = getUniformID(name); if (id==-1) return; ARBShaderObjects.glUniformMatrix3ARB(id, transpose, buf); } /** * Sets a uniform matrix4 with the given name and transpose. * @param name the name to use * @param transpose whether to transpose the matrix * @param buf the buffer representing the matrix4 */ public void setMatrix4(String name, boolean transpose, FloatBuffer buf) { int id = getUniformID(name); if (id==-1) return; ARBShaderObjects.glUniformMatrix4ARB(id, transpose, buf); } }