class TestContext(BaseContext): """Demonstrates use of attribute types in GLSL """ def OnInit(self): """Initialize the context""" '''== Phong and Blinn Reflectance == A shiny surface will tend to have a "bright spot" at the point on the surface where the angle of incidence for the reflected light ray and the viewer's ray are (close to) equal. A perfect mirror would have the brights spot solely when the two vectors are exactly equal, while a perfect Lambertian surface would have the "bright spot" spread across the entire surface. The Phong rendering process models this as a setting, traditionally called material "shininess" in Legacy OpenGL. This setting acts as a power which raises the cosine (dot product) of the angle between the reflected ray and the eye. The calculation of the cosine (dot product) of the two angles requires that we do a dot product of the two angles once for each vertex/fragment for which we wish to calculate the specular reflectance, we also have to find the angle of reflectance before we can do the calculation:''' """ L_dir = (V_pos-L_pos) R = 2N*(dot( N, L_dir))-L_dir // Note: in eye-coordinate system, Eye_pos == (0,0,0) Spec_factor = pow( dot( R, V_pos-Eye_pos ), shininess) """ '''which, as we can see, involves the vertex position in a number of stages of the operation, so requires recalculation all through the rendering operation. There is, however, a simplified version of Phong Lighting called [http://en.wikipedia.org/wiki/Blinn%E2%80%93Phong_shading_model Blinn-Phong] which notes that if we were to do all of our calculations in "eye space", and were to assume that (as is normal), the eye and light coordinates will not change for a rendering pass, (note: this limits us to directional lights!) we can use a pre-calculated value which is the bisecting angle between the light-vector and the view-vector, called the "half vector" to perform approximately the same calculation. With this value:''' """ // note that in Eye coordinates, Eye_EC_dir == 0,0,-1 H = normalize( Eye_EC_dir + Light_EC_dir ) Spec_factor = pow( dot( H, N ), shininess ) """ '''Note: however, that the resulting Spec_factor is not *precisely* the same value as the original calculation, so the "shininess" exponent must be slightly lower to approximate the value that Phong rendering would achieve. The value is, however, considered close to "real world" materials, so the Blinn method is generally preferred to Phong. Traditionally, n_dot_pos would be cut off at 0.0, but that would create extremely hard-edged cut-offs for specular color. Here we "fudge" the result by 0.05 ''' phong_weightCalc = """ vec2 phong_weightCalc( in vec3 light_pos, // light position in vec3 half_light, // half-way vector between light and view in vec3 frag_normal, // geometry normal in float shininess ) { // returns vec2( ambientMult, diffuseMult ) float n_dot_pos = max( 0.0, dot( frag_normal, light_pos )); float n_dot_half = 0.0; if (n_dot_pos > -.05) { n_dot_half = pow(max(0.0,dot( half_light, frag_normal )), shininess); } return vec2( n_dot_pos, n_dot_half); } """ '''We are going to use per-fragment rendering. As a result, our vertex shader becomes very simple, just arranging for the Normals to be varied across the surface. ''' vertex = shaders.compileShader( """ attribute vec3 Vertex_position; attribute vec3 Vertex_normal; varying vec3 baseNormal; void main() { gl_Position = gl_ModelViewProjectionMatrix * vec4( Vertex_position, 1.0 ); baseNormal = gl_NormalMatrix * normalize(Vertex_normal); }""", GL_VERTEX_SHADER) '''Our fragment shader looks much like our previous tutorial's vertex shader. As before, we have lots of uniform values, but now we also calculate the light's half-vector (in eye-space coordinates). The phong_weightCalc function does the core Blinn calculation, and we simply use the resulting factor to add to the colour value for the fragment. Note the use of the eye-coordinate-space to simplify the half-vector calculation, the eye-space eye-vector is always the same value (pointing down the negative Z axis), and the eye-space eye-coordinate is always (0,0,0), so the eye-to-vertex vector is always the eye-space vector position. ''' fragment = shaders.compileShader( phong_weightCalc + """ uniform vec4 Global_ambient; uniform vec4 Light_ambient; uniform vec4 Light_diffuse; uniform vec4 Light_specular; uniform vec3 Light_location; uniform float Material_shininess; uniform vec4 Material_specular; uniform vec4 Material_ambient; uniform vec4 Material_diffuse; varying vec3 baseNormal; void main() { // normalized eye-coordinate Light location vec3 EC_Light_location = normalize( gl_NormalMatrix * Light_location ); // half-vector calculation vec3 Light_half = normalize( EC_Light_location - vec3( 0,0,-1 ) ); vec2 weights = phong_weightCalc( EC_Light_location, Light_half, baseNormal, Material_shininess ); gl_FragColor = clamp( ( (Global_ambient * Material_ambient) + (Light_ambient * Material_ambient) + (Light_diffuse * Material_diffuse * weights.x) // material's shininess is the only change here... + (Light_specular * Material_specular * weights.y) ), 0.0, 1.0); } """, GL_FRAGMENT_SHADER) self.shader = shaders.compileProgram(vertex, fragment) '''Here's the call that creates the two VBOs and the count of records to render from them. If you're curious you can read through the source code of the OpenGLContext.scenegraph.quadrics module to read the mechanism that generates the values. The sphere is a simple rendering mechanism, as for a unit-sphere at the origin, the sphere's normals are the same as the sphere's vertex coordinate. The complexity comes primarily in generating the triangle indices that link the points generated. ''' self.coords, self.indices, self.count = Sphere(radius=1).compile() '''We have a few more uniforms to control the specular components. Real-world coding would also calculate the light's half-vector and provide it as a uniform (so that it would only need to be calculated once), but we are going to do the half-vector calculation in the shader to make it obvious what is going on. The legacy OpenGL pipeline provides the value pre-calculated as part of the light structure in GLSL. ''' for uniform in ( 'Global_ambient', 'Light_ambient', 'Light_diffuse', 'Light_location', 'Light_specular', 'Material_ambient', 'Material_diffuse', 'Material_shininess', 'Material_specular', ): location = glGetUniformLocation(self.shader, uniform) if location in (None, -1): print 'Warning, no uniform: %s' % (uniform) setattr(self, uniform + '_loc', location) for attribute in ( 'Vertex_position', 'Vertex_normal', ): location = glGetAttribLocation(self.shader, attribute) if location in (None, -1): print 'Warning, no attribute: %s' % (uniform) setattr(self, attribute + '_loc', location) def Render(self, mode=None): """Render the geometry for the scene.""" BaseContext.Render(self, mode) glUseProgram(self.shader) try: '''==Indexed VBO Rendering== You'll notice here that we are binding two different VBO objects. As we mentioned above, the Sphere renderer generated both VBOs, but doesn't the second binding replace the first binding? That is, why doesn't OpenGL try to read the Vertex data out of the indices VBO? OpenGL defines multiple binding "targets" for VBOs, the first VBO (vertices) was bound to the GL_ARRAY_BUFFER target (the default for the class), which is used for reading per-vertex data arrays, while the indices buffer was defined as targetting the GL_ELEMENT_ARRAY_BUFFER, which is used solely for reading indices. Each target can be bound to a different VBO, and thus we can bind both VBOs at the same time without confusion. ''' self.coords.bind() self.indices.bind() '''Here, being lazy, we use the numpy array's nbytes value to specify the stride between records. The VBO object has a "data" value which is the data-set which was initially passed to the VBO constructor. The first element in this array is a single vertex record. This array happens to have 8 floating-point values (24 bytes), the first three being the vertex position, the next two being the texture coordinate and the last three being the vertex normal. We'll ignore the texture coordinate for now. ''' stride = self.coords.data[0].nbytes try: glUniform4f(self.Global_ambient_loc, .05, .05, .05, .1) glUniform4f(self.Light_ambient_loc, .1, .1, .1, 1.0) glUniform4f(self.Light_diffuse_loc, .25, .25, .25, 1) '''We set up a yellow-ish specular component in the light and move it to rest "just over our right shoulder" in relation to the initial camera.''' glUniform4f(self.Light_specular_loc, 0.0, 1.0, 0, 1) glUniform3f(self.Light_location_loc, 6, 2, 4) glUniform4f(self.Material_ambient_loc, .1, .1, .1, 1.0) glUniform4f(self.Material_diffuse_loc, .15, .15, .15, 1) '''We make the material have a bright specular white colour and an extremely "shiny" surface. The shininess value has the effect of reducing the area of the highlight, as the cos of the angle is raised to the power of the (fractional) shininess.''' glUniform4f(self.Material_specular_loc, 1.0, 1.0, 1.0, 1.0) glUniform1f(self.Material_shininess_loc, .95) glEnableVertexAttribArray(self.Vertex_position_loc) glEnableVertexAttribArray(self.Vertex_normal_loc) glVertexAttribPointer(self.Vertex_position_loc, 3, GL_FLOAT, False, stride, self.coords) glVertexAttribPointer(self.Vertex_normal_loc, 3, GL_FLOAT, False, stride, self.coords + (5 * 4)) '''Here we introduce the OpenGL call which renders via an index-array rather than just rendering vertices in definition order. The last two arguments tell OpenGL what data-type we've used for the indices (the Sphere renderer uses shorts). The indices VBO is actually just passing the value c_void_p( 0 ) (i.e. a null pointer), which causes OpenGL to use the currently bound VBO for the GL_ELEMENT_ARRAY_BUFFER target. ''' glDrawElements(GL_TRIANGLES, self.count, GL_UNSIGNED_SHORT, self.indices) finally: '''Note the need to unbind *both* VBOs, we have to free *both* VBO targets to avoid any other rendering operation from trying to access the VBOs.''' self.coords.unbind() self.indices.unbind() glDisableVertexAttribArray(self.Vertex_position_loc) glDisableVertexAttribArray(self.Vertex_normal_loc) finally: glUseProgram(0)
class TestContext(BaseContext): """Demonstrates use of attribute types in GLSL """ LIGHT_COUNT = 3 LIGHT_SIZE = 4 def OnInit(self): """Initialize the context""" '''==Sharing Declarations= Since we are going to use these values in both the vertex and fragment shaders, it is handy to separate out the constants we'll use into a separate block of code that we can add to both shaders. The use of the constants also makes the code far easier to read than using the bare numbers. Note that the varying baseNormal value is part of the lighting calculation, so we have included it in our common lighting declarations. We've also parameterized the LIGHT count and size, so that we can use them in both Python and GLSL code. ''' lightConst = """ const int LIGHT_COUNT = %s; const int LIGHT_SIZE = %s; const int AMBIENT = 0; const int DIFFUSE = 1; const int SPECULAR = 2; const int POSITION = 3; uniform vec4 lights[ LIGHT_COUNT*LIGHT_SIZE ]; varying vec3 EC_Light_half[LIGHT_COUNT]; varying vec3 EC_Light_location[LIGHT_COUNT]; varying vec3 baseNormal; """ % (self.LIGHT_COUNT, self.LIGHT_SIZE) '''As you can see, we're going to create two new varying values, the EC_Light_half and EC_Light_location values. These are going to hold the normalized partial calculations for the lights. The other declarations are the same as before, they are just being shared between the shaders. Our phong_weightCalc calculation hasn't changed. ''' phong_weightCalc = """ vec2 phong_weightCalc( in vec3 light_pos, // light position in vec3 half_light, // half-way vector between light and view in vec3 frag_normal, // geometry normal in float shininess ) { // returns vec2( ambientMult, diffuseMult ) float n_dot_pos = max( 0.0, dot( frag_normal, light_pos )); float n_dot_half = 0.0; if (n_dot_pos > -.05) { n_dot_half = pow(max(0.0,dot( half_light, frag_normal )), shininess); } return vec2( n_dot_pos, n_dot_half); } """ '''Our new vertex shader has a loop in it. It iterates over the set of lights doing the partial calculations for half-vector and eye-space location. It stores the results of these in our new, varying array values. ''' vertex = shaders.compileShader( lightConst + """ attribute vec3 Vertex_position; attribute vec3 Vertex_normal; void main() { gl_Position = gl_ModelViewProjectionMatrix * vec4( Vertex_position, 1.0 ); baseNormal = gl_NormalMatrix * normalize(Vertex_normal); for (int i = 0; i< LIGHT_COUNT; i++ ) { EC_Light_location[i] = normalize( gl_NormalMatrix * lights[(i*LIGHT_SIZE)+POSITION].xyz ); // half-vector calculation EC_Light_half[i] = normalize( EC_Light_location[i] - vec3( 0,0,-1 ) ); } }""", GL_VERTEX_SHADER) '''Our fragment shader looks much the same, save that we have now moved the complex half-vector and eye-space location calculations out. We've also separated out the concept of which light we are processing and what array-offset we are using, to make it clearer which value is being accessed. ''' fragment = shaders.compileShader( lightConst + phong_weightCalc + """ struct Material { vec4 ambient; vec4 diffuse; vec4 specular; float shininess; }; uniform Material material; uniform vec4 Global_ambient; void main() { vec4 fragColor = Global_ambient * material.ambient; int i,j; for (i=0;i<LIGHT_COUNT;i++) { j = i* LIGHT_SIZE; vec2 weights = phong_weightCalc( EC_Light_location[i], EC_Light_half[i], baseNormal, material.shininess ); fragColor = ( fragColor + (lights[j+AMBIENT] * material.ambient) + (lights[j+DIFFUSE] * material.diffuse * weights.x) + (lights[j+SPECULAR] * material.specular * weights.y) ); } gl_FragColor = fragColor; } """, GL_FRAGMENT_SHADER) '''The rest of our code is very familiar.''' self.shader = shaders.compileProgram(vertex, fragment) self.coords, self.indices, self.count = Sphere(radius=1).compile() self.uniform_locations = {} for uniform, value in self.UNIFORM_VALUES: location = glGetUniformLocation(self.shader, uniform) if location in (None, -1): print 'Warning, no uniform: %s' % (uniform) self.uniform_locations[uniform] = location self.uniform_locations['lights'] = glGetUniformLocation( self.shader, 'lights') for attribute in ( 'Vertex_position', 'Vertex_normal', ): location = glGetAttribLocation(self.shader, attribute) if location in (None, -1): print 'Warning, no attribute: %s' % (uniform) setattr(self, attribute + '_loc', location) UNIFORM_VALUES = [ ('Global_ambient', (.05, .05, .05, 1.0)), ('material.ambient', (.2, .2, .2, 1.0)), ('material.diffuse', (.5, .5, .5, 1.0)), ('material.specular', (.8, .8, .8, 1.0)), ('material.shininess', (.995, )), ] LIGHTS = array([ x[1] for x in [ ('lights[0].ambient', (.05, .05, .05, 1.0)), ('lights[0].diffuse', (.3, .3, .3, 1.0)), ('lights[0].specular', (1.0, 0.0, 0.0, 1.0)), ('lights[0].position', (4.0, 2.0, 10.0, 0.0)), ('lights[1].ambient', (.05, .05, .05, 1.0)), ('lights[1].diffuse', (.3, .3, .3, 1.0)), ('lights[1].specular', (0.0, 1.0, 0.0, 1.0)), ('lights[1].position', (-4.0, 2.0, 10.0, 0.0)), ('lights[2].ambient', (.05, .05, .05, 1.0)), ('lights[2].diffuse', (.3, .3, .3, 1.0)), ('lights[2].specular', (0.0, 0.0, 1.0, 1.0)), ('lights[2].position', (-4.0, 2.0, -10.0, 0.0)), ] ], 'f') def Render(self, mode=None): """Render the geometry for the scene.""" BaseContext.Render(self, mode) if not mode.visible: return glUseProgram(self.shader) try: self.coords.bind() stride = self.coords.data[0].nbytes try: '''Note the use of the parameterized values to specify the size of the light-parameter array.''' glUniform4fv(self.uniform_locations['lights'], self.LIGHT_COUNT * self.LIGHT_SIZE, self.LIGHTS) for uniform, value in self.UNIFORM_VALUES: location = self.uniform_locations.get(uniform) if location not in (None, -1): if len(value) == 4: glUniform4f(location, *value) elif len(value) == 3: glUniform3f(location, *value) elif len(value) == 1: glUniform1f(location, *value) glEnableVertexAttribArray(self.Vertex_position_loc) glEnableVertexAttribArray(self.Vertex_normal_loc) glVertexAttribPointer(self.Vertex_position_loc, 3, GL_FLOAT, False, stride, self.coords) glVertexAttribPointer(self.Vertex_normal_loc, 3, GL_FLOAT, False, stride, self.coords + (5 * 4)) self.indices.bind() glDrawElements(GL_TRIANGLES, self.count, GL_UNSIGNED_SHORT, self.indices) finally: self.coords.unbind() self.indices.unbind() glDisableVertexAttribArray(self.Vertex_position_loc) glDisableVertexAttribArray(self.Vertex_normal_loc) finally: glUseProgram(0)
class TestContext(BaseContext): """Demonstrates use of attribute types in GLSL """ LIGHT_COUNT = 3 '''Note that we're going to add 2 new vec4 fields to our light, the legacy GL has 2 floats and a direction vector, but we're combining all the values into a uniform vector set.''' LIGHT_SIZE = 7 def OnInit(self): """Initialize the context""" '''As you can see, we've created a new 4-element vector called SPOT_PARAMS which holds 3 "meaningful" float values. The first element cos_spot_cutoff represents the cosign of the angle beyond which the light is cut off. This angle is measured from the light's spot_direction compared with the light_location vector. In effect, it is a check to see if the fragment is within the cone of the light. The use of the *cosine* of the angle is to allow for very fast checks against the dot-product of the normalized vectors. The second element, spot_exponent, is used to calculate the amount of "spotiness" of the spotlight, that is, the amount to which the spotlight focusses light on the center of the beam versus the outside of the beam. A higher spot_exponent will cause the spotlight to "focus" more, a lower exponent will cause the spotlight to act more like a "shielded" point-light (such as a lamp with blockers rather than reflectors). The last component of the SPOT_PARAMS is being used as a simple flag to tell us whether to apply the spot calculation. We could have shaved a whole vec4 by packing the spot_exponent into the ATTENUATION vector's unused .w component, and then packing the spot_cutoff into the spot_direction's unused .w, but that becomes a bit awkward looking. ''' lightConst = """ const int LIGHT_COUNT = %s; const int LIGHT_SIZE = %s; const int AMBIENT = 0; const int DIFFUSE = 1; const int SPECULAR = 2; const int POSITION = 3; const int ATTENUATION = 4; //SPOT_PARAMS [ cos_spot_cutoff, spot_exponent, ignored, is_spot ] const int SPOT_PARAMS = 5; const int SPOT_DIR = 6; uniform vec4 lights[ LIGHT_COUNT*LIGHT_SIZE ]; varying vec3 EC_Light_half[LIGHT_COUNT]; varying vec3 EC_Light_location[LIGHT_COUNT]; varying float Light_distance[LIGHT_COUNT]; varying vec3 baseNormal; """ % (self.LIGHT_COUNT, self.LIGHT_SIZE) '''Our phong_weightCalc function receives its final tweaks here. We provide the two vec4 spot elements for the current light. The spotlight operation modifies the point-light code such that the "attenuation" numerator is either 1.0 (for non-directional point-lights) or a calculated value for spot-lights. To calculate this value, we take the (cos of the) angle between the light direction (spot_direction) and the vector between the fragment and the light location (-light_pos). If this value is lower than our spot_cutoff, then we do not want to provide any lighting whatsoever from this light, so we short-circuit and return a null vector of weights. If the value is higher than the cutoff, we calculate the "spotiness" multiplier. Here we are *not* using the OpenGL standard method, instead we calculate the fraction of total cosine-space which is displayed and raise it to the power of our spot_exponent value. This is our last tweak to the blinn-phong lighting model so we'll make this version of the function available like so: from OpenGLContext.resources.phongweights_frag import data as phong_weightCalc ''' phong_weightCalc = """ vec3 phong_weightCalc( in vec3 light_pos, // light position/direction in vec3 half_light, // half-way vector between light and view in vec3 frag_normal, // geometry normal in float shininess, // shininess exponent in float distance, // distance for attenuation calculation... in vec4 attenuations, // attenuation parameters... in vec4 spot_params, // spot control parameters... in vec4 spot_direction // model-space direction ) { // returns vec3( ambientMult, diffuseMult, specularMult ) float n_dot_pos = max( 0.0, dot( frag_normal, light_pos )); float n_dot_half = 0.0; float attenuation = 1.0; if (n_dot_pos > -.05) { float spot_effect = 1.0; if (spot_params.w != 0.0) { // is a spot... float spot_cos = dot( gl_NormalMatrix * normalize(spot_direction.xyz), normalize(-light_pos) ); if (spot_cos <= spot_params.x) { // is a spot, and is outside the cone-of-light... return vec3( 0.0, 0.0, 0.0 ); } else { if (spot_cos == 1.0) { spot_effect = 1.0; } else { spot_effect = pow( (1.0-spot_params.x)/(1.0-spot_cos), spot_params.y ); } } } n_dot_half = pow( max(0.0,dot( half_light, frag_normal )), shininess ); if (distance != 0.0) { float attenuation = 1.0/( attenuations.x + (attenuations.y * distance) + (attenuations.z * distance * distance) ); n_dot_half *= spot_effect; n_dot_pos *= attenuation; n_dot_half *= attenuation; } } return vec3( attenuation, n_dot_pos, n_dot_half); } """ '''Nothing needs to change in our vertex shader, save that we're using the functions we stored to external files in the previous tutorial.''' from OpenGLContext.resources.phongprecalc_vert import data as phong_preCalc light_preCalc = open('_shader_tut_lightprecalc.vert').read() vertex = shaders.compileShader( lightConst + phong_preCalc + light_preCalc + """ attribute vec3 Vertex_position; attribute vec3 Vertex_normal; void main() { gl_Position = gl_ModelViewProjectionMatrix * vec4( Vertex_position, 1.0 ); baseNormal = gl_NormalMatrix * normalize(Vertex_normal); light_preCalc( Vertex_position ); }""", GL_VERTEX_SHADER) '''Our only change for the fragment shader is to pass in the spot components of the current light when calling phong_weightCalc.''' fragment = shaders.compileShader( lightConst + phong_weightCalc + """ struct Material { vec4 ambient; vec4 diffuse; vec4 specular; float shininess; }; uniform Material material; uniform vec4 Global_ambient; void main() { vec4 fragColor = Global_ambient * material.ambient; int i,j; for (i=0;i<LIGHT_COUNT;i++) { j = i* LIGHT_SIZE; vec3 weights = phong_weightCalc( normalize(EC_Light_location[i]), normalize(EC_Light_half[i]), normalize(baseNormal), material.shininess, abs(Light_distance[i]), // see note tutorial 9 lights[j+ATTENUATION], lights[j+SPOT_PARAMS], lights[j+SPOT_DIR] ); fragColor = ( fragColor + (lights[j+AMBIENT] * material.ambient * weights.x) + (lights[j+DIFFUSE] * material.diffuse * weights.y) + (lights[j+SPECULAR] * material.specular * weights.z) ); } gl_FragColor = fragColor; } """, GL_FRAGMENT_SHADER) '''Our uniform/geometry handling code is unchanged.''' self.shader = shaders.compileProgram(vertex, fragment) self.coords, self.indices, self.count = Sphere(radius=1).compile() self.uniform_locations = {} for uniform, value in self.UNIFORM_VALUES: location = glGetUniformLocation(self.shader, uniform) if location in (None, -1): print 'Warning, no uniform: %s' % (uniform) self.uniform_locations[uniform] = location self.uniform_locations['lights'] = glGetUniformLocation( self.shader, 'lights') for attribute in ( 'Vertex_position', 'Vertex_normal', ): location = glGetAttribLocation(self.shader, attribute) if location in (None, -1): print 'Warning, no attribute: %s' % (uniform) setattr(self, attribute + '_loc', location) '''We'll dial down the shininess on our material a little so that it's easier to see the spotlight cones on the sphere.''' UNIFORM_VALUES = [ ('Global_ambient', (.05, .05, .05, 1.0)), ('material.ambient', (.8, .8, .8, 1.0)), ('material.diffuse', (.8, .8, .8, 1.0)), ('material.specular', (.8, .8, .8, 1.0)), ('material.shininess', (.8, )), ] '''Our lights array now has more fields per light. The spotlight vectors always have to be present, even if we are not using them for a particular light. We're going to define 3 lights here with fairly high "spotiness" values so that we can see the focussed beam effect on the sphere. The spot exponents in this case tend to cause an area in the center of the beam to saturate completely. ''' LIGHTS = array([ x[1] for x in [ ('lights[0].ambient', (.05, .05, .05, 1.0)), ('lights[0].diffuse', (.1, .8, .1, 1.0)), ('lights[0].specular', (0.0, .05, 0.0, 1.0)), ('lights[0].position', (2.5, 3.5, 2.5, 1.0)), ('lights[0].attenuation', (0.0, 1.0, 1.0, 1.0)), ('lights[0].spot_params', (cos(.25), 1.0, 0.0, 1.0)), ('lights[0].spot_dir', (-8, -20, -8.0, 1.0)), ('lights[1].ambient', (.05, .05, .05, 1.0)), ('lights[1].diffuse', (.8, .1, .1, 1.0)), ('lights[1].specular', (.25, 0.0, 0.0, 1.0)), ('lights[1].position', (-2.5, 2.5, 2.5, 1.0)), ('lights[1].attenuation', (0.0, 0.0, .125, 1.0)), ('lights[1].spot_params', (cos(.25), 1.25, 0.0, 1.0)), ('lights[1].spot_dir', (2.5, -5.5, -2.5, 1.0)), ('lights[2].ambient', (.05, .05, .05, 1.0)), ('lights[2].diffuse', (.1, .1, 1.0, 1.0)), ('lights[2].specular', (0.0, .25, .25, 1.0)), ('lights[2].position', (0.0, -3.06, 3.06, 1.0)), ('lights[2].attenuation', (2.0, 0.0, 0.0, 1.0)), ('lights[2].spot_params', (cos(.15), .75, 0.0, 1.0)), ('lights[2].spot_dir', (0.0, 3.06, -3.06, 1.0)), ] ], 'f') '''Nothing else needs to change from the previous tutorial.''' def Render(self, mode=None): """Render the geometry for the scene.""" BaseContext.Render(self, mode) if not mode.visible: return glUseProgram(self.shader) try: self.coords.bind() self.indices.bind() stride = self.coords.data[0].nbytes try: glUniform4fv(self.uniform_locations['lights'], self.LIGHT_COUNT * self.LIGHT_SIZE, self.LIGHTS) for uniform, value in self.UNIFORM_VALUES: location = self.uniform_locations.get(uniform) if location not in (None, -1): if len(value) == 4: glUniform4f(location, *value) elif len(value) == 3: glUniform3f(location, *value) elif len(value) == 1: glUniform1f(location, *value) glEnableVertexAttribArray(self.Vertex_position_loc) glEnableVertexAttribArray(self.Vertex_normal_loc) glVertexAttribPointer(self.Vertex_position_loc, 3, GL_FLOAT, False, stride, self.coords) glVertexAttribPointer(self.Vertex_normal_loc, 3, GL_FLOAT, False, stride, self.coords + (5 * 4)) glDrawElements(GL_TRIANGLES, self.count, GL_UNSIGNED_SHORT, self.indices) finally: self.coords.unbind() self.indices.unbind() glDisableVertexAttribArray(self.Vertex_position_loc) glDisableVertexAttribArray(self.Vertex_normal_loc) finally: glUseProgram(0)
class TestContext( BaseContext ): """Demonstrates use of attribute types in GLSL """ LIGHT_COUNT = 3 LIGHT_SIZE = 5 def OnInit( self ): """Initialize the context""" '''Our common light-model declarations are getting slightly more involved. We're adding a single field to the light "structure", the attenuation field. This is a 4-item vector where the first item is a constant attenuation factor, the second is a linear attenuation factor, and the third a quadratic attenuation factor. The fourth item is ignored, but we are using an array of vec4s for the light parameters, so it is easiest to just ignore the w value. ''' lightConst = """ const int LIGHT_COUNT = %s; const int LIGHT_SIZE = %s; const int AMBIENT = 0; const int DIFFUSE = 1; const int SPECULAR = 2; const int POSITION = 3; const int ATTENUATION = 4; uniform vec4 lights[ LIGHT_COUNT*LIGHT_SIZE ]; varying vec3 EC_Light_half[LIGHT_COUNT]; varying vec3 EC_Light_location[LIGHT_COUNT]; varying float Light_distance[LIGHT_COUNT]; varying vec3 baseNormal; """%( self.LIGHT_COUNT, self.LIGHT_SIZE ) '''==Lighting Attenuation= For the first time in many tutorials we're altering out lighting calculation. We're adding 2 inputs to the function, the first is the distance from the fragment to the light, the second is the attenuation vector for the in-process light. We are also going to return one extra value, the ambient-light multiplier for this light. For our directional lights this was always 1.0, but now our light's ambient contribution can be controlled by attenuation. The core calculation for attenuation looks like this: ''' """attenuation = clamp( 0.0, 1.0, 1.0 / ( attenuations.x + (attenuations.y * distance) + (attenuations.z * distance * distance) ) );""" '''The default attenuation for legacy OpenGL was (1.0, 0.0, 0.0), which is to say, no attenuation at all. The attenuation values are not particularly "human friendly", but they give you some control over the distance at which lights cause effects. Keep in mind when using attenuation coefficients that smaller values mean the light goes farther, so a coefficient of .5 is "brighter" than a coefficient of 1.0. ''' phong_weightCalc = """ vec3 phong_weightCalc( in vec3 light_pos, // light position/direction in vec3 half_light, // half-way vector between light and view in vec3 frag_normal, // geometry normal in float shininess, // shininess exponent in float distance, // distance for attenuation calculation... in vec4 attenuations // attenuation parameters... ) { // returns vec3( ambientMult, diffuseMult, specularMult ) float n_dot_pos = max( 0.0, dot( frag_normal, light_pos )); float n_dot_half = 0.0; float attenuation = 1.0; if (n_dot_pos > -.05) { n_dot_half = pow( max(0.0,dot( half_light, frag_normal )), shininess ); if (distance != 0.0) { attenuation = clamp( 0.0, 1.0, 1.0 / ( attenuations.x + (attenuations.y * distance) + (attenuations.z * distance * distance) ) ); n_dot_pos *= attenuation; n_dot_half *= attenuation; } } return vec3( attenuation, n_dot_pos, n_dot_half); } """ '''==Calculating Distance and Direction== Our new lights are "point sources", that is, they have a model-space location which is not at "infinite distance". Because of this, unlike "directional lights", we have to recalculate the light position/location/direction vector for each fragment. We also need to know the distance of the light from each fragment. While we could perform those calculations in the fragment shader, the vectors and distances we need vary smoothly across the triangles involved, so we'll calculate them at each vertex and allow the hardware to interpolate them. We'll have to normalize the interpolated values, but this is less processor intensive than doing the calculations for each fragment. We are doing our vector calculations for the light location and distance in model-space. You could do them in view-space as well. Our vertex calculations are getting complex enough that we're going to split them into a separate function for readability and (eventual) reusability. We'll create a function which encodes the algorithmic operation (phong_preCalc) and another which takes our particular attributes/uniforms and accumulates the results from that function. We're defining phong_preCalc inline here, but we'll also store it as a resource we can use from anywhere: from OpenGLContext.resources.phongprecalc_vert import data as phong_preCalc ''' phong_preCalc = """ // Vertex-shader pre-calculation for lighting... void phong_preCalc( in vec3 vertex_position, in vec4 light_position, out float light_distance, out vec3 ec_light_location, out vec3 ec_light_half ) { // This is the core setup for a phong lighting pass // as a reusable fragment of code. // vertex_position -- un-transformed vertex position (world-space) // light_position -- un-transformed light location (direction) // light_distance -- output giving world-space distance-to-light // ec_light_location -- output giving location of light in eye coords // ec_light_half -- output giving the half-vector optimization if (light_position.w == 0.0) { // directional rather than positional light... ec_light_location = normalize( gl_NormalMatrix * light_position.xyz ); light_distance = 0.0; } else { // positional light, we calculate distance in // model-view space here, so we take a partial // solution... vec3 ms_vec = ( light_position.xyz - vertex_position ); vec3 light_direction = gl_NormalMatrix * ms_vec; ec_light_location = normalize( light_direction ); light_distance = abs(length( ms_vec )); } // half-vector calculation ec_light_half = normalize( ec_light_location + vec3( 0,0,1 ) ); }""" '''This function is not as generally reusable, so we'll store it to a separate file named '_shader_tut_lightprecalc.vert'.''' light_preCalc = """ void light_preCalc( in vec3 vertex_position ) { // This function is dependent on the uniforms and // varying values we've been using, it basically // just iterates over the phong_lightCalc passing in // the appropriate pointers... vec3 light_direction; for (int i = 0; i< LIGHT_COUNT; i++ ) { int j = i * LIGHT_SIZE; phong_preCalc( vertex_position, lights[j+POSITION], // following are the values to fill in... Light_distance[i], EC_Light_location[i], EC_Light_half[i] ); } } """ vertex = shaders.compileShader( lightConst + phong_preCalc + light_preCalc + """ attribute vec3 Vertex_position; attribute vec3 Vertex_normal; void main() { gl_Position = gl_ModelViewProjectionMatrix * vec4( Vertex_position, 1.0 ); baseNormal = gl_NormalMatrix * normalize(Vertex_normal); light_preCalc(Vertex_position); }""", GL_VERTEX_SHADER) '''Our fragment shader is only slightly modified to use our new phong_weightCalc function. We need a larger "weights" variable and need to pass in more information. We also need to multiply the per-light ambient value by the new weight we've added. You will also notice that since we are using the 'i' variable to directly index the varying arrays, we've introduced a 'j' variable that tracks the offset into the light array which begins the current light. ''' fragment = shaders.compileShader( lightConst + phong_weightCalc + """ struct Material { vec4 ambient; vec4 diffuse; vec4 specular; float shininess; }; uniform Material material; uniform vec4 Global_ambient; void main() { vec4 fragColor = Global_ambient * material.ambient; int i,j; for (i=0;i<LIGHT_COUNT;i++) { j = i* LIGHT_SIZE; vec3 weights = phong_weightCalc( normalize(EC_Light_location[i]), normalize(EC_Light_half[i]), baseNormal, material.shininess, // some implementations will produce negative values interpolating positive float-arrays! // so we have to do an extra abs call for distance abs(Light_distance[i]), lights[j+ATTENUATION] ); fragColor = ( fragColor + (lights[j+AMBIENT] * material.ambient * weights.x) + (lights[j+DIFFUSE] * material.diffuse * weights.y) + (lights[j+SPECULAR] * material.specular * weights.z) ); } //fragColor = vec4(Light_distance[0],Light_distance[1],Light_distance[2],1.0); gl_FragColor = fragColor; } """, GL_FRAGMENT_SHADER) '''Our general uniform setup should look familiar by now.''' self.shader = shaders.compileProgram(vertex,fragment) self.coords,self.indices,self.count = Sphere( radius = 1 ).compile() self.uniform_locations = {} for uniform,value in self.UNIFORM_VALUES: location = glGetUniformLocation( self.shader, uniform ) if location in (None,-1): print 'Warning, no uniform: %s'%( uniform ) self.uniform_locations[uniform] = location self.uniform_locations['lights'] = glGetUniformLocation( self.shader, 'lights' ) for attribute in ( 'Vertex_position','Vertex_normal', ): location = glGetAttribLocation( self.shader, attribute ) if location in (None,-1): print 'Warning, no attribute: %s'%( uniform ) setattr( self, attribute+ '_loc', location ) UNIFORM_VALUES = [ ('Global_ambient',(.05,.05,.05,1.0)), ('material.ambient',(.2,.2,.2,1.0)), ('material.diffuse',(.5,.5,.5,1.0)), ('material.specular',(.8,.8,.8,1.0)), ('material.shininess',(2.0,)), ] '''We've created 3 equal-distance lights here, in red, green and blue. The green light uses linear attenuation, the red quadratic and the blue constant. ''' LIGHTS = array([ x[1] for x in [ ('lights[0].ambient',(.05,.05,.05,1.0)), ('lights[0].diffuse',(.1,.8,.1,1.0)), ('lights[0].specular',(0.0,1.0,0.0,1.0)), ('lights[0].position',(2.5,2.5,2.5,1.0)), ('lights[0].attenuation',(0.0,.15,0.0,1.0)), ('lights[1].ambient',(.05,.05,.05,1.0)), ('lights[1].diffuse',(.8,.1,.1,1.0)), ('lights[1].specular',(1.0,0.0,0.0,1.0)), ('lights[1].position',(-2.5,2.5,2.5,1.0)), ('lights[1].attenuation',(0.0,0.0,.15,1.0)), ('lights[2].ambient',(.05,.05,.05,1.0)), ('lights[2].diffuse',(.1,.1,.8,1.0)), ('lights[2].specular',(0.0,0.0,1.0,1.0)), ('lights[2].position',(0.0,-3.06,3.06,1.0)), ('lights[2].attenuation',(.15,0.0,0.0,1.0)), ] ], 'f') def Render( self, mode = None): """Render the geometry for the scene.""" BaseContext.Render( self, mode ) if not mode.visible: return glUseProgram(self.shader) try: self.coords.bind() self.indices.bind() stride = self.coords.data[0].nbytes try: '''Again, we're using the parameterized light size/count to pass in the array.''' glUniform4fv( self.uniform_locations['lights'], self.LIGHT_COUNT * self.LIGHT_SIZE, self.LIGHTS ) for uniform,value in self.UNIFORM_VALUES: location = self.uniform_locations.get( uniform ) if location not in (None,-1): if len(value) == 4: glUniform4f( location, *value ) elif len(value) == 3: glUniform3f( location, *value ) elif len(value) == 1: glUniform1f( location, *value ) glEnableVertexAttribArray( self.Vertex_position_loc ) glEnableVertexAttribArray( self.Vertex_normal_loc ) glVertexAttribPointer( self.Vertex_position_loc, 3, GL_FLOAT,False, stride, self.coords ) glVertexAttribPointer( self.Vertex_normal_loc, 3, GL_FLOAT,False, stride, self.coords+(5*4) ) glDrawElements( GL_TRIANGLES, self.count, GL_UNSIGNED_SHORT, self.indices ) finally: self.coords.unbind() self.indices.unbind() glDisableVertexAttribArray( self.Vertex_position_loc ) glDisableVertexAttribArray( self.Vertex_normal_loc ) finally: glUseProgram( 0 )
class TestContext(BaseContext): LIGHT_COUNT = 3 LIGHT_SIZE = 4 def OnInit(self): lightConst = ''' const int LIGHT_COUNT = %s; const int LIGHT_SIZE = %s; const int AMBIENT = 0; const int DIFFUSE = 1; const int SPECULAR = 2; const int POSITION = 3; uniform vec4 lights[LIGHT_COUNT*LIGHT_SIZE]; varying vec3 EC_Light_half[LIGHT_COUNT]; varying vec3 EC_Light_location[LIGHT_COUNT]; varying vec3 baseNormal; ''' % (self.LIGHT_COUNT, self.LIGHT_SIZE) phong_weightCalc = ''' vec2 phong_weightCalc( in vec3 light_pos, in vec3 half_light, in vec3 frag_normal, in float shininess ){ float n_dot_pos = max(0.0, dot(frag_normal, light_pos)); float n_dot_half = 0.0; if (n_dot_pos > -0.05){ n_dot_half = pow(max(0.0, dot(half_light, frag_normal)), shininess); return vec2(n_dot_pos, n_dot_half); } } ''' vs = lightConst + ''' attribute vec3 Vertex_position; attribute vec3 Vertex_normal; void main(){ gl_Position = gl_ModelViewProjectionMatrix * vec4(Vertex_position, 1.0); baseNormal = gl_NormalMatrix * normalize(Vertex_normal); for (int i=0; i<LIGHT_COUNT; i++){ EC_Light_location[i] = normalize(gl_NormalMatrix * lights[(i*LIGHT_SIZE)+POSITION].xyz); EC_Light_half[i] = normalize(EC_Light_location[i] - vec3(0,0,-1)); } } ''' vertex = shaders.compileShader(vs, GL_VERTEX_SHADER) fs = lightConst + phong_weightCalc + ''' struct Material { vec4 ambient; vec4 diffuse; vec4 specular; float shininess; }; uniform Material material; uniform vec4 Global_ambient; void main(){ vec4 fragColor = Global_ambient * material.ambient; int i,j; for (i=0; i<LIGHT_COUNT;i++){ j = i * LIGHT_SIZE; vec2 weights = phong_weightCalc( EC_Light_location[i], EC_Light_half[i], baseNormal, material.shininess ); fragColor = ( fragColor + (lights[i+AMBIENT] * material.ambient) + (lights[i+DIFFUSE] * material.diffuse * weights.x) + (lights[i+SPECULAR] * material.specular * weights.y) ); } gl_FragColor = fragColor; } ''' fragment = shaders.compileShader(fs, GL_FRAGMENT_SHADER) self.shader = shaders.compileProgram(vertex, fragment) self.coords, self.indices, self.count = Sphere(radius=1).compile() self.UNIFORM_VALUES = [ ('Global_ambient', (.05, .05, .05, 1.0)), ('material.ambient', (.2, .2, .2, 1.0)), ('material.diffuse', (.5, .5, .5, 1.0)), ('material.specular', (.8, .8, .8, 1.0)), ('material.shininess', (.995, )), ] self.uniform_locations = {} for uniform, value in self.UNIFORM_VALUES: location = glGetUniformLocation(self.shader, uniform) if location in (None, -1): print('Warning, no uniform: {}'.format(uniform)) self.uniform_locations[uniform] = location self.uniform_locations['lights'] = glGetUniformLocation( self.shader, 'lights') attribute_vals = ('Vertex_position', 'Vertex_normal') for attribute in attribute_vals: location = glGetAttribLocation(self.shader, attribute) if location in (None, -1): print('Warning no attribute: {}'.format(attribute)) setattr(self, attribute + '_loc', location) self.LIGHTS = array( [[ (.05, .05, .05, 1.0), #'lights[0].ambient' (.3, .3, .3, 1.0), #'lights[0].diffuse' (1.0, 0.0, 0.0, 1.0), #'lights[0].specular' (4.0, 2.0, 10.0, 0.0), #'lights[0].position (.05, .05, .05, 1.0), #'lights[1].ambient' (.3, .3, .3, 1.0), #'lights[1].diffuse' (0.0, 1.0, 0.0, 1.0), #'lights[1].specular' (-4.0, 2.0, 10.0, 0.0), # 'lights[1].position (.05, .05, .05, 1.0), #('lights[2].ambient', (.3, .3, .3, 1.0), #lights[2].diffuse' (0.0, 0.0, 1.0, 1.0), #'lights[2].specular' (-4.0, 2.0, -10.0, 0.0) #'lights[2].position ]], 'f') def Render(self, mode=None): BaseContext.Render(self, mode) if not mode.visible: return glUseProgram(self.shader) try: self.coords.bind() self.indices.bind() stride = self.coords.data[0].nbytes try: glUniform4fv(self.uniform_locations['lights'], self.LIGHT_COUNT * self.LIGHT_SIZE, self.LIGHTS) for uniform, value in self.UNIFORM_VALUES: location = self.uniform_locations.get(uniform) if location not in (None, -1): if len(value) == 4: glUniform4f(location, *value) elif len(value) == 3: glUniform3f(location, *value) elif len(value) == 1: glUniform1f(location, *value) glEnableVertexAttribArray(self.Vertex_position_loc) glEnableVertexAttribArray(self.Vertex_normal_loc) glVertexAttribPointer(self.Vertex_position_loc, 3, GL_FLOAT, False, stride, self.coords) glVertexAttribPointer(self.Vertex_normal_loc, 3, GL_FLOAT, False, stride, self.coords + (5 * 4)) glDrawElements(GL_TRIANGLES, self.count, GL_UNSIGNED_SHORT, self.indices) finally: self.coords.unbind() self.indices.unbind() glDisableVertexAttribArray(self.Vertex_position_loc) glDisableVertexAttribArray(self.Vertex_normal_loc) finally: glUseProgram(0)
class TestContext(BaseContext): """Demonstrates use of attribute types in GLSL """ LIGHT_COUNT = 3 LIGHT_SIZE = 5 def OnInit(self): """Initialize the context""" '''Our common light-model declarations are getting slightly more involved. We're adding a single field to the light "structure", the attenuation field. This is a 4-item vector where the first item is a constant attenuation factor, the second is a linear attenuation factor, and the third a quadratic attenuation factor. The fourth item is ignored, but we are using an array of vec4s for the light parameters, so it is easiest to just ignore the w value. ''' lightConst = """ const int LIGHT_COUNT = %s; const int LIGHT_SIZE = %s; const int AMBIENT = 0; const int DIFFUSE = 1; const int SPECULAR = 2; const int POSITION = 3; const int ATTENUATION = 4; uniform vec4 lights[ LIGHT_COUNT*LIGHT_SIZE ]; varying vec3 EC_Light_half[LIGHT_COUNT]; varying vec3 EC_Light_location[LIGHT_COUNT]; varying float Light_distance[LIGHT_COUNT]; varying vec3 baseNormal; """ % (self.LIGHT_COUNT, self.LIGHT_SIZE) '''==Lighting Attenuation= For the first time in many tutorials we're altering out lighting calculation. We're adding 2 inputs to the function, the first is the distance from the fragment to the light, the second is the attenuation vector for the in-process light. We are also going to return one extra value, the ambient-light multiplier for this light. For our directional lights this was always 1.0, but now our light's ambient contribution can be controlled by attenuation. The core calculation for attenuation looks like this: ''' """attenuation = clamp( 0.0, 1.0, 1.0 / ( attenuations.x + (attenuations.y * distance) + (attenuations.z * distance * distance) ) );""" '''The default attenuation for legacy OpenGL was (1.0, 0.0, 0.0), which is to say, no attenuation at all. The attenuation values are not particularly "human friendly", but they give you some control over the distance at which lights cause effects. Keep in mind when using attenuation coefficients that smaller values mean the light goes farther, so a coefficient of .5 is "brighter" than a coefficient of 1.0. ''' phong_weightCalc = """ vec3 phong_weightCalc( in vec3 light_pos, // light position/direction in vec3 half_light, // half-way vector between light and view in vec3 frag_normal, // geometry normal in float shininess, // shininess exponent in float distance, // distance for attenuation calculation... in vec4 attenuations // attenuation parameters... ) { // returns vec3( ambientMult, diffuseMult, specularMult ) float n_dot_pos = max( 0.0, dot( frag_normal, light_pos )); float n_dot_half = 0.0; float attenuation = 1.0; if (n_dot_pos > -.05) { n_dot_half = pow( max(0.0,dot( half_light, frag_normal )), shininess ); if (distance != 0.0) { attenuation = clamp( 0.0, 1.0, 1.0 / ( attenuations.x + (attenuations.y * distance) + (attenuations.z * distance * distance) ) ); n_dot_pos *= attenuation; n_dot_half *= attenuation; } } return vec3( attenuation, n_dot_pos, n_dot_half); } """ '''==Calculating Distance and Direction== Our new lights are "point sources", that is, they have a model-space location which is not at "infinite distance". Because of this, unlike "directional lights", we have to recalculate the light position/location/direction vector for each fragment. We also need to know the distance of the light from each fragment. While we could perform those calculations in the fragment shader, the vectors and distances we need vary smoothly across the triangles involved, so we'll calculate them at each vertex and allow the hardware to interpolate them. We'll have to normalize the interpolated values, but this is less processor intensive than doing the calculations for each fragment. We are doing our vector calculations for the light location and distance in model-space. You could do them in view-space as well. Our vertex calculations are getting complex enough that we're going to split them into a separate function for readability and (eventual) reusability. We'll create a function which encodes the algorithmic operation (phong_preCalc) and another which takes our particular attributes/uniforms and accumulates the results from that function. We're defining phong_preCalc inline here, but we'll also store it as a resource we can use from anywhere: from OpenGLContext.resources.phongprecalc_vert import data as phong_preCalc ''' phong_preCalc = """ // Vertex-shader pre-calculation for lighting... void phong_preCalc( in vec3 vertex_position, in vec4 light_position, out float light_distance, out vec3 ec_light_location, out vec3 ec_light_half ) { // This is the core setup for a phong lighting pass // as a reusable fragment of code. // vertex_position -- un-transformed vertex position (world-space) // light_position -- un-transformed light location (direction) // light_distance -- output giving world-space distance-to-light // ec_light_location -- output giving location of light in eye coords // ec_light_half -- output giving the half-vector optimization if (light_position.w == 0.0) { // directional rather than positional light... ec_light_location = normalize( gl_NormalMatrix * light_position.xyz ); light_distance = 0.0; } else { // positional light, we calculate distance in // model-view space here, so we take a partial // solution... vec3 ms_vec = ( light_position.xyz - vertex_position ); vec3 light_direction = gl_NormalMatrix * ms_vec; ec_light_location = normalize( light_direction ); light_distance = abs(length( ms_vec )); } // half-vector calculation ec_light_half = normalize( ec_light_location + vec3( 0,0,1 ) ); }""" '''This function is not as generally reusable, so we'll store it to a separate file named '_shader_tut_lightprecalc.vert'.''' light_preCalc = """ void light_preCalc( in vec3 vertex_position ) { // This function is dependent on the uniforms and // varying values we've been using, it basically // just iterates over the phong_lightCalc passing in // the appropriate pointers... vec3 light_direction; for (int i = 0; i< LIGHT_COUNT; i++ ) { int j = i * LIGHT_SIZE; phong_preCalc( vertex_position, lights[j+POSITION], // following are the values to fill in... Light_distance[i], EC_Light_location[i], EC_Light_half[i] ); } } """ vertex = shaders.compileShader( lightConst + phong_preCalc + light_preCalc + """ attribute vec3 Vertex_position; attribute vec3 Vertex_normal; void main() { gl_Position = gl_ModelViewProjectionMatrix * vec4( Vertex_position, 1.0 ); baseNormal = gl_NormalMatrix * normalize(Vertex_normal); light_preCalc(Vertex_position); }""", GL_VERTEX_SHADER) '''Our fragment shader is only slightly modified to use our new phong_weightCalc function. We need a larger "weights" variable and need to pass in more information. We also need to multiply the per-light ambient value by the new weight we've added. You will also notice that since we are using the 'i' variable to directly index the varying arrays, we've introduced a 'j' variable that tracks the offset into the light array which begins the current light. ''' fragment = shaders.compileShader( lightConst + phong_weightCalc + """ struct Material { vec4 ambient; vec4 diffuse; vec4 specular; float shininess; }; uniform Material material; uniform vec4 Global_ambient; void main() { vec4 fragColor = Global_ambient * material.ambient; int i,j; for (i=0;i<LIGHT_COUNT;i++) { j = i* LIGHT_SIZE; vec3 weights = phong_weightCalc( normalize(EC_Light_location[i]), normalize(EC_Light_half[i]), baseNormal, material.shininess, // some implementations will produce negative values interpolating positive float-arrays! // so we have to do an extra abs call for distance abs(Light_distance[i]), lights[j+ATTENUATION] ); fragColor = ( fragColor + (lights[j+AMBIENT] * material.ambient * weights.x) + (lights[j+DIFFUSE] * material.diffuse * weights.y) + (lights[j+SPECULAR] * material.specular * weights.z) ); } //fragColor = vec4(Light_distance[0],Light_distance[1],Light_distance[2],1.0); gl_FragColor = fragColor; } """, GL_FRAGMENT_SHADER) '''Our general uniform setup should look familiar by now.''' self.shader = shaders.compileProgram(vertex, fragment) self.coords, self.indices, self.count = Sphere(radius=1).compile() self.uniform_locations = {} for uniform, value in self.UNIFORM_VALUES: location = glGetUniformLocation(self.shader, uniform) if location in (None, -1): print('Warning, no uniform: %s' % (uniform)) self.uniform_locations[uniform] = location self.uniform_locations['lights'] = glGetUniformLocation( self.shader, 'lights') for attribute in ( 'Vertex_position', 'Vertex_normal', ): location = glGetAttribLocation(self.shader, attribute) if location in (None, -1): print('Warning, no attribute: %s' % (uniform)) setattr(self, attribute + '_loc', location) UNIFORM_VALUES = [ ('Global_ambient', (.05, .05, .05, 1.0)), ('material.ambient', (.2, .2, .2, 1.0)), ('material.diffuse', (.5, .5, .5, 1.0)), ('material.specular', (.8, .8, .8, 1.0)), ('material.shininess', (2.0, )), ] '''We've created 3 equal-distance lights here, in red, green and blue. The green light uses linear attenuation, the red quadratic and the blue constant. ''' LIGHTS = array([ x[1] for x in [ ('lights[0].ambient', (.05, .05, .05, 1.0)), ('lights[0].diffuse', (.1, .8, .1, 1.0)), ('lights[0].specular', (0.0, 1.0, 0.0, 1.0)), ('lights[0].position', (2.5, 2.5, 2.5, 1.0)), ('lights[0].attenuation', (0.0, .15, 0.0, 1.0)), ('lights[1].ambient', (.05, .05, .05, 1.0)), ('lights[1].diffuse', (.8, .1, .1, 1.0)), ('lights[1].specular', (1.0, 0.0, 0.0, 1.0)), ('lights[1].position', (-2.5, 2.5, 2.5, 1.0)), ('lights[1].attenuation', (0.0, 0.0, .15, 1.0)), ('lights[2].ambient', (.05, .05, .05, 1.0)), ('lights[2].diffuse', (.1, .1, .8, 1.0)), ('lights[2].specular', (0.0, 0.0, 1.0, 1.0)), ('lights[2].position', (0.0, -3.06, 3.06, 1.0)), ('lights[2].attenuation', (.15, 0.0, 0.0, 1.0)), ] ], 'f') def Render(self, mode=None): """Render the geometry for the scene.""" BaseContext.Render(self, mode) if not mode.visible: return glUseProgram(self.shader) try: self.coords.bind() self.indices.bind() stride = self.coords.data[0].nbytes try: '''Again, we're using the parameterized light size/count to pass in the array.''' glUniform4fv(self.uniform_locations['lights'], self.LIGHT_COUNT * self.LIGHT_SIZE, self.LIGHTS) for uniform, value in self.UNIFORM_VALUES: location = self.uniform_locations.get(uniform) if location not in (None, -1): if len(value) == 4: glUniform4f(location, *value) elif len(value) == 3: glUniform3f(location, *value) elif len(value) == 1: glUniform1f(location, *value) glEnableVertexAttribArray(self.Vertex_position_loc) glEnableVertexAttribArray(self.Vertex_normal_loc) glVertexAttribPointer(self.Vertex_position_loc, 3, GL_FLOAT, False, stride, self.coords) glVertexAttribPointer(self.Vertex_normal_loc, 3, GL_FLOAT, False, stride, self.coords + (5 * 4)) glDrawElements(GL_TRIANGLES, self.count, GL_UNSIGNED_SHORT, self.indices) finally: self.coords.unbind() self.indices.unbind() glDisableVertexAttribArray(self.Vertex_position_loc) glDisableVertexAttribArray(self.Vertex_normal_loc) finally: glUseProgram(0)
class TestContext(BaseContext): def OnInit(self): phong_weightCalc = ''' vec2 phong_weightCalc( in vec3 light_pos, in vec3 half_light, in vec3 frag_normal, in float shininess ){ // get the dot product of the normal and the light ray float n_dot_pos = max(0.0, dot(frag_normal, light_pos)); // set the specular reflection to 0 float n_dot_half = 0.0; if (n_dot_pos >= -0.05){ // get the transformed dot product of the 'half light' // this is our new speccular reflection weight n_dot_half = pow(max(0.0, dot(half_light, frag_normal)), shininess); } // return both the diffuse and specular reflection, n_dot_pos is the // weight of the diffuse light, n_dot_half is the weight for the // specular light return vec2(n_dot_pos, n_dot_half); } ''' vs = ''' attribute vec3 Vertex_position; attribute vec3 Vertex_normal; varying vec3 baseNormal; void main(){ // store the vertex position gl_Position = gl_ModelViewProjectionMatrix * vec4(Vertex_position, 1.0); // store the vertex normal baseNormal = gl_NormalMatrix * normalize(Vertex_normal); } ''' vertex = shaders.compileShader(vs, GL_VERTEX_SHADER) fs = ''' // define a bunch of stuff uniform vec4 Global_ambient; uniform vec4 Light_ambient; uniform vec4 Light_diffuse; uniform vec4 Light_specular; uniform vec3 Light_location; uniform float Material_shininess; uniform vec4 Material_specular; uniform vec4 Material_ambient; uniform vec4 Material_diffuse; varying vec3 baseNormal; void main(){ // get the light location in eye space vec3 EC_Light_location = normalize(gl_NormalMatrix * Light_location); // get the half light between the light ray and the viewpoint vec3 Light_half = normalize(EC_Light_location - vec3(0,0,-1)); //get the weights for specular and diffuse light from the current view vec2 weights = phong_weightCalc( EC_Light_location, Light_half, baseNormal, Material_shininess ); // return the fragment color with various lighting added in gl_FragColor = clamp( // global ambient light (Global_ambient * Material_ambient) // ambient light from light source + (Light_ambient * Material_ambient) // the diffuse light calcualted by blinn-phong function + (Light_diffuse * Material_diffuse * weights.x) // the specular light (spotlight) calculated by blinn-phong function + (Light_specular * Material_specular * weights.y) , 0.0, 1.0); } ''' fragment = shaders.compileShader(phong_weightCalc + fs, GL_FRAGMENT_SHADER) self.shader = shaders.compileProgram(vertex, fragment) # create two vbos to represent a sphere, coordinates and indicies # and return a count to tell us how many things to display self.coords, self.indices, self.count = Sphere(radius=1).compile() # setup uniform values, # basically map each memory # location in our script with # an attribute on our python class uniform_vals = ('Global_ambient', 'Light_ambient', 'Light_diffuse', 'Light_location', 'Light_specular', 'Material_ambient', 'Material_diffuse', 'Material_shininess', 'Material_specular') for uniform in uniform_vals: location = glGetUniformLocation(self.shader, uniform) if location in (None, -1): print('Warning, no uniform: {}'.format(uniform)) setattr(self, uniform + '_loc', location) # do what we did before for shader attribute values attribute_vals = ('Vertex_position', 'Vertex_normal') for attribute in attribute_vals: location = glGetAttribLocation(self.shader, attribute) if location in (None, -1): print('Warning no attribute: {}'.format(attribute)) setattr(self, attribute + '_loc', location) def Render(self, mode=None): BaseContext.Render(self, mode) glUseProgram(self.shader) try: self.coords.bind() self.indices.bind() # set our stride value to be the number of bytes per coordinate stride = self.coords.data[0].nbytes try: # set our uniform values to their values glUniform4f(self.Global_ambient_loc, .05, .05, .05, .1) glUniform4f(self.Light_ambient_loc, .1, .1, .1, 1.0) glUniform4f(self.Light_diffuse_loc, .25, .25, .25, 1) glUniform4f(self.Light_specular_loc, 0.0, 1.0, 0, 1) glUniform3f(self.Light_location_loc, 6, 2, 4) glUniform4f(self.Material_ambient_loc, .1, .1, .1, 1.0) glUniform4f(self.Material_diffuse_loc, .15, .15, .15, 1) glUniform4f(self.Material_specular_loc, 1.0, 1.0, 1.0, 1.0) glUniform1f(self.Material_shininess_loc, .95) # enable and draw all our data points glEnableVertexAttribArray(self.Vertex_position_loc) glEnableVertexAttribArray(self.Vertex_normal_loc) glVertexAttribPointer(self.Vertex_position_loc, 3, GL_FLOAT, False, stride, self.coords) glVertexAttribPointer(self.Vertex_normal_loc, 3, GL_FLOAT, False, stride, self.coords + (5 * 4)) # draw 'self.count' number of things # (this was stored when we made the sphere) # notice that glDrawElements is different # from glDrawArrays, this is the indexed # vbo scheme where we reuse verts and render # based on indices glDrawElements(GL_TRIANGLES, self.count, GL_UNSIGNED_SHORT, self.indices) finally: self.coords.unbind() self.indices.unbind() glDisableVertexAttribArray(self.Vertex_position_loc) glDisableVertexAttribArray(self.Vertex_normal_loc) finally: glUseProgram(0)
class TestContext(BaseContext): def OnInit(self): # new structure that contains # material properties materialStruct = ''' struct Material { vec4 ambient; vec4 diffuse; vec4 specular; float shininess; }; ''' phong_weightCalc = ''' vec2 phong_weightCalc( in vec3 light_pos, in vec3 half_light, in vec3 frag_normal, in float shininess ){ float n_dot_pos = max(0.0, dot(frag_normal, light_pos)); float n_dot_half = 0.0; // changed from tutorial 6 phong weight calc after my inblog // investigation if (n_dot_pos >= 0.0){ n_dot_half = pow(max(0.0, dot(half_light, frag_normal)), shininess); } return vec2(n_dot_pos, n_dot_half); } ''' vs = ''' attribute vec3 Vertex_position; attribute vec3 Vertex_normal; varying vec3 baseNormal; void main(){ gl_Position = gl_ModelViewProjectionMatrix * vec4(Vertex_position, 1.0); baseNormal = gl_NormalMatrix * normalize(Vertex_normal); } ''' vertex = shaders.compileShader(vs, GL_VERTEX_SHADER) fs = ''' // define 3 lights containing // ambient, diffuse, and specular, and position (4 things) // 4(vectors per light)*3(lights) = 12 uniform vec4 lights [12]; uniform Material material; uniform vec4 Global_ambient; varying vec3 baseNormal; void main(){ // setup the baseline color for the fragment vec4 fragColor = Global_ambient * material.ambient; // setup some constant indices for referencing parts // of lights int AMBIENT = 0; int DIFFUSE = 1; int SPECULAR = 2; int POSITION = 3; // for every light int i; for (i=0; i<12;i=i+4){ //normalize light location, eye coordinates vec3 EC_Light_location = normalize(gl_NormalMatrix * lights[i+POSITION].xyz); // half light vector calculation vec3 Light_half = normalize(EC_Light_location - vec3(0,0,-1)); // get phong weights vec2 weights = phong_weightCalc( EC_Light_location, Light_half, baseNormal, material.shininess ); // calculate/update the fragment color with new information fragColor = ( fragColor + (lights[i+AMBIENT] * material.ambient) + (lights[i+DIFFUSE] * material.diffuse * weights.x) + (lights[i+SPECULAR] * material.specular * weights.y) ); } gl_FragColor = fragColor; } ''' fragment = shaders.compileShader( phong_weightCalc + materialStruct + fs, GL_FRAGMENT_SHADER) self.shader = shaders.compileProgram(vertex, fragment) # create two vbos to represent a sphere, coordinates and indicies # and return a count to tell us how many things to display self.coords, self.indices, self.count = Sphere(radius=1).compile() # get uniform variable locations ready to be populated self.UNIFORM_VALUES = [ ('Global_ambient', (.05, .05, .05, 1.0)), ('material.ambient', (.2, .2, .2, 1.0)), ('material.diffuse', (.5, .5, .5, 1.0)), ('material.specular', (.8, .8, .8, 1.0)), ('material.shininess', (.995, )), ] self.uniform_locations = {} for uniform, _ in self.UNIFORM_VALUES: location = glGetUniformLocation(self.shader, uniform) if location in (None, -1): print('Warning, no uniform: {}'.format(uniform)) self.uniform_locations[uniform] = location self.uniform_locations['lights'] = glGetUniformLocation( self.shader, 'lights') # get attribute values ready to be populated attribute_vals = ('Vertex_position', 'Vertex_normal') for attribute in attribute_vals: location = glGetAttribLocation(self.shader, attribute) if location in (None, -1): print('Warning no attribute: {}'.format(attribute)) setattr(self, attribute + '_loc', location) # define our actual lights, ambient, diffuse, specular # contributions as well as position self.LIGHTS = array( [[ (.05, .05, .05, 1.0), #'lights[0].ambient' (.3, .3, .3, 1.0), #'lights[0].diffuse' (1.0, 0.0, 0.0, 1.0), #'lights[0].specular' (4.0, 2.0, 10.0, 0.0), #'lights[0].position (.05, .05, .05, 1.0), #'lights[1].ambient' (.3, .3, .3, 1.0), #'lights[1].diffuse' (0.0, 1.0, 0.0, 1.0), #'lights[1].specular' (-4.0, 2.0, 10.0, 0.0), # 'lights[1].position (.05, .05, .05, 1.0), #('lights[2].ambient', (.3, .3, .3, 1.0), #lights[2].diffuse' (0.0, 0.0, 1.0, 1.0), #'lights[2].specular' (-4.0, 2.0, -10.0, 0.0) #'lights[2].position ]], 'f') def Render(self, mode=None): BaseContext.Render(self, mode) glUseProgram(self.shader) try: # bind in our coordinates and indices # into gpu ram self.coords.bind() self.indices.bind() stride = self.coords.data[0].nbytes try: # set our lights with the values defined above # this is a different function '4fv', instead of '4f' glUniform4fv(self.uniform_locations['lights'], 12, self.LIGHTS) for uniform, value in self.UNIFORM_VALUES: location = self.uniform_locations.get(uniform) if location not in (None, -1): if len(value) == 4: glUniform4f(location, *value) elif len(value) == 3: glUniform3f(location, *value) elif len(value) == 1: glUniform1f(location, *value) # setup all our vertices / normals and draw them glEnableVertexAttribArray(self.Vertex_position_loc) glEnableVertexAttribArray(self.Vertex_normal_loc) glVertexAttribPointer(self.Vertex_position_loc, 3, GL_FLOAT, False, stride, self.coords) glVertexAttribPointer(self.Vertex_normal_loc, 3, GL_FLOAT, False, stride, self.coords + (5 * 4)) glDrawElements(GL_TRIANGLES, self.count, GL_UNSIGNED_SHORT, self.indices) finally: self.coords.unbind() self.indices.unbind() glDisableVertexAttribArray(self.Vertex_position_loc) glDisableVertexAttribArray(self.Vertex_normal_loc) finally: glUseProgram(0)
class TestContext( BaseContext ): """Demonstrates use of attribute types in GLSL """ def OnInit( self ): """Initialize the context""" '''== Phong and Blinn Reflectance == A shiny surface will tend to have a "bright spot" at the point on the surface where the angle of incidence for the reflected light ray and the viewer's ray are (close to) equal. A perfect mirror would have the brights spot solely when the two vectors are exactly equal, while a perfect Lambertian surface would have the "bright spot" spread across the entire surface. The Phong rendering process models this as a setting, traditionally called material "shininess" in Legacy OpenGL. This setting acts as a power which raises the cosine (dot product) of the angle between the reflected ray and the eye. The calculation of the cosine (dot product) of the two angles requires that we do a dot product of the two angles once for each vertex/fragment for which we wish to calculate the specular reflectance, we also have to find the angle of reflectance before we can do the calculation:''' """ L_dir = (V_pos-L_pos) R = 2N*(dot( N, L_dir))-L_dir // Note: in eye-coordinate system, Eye_pos == (0,0,0) Spec_factor = pow( dot( R, V_pos-Eye_pos ), shininess) """ '''which, as we can see, involves the vertex position in a number of stages of the operation, so requires recalculation all through the rendering operation. There is, however, a simplified version of Phong Lighting called [http://en.wikipedia.org/wiki/Blinn%E2%80%93Phong_shading_model Blinn-Phong] which notes that if we were to do all of our calculations in "eye space", and were to assume that (as is normal), the eye and light coordinates will not change for a rendering pass, (note: this limits us to directional lights!) we can use a pre-calculated value which is the bisecting angle between the light-vector and the view-vector, called the "half vector" to perform approximately the same calculation. With this value:''' """ // note that in Eye coordinates, Eye_EC_dir == 0,0,-1 H = normalize( Eye_EC_dir + Light_EC_dir ) Spec_factor = pow( dot( H, N ), shininess ) """ '''Note: however, that the resulting Spec_factor is not *precisely* the same value as the original calculation, so the "shininess" exponent must be slightly lower to approximate the value that Phong rendering would achieve. The value is, however, considered close to "real world" materials, so the Blinn method is generally preferred to Phong. Traditionally, n_dot_pos would be cut off at 0.0, but that would create extremely hard-edged cut-offs for specular color. Here we "fudge" the result by 0.05 ''' phong_weightCalc = """ vec2 phong_weightCalc( in vec3 light_pos, // light position in vec3 half_light, // half-way vector between light and view in vec3 frag_normal, // geometry normal in float shininess ) { // returns vec2( ambientMult, diffuseMult ) float n_dot_pos = max( 0.0, dot( frag_normal, light_pos )); float n_dot_half = 0.0; if (n_dot_pos > -.05) { n_dot_half = pow(max(0.0,dot( half_light, frag_normal )), shininess); } return vec2( n_dot_pos, n_dot_half); } """ '''We are going to use per-fragment rendering. As a result, our vertex shader becomes very simple, just arranging for the Normals to be varied across the surface. ''' vertex = shaders.compileShader( """ attribute vec3 Vertex_position; attribute vec3 Vertex_normal; varying vec3 baseNormal; void main() { gl_Position = gl_ModelViewProjectionMatrix * vec4( Vertex_position, 1.0 ); baseNormal = gl_NormalMatrix * normalize(Vertex_normal); }""", GL_VERTEX_SHADER) '''Our fragment shader looks much like our previous tutorial's vertex shader. As before, we have lots of uniform values, but now we also calculate the light's half-vector (in eye-space coordinates). The phong_weightCalc function does the core Blinn calculation, and we simply use the resulting factor to add to the colour value for the fragment. Note the use of the eye-coordinate-space to simplify the half-vector calculation, the eye-space eye-vector is always the same value (pointing down the negative Z axis), and the eye-space eye-coordinate is always (0,0,0), so the eye-to-vertex vector is always the eye-space vector position. ''' fragment = shaders.compileShader( phong_weightCalc + """ uniform vec4 Global_ambient; uniform vec4 Light_ambient; uniform vec4 Light_diffuse; uniform vec4 Light_specular; uniform vec3 Light_location; uniform float Material_shininess; uniform vec4 Material_specular; uniform vec4 Material_ambient; uniform vec4 Material_diffuse; varying vec3 baseNormal; void main() { // normalized eye-coordinate Light location vec3 EC_Light_location = normalize( gl_NormalMatrix * Light_location ); // half-vector calculation vec3 Light_half = normalize( EC_Light_location - vec3( 0,0,-1 ) ); vec2 weights = phong_weightCalc( EC_Light_location, Light_half, baseNormal, Material_shininess ); gl_FragColor = clamp( ( (Global_ambient * Material_ambient) + (Light_ambient * Material_ambient) + (Light_diffuse * Material_diffuse * weights.x) // material's shininess is the only change here... + (Light_specular * Material_specular * weights.y) ), 0.0, 1.0); } """, GL_FRAGMENT_SHADER) self.shader = shaders.compileProgram(vertex,fragment) '''Here's the call that creates the two VBOs and the count of records to render from them. If you're curious you can read through the source code of the OpenGLContext.scenegraph.quadrics module to read the mechanism that generates the values. The sphere is a simple rendering mechanism, as for a unit-sphere at the origin, the sphere's normals are the same as the sphere's vertex coordinate. The complexity comes primarily in generating the triangle indices that link the points generated. ''' self.coords,self.indices,self.count = Sphere( radius = 1 ).compile() '''We have a few more uniforms to control the specular components. Real-world coding would also calculate the light's half-vector and provide it as a uniform (so that it would only need to be calculated once), but we are going to do the half-vector calculation in the shader to make it obvious what is going on. The legacy OpenGL pipeline provides the value pre-calculated as part of the light structure in GLSL. ''' for uniform in ( 'Global_ambient', 'Light_ambient','Light_diffuse','Light_location', 'Light_specular', 'Material_ambient','Material_diffuse', 'Material_shininess','Material_specular', ): location = glGetUniformLocation( self.shader, uniform ) if location in (None,-1): print 'Warning, no uniform: %s'%( uniform ) setattr( self, uniform+ '_loc', location ) for attribute in ( 'Vertex_position','Vertex_normal', ): location = glGetAttribLocation( self.shader, attribute ) if location in (None,-1): print 'Warning, no attribute: %s'%( uniform ) setattr( self, attribute+ '_loc', location ) def Render( self, mode = None): """Render the geometry for the scene.""" BaseContext.Render( self, mode ) glUseProgram(self.shader) try: '''==Indexed VBO Rendering== You'll notice here that we are binding two different VBO objects. As we mentioned above, the Sphere renderer generated both VBOs, but doesn't the second binding replace the first binding? That is, why doesn't OpenGL try to read the Vertex data out of the indices VBO? OpenGL defines multiple binding "targets" for VBOs, the first VBO (vertices) was bound to the GL_ARRAY_BUFFER target (the default for the class), which is used for reading per-vertex data arrays, while the indices buffer was defined as targetting the GL_ELEMENT_ARRAY_BUFFER, which is used solely for reading indices. Each target can be bound to a different VBO, and thus we can bind both VBOs at the same time without confusion. ''' self.coords.bind() self.indices.bind() '''Here, being lazy, we use the numpy array's nbytes value to specify the stride between records. The VBO object has a "data" value which is the data-set which was initially passed to the VBO constructor. The first element in this array is a single vertex record. This array happens to have 8 floating-point values (24 bytes), the first three being the vertex position, the next two being the texture coordinate and the last three being the vertex normal. We'll ignore the texture coordinate for now. ''' stride = self.coords.data[0].nbytes try: glUniform4f( self.Global_ambient_loc, .05,.05,.05,.1 ) glUniform4f( self.Light_ambient_loc, .1,.1,.1, 1.0 ) glUniform4f( self.Light_diffuse_loc, .25,.25,.25,1 ) '''We set up a yellow-ish specular component in the light and move it to rest "just over our right shoulder" in relation to the initial camera.''' glUniform4f( self.Light_specular_loc, 0.0,1.0,0,1 ) glUniform3f( self.Light_location_loc, 6,2,4 ) glUniform4f( self.Material_ambient_loc, .1,.1,.1, 1.0 ) glUniform4f( self.Material_diffuse_loc, .15,.15,.15, 1 ) '''We make the material have a bright specular white colour and an extremely "shiny" surface. The shininess value has the effect of reducing the area of the highlight, as the cos of the angle is raised to the power of the (fractional) shininess.''' glUniform4f( self.Material_specular_loc, 1.0,1.0,1.0, 1.0 ) glUniform1f( self.Material_shininess_loc, .95) glEnableVertexAttribArray( self.Vertex_position_loc ) glEnableVertexAttribArray( self.Vertex_normal_loc ) glVertexAttribPointer( self.Vertex_position_loc, 3, GL_FLOAT,False, stride, self.coords ) glVertexAttribPointer( self.Vertex_normal_loc, 3, GL_FLOAT,False, stride, self.coords+(5*4) ) '''Here we introduce the OpenGL call which renders via an index-array rather than just rendering vertices in definition order. The last two arguments tell OpenGL what data-type we've used for the indices (the Sphere renderer uses shorts). The indices VBO is actually just passing the value c_void_p( 0 ) (i.e. a null pointer), which causes OpenGL to use the currently bound VBO for the GL_ELEMENT_ARRAY_BUFFER target. ''' glDrawElements( GL_TRIANGLES, self.count, GL_UNSIGNED_SHORT, self.indices ) finally: '''Note the need to unbind *both* VBOs, we have to free *both* VBO targets to avoid any other rendering operation from trying to access the VBOs.''' self.coords.unbind() self.indices.unbind() glDisableVertexAttribArray( self.Vertex_position_loc ) glDisableVertexAttribArray( self.Vertex_normal_loc ) finally: glUseProgram( 0 )
class TestContext(BaseContext): """Demonstrates use of attribute types in GLSL """ def OnInit(self): """Initialize the context""" '''==GLSL Structures== We have previously been using values named Material_ambient, Material_diffuse, etceteras to specify our Material's properties. GLSL allows us to bind these kinds of values together into a structure. The structure doesn't provide many benefits other than keeping the namespaces of your code clean and allowing for declaring multiple uniforms of the same type, such as a "front" and "back" material. We are going to define a very simple Material struct which is a subset of the built-in gl_MaterialParameters structure (which also has an "emission" parameter). GLSL defines two built-in Material uniforms gl_FrontMaterial and gl_BackMaterial. It is possible (though seldom done) to fill in these uniform values with glUniform calls rather than the legacy glMaterial calls. ''' materialStruct = """ struct Material { vec4 ambient; vec4 diffuse; vec4 specular; float shininess; }; """ '''Note that each sub-element must be terminated with a semi-colon ';' character, and that qualifiers (in, out, uniform, etceteras) are not allowed within the structure definition. This statement has to occur *before* any use of the structure to declare a variable as being of this type. Our light-weighting code has not changed from the previous tutorial. It is still a Blinn-Phong calculation based on the half-vector of light and view vector. ''' phong_weightCalc = """ vec2 phong_weightCalc( in vec3 light_pos, // light position in vec3 half_light, // half-way vector between light and view in vec3 frag_normal, // geometry normal in float shininess ) { // returns vec2( ambientMult, diffuseMult ) float n_dot_pos = max( 0.0, dot( frag_normal, light_pos )); float n_dot_half = 0.0; if (n_dot_pos > -.05) { n_dot_half = pow(max(0.0,dot( half_light, frag_normal )), shininess); } return vec2( n_dot_pos, n_dot_half); } """ '''Our vertex shader is also unchanged. We could move many of the operations currently done in our fragment shader here to reduce the processing load for our shader. ''' vertex = shaders.compileShader( """ attribute vec3 Vertex_position; attribute vec3 Vertex_normal; varying vec3 baseNormal; void main() { gl_Position = gl_ModelViewProjectionMatrix * vec4( Vertex_position, 1.0 ); baseNormal = gl_NormalMatrix * normalize(Vertex_normal); }""", GL_VERTEX_SHADER) '''To create a uniform with a structure type, we simply use the structure as the data-type declaration for the uniform. As opposed to using e.g. vec4 or vec3, we use Material (our structure name defined above) and give the uniform a name. ''' """uniform Material material;""" '''==GLSL Arrays== Each light we have defined (so far) is composed to 4 4-component vectors, ambient, diffuse and specular colour, along with the "position" (direction) vector. If we wanted to provide, for instance, 3 lights of this type, we *could* create 12 different uniform values, and set each of these uniforms individually. GLSL, however, provides for array data-types. The array types must be "sized" (have a specific, final size) in order to be usable, so no "pointer" types are available, but we don't need them for this type of operation. We can define a "lights" uniform which is declared as a sized array of 12 vec4 elements:''' """uniform vec4 lights[ 12 ];""" '''And we can loop over "lights" using 0-indexed [i] indices, where i must be an integer. The for loop will be familiar to those who have used C looping:''' """for (i=0;i<12;i=i+4) { blah; }""" '''Note that you must declare the iterator variable ("i" here) We iterate over each light in our array of lights accumulating the results into the fragColor variable we've defined. The global component is used to initialize the variable, with the contribution of each light added to the result. ''' fragment = shaders.compileShader( phong_weightCalc + materialStruct + """ uniform Material material; uniform vec4 Global_ambient; uniform vec4 lights[ 12 ]; // 3 possible lights 4 vec4's each varying vec3 baseNormal; void main() { vec4 fragColor = Global_ambient * material.ambient; int AMBIENT = 0; int DIFFUSE = 1; int SPECULAR = 2; int POSITION = 3; int i; for (i=0;i<12;i=i+4) { // normalized eye-coordinate Light location vec3 EC_Light_location = normalize( gl_NormalMatrix * lights[i+POSITION].xyz ); // half-vector calculation vec3 Light_half = normalize( EC_Light_location - vec3( 0,0,-1 ) ); vec2 weights = phong_weightCalc( EC_Light_location, Light_half, baseNormal, material.shininess ); fragColor = ( fragColor + (lights[i+AMBIENT] * material.ambient) + (lights[i+DIFFUSE] * material.diffuse * weights.x) + (lights[i+SPECULAR] * material.specular * weights.y) ); } gl_FragColor = fragColor; } """, GL_FRAGMENT_SHADER) '''===Why not an Array of Structures?=== Originally this tutorial was going to use an array of LightSource structures as a Uniform, with the components of the structures specified with separate calls to glUniform4f. Problem is, that doesn't actually *work*. While glUniform *should* be able to handle array-of-structure indexing, it doesn't actually support this type of operation in the real world. The built-in gl_LightSourceParameters are an array-of-structures, but apparently the GL implementations consider this a special case, rather than a generic type of functionality to be supported. An array-of-structures value looks like this when declared in GLSL: ''' lightStruct = """ // NOTE: this does not work, it compiles, but you will // not be able to fill in the individual members... struct LightSource { vec4 ambient; vec4 diffuse; vec4 specular; vec4 position; }; uniform LightSource lights[3]; """ '''When you attempt to retrieve the location for the Uniform via: glGetUniformLocation( shader, 'lights[0].ambient' ) you will always get a -1 (invalid) location. OpenGL 3.1 introduced the concept of Uniform Buffers, which allow for packing Uniform data into VBO storage, but it's not yet clear whether they will support array-of-structure specification. ''' self.shader = shaders.compileProgram(vertex, fragment) self.coords, self.indices, self.count = Sphere(radius=1).compile() self.uniform_locations = {} for uniform, value in self.UNIFORM_VALUES: location = glGetUniformLocation(self.shader, uniform) if location in (None, -1): print('Warning, no uniform: %s' % (uniform)) self.uniform_locations[uniform] = location '''There's no real reason to treat the "lights" uniform specially, other than that we want to call attention to it. We get the uniform as normal. Note that we *could* also retrieve a sub-element of the array by specifying 'lights[3]' or the like. ''' self.uniform_locations['lights'] = glGetUniformLocation( self.shader, 'lights') for attribute in ( 'Vertex_position', 'Vertex_normal', ): location = glGetAttribLocation(self.shader, attribute) if location in (None, -1): print('Warning, no attribute: %s' % (uniform)) setattr(self, attribute + '_loc', location) '''Our individually-specified uniform values''' UNIFORM_VALUES = [ ('Global_ambient', (.05, .05, .05, 1.0)), ('material.ambient', (.2, .2, .2, 1.0)), ('material.diffuse', (.5, .5, .5, 1.0)), ('material.specular', (.8, .8, .8, 1.0)), ('material.shininess', (.995, )), ] '''The parameters we use to specify our lights, note that the first item in the tuples is dropped, it is the value that *should* work in glGetUniformLocation, but does not. What actually gets passed in is a single float array with 12 4-float values representing all of the data-values for all of the enabled lights. You'll notice that we're using 0.0 as the 'w' coordinate for the light positions. We're using this to flag that the position is actually a *direction*. This will become useful in later tutorials where we have multiple light-types. ''' LIGHTS = array([ x[1] for x in [ ('lights[0].ambient', (.05, .05, .05, 1.0)), ('lights[0].diffuse', (.3, .3, .3, 1.0)), ('lights[0].specular', (1.0, 0.0, 0.0, 1.0)), ('lights[0].position', (4.0, 2.0, 10.0, 0.0)), ('lights[1].ambient', (.05, .05, .05, 1.0)), ('lights[1].diffuse', (.3, .3, .3, 1.0)), ('lights[1].specular', (0.0, 1.0, 0.0, 1.0)), ('lights[1].position', (-4.0, 2.0, 10.0, 0.0)), ('lights[2].ambient', (.05, .05, .05, 1.0)), ('lights[2].diffuse', (.3, .3, .3, 1.0)), ('lights[2].specular', (0.0, 0.0, 1.0, 1.0)), ('lights[2].position', (-4.0, 2.0, -10.0, 0.0)), ] ], 'f') def Render(self, mode=None): """Render the geometry for the scene.""" BaseContext.Render(self, mode) glUseProgram(self.shader) try: self.coords.bind() self.indices.bind() stride = self.coords.data[0].nbytes try: '''Here's our only change to the rendering process, we pass in the entire array of light-related data with a single call to glUniform4fv. The 'v' forms of glUniform all allow for passing arrays of values, and all require that you specify the number of elements being passed (here 12). Aside: Incidentally, Uniforms are actually stored with the shader until the shader is re-linked, so specifying the uniforms on each rendering pass (as we do here) is not necessary. The shader merely needs to be "in use" during the glUniform call, and this is a convenient, if inefficient, way to ensure it is in use at the time we are calling glUniform. ''' glUniform4fv(self.uniform_locations['lights'], 12, self.LIGHTS) test_lights = (GLfloat * 12)() glGetUniformfv(self.shader, self.uniform_locations['lights'], test_lights) print('Lights', list(test_lights)) for uniform, value in self.UNIFORM_VALUES: location = self.uniform_locations.get(uniform) if location not in (None, -1): if len(value) == 4: glUniform4f(location, *value) elif len(value) == 3: glUniform3f(location, *value) elif len(value) == 1: glUniform1f(location, *value) glEnableVertexAttribArray(self.Vertex_position_loc) glEnableVertexAttribArray(self.Vertex_normal_loc) glVertexAttribPointer(self.Vertex_position_loc, 3, GL_FLOAT, False, stride, self.coords) glVertexAttribPointer(self.Vertex_normal_loc, 3, GL_FLOAT, False, stride, self.coords + (5 * 4)) glDrawElements(GL_TRIANGLES, self.count, GL_UNSIGNED_SHORT, self.indices) finally: self.coords.unbind() self.indices.unbind() glDisableVertexAttribArray(self.Vertex_position_loc) glDisableVertexAttribArray(self.Vertex_normal_loc) finally: glUseProgram(0)
class TestContext( BaseContext ): """Demonstrates use of attribute types in GLSL """ def OnInit( self ): """Initialize the context""" '''==GLSL Structures== We have previously been using values named Material_ambient, Material_diffuse, etceteras to specify our Material's properties. GLSL allows us to bind these kinds of values together into a structure. The structure doesn't provide many benefits other than keeping the namespaces of your code clean and allowing for declaring multiple uniforms of the same type, such as a "front" and "back" material. We are going to define a very simple Material struct which is a subset of the built-in gl_MaterialParameters structure (which also has an "emission" parameter). GLSL defines two built-in Material uniforms gl_FrontMaterial and gl_BackMaterial. It is possible (though seldom done) to fill in these uniform values with glUniform calls rather than the legacy glMaterial calls. ''' materialStruct = """ struct Material { vec4 ambient; vec4 diffuse; vec4 specular; float shininess; }; """ '''Note that each sub-element must be terminated with a semi-colon ';' character, and that qualifiers (in, out, uniform, etceteras) are not allowed within the structure definition. This statement has to occur *before* any use of the structure to declare a variable as being of this type. Our light-weighting code has not changed from the previous tutorial. It is still a Blinn-Phong calculation based on the half-vector of light and view vector. ''' phong_weightCalc = """ vec2 phong_weightCalc( in vec3 light_pos, // light position in vec3 half_light, // half-way vector between light and view in vec3 frag_normal, // geometry normal in float shininess ) { // returns vec2( ambientMult, diffuseMult ) float n_dot_pos = max( 0.0, dot( frag_normal, light_pos )); float n_dot_half = 0.0; if (n_dot_pos > -.05) { n_dot_half = pow(max(0.0,dot( half_light, frag_normal )), shininess); } return vec2( n_dot_pos, n_dot_half); } """ '''Our vertex shader is also unchanged. We could move many of the operations currently done in our fragment shader here to reduce the processing load for our shader. ''' vertex = shaders.compileShader( """ attribute vec3 Vertex_position; attribute vec3 Vertex_normal; varying vec3 baseNormal; void main() { gl_Position = gl_ModelViewProjectionMatrix * vec4( Vertex_position, 1.0 ); baseNormal = gl_NormalMatrix * normalize(Vertex_normal); }""", GL_VERTEX_SHADER) '''To create a uniform with a structure type, we simply use the structure as the data-type declaration for the uniform. As opposed to using e.g. vec4 or vec3, we use Material (our structure name defined above) and give the uniform a name. ''' """uniform Material material;""" '''==GLSL Arrays== Each light we have defined (so far) is composed to 4 4-component vectors, ambient, diffuse and specular colour, along with the "position" (direction) vector. If we wanted to provide, for instance, 3 lights of this type, we *could* create 12 different uniform values, and set each of these uniforms individually. GLSL, however, provides for array data-types. The array types must be "sized" (have a specific, final size) in order to be usable, so no "pointer" types are available, but we don't need them for this type of operation. We can define a "lights" uniform which is declared as a sized array of 12 vec4 elements:''' """uniform vec4 lights[ 12 ];""" '''And we can loop over "lights" using 0-indexed [i] indices, where i must be an integer. The for loop will be familiar to those who have used C looping:''' """for (i=0;i<12;i=i+4) { blah; }""" '''Note that you must declare the iterator variable ("i" here) We iterate over each light in our array of lights accumulating the results into the fragColor variable we've defined. The global component is used to initialize the variable, with the contribution of each light added to the result. ''' fragment = shaders.compileShader( phong_weightCalc + materialStruct + """ uniform Material material; uniform vec4 Global_ambient; uniform vec4 lights[ 12 ]; // 3 possible lights 4 vec4's each varying vec3 baseNormal; void main() { vec4 fragColor = Global_ambient * material.ambient; int AMBIENT = 0; int DIFFUSE = 1; int SPECULAR = 2; int POSITION = 3; int i; for (i=0;i<12;i=i+4) { // normalized eye-coordinate Light location vec3 EC_Light_location = normalize( gl_NormalMatrix * lights[i+POSITION].xyz ); // half-vector calculation vec3 Light_half = normalize( EC_Light_location - vec3( 0,0,-1 ) ); vec2 weights = phong_weightCalc( EC_Light_location, Light_half, baseNormal, material.shininess ); fragColor = ( fragColor + (lights[i+AMBIENT] * material.ambient) + (lights[i+DIFFUSE] * material.diffuse * weights.x) + (lights[i+SPECULAR] * material.specular * weights.y) ); } gl_FragColor = fragColor; } """, GL_FRAGMENT_SHADER) '''===Why not an Array of Structures?=== Originally this tutorial was going to use an array of LightSource structures as a Uniform, with the components of the structures specified with separate calls to glUniform4f. Problem is, that doesn't actually *work*. While glUniform *should* be able to handle array-of-structure indexing, it doesn't actually support this type of operation in the real world. The built-in gl_LightSourceParameters are an array-of-structures, but apparently the GL implementations consider this a special case, rather than a generic type of functionality to be supported. An array-of-structures value looks like this when declared in GLSL: ''' lightStruct = """ // NOTE: this does not work, it compiles, but you will // not be able to fill in the individual members... struct LightSource { vec4 ambient; vec4 diffuse; vec4 specular; vec4 position; }; uniform LightSource lights[3]; """ '''When you attempt to retrieve the location for the Uniform via: glGetUniformLocation( shader, 'lights[0].ambient' ) you will always get a -1 (invalid) location. OpenGL 3.1 introduced the concept of Uniform Buffers, which allow for packing Uniform data into VBO storage, but it's not yet clear whether they will support array-of-structure specification. ''' self.shader = shaders.compileProgram(vertex,fragment) self.coords,self.indices,self.count = Sphere( radius = 1 ).compile() self.uniform_locations = {} for uniform,value in self.UNIFORM_VALUES: location = glGetUniformLocation( self.shader, uniform ) if location in (None,-1): print 'Warning, no uniform: %s'%( uniform ) self.uniform_locations[uniform] = location '''There's no real reason to treat the "lights" uniform specially, other than that we want to call attention to it. We get the uniform as normal. Note that we *could* also retrieve a sub-element of the array by specifying 'lights[3]' or the like. ''' self.uniform_locations['lights'] = glGetUniformLocation( self.shader, 'lights' ) for attribute in ( 'Vertex_position','Vertex_normal', ): location = glGetAttribLocation( self.shader, attribute ) if location in (None,-1): print 'Warning, no attribute: %s'%( uniform ) setattr( self, attribute+ '_loc', location ) '''Our individually-specified uniform values''' UNIFORM_VALUES = [ ('Global_ambient',(.05,.05,.05,1.0)), ('material.ambient',(.2,.2,.2,1.0)), ('material.diffuse',(.5,.5,.5,1.0)), ('material.specular',(.8,.8,.8,1.0)), ('material.shininess',(.995,)), ] '''The parameters we use to specify our lights, note that the first item in the tuples is dropped, it is the value that *should* work in glGetUniformLocation, but does not. What actually gets passed in is a single float array with 12 4-float values representing all of the data-values for all of the enabled lights. You'll notice that we're using 0.0 as the 'w' coordinate for the light positions. We're using this to flag that the position is actually a *direction*. This will become useful in later tutorials where we have multiple light-types. ''' LIGHTS = array([ x[1] for x in [ ('lights[0].ambient',(.05,.05,.05,1.0)), ('lights[0].diffuse',(.3,.3,.3,1.0)), ('lights[0].specular',(1.0,0.0,0.0,1.0)), ('lights[0].position',(4.0,2.0,10.0,0.0)), ('lights[1].ambient',(.05,.05,.05,1.0)), ('lights[1].diffuse',(.3,.3,.3,1.0)), ('lights[1].specular',(0.0,1.0,0.0,1.0)), ('lights[1].position',(-4.0,2.0,10.0,0.0)), ('lights[2].ambient',(.05,.05,.05,1.0)), ('lights[2].diffuse',(.3,.3,.3,1.0)), ('lights[2].specular',(0.0,0.0,1.0,1.0)), ('lights[2].position',(-4.0,2.0,-10.0,0.0)), ] ], 'f') def Render( self, mode = None): """Render the geometry for the scene.""" BaseContext.Render( self, mode ) glUseProgram(self.shader) try: self.coords.bind() self.indices.bind() stride = self.coords.data[0].nbytes try: '''Here's our only change to the rendering process, we pass in the entire array of light-related data with a single call to glUniform4fv. The 'v' forms of glUniform all allow for passing arrays of values, and all require that you specify the number of elements being passed (here 12). Aside: Incidentally, Uniforms are actually stored with the shader until the shader is re-linked, so specifying the uniforms on each rendering pass (as we do here) is not necessary. The shader merely needs to be "in use" during the glUniform call, and this is a convenient, if inefficient, way to ensure it is in use at the time we are calling glUniform. ''' glUniform4fv( self.uniform_locations['lights'], 12, self.LIGHTS ) test_lights = (GLfloat * 12)() glGetUniformfv( self.shader, self.uniform_locations['lights'], test_lights ) print 'Lights', list(test_lights) for uniform,value in self.UNIFORM_VALUES: location = self.uniform_locations.get( uniform ) if location not in (None,-1): if len(value) == 4: glUniform4f( location, *value ) elif len(value) == 3: glUniform3f( location, *value ) elif len(value) == 1: glUniform1f( location, *value ) glEnableVertexAttribArray( self.Vertex_position_loc ) glEnableVertexAttribArray( self.Vertex_normal_loc ) glVertexAttribPointer( self.Vertex_position_loc, 3, GL_FLOAT,False, stride, self.coords ) glVertexAttribPointer( self.Vertex_normal_loc, 3, GL_FLOAT,False, stride, self.coords+(5*4) ) glDrawElements( GL_TRIANGLES, self.count, GL_UNSIGNED_SHORT, self.indices ) finally: self.coords.unbind() self.indices.unbind() glDisableVertexAttribArray( self.Vertex_position_loc ) glDisableVertexAttribArray( self.Vertex_normal_loc ) finally: glUseProgram( 0 )
class TestContext( BaseContext ): """Demonstrates use of attribute types in GLSL """ LIGHT_COUNT = 3 LIGHT_SIZE = 4 def OnInit( self ): """Initialize the context""" '''==Sharing Declarations= Since we are going to use these values in both the vertex and fragment shaders, it is handy to separate out the constants we'll use into a separate block of code that we can add to both shaders. The use of the constants also makes the code far easier to read than using the bare numbers. Note that the varying baseNormal value is part of the lighting calculation, so we have included it in our common lighting declarations. We've also parameterized the LIGHT count and size, so that we can use them in both Python and GLSL code. ''' lightConst = """ const int LIGHT_COUNT = %s; const int LIGHT_SIZE = %s; const int AMBIENT = 0; const int DIFFUSE = 1; const int SPECULAR = 2; const int POSITION = 3; uniform vec4 lights[ LIGHT_COUNT*LIGHT_SIZE ]; varying vec3 EC_Light_half[LIGHT_COUNT]; varying vec3 EC_Light_location[LIGHT_COUNT]; varying vec3 baseNormal; """%( self.LIGHT_COUNT, self.LIGHT_SIZE ) '''As you can see, we're going to create two new varying values, the EC_Light_half and EC_Light_location values. These are going to hold the normalized partial calculations for the lights. The other declarations are the same as before, they are just being shared between the shaders. Our phong_weightCalc calculation hasn't changed. ''' phong_weightCalc = """ vec2 phong_weightCalc( in vec3 light_pos, // light position in vec3 half_light, // half-way vector between light and view in vec3 frag_normal, // geometry normal in float shininess ) { // returns vec2( ambientMult, diffuseMult ) float n_dot_pos = max( 0.0, dot( frag_normal, light_pos )); float n_dot_half = 0.0; if (n_dot_pos > -.05) { n_dot_half = pow(max(0.0,dot( half_light, frag_normal )), shininess); } return vec2( n_dot_pos, n_dot_half); } """ '''Our new vertex shader has a loop in it. It iterates over the set of lights doing the partial calculations for half-vector and eye-space location. It stores the results of these in our new, varying array values. ''' vertex = shaders.compileShader( lightConst + """ attribute vec3 Vertex_position; attribute vec3 Vertex_normal; void main() { gl_Position = gl_ModelViewProjectionMatrix * vec4( Vertex_position, 1.0 ); baseNormal = gl_NormalMatrix * normalize(Vertex_normal); for (int i = 0; i< LIGHT_COUNT; i++ ) { EC_Light_location[i] = normalize( gl_NormalMatrix * lights[(i*LIGHT_SIZE)+POSITION].xyz ); // half-vector calculation EC_Light_half[i] = normalize( EC_Light_location[i] - vec3( 0,0,-1 ) ); } }""", GL_VERTEX_SHADER) '''Our fragment shader looks much the same, save that we have now moved the complex half-vector and eye-space location calculations out. We've also separated out the concept of which light we are processing and what array-offset we are using, to make it clearer which value is being accessed. ''' fragment = shaders.compileShader( lightConst + phong_weightCalc + """ struct Material { vec4 ambient; vec4 diffuse; vec4 specular; float shininess; }; uniform Material material; uniform vec4 Global_ambient; void main() { vec4 fragColor = Global_ambient * material.ambient; int i,j; for (i=0;i<LIGHT_COUNT;i++) { j = i* LIGHT_SIZE; vec2 weights = phong_weightCalc( EC_Light_location[i], EC_Light_half[i], baseNormal, material.shininess ); fragColor = ( fragColor + (lights[j+AMBIENT] * material.ambient) + (lights[j+DIFFUSE] * material.diffuse * weights.x) + (lights[j+SPECULAR] * material.specular * weights.y) ); } gl_FragColor = fragColor; } """, GL_FRAGMENT_SHADER) '''The rest of our code is very familiar.''' self.shader = shaders.compileProgram(vertex,fragment) self.coords,self.indices,self.count = Sphere( radius = 1 ).compile() self.uniform_locations = {} for uniform,value in self.UNIFORM_VALUES: location = glGetUniformLocation( self.shader, uniform ) if location in (None,-1): print 'Warning, no uniform: %s'%( uniform ) self.uniform_locations[uniform] = location self.uniform_locations['lights'] = glGetUniformLocation( self.shader, 'lights' ) for attribute in ( 'Vertex_position','Vertex_normal', ): location = glGetAttribLocation( self.shader, attribute ) if location in (None,-1): print 'Warning, no attribute: %s'%( uniform ) setattr( self, attribute+ '_loc', location ) UNIFORM_VALUES = [ ('Global_ambient',(.05,.05,.05,1.0)), ('material.ambient',(.2,.2,.2,1.0)), ('material.diffuse',(.5,.5,.5,1.0)), ('material.specular',(.8,.8,.8,1.0)), ('material.shininess',(.995,)), ] LIGHTS = array([ x[1] for x in [ ('lights[0].ambient',(.05,.05,.05,1.0)), ('lights[0].diffuse',(.3,.3,.3,1.0)), ('lights[0].specular',(1.0,0.0,0.0,1.0)), ('lights[0].position',(4.0,2.0,10.0,0.0)), ('lights[1].ambient',(.05,.05,.05,1.0)), ('lights[1].diffuse',(.3,.3,.3,1.0)), ('lights[1].specular',(0.0,1.0,0.0,1.0)), ('lights[1].position',(-4.0,2.0,10.0,0.0)), ('lights[2].ambient',(.05,.05,.05,1.0)), ('lights[2].diffuse',(.3,.3,.3,1.0)), ('lights[2].specular',(0.0,0.0,1.0,1.0)), ('lights[2].position',(-4.0,2.0,-10.0,0.0)), ] ], 'f') def Render( self, mode = None): """Render the geometry for the scene.""" BaseContext.Render( self, mode ) if not mode.visible: return glUseProgram(self.shader) try: self.coords.bind() stride = self.coords.data[0].nbytes try: '''Note the use of the parameterized values to specify the size of the light-parameter array.''' glUniform4fv( self.uniform_locations['lights'], self.LIGHT_COUNT * self.LIGHT_SIZE, self.LIGHTS ) for uniform,value in self.UNIFORM_VALUES: location = self.uniform_locations.get( uniform ) if location not in (None,-1): if len(value) == 4: glUniform4f( location, *value ) elif len(value) == 3: glUniform3f( location, *value ) elif len(value) == 1: glUniform1f( location, *value ) glEnableVertexAttribArray( self.Vertex_position_loc ) glEnableVertexAttribArray( self.Vertex_normal_loc ) glVertexAttribPointer( self.Vertex_position_loc, 3, GL_FLOAT,False, stride, self.coords ) glVertexAttribPointer( self.Vertex_normal_loc, 3, GL_FLOAT,False, stride, self.coords+(5*4) ) self.indices.bind() glDrawElements( GL_TRIANGLES, self.count, GL_UNSIGNED_SHORT, self.indices ) finally: self.coords.unbind() self.indices.unbind() glDisableVertexAttribArray( self.Vertex_position_loc ) glDisableVertexAttribArray( self.Vertex_normal_loc ) finally: glUseProgram( 0 )