"""
raycast.py

Autor: Mahesh Venkitachalam

Ten moduł ma klasy i metody związane z renderowaniem objętościowym 
przy użyciu metody Ray Casting.
"""


import OpenGL
from OpenGL.GL import *
from OpenGL.GL.shaders import *

import numpy as np
import math, sys 

import raycube, glutils, volreader

strVS = """
#version 330 core

layout(location = 1) in vec3 cubePos;
layout(location = 2) in vec3 cubeCol;

uniform mat4 uMVMatrix;
uniform mat4 uPMatrix;

out vec4 vColor;

void main()
{    
    // ustawienie pozycji
    gl_Position = uPMatrix * uMVMatrix * vec4(cubePos.xyz, 1.0);

    // ustawienie koloru
    vColor = vec4(cubeCol.rgb, 1.0);
}
"""
strFS = """
#version 330 core

in vec4 vColor;

uniform sampler2D texBackFaces;
uniform sampler3D texVolume;
uniform vec2 uWinDims;

out vec4 fragColor;

void main()
{
    // początek promienia
    vec3 start = vColor.rgb;

    // obliczanie dla fragmentu współrzędnych tekstury, 
    // które są ułamkiem wspórzędnych okna
    vec2 texc = gl_FragCoord.xy/uWinDims.xy;

    // uzyskanie końca promienia poprzez sprawdzenie koloru tylnej ściany
    vec3 end = texture(texBackFaces, texc).rgb;

    // obliczanie kierunku promienia
    vec3 dir = end - start;

    // znormalizowany kierunek promienia
    vec3 norm_dir = normalize(dir);

    // obliczanie odległości od przedniej do tylnej ściany 
    // i użycie jej do zakończenia promienia
    float len = length(dir.xyz);

    // rozmiar kroku promienia
    float stepSize = 0.01;

    // rzutowanie promieniami x
    vec4 dst = vec4(0.0);
       
    // przechodzenie przez promień
    for(float t = 0.0; t < len; t += stepSize) {

        // ustawianie pozycji na koniec promienia
        vec3 samplePos = start + t*norm_dir;

        // uzyskanie wartości tekstury w pozycji
        float val = texture(texVolume, samplePos).r;
        vec4 src = vec4(val);

        // ustawianie nieprzezroczystości
        src.a *= 0.1; 
        src.rgb *= src.a;

        // blendowanie z poprzednia wartością
        dst = (1.0 - dst.a)*src + dst;		
            
        // wyjście z pętli, gdy alfa przekroczy próg
        if(dst.a >= 0.95)
            break;
    }
        
    // ustawienie koloru fragmentu
    fragColor =  dst;   
}
"""

class Camera:
    """Klasa pomocznicza dla oglądania"""
    def __init__(self):
        self.r = 1.5
        self.theta = 0
        self.center = [0.5, 0.5, 0.5]
        self.eye = [0.5 + self.r, 0.5, 0.5]
        self.up = [0.0, 0.0, 1.0]

    def rotate(self, clockWise):
        """Obrót oka o jeden krok"""
        if clockWise:
            self.theta = (self.theta + 5) % 360
        else:
            self.theta = (self.theta - 5) % 360
        # ponowne obliczenie oka
        self.eye = [0.5 + self.r*math.cos(math.radians(self.theta)), 
                    0.5 + self.r*math.sin(math.radians(self.theta)), 
                    0.5]

class RayCastRender:
    """Klasa wykonująca ray casting"""
    
    def __init__(self, width, height, volume):
        """Konstrukcja klasy RayCastRender"""
        
        # tworzenie obiektu RayCube
        self.raycube = raycube.RayCube(width, height)
        
        # ustawianie wymiarów
        self.width = width
        self.height = height
        self.aspect = width/float(height)

        # tworzenie shadera
        self.program = glutils.loadShaders(strVS, strFS)
        # tekstura
        self.texVolume, self.Nx, self.Ny, self.Nz = volume
        
        # inicjowanie kamery
        self.camera = Camera()
        
    def draw(self):

        # budowanie macierzy rzutowania
        pMatrix = glutils.perspective(45.0, self.aspect, 0.1, 100.0)
       
        # macierz widoku modelu
        mvMatrix = glutils.lookAt(self.camera.eye, self.camera.center, 
                                  self.camera.up)
        # renderowanie
        
        # generowanie tekstury tylnych ścian sześcianu
        texture = self.raycube.renderBackFace(pMatrix, mvMatrix)
        
        # ustawianie programu shadera
        glUseProgram(self.program)

        # ustawianie wymiarów okna
        glUniform2f(glGetUniformLocation(self.program, b"uWinDims"),
                    float(self.width), float(self.height))

        # wiązanie do jednostki tekstury 0, która reprezentuje tylne ściany sześcianu
        glActiveTexture(GL_TEXTURE0)
        glBindTexture(GL_TEXTURE_2D, texture)
        glUniform1i(glGetUniformLocation(self.program, b"texBackFaces"), 0)
        
        # jednostka tekstury 1 — tekstura wolumenu 3D
        glActiveTexture(GL_TEXTURE1)
        glBindTexture(GL_TEXTURE_3D, self.texVolume)
        glUniform1i(glGetUniformLocation(self.program, b"texVolume"), 1)

        # rysowanie przednich ścian sześcianu
        self.raycube.renderFrontFace(pMatrix, mvMatrix, self.program)
                
        #self.render(pMatrix, mvMatrix)

    def keyPressed(self, key):
        if key == 'l':
            self.camera.rotate(True)
        elif key == 'r':
            self.camera.rotate(False)
            
    def reshape(self, width, height):
        self.width = width
        self.height = height
        self.aspect = width/float(height)
        self.raycube.reshape(width, height)

    def close(self):
        self.raycube.close()
