Search code examples
pythonpython-3.xopenglglutopengl-3

How to get clip distances and textures on green dragon rendering


The source is of a green dragon rendering. My question is how to get the clip distances working on it? Also, the textures are not appearing as the expected output. Any ideas what can be modified in the source code to render the program as expected?

Update: With the excellent help and superb answers of Rabbid76 the clip distance is working and texture loading is working! Thank You.

Bonus example: clipdistance_torus_package.zip a clip distance example with a torus and textures!

Expected output:

enter image description here

Files to run: clipdistance_dragon.zip

#!/usr/bin/python3

import sys
import time
import ctypes

fullscreen = True

sys.path.append("./shared")

from sbmloader import SBMObject    # location of sbm file format loader
from ktxloader import KTXObject

from sbmath import m3dDegToRad, m3dRadToDeg, m3dTranslateMatrix44, m3dRotationMatrix44, m3dMultiply, m3dOrtho, m3dPerspective, rotation_matrix, translate, m3dScaleMatrix44, \
    scale, m3dLookAt, normalize

try:
    from OpenGL.GLUT import *
    from OpenGL.GL import *
    from OpenGL.GLU import *
    from OpenGL.raw.GL.ARB.vertex_array_object import glGenVertexArrays, glBindVertexArray
except:
    print ('''
    ERROR: PyOpenGL not installed properly.
        ''')
    sys.exit()

import numpy as np 
from math import cos, sin 
identityMatrix = [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]


myobject = SBMObject()

render_program = GLuint(0)
paused = False

class uniforms:
    proj_matrix = GLint(0)
    mv_matrix = GLint(0)
    clip_plane = GLint(0)
    clip_sphere = GLint(0)

uniform = uniforms()


def shader_load(filename, shader_type):
    result = GLuint(0)
    with open ( filename, "rb") as data:
        result = glCreateShader(shader_type)
        glShaderSource(result, data.read() )

    glCompileShader(result)
    if not glGetShaderiv(result, GL_COMPILE_STATUS):
        print( 'compile error:' )
        print( glGetShaderInfoLog(result) )
    return result

def link_from_shaders(shaders, shader_count, delete_shaders, check_errors=False):
    program = GLuint(0)
    program = glCreateProgram()

    for i in range(0, shader_count):
        glAttachShader(program, shaders[i])

    glLinkProgram(program)
    if not glGetProgramiv(program, GL_LINK_STATUS):
        print( 'link error:' )
        print( glGetProgramInfoLog(program) )

    if (delete_shaders):
        for i in range(0, shader_count):
            glDeleteShader(shaders[i])
    return program


def load_shaders():
    global render_program
    global uniform

    if (render_program):
        glDeleteProgram(render_program);

    shaders = [
        shader_load("render.vs.glsl", GL_VERTEX_SHADER),
        shader_load("render.fs.glsl", GL_FRAGMENT_SHADER)
    ]

    render_program = link_from_shaders(shaders, 2, True)

    uniform.proj_matrix = glGetUniformLocation(render_program, "proj_matrix");
    uniform.mv_matrix = glGetUniformLocation(render_program, "mv_matrix");
    uniform.clip_plane = glGetUniformLocation(render_program, "clip_plane");
    uniform.clip_sphere = glGetUniformLocation(render_program, "clip_sphere");

tex_dragon=None

class Scene:

    def __init__(self, width, height):
        global myobject, tex_dragon

        myobject.load("dragon.sbm");

        load_shaders()

        ktxobj = KTXObject()
        tex_dragon = ktxobj.ktx_load("pattern1.ktx")

    def display(self):
        global paused

        currentTime = time.time()

        black = [ 0.0, 0.0, 0.0, 0.0 ]
        one = 1.0

        last_time = 0.0
        total_time = 0.0

        if (not paused):
            total_time += (currentTime - last_time)
        last_time = currentTime

        f = total_time

        glClearBufferfv(GL_COLOR, 0, black)
        glClearBufferfv(GL_DEPTH, 0, one)

        glUseProgram(render_program)

        proj_matrix = (GLfloat * 16)(*identityMatrix)
        proj_matrix = m3dPerspective(m3dDegToRad(50.0), float(self.width) / float(self.height), 0.1, 1000.0)

        T1 = (GLfloat * 16)(*identityMatrix)
        m3dTranslateMatrix44(T1, 0.0, 0.0, -15.0)

        RY = (GLfloat * 16)(*identityMatrix)
        m3dRotationMatrix44(RY, f * 0.34, 0.0, 1.0, 0.0)

        T2 = (GLfloat * 16)(*identityMatrix)
        m3dTranslateMatrix44(T2, 0.0, -4.0, 0.0)

        mv_matrix = (GLfloat * 16)(*identityMatrix)
        mv_matrix = m3dMultiply(T1, m3dMultiply(RY, T2))


        RX = (GLfloat * 16)(*identityMatrix)
        m3dRotationMatrix44(RX, f * 6.0, 1.0, 0.0, 0.0)

        RY = (GLfloat * 16)(*identityMatrix)
        m3dRotationMatrix44(RY, f * 7.3, 0.0, 1.0, 0.0)

        plane_matrix = (GLfloat * 16)(*identityMatrix)
        plane_matrix = m3dMultiply(RX , RY )

        plane = plane_matrix[0:4]
        plane[3] = 0
        plane = normalize(plane)

        clip_sphere = [sin(f * 0.7) * 3.0, cos(f * 1.9) * 3.0, sin(f * 0.1) * 3.0, cos(f * 1.7) + 2.5]

        glUniformMatrix4fv(uniform.proj_matrix, 1, GL_FALSE, proj_matrix)
        glUniformMatrix4fv(uniform.mv_matrix, 1, GL_FALSE, mv_matrix)
        glUniform4fv(uniform.clip_plane, 1, plane)
        glUniform4fv(uniform.clip_sphere, 1, clip_sphere)

        glEnable(GL_DEPTH_TEST)
        glEnable(GL_CLIP_DISTANCE0)
        glEnable(GL_CLIP_DISTANCE1)

        glActiveTexture(GL_TEXTURE0)
        glBindTexture(GL_TEXTURE_2D, tex_dragon)
        myobject.render()

        glutSwapBuffers()

    def reshape(self, width, height):
        self.width = width
        self.height = height

    def keyboard(self, key, x, y ):
        global fullscreen
        global paused

        print ('key:' , key)
        if key == b'\x1b': # ESC
            sys.exit()

        elif key == b'f' or key == b'F': #fullscreen toggle

            if (fullscreen == True):
                glutReshapeWindow(512, 512)
                glutPositionWindow(int((1360/2)-(512/2)), int((768/2)-(512/2)))
                fullscreen = False
            else:
                glutFullScreen()
                fullscreen = True

        elif key == b'p' or key == b'P':
            paused = not paused

        elif key == b'r' or key == b'R':
            pass 

        print('done')

    def init(self):
        pass

    def timer(self, blah):

        glutPostRedisplay()
        glutTimerFunc( int(1/60), self.timer, 0)
        time.sleep(1/60.0)


if __name__ == '__main__':
    start = time.time()

    glutInit()


    glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH)

    glutInitWindowSize(512, 512)

    w1 = glutCreateWindow('OpenGL SuperBible - Clip Distance')
    glutInitWindowPosition(int((1360/2)-(512/2)), int((768/2)-(512/2)))

    fullscreen = False
    #glutFullScreen()

    scene = Scene(512,512)
    glutReshapeFunc(scene.reshape)
    glutDisplayFunc(scene.display)
    glutKeyboardFunc(scene.keyboard)

    glutIdleFunc(scene.display)
    #glutTimerFunc( int(1/60), scene.timer, 0)

    scene.init()

    glutMainLoop()

ported into python from clipdistance.cpp , source of sbmobject.cpp just in case sbmloader.py is in question for the texture issue.


Solution

  • The C++ code from the example

    vmath::mat4 plane_matrix = vmath::rotate(f * 6.0f, 1.0f, 0.0f, 0.0f) *
                               vmath::rotate(f * 7.3f, 0.0f, 1.0f, 0.0f);
    

    corresponds to the following python code

    RX = (GLfloat * 16)(*identityMatrix)
    m3dRotationMatrix44(RX, f * 6.0, 1.0, 0.0, 0.0)
    
    RY = (GLfloat * 16)(*identityMatrix)
    m3dRotationMatrix44(RY, f * 7.3, 0.0, 1.0, 0.0)
    
    plane_matrix = (GLfloat * 16)(*identityMatrix)
    plane_matrix = m3dMultiply(RX , RY)
    

    Note, you've to swap RX and RY in the matrix multiplication.


    Your functions length and normalize can only deal with vectors which have 3 components (x, y, z). In compare the C++ function vmath::normalize from the example can handle vectors with 4 components (x, y, z, w), too.
    Further the "division by zero" handling is missing in normalize.

    Adapt the functions normalize and length, to deal with any vector size.
    If the length of a vector is 0, then all its components are 0. There is no correct solution for that, so just return a copy of the vector itself.

    def length(v):
        sum_sq = sum([s*s for s in v])
        return math.sqrt(sum_sq)
    
    def normalize(v):
        l = length(v)
        if l == 0.0:
            return v[:]
        return [s/l for s in v]
    

    Now you can port the C++ code from the example

    vmath::vec4 plane = plane_matrix[0];
    plane[3] = 0.0f;
    plane = vmath::normalize(plane);
    

    very straight to python:

    plane = plane_matrix[0:4]
    plane[3] = 0
    plane = normalize(plane)
    

    Further, there is an issue in the sbmloader module.
    Only the array of vertex coordinates and texture coordinates is specified. The normal vectors are skipped.

    Just skip the line

    if attrib.name=='position' or attrib.name=='map1':

    to fix the issue:

    for attrib_i, attrib in enumerate(vertex_attrib_chunk.attrib_data):
    
        #if attrib.name=='position' or attrib.name=='map1': 
    
        glVertexAttribPointer(attrib_i,
            attrib.size, attrib.type,
            GL_TRUE if (attrib.flags & SB6M_VERTEX_ATTRIB_FLAG_NORMALIZED) != 0 else GL_FALSE,
            attrib.stride, ctypes.c_void_p(int(attrib.data_offset)))
        glEnableVertexAttribArray(attrib_i)
    


    If you additionally want to wrap a texture to the model, then you've to add the texture coordinate attribute to the vertex shader:

    layout (location = 2) in vec2 tc;
    

    And to pass it by an output to the next shader stage

    out VS_OUT
    {
        vec3 N;
        vec3 L;
        vec3 V;
        vec2 T;
    } vs_out;
    
    void main()
    {
       // ...
    
       vs_out.T = tc;
    
       // ...  
    }    
    

    In the fragment shader you've to add the texture sampler uniform

    layout (binding = 0) uniform sampler2D tex;
    

    Read the color form the texture

    vec4 texColor = texture(tex, fs_in.T); 
    

    Multiply the output color by the texture color

    color = vec4(diffuse + specular + rim, 1.0) * texColor;
    

    Fragment shader (note, I changed diffuse_albedo):

    #version 420 core
    
    // Output
    layout (location = 0) out vec4 color;
    
    // Input from vertex shader
    in VS_OUT
    {
        vec3 N;
        vec3 L;
        vec3 V;
        vec2 T;
    } fs_in;
    
    // Material properties
    uniform vec3 diffuse_albedo = vec3(0.5);
    uniform vec3 specular_albedo = vec3(0.7);
    uniform float specular_power = 128.0;
    uniform vec3 rim_color = vec3(0.1, 0.2, 0.2);
    uniform float rim_power = 5.0;
    layout (binding = 0) uniform sampler2D tex;
    
    vec3 calculate_rim(vec3 N, vec3 V)
    {
        float f = 1.0 - dot(N, V);
    
        f = smoothstep(0.0, 1.0, f);
        f = pow(f, rim_power);
    
        return f * rim_color;
    }
    
    void main(void)
    {
        // Normalize the incoming N, L and V vectors
        vec3 N = normalize(fs_in.N);
        vec3 L = normalize(fs_in.L);
        vec3 V = normalize(fs_in.V);
    
        // Calculate R locally
        vec3 R = reflect(-L, N);
    
        // Compute the diffuse and specular components for each fragment
        vec3 diffuse = max(dot(N, L), 0.0) * diffuse_albedo;
        vec3 specular = pow(max(dot(R, V), 0.0), specular_power) * specular_albedo;
        vec3 rim = calculate_rim(N, V);
    
        // read color from the texture
        vec4 texColor = texture(tex, fs_in.T);
    
        // Write final color to the framebuffer
        color = vec4(diffuse + specular + rim, 1.0) * texColor;
    }
    

    I recommend to add shader compile and link error logging:

    glCompileShader(result)
    if not glGetShaderiv(result, GL_COMPILE_STATUS):
        print( 'compile error:' )
        print( glGetShaderInfoLog(result) )
    
    glLinkProgram(program)
    if not glGetProgramiv(program, GL_LINK_STATUS):
        print( 'link error:' )
        print( glGetProgramInfoLog(program) )
    

    Read a texture at the initialization of the application

    class Scene:
    
        def __init__(self, width, height):
            global myobject, tex_dragon
    
            myobject.load("dragon.sbm")
    
            load_shaders()
    
            ktxobj = KTXObject()
            tex_dragon = ktxobj.ktx_load("texture_file_name.ktx")
    

    Bind the texture before drawing the model

    glActiveTexture(GL_TEXTURE0)
    glBindTexture(GL_TEXTURE_2D, tex_dragon)
    myobject.render()