├── ptmviewer ├── __init__.py ├── utils │ ├── __init__.py │ ├── camera.py │ ├── utils.py │ ├── obj3d.py │ ├── window.py │ └── light.py ├── interface │ ├── __init__.py │ ├── interface.ui │ └── window.py ├── assets │ └── shaders │ │ ├── lamp.frag │ │ ├── lamp.vert │ │ ├── portBlinnPhong.vert │ │ ├── blinnPhong.vert │ │ ├── phong.vert │ │ ├── blinnPhong.frag │ │ ├── phong.frag │ │ └── portBlinnPhong.frag ├── ptmparser.py ├── rgbptm.py ├── rgbptm_old.py └── qtapp.py ├── .gitignore ├── docs └── install.rst ├── setup.py ├── README.rst ├── ptmviewer.yml ├── ptmviewer-spec-file.txt └── LICENSE /ptmviewer/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ptmviewer/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ptmviewer/interface/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ptmviewer/assets/shaders/lamp.frag: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ptmviewer/assets/shaders/lamp.vert: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **.png 2 | **/__pycache__/ 3 | **/ptms/** 4 | **/assets/** 5 | **/.vim 6 | **/.mypy* 7 | -------------------------------------------------------------------------------- /ptmviewer/assets/shaders/portBlinnPhong.vert: -------------------------------------------------------------------------------- 1 | attribute vec3 vertexPosition; 2 | attribute vec3 surfaceNormal; 3 | attribute vec2 aTextureCoord; 4 | 5 | varying highp vec3 FragPos; 6 | varying highp vec3 Normal; 7 | varying highp vec2 TexCoord; 8 | 9 | uniform mat4 model; 10 | uniform mat4 view; 11 | uniform mat4 projection; 12 | 13 | void main() 14 | { 15 | FragPos = vec3(model * vec4(aPos, 1.0)); 16 | Normal = mat3(transpose(inverse(model))) * aNormal; 17 | TexCoord = aTexCoord; 18 | 19 | gl_Position = projection * view * vec4(FragPos, 1.0); 20 | } 21 | -------------------------------------------------------------------------------- /ptmviewer/assets/shaders/blinnPhong.vert: -------------------------------------------------------------------------------- 1 | #version 420 core 2 | layout (location = 0) in vec3 aPos; 3 | layout (location = 1) in vec3 aNormal; 4 | layout (location = 2) in vec2 aTexCoord; 5 | 6 | out vec3 FragPos; 7 | out vec3 Normal; 8 | out vec2 TexCoord; 9 | 10 | uniform mat4 model; 11 | uniform mat4 view; 12 | uniform mat4 projection; 13 | 14 | void main() 15 | { 16 | FragPos = vec3(model * vec4(aPos, 1.0)); 17 | Normal = mat3(transpose(inverse(model))) * aNormal; 18 | TexCoord = aTexCoord; 19 | 20 | gl_Position = projection * view * vec4(FragPos, 1.0); 21 | } 22 | -------------------------------------------------------------------------------- /ptmviewer/assets/shaders/phong.vert: -------------------------------------------------------------------------------- 1 | #version 420 core 2 | layout (location = 0) in vec3 aPos; 3 | layout (location = 1) in vec3 aNormal; 4 | layout (location = 2) in vec2 aTexCoord; 5 | 6 | out vec3 FragPos; 7 | out vec3 Normal; 8 | out vec2 TexCoord; 9 | 10 | uniform mat4 model; 11 | uniform mat4 view; 12 | uniform mat4 projection; 13 | 14 | void main() 15 | { 16 | FragPos = vec3(model * vec4(aPos, 1.0)); 17 | Normal = mat3(transpose(inverse(model))) * aNormal; 18 | TexCoord = aTexCoord; 19 | 20 | gl_Position = projection * view * vec4(FragPos, 1.0); 21 | } 22 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | ################# 2 | Installation 3 | ################# 4 | 5 | Quick Start 6 | ============ 7 | 8 | For those using :code:`conda` package as package manager 9 | the installation is quite easy: 10 | 11 | - :code:`git clone https://github.com/D-K-E/ptm-viewer.git` 12 | 13 | - :code:`cd PATH_TO_REPO/ptm-viewer` 14 | 15 | - :code:`conda create --name ptmviewer --file ptmviewer-spec-file.txt` 16 | 17 | - Optional: If you do not want to use spec file you can use the yaml file to 18 | create your environment :code:`conda env create -f ptmviewer.yml` 19 | 20 | - :code:`conda activate ptmviewer` 21 | 22 | - :code:`cd ptmviewer` 23 | 24 | - :code:`python qtapp.py` 25 | 26 | 27 | Requirements for Existing Environments 28 | ======================================= 29 | 30 | If you want to conserve your working environment, the spec file contains all 31 | the libraries necessary to run the interface. 32 | 33 | You can install the libraries by: 34 | 35 | - :code:`conda install --name YOUR_ENV_NAME --file ptmviewer-spec-file.txt` 36 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import setuptools 3 | 4 | 5 | # currentdir = os.getcwd() 6 | 7 | with open("README.rst", "r", encoding="utf-8") as f: 8 | long_desc = f.read() 9 | 10 | with open("LICENSE", "r", encoding="utf-8") as f: 11 | license_str = f.read() 12 | 13 | setuptools.setup( 14 | name="ptmviewer", 15 | version="0.1", 16 | author='Kaan Eraslan', 17 | python_requires='>=3.5.0', 18 | author_email="kaaneraslan@gmail.com", 19 | description="Polynomial Textual Map Viewer", 20 | long_description=long_desc, 21 | long_description_content_type="text/markdown", 22 | license=license_str, 23 | url="https://github.com/D-K-E/ptm-viewer/", 24 | packages=setuptools.find_packages( 25 | exclude=["tests", "*.tests", "*.tests.*", "tests.*", 26 | "docs", ".gitignore", "README.md"] 27 | ), 28 | test_suite="tests", 29 | install_requires=[ 30 | "numpy", 31 | "pillow", 32 | #"PySide2" 33 | ], 34 | classifiers=[ 35 | "Programming Language :: Python :: 3", 36 | "License :: OSI Approved :: MIT License", 37 | "Operating System :: OS Independent", 38 | ], 39 | ) 40 | -------------------------------------------------------------------------------- /ptmviewer/assets/shaders/blinnPhong.frag: -------------------------------------------------------------------------------- 1 | #version 420 core 2 | out vec4 FragColor; 3 | 4 | in vec3 Normal; 5 | in vec3 FragPos; 6 | 7 | // material struct 8 | 9 | struct Material { 10 | vec3 ambientColor; 11 | vec3 diffuseColor; 12 | vec3 specularColor; 13 | float shininess; 14 | }; 15 | 16 | uniform Material material; 17 | 18 | struct Light { 19 | vec3 position; 20 | 21 | vec3 ambientColor; 22 | vec3 diffuseColor; 23 | vec3 specularColor; 24 | }; 25 | 26 | uniform Light light; 27 | 28 | struct Coeffs { 29 | float ambient; 30 | float diffuse; 31 | float specular; 32 | }; 33 | 34 | uniform Coeffs coeffs; 35 | 36 | uniform vec3 viewerPosition; 37 | 38 | void main() { 39 | // normalize normals 40 | vec3 norm = normalize(Normal); 41 | vec3 lightDirection = normalize(light.position - FragPos); 42 | float costheta = max(dot(norm, lightDirection), 0.0); 43 | 44 | // diffuse color 45 | vec3 diffuseColor = light.diffuseColor * material.diffuseColor; 46 | diffuseColor = diffuseColor * coeffs.diffuse * costheta; 47 | 48 | // ambient term 49 | vec3 ambientTerm = light.ambientColor * material.ambientColor; 50 | ambientTerm = ambientTerm * coeffs.ambient; 51 | 52 | // specular color 53 | vec3 viewerDirection = normalize(viewerPosition - FragPos); 54 | vec3 halfwayDirection = normalize(lightDirection + viewerDirection); 55 | float specularAngle = max(dot(norm, halfwayDirection), 0.0); 56 | specularAngle = pow(specularAngle, material.shininess); 57 | vec3 specular = light.specularColor * material.specularColor; 58 | specular = specular * specularAngle * coeffs.specular; 59 | 60 | vec3 result = specular + ambientTerm + diffuseColor; 61 | FragColor = vec4(result, 1.0); 62 | } 63 | -------------------------------------------------------------------------------- /ptmviewer/assets/shaders/phong.frag: -------------------------------------------------------------------------------- 1 | #version 420 core 2 | out vec4 FragColor; 3 | 4 | in vec3 Normal; 5 | in vec3 FragPos; 6 | 7 | // material struct 8 | 9 | struct Material { 10 | vec3 ambientColor; 11 | vec3 diffuseColor; 12 | vec3 specularColor; 13 | float shininess; 14 | }; 15 | 16 | uniform Material material; 17 | 18 | struct Light { 19 | vec3 position; 20 | 21 | vec3 ambientColor; 22 | vec3 diffuseColor; 23 | vec3 specularColor; 24 | }; 25 | 26 | uniform Light light; 27 | 28 | struct Coeffs { 29 | float ambient; 30 | float diffuse; 31 | float specular; 32 | }; 33 | 34 | uniform Coeffs coeffs; 35 | 36 | uniform vec3 viewerPosition; 37 | 38 | void main() { 39 | // normalize normals 40 | vec3 norm = normalize(Normal); 41 | vec3 lightDirection = normalize(light.position - FragPos); 42 | float costheta = max(dot(norm, lightDirection), 0.0); 43 | 44 | // diffuse color 45 | vec3 diffuseColor = light.diffuseColor * material.diffuseColor; 46 | diffuseColor = diffuseColor * coeffs.diffuse * costheta; 47 | 48 | // ambient term 49 | vec3 ambientTerm = light.ambientColor * material.ambientColor; 50 | ambientTerm = ambientTerm * coeffs.ambient; 51 | 52 | // specular color 53 | vec3 viewerDirection = normalize(viewerPosition - FragPos); 54 | vec3 reflectionDirection = reflect(-lightDirection, norm); 55 | float specularAngle = max(dot(viewerDirection, reflectionDirection), 0.0); 56 | specularAngle = pow(specularAngle, material.shininess); 57 | vec3 specular = light.specularColor * material.specularColor; 58 | specular = specular * specularAngle * coeffs.specular; 59 | 60 | vec3 result = specular + ambientTerm + diffuseColor; 61 | FragColor = vec4(result, 1.0); 62 | } 63 | -------------------------------------------------------------------------------- /ptmviewer/assets/shaders/portBlinnPhong.frag: -------------------------------------------------------------------------------- 1 | #version 420 core 2 | out vec4 FragColor; 3 | 4 | varying vec3 Normal; 5 | varying vec3 FragPos; 6 | 7 | // material struct 8 | 9 | struct Material { 10 | vec3 ambientColor; 11 | vec3 diffuseColor; 12 | vec3 specularColor; 13 | float shininess; 14 | }; 15 | 16 | uniform Material material; 17 | 18 | struct Light { 19 | vec3 position; 20 | 21 | vec3 ambientColor; 22 | vec3 diffuseColor; 23 | vec3 specularColor; 24 | }; 25 | 26 | uniform Light light; 27 | 28 | struct Coeffs { 29 | float ambient; 30 | float diffuse; 31 | float specular; 32 | }; 33 | 34 | uniform Coeffs coeffs; 35 | 36 | uniform vec3 viewerPosition; 37 | 38 | void main() { 39 | // normalize normals 40 | vec3 norm = normalize(Normal); 41 | vec3 lightDirection = normalize(light.position - FragPos); 42 | float costheta = max(dot(norm, lightDirection), 0.0); 43 | 44 | // diffuse color 45 | vec3 diffuseColor = light.diffuseColor * material.diffuseColor; 46 | diffuseColor = diffuseColor * coeffs.diffuse * costheta; 47 | 48 | // ambient term 49 | vec3 ambientTerm = light.ambientColor * material.ambientColor; 50 | ambientTerm = ambientTerm * coeffs.ambient; 51 | 52 | // specular color 53 | vec3 viewerDirection = normalize(viewerPosition - FragPos); 54 | vec3 halfwayDirection = normalize(lightDirection + viewerDirection); 55 | float specularAngle = max(dot(norm, halfwayDirection), 0.0); 56 | specularAngle = pow(specularAngle, material.shininess); 57 | vec3 specular = light.specularColor * material.specularColor; 58 | specular = specular * specularAngle * coeffs.specular; 59 | 60 | vec3 result = specular + ambientTerm + diffuseColor; 61 | FragColor = vec4(result, 1.0); 62 | } 63 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ########### 2 | ptm-viewer 3 | ########### 4 | 5 | Lightweight PTM viewer based on Pyside2 and OpenGL 6 | 7 | General view of the interface 8 | ============================== 9 | 10 | Lightweight ptm viewer based on PySide2 and OpenGL 11 | 12 | General view of the interface 13 | 14 | Viewing ptm under different light conditions 15 | 16 | 17 | Installation 18 | ============= 19 | 20 | See `Installation `_ 21 | 22 | 23 | Quick Start 24 | =========== 25 | 26 | - Follow the installation procedure detailed in `here `_ 27 | - Activate your conda environment from terminal 28 | - Go to repository 29 | - From the location of `setup.py` at the main folder, start the program with 30 | `python ptmviewer/qtapp.py` 31 | 32 | Roadmap 33 | ======== 34 | 35 | Milestone 0.1.0 36 | ------------- 37 | 38 | - [✓] Smoother UI with dock widgets 39 | 40 | - [x] Reproducible lightening conditions using lightning parameter serialization 41 | 42 | - [✓] Handle light and camera rotation in all axes. 43 | 44 | - [✓] Handle light and camera movement in all axes. 45 | 46 | - [✓] Small notepad for taking notes about the object as it is seen by the user. 47 | 48 | 49 | 50 | Milestone 0.2.0 51 | --------------- 52 | 53 | - Add a couple of other shaders. 54 | 55 | - PBR if possible 56 | 57 | - Change light controller 58 | 59 | - Give more control on illumination parameters 60 | 61 | 62 | Known Issues 63 | ============= 64 | 65 | - Camera pierces through the ptm surface 66 | - Light also passes through ptm surface. 67 | 68 | 69 | Remarks 70 | ======== 71 | 72 | The rendering should work in real time with no hassle since its all done in 73 | OpenGL. Just make sure that you have OpenGL 3.3 and above. Your driver 74 | information is also shown in status bar during the loading of a ptm. 75 | -------------------------------------------------------------------------------- /ptmviewer.yml: -------------------------------------------------------------------------------- 1 | name: ptmviewer 2 | channels: 3 | - conda-forge 4 | - anaconda 5 | - defaults 6 | dependencies: 7 | - _libgcc_mutex=0.1=main 8 | - blas=1.0=mkl 9 | - bzip2=1.0.8=h516909a_0 10 | - ca-certificates=2019.5.15=1 11 | - certifi=2019.6.16=py37_1 12 | - freeglut=3.0.0=hf484d3e_5 13 | - intel-openmp=2019.4=243 14 | - libffi=3.2.1=he1b5a44_1006 15 | - libgcc-ng=9.1.0=hdf63c60_0 16 | - libgfortran-ng=7.3.0=hdf63c60_0 17 | - libstdcxx-ng=9.1.0=hdf63c60_0 18 | - mkl=2019.4=243 19 | - mkl-service=2.0.2=py37h7b6447c_0 20 | - mkl_fft=1.0.14=py37ha843d7b_0 21 | - mkl_random=1.0.2=py37hd81dba3_0 22 | - ncurses=6.1=hf484d3e_1002 23 | - numpy-base=1.16.4=py37hde5b4d6_0 24 | - openssl=1.1.1=h7b6447c_0 25 | - pip=19.2.2=py37_0 26 | - pyopengl=3.1.1a1=py37_0 27 | - pyopengl-accelerate=3.1.3b1=py37hdd07704_0 28 | - python=3.7.3=h33d41f4_1 29 | - readline=8.0=hf8c457e_0 30 | - setuptools=41.2.0=py37_0 31 | - six=1.12.0=py37_0 32 | - sqlite=3.29.0=hcee41ef_0 33 | - tk=8.6.9=hed695b0_1002 34 | - wheel=0.33.6=py37_0 35 | - xz=5.2.4=h14c3975_1001 36 | - zlib=1.2.11=h516909a_1005 37 | - pip: 38 | - attrs==19.1.0 39 | - backcall==0.1.0 40 | - bleach==3.1.0 41 | - decorator==4.4.0 42 | - defusedxml==0.6.0 43 | - entrypoints==0.3 44 | - ipykernel==5.1.2 45 | - ipython==7.7.0 46 | - ipython-genutils==0.2.0 47 | - ipywidgets==7.5.1 48 | - jedi==0.15.1 49 | - jinja2==2.10.1 50 | - jsonschema==3.0.2 51 | - jupyter==1.0.0 52 | - jupyter-client==5.3.1 53 | - jupyter-console==6.0.0 54 | - jupyter-core==4.5.0 55 | - markupsafe==1.1.1 56 | - mistune==0.8.4 57 | - nbconvert==5.6.0 58 | - nbformat==4.4.0 59 | - notebook==6.0.1 60 | - numpy==1.17.0 61 | - pandocfilters==1.4.2 62 | - parso==0.5.1 63 | - pexpect==4.7.0 64 | - pickleshare==0.7.5 65 | - pillow==6.1.0 66 | - prometheus-client==0.7.1 67 | - prompt-toolkit==2.0.9 68 | - ptmviewer==0.1 69 | - ptyprocess==0.6.0 70 | - pygments==2.4.2 71 | - pyrsistent==0.15.4 72 | - pyside2==5.11.0 73 | - pysideopengltutorials==0.1 74 | - python-dateutil==2.8.0 75 | - pyzmq==18.1.0 76 | - qtconsole==4.5.4 77 | - send2trash==1.5.0 78 | - shiboken2==5.12.0 79 | - terminado==0.8.2 80 | - testpath==0.4.2 81 | - tornado==6.0.3 82 | - traitlets==4.3.2 83 | - wcwidth==0.1.7 84 | - webencodings==0.5.1 85 | - widgetsnbextension==3.5.1 86 | prefix: /home/kaan/miniconda3/envs/ptmviewer 87 | 88 | -------------------------------------------------------------------------------- /ptmviewer-spec-file.txt: -------------------------------------------------------------------------------- 1 | # This file may be used to create an environment using: 2 | # $ conda create --name --file 3 | # platform: linux-64 4 | @EXPLICIT 5 | https://repo.anaconda.com/pkgs/main/linux-64/_libgcc_mutex-0.1-main.conda 6 | https://conda.anaconda.org/anaconda/linux-64/blas-1.0-mkl.tar.bz2 7 | https://conda.anaconda.org/anaconda/linux-64/ca-certificates-2019.5.15-1.tar.bz2 8 | https://conda.anaconda.org/anaconda/linux-64/intel-openmp-2019.4-243.tar.bz2 9 | https://conda.anaconda.org/anaconda/linux-64/libgfortran-ng-7.3.0-hdf63c60_0.tar.bz2 10 | https://repo.anaconda.com/pkgs/main/linux-64/libstdcxx-ng-9.1.0-hdf63c60_0.conda 11 | https://repo.anaconda.com/pkgs/main/linux-64/libgcc-ng-9.1.0-hdf63c60_0.conda 12 | https://conda.anaconda.org/anaconda/linux-64/mkl-2019.4-243.tar.bz2 13 | https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h516909a_0.tar.bz2 14 | https://conda.anaconda.org/anaconda/linux-64/freeglut-3.0.0-hf484d3e_5.tar.bz2 15 | https://conda.anaconda.org/conda-forge/linux-64/libffi-3.2.1-he1b5a44_1006.tar.bz2 16 | https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.1-hf484d3e_1002.tar.bz2 17 | https://conda.anaconda.org/anaconda/linux-64/openssl-1.1.1-h7b6447c_0.tar.bz2 18 | https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.4-h14c3975_1001.tar.bz2 19 | https://conda.anaconda.org/conda-forge/linux-64/zlib-1.2.11-h516909a_1005.tar.bz2 20 | https://conda.anaconda.org/conda-forge/linux-64/readline-8.0-hf8c457e_0.tar.bz2 21 | https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.9-hed695b0_1002.tar.bz2 22 | https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.29.0-hcee41ef_0.tar.bz2 23 | https://conda.anaconda.org/conda-forge/linux-64/python-3.7.3-h33d41f4_1.tar.bz2 24 | https://conda.anaconda.org/anaconda/linux-64/certifi-2019.6.16-py37_1.tar.bz2 25 | https://conda.anaconda.org/anaconda/linux-64/numpy-base-1.16.4-py37hde5b4d6_0.tar.bz2 26 | https://conda.anaconda.org/anaconda/linux-64/pyopengl-3.1.1a1-py37_0.tar.bz2 27 | https://conda.anaconda.org/anaconda/linux-64/six-1.12.0-py37_0.tar.bz2 28 | https://conda.anaconda.org/anaconda/linux-64/mkl_random-1.0.2-py37hd81dba3_0.tar.bz2 29 | https://conda.anaconda.org/conda-forge/linux-64/setuptools-41.2.0-py37_0.tar.bz2 30 | https://conda.anaconda.org/conda-forge/linux-64/wheel-0.33.6-py37_0.tar.bz2 31 | https://conda.anaconda.org/conda-forge/linux-64/pip-19.2.2-py37_0.tar.bz2 32 | https://conda.anaconda.org/anaconda/linux-64/mkl-service-2.0.2-py37h7b6447c_0.tar.bz2 33 | https://conda.anaconda.org/anaconda/linux-64/mkl_fft-1.0.14-py37ha843d7b_0.tar.bz2 34 | https://conda.anaconda.org/anaconda/linux-64/numpy-1.16.4-py37h7e9f1db_0.tar.bz2 35 | https://conda.anaconda.org/anaconda/linux-64/pyopengl-accelerate-3.1.3b1-py37hdd07704_0.tar.bz2 36 | -------------------------------------------------------------------------------- /ptmviewer/ptmparser.py: -------------------------------------------------------------------------------- 1 | # author: Kaan Eraslan 2 | # license: see, LICENSE 3 | # No warranties, see LICENSE 4 | import numpy as np 5 | 6 | 7 | class PTMFileParse: 8 | """ 9 | Parse .ptm files according to 10 | http://www.hpl.hp.com/research/ptm/downloads/PtmFormat12.pdf 11 | """ 12 | 13 | def __init__(self, ptmpath: str): 14 | self.path = ptmpath 15 | with open(self.path, "rb") as f: 16 | self.raw = f.readlines() 17 | self.raw = [raw_line for raw_line in self.raw if raw_line] 18 | self.header = self.raw[0].decode("utf-8").strip() 19 | self.format = self.raw[1].decode("utf-8").strip() 20 | if self.format != "PTM_FORMAT_RGB": 21 | raise ValueError( 22 | "ptm format {0} not supported".format(self.format) 23 | ) 24 | 25 | @property 26 | def image_width(self): 27 | return int(self.raw[2].decode("utf-8").strip()) 28 | 29 | @property 30 | def image_height(self): 31 | return int(self.raw[3].decode("utf-8").strip()) 32 | 33 | @property 34 | def scales(self): 35 | scales = self.raw[4].decode("utf-8").strip().split() 36 | return np.array([float(s) for s in scales], dtype=np.float32) 37 | 38 | @property 39 | def biases(self): 40 | biases = self.raw[5].decode("utf-8").strip().split() 41 | return np.array([int(b) for b in biases], dtype=np.int32) 42 | 43 | def get_coeffarr(self): 44 | "Get coefficients array from bytelist" 45 | if self.format == "PTM_FORMAT_RGB": 46 | bytelist = self.raw[6:] 47 | # bytelist = reversed(bytelist) 48 | bstr = b"".join(bytelist) 49 | bstr = bstr[::-1] # reverses the byte string due to format 50 | flatarr = np.frombuffer(bstr, dtype=np.uint8) 51 | flatarr = flatarr.reshape((-1, 6)) 52 | flatarr = self.get_final_coefficient(flatarr) 53 | else: 54 | raise ValueError( 55 | """ 56 | Working with an unsupported format {0}. 57 | Only uncompressed PTM_FORMAT_RGB is supported 58 | """.format( 59 | self.format 60 | ) 61 | ) 62 | return flatarr 63 | 64 | def get_final_coefficient(self, coeffarr: np.ndarray): 65 | """ 66 | Get final coefficient using: 67 | 68 | Coeff_final = (Coeff_raw - bias) * scale 69 | """ 70 | newcoeffarr = (coeffarr - self.biases) * self.scales 71 | return newcoeffarr 72 | 73 | def parse(self) -> dict: 74 | "Parse document and give output as dict" 75 | flatarr = self.get_coeffarr() 76 | coeffarr = self.get_final_coefficient(flatarr) 77 | out = {} 78 | out["coeffarr"] = coeffarr 79 | out["scales"] = self.scales 80 | out["biases"] = self.biases 81 | out["image_width"] = self.image_width 82 | out["image_height"] = self.image_height 83 | out["format"] = self.format 84 | return out 85 | -------------------------------------------------------------------------------- /ptmviewer/rgbptm.py: -------------------------------------------------------------------------------- 1 | # author: Kaan Eraslan 2 | # license: see, LICENSE 3 | # No warranties, see LICENSE 4 | 5 | import pdb 6 | import numpy as np 7 | 8 | from ptmviewer.ptmparser import PTMFileParse 9 | from ptmviewer.utils.utils import getInterpolationTable 10 | 11 | from ctypes import c_float, c_uint 12 | from PIL import Image 13 | 14 | 15 | class RGBPTM(PTMFileParse): 16 | "RGB ptm file handler" 17 | 18 | def __init__(self, ptmpath: str): 19 | super().__init__(ptmpath) 20 | self.ptmfile = self.parse() 21 | self.imheight = self.ptmfile["image_height"] 22 | self.imwidth = self.ptmfile["image_width"] 23 | self.coefficients = self.ptmfile["coeffarr"] 24 | self.imcoeffs = self.coefficients.reshape((3, self.imheight * self.imwidth, 6)) 25 | self.normal = None 26 | self.image = None 27 | 28 | def get_light_dirU_vec(self, coeffarr: np.ndarray): 29 | """ 30 | Get light direction U vector using formula: 31 | l_u0 = (a_2 * a_4 - 2 * a_1 * a_3) / (4 * a_0 * a_1 - a_2**2) 32 | """ 33 | nomin = (coeffarr[:, 2] * coeffarr[:, 4]) - ( 34 | 2 * coeffarr[:, 1] * coeffarr[:, 3] 35 | ) 36 | denomin = (4 * coeffarr[:, 0] * coeffarr[:, 1]) - (coeffarr[:, 2] ** 2) 37 | 38 | newcoeffarr = nomin / denomin 39 | return newcoeffarr 40 | 41 | def get_light_dirV_vec(self, coeffarr: np.ndarray): 42 | """ 43 | Get light direction U vector using formula: 44 | l_v0 = (a_2 * a_3 - 2 * a_0 * a_4) / (4 * a_0 * a_1 - a_2**2) 45 | """ 46 | nomin = (coeffarr[:, 2] * coeffarr[:, 3]) - ( 47 | 2 * coeffarr[:, 0] * coeffarr[:, 4] 48 | ) 49 | denomin = (4 * coeffarr[:, 0] * coeffarr[:, 1]) - (coeffarr[:, 2] ** 2) 50 | newcoeffarr = nomin / denomin 51 | return newcoeffarr 52 | 53 | def form_light_direction_mat(self, luvec, lvvec): 54 | "Form the light direction matrice" 55 | l_mat = np.array( 56 | [luvec ** 2, lvvec ** 2, lvvec * luvec, luvec, lvvec, np.ones_like(lvvec),], 57 | dtype=np.float, 58 | ) 59 | return l_mat.T 60 | 61 | def get_light_direction_matrix(self, coeffarr: np.ndarray): 62 | "Set light direction vector from coeffarr" 63 | luvec = self.get_light_dirU_vec(coeffarr) 64 | lvvec = self.get_light_dirV_vec(coeffarr) 65 | return self.form_light_direction_mat(luvec, lvvec) 66 | 67 | def get_channel_intensity(self, channel_coeffarr: np.ndarray): 68 | "Get channel intensity given channel coefficient array" 69 | arr = channel_coeffarr.reshape((-1, 6)) 70 | light_dir_mat = self.get_light_direction_matrix(arr) 71 | intensity = np.sum(arr * light_dir_mat, axis=1, dtype=np.float32) 72 | intensity = np.squeeze(intensity.reshape((self.imheight, self.imwidth, -1))) 73 | return intensity 74 | 75 | def form_surface_normal(self, luvec, lvvec): 76 | "Form surface normal matrice" 77 | normal = np.array( 78 | [luvec, lvvec, np.sqrt(1 - (luvec ** 2) - (lvvec ** 2))], dtype=np.float, 79 | ) 80 | return normal.T 81 | 82 | def get_surface_normal(self, coeffarr): 83 | """ 84 | Surface normal 85 | """ 86 | luvec = self.get_light_dirU_vec(coeffarr) 87 | lvvec = self.get_light_dirV_vec(coeffarr) 88 | return self.form_surface_normal(luvec, lvvec) 89 | 90 | def setSurfaceNormal(self) -> None: 91 | "Set surface normal values to ptm" 92 | coeffs = self.imcoeffs.reshape((3, -1, 6)) 93 | normals = np.empty((3, coeffs.shape[1], 3), dtype=np.float) 94 | normalR = self.get_surface_normal(coeffs[0, :, :]) 95 | normalG = self.get_surface_normal(coeffs[1, :, :]) 96 | normalB = self.get_surface_normal(coeffs[2, :, :]) 97 | normals[0, :, :] = normalR 98 | normals[1, :, :] = normalG 99 | normals[2, :, :] = normalB 100 | self.normal = normals 101 | return 102 | 103 | def interpolate_normal(self, normal: np.ndarray): 104 | "interpolate normals" 105 | normal[:, 0] = np.interp( 106 | normal[:, 0], (normal[:, 0].min(), normal[:, 0].max()), (0, 255) 107 | ) 108 | normal[:, 1] = np.interp( 109 | normal[:, 1], (normal[:, 1].min(), normal[:, 1].max()), (0, 255) 110 | ) 111 | normal[:, 2] = np.interp( 112 | normal[:, 2], (normal[:, 2].min(), normal[:, 2].max()), (0, 255) 113 | ) 114 | normalMap = normal.reshape((self.imheight, self.imwidth, 3)) 115 | nmap = normalMap.astype("uint8", copy=False) 116 | return nmap 117 | 118 | def getChannelNormalMap(self, channel: str): 119 | "get normal map for surface normals per channel" 120 | channel = channel.lower() 121 | if channel == "r" or channel == "red": 122 | normal = self.normal[0, :, :] 123 | elif channel == "g" or channel == "green": 124 | normal = self.normal[1, :, :] 125 | elif channel == "b" or channel == "blue": 126 | normal = self.normal[2, :, :] 127 | nshape = normal.shape 128 | nmap = self.interpolate_normal(normal) 129 | return Image.fromarray(nmap) 130 | 131 | def getNormalMaps(self): 132 | "get normal map" 133 | if not self.normal: 134 | self.setSurfaceNormal() 135 | nmapR = self.getChannelNormalMap("red") 136 | nmapG = self.getChannelNormalMap("green") 137 | nmapB = self.getChannelNormalMap("blue") 138 | return nmapR, nmapG, nmapB 139 | 140 | def setImage(self): 141 | "set image rgb values" 142 | image = np.empty((self.imheight, self.imwidth, 3), dtype=np.uint8) 143 | blue = self.get_channel_intensity(self.imcoeffs[2, :]) 144 | red = self.get_channel_intensity(self.imcoeffs[0, :]) 145 | green = self.get_channel_intensity(self.imcoeffs[1, :]) 146 | image[:, :, 0] = np.interp(red, (red.min(), red.max()), (0, 255)) 147 | image[:, :, 1] = np.interp(green, (green.min(), green.max()), (0, 255)) 148 | image[:, :, 2] = np.interp(blue, (blue.min(), blue.max()), (0, 255)) 149 | self.image = image 150 | return 151 | 152 | def getImage(self): 153 | if self.image: 154 | return Image.fromarray(self.image) 155 | self.setImage() 156 | return Image.fromarray(self.image) 157 | 158 | def getNbVertices(self): 159 | """ 160 | obtain vertices from coefficients suitable for opengl drawing 161 | 162 | The idea is to create the content of the vbo here, than pass it 163 | directly for rendering. 164 | The two for loops are heavy but necessary for stocking the 165 | coordinates, since they themselves play a role in rendering. 166 | 167 | We split the coefficients array into channels. 168 | Then reshape the resulting channel coefficients into the image shape 169 | The reason is that rgbptm shader takes in a position, and 18 170 | coefficients to come up with a fragment color. 171 | Due to our specification of the memory layout, the first three 172 | components of the VAO has to be position in 3d coordinates 173 | """ 174 | rowsize = self.imwidth * self.imheight 175 | warr = np.array(range(self.imwidth), dtype=c_float) 176 | wnorm = np.interp(warr, (warr.min(), warr.max()), (-1, 1)) 177 | harr = np.array(range(self.imheight), dtype=c_float) 178 | hnorm = np.interp(harr, (harr.min(), harr.max()), (-1, 1)) 179 | # 180 | vertices = np.empty((self.imheight, self.imwidth, 21), dtype=c_float) 181 | # add width arr to vertices 182 | vertices = vertices.reshape(self.imwidth, 21 * self.imheight, 1) 183 | vertices[:, 0, 0] = wnorm 184 | vertices = vertices.reshape(self.imheight, 21 * self.imwidth, 1) 185 | vertices[:, 1, 0] = hnorm 186 | vertices = vertices.reshape(rowsize, 21, 1) 187 | vertices[:, 2, 0] = 1.0 188 | vertices = vertices.reshape(rowsize, 21) 189 | indices = np.array([i for i in range(rowsize)], dtype=c_uint) 190 | 191 | rcoeff = self.imcoeffs[0, :, :] 192 | gcoeff = self.imcoeffs[1, :, :] 193 | bcoeff = self.imcoeffs[2, :, :] 194 | vertices[:, 3:9] = rcoeff 195 | vertices[:, 9:15] = gcoeff 196 | vertices[:, 15:] = bcoeff 197 | vnb = vertices.shape[0] 198 | return vertices.reshape(-1), vnb 199 | -------------------------------------------------------------------------------- /ptmviewer/utils/camera.py: -------------------------------------------------------------------------------- 1 | # author: Kaan Eraslan 2 | # camera 3 | 4 | import numpy as np 5 | import math 6 | from ptmviewer.utils.utils import computeLookAtPure 7 | from ptmviewer.utils.utils import normalize_tuple 8 | from ptmviewer.utils.utils import crossProduct 9 | from ptmviewer.utils.utils import scalar2vecMult 10 | from ptmviewer.utils.utils import vec2vecAdd 11 | from ptmviewer.utils.utils import vec2vecSubs 12 | from ptmviewer.utils.utils import move3dObjPure 13 | from ptmviewer.utils.utils import move3dObjQt 14 | from PySide2.QtGui import QVector3D 15 | from PySide2.QtGui import QMatrix4x4 16 | from PySide2.QtGui import QVector4D 17 | from ptmviewer.utils.obj3d import PureRigid3dObject 18 | from ptmviewer.utils.obj3d import QtRigid3dObject 19 | 20 | from abc import ABC, abstractmethod 21 | 22 | 23 | class AbstractCamera(ABC): 24 | "An abstract camera class" 25 | 26 | def __init__(self): 27 | self.zoom = 45.0 28 | 29 | @abstractmethod 30 | def lookAround(self, xoffset: float, yoffset: float, pitchBound: bool): 31 | raise NotImplementedError 32 | 33 | @abstractmethod 34 | def zoomInOut(self, yoffset: float, zoomBound=45.0): 35 | raise NotImplementedError 36 | 37 | @abstractmethod 38 | def getViewMatrix(self): 39 | raise NotImplementedError 40 | 41 | 42 | class PureCamera(PureRigid3dObject, AbstractCamera): 43 | "A camera that is in pure python for 3d movement" 44 | 45 | def __init__(self): 46 | "" 47 | super().__init__() 48 | 49 | # movement speed, sensitivity, moves, zoom 50 | self.zoom = 45.0 51 | 52 | # update camera vectors 53 | self.update_vectors() 54 | 55 | def lookAround(self, xoffset: float, yoffset: float, pitchBound: bool): 56 | "Look around with camera" 57 | xoffset *= self.movementSensitivity 58 | yoffset *= self.movementSensitivity 59 | self.yaw += xoffset 60 | self.pitch += yoffset 61 | 62 | if pitchBound: 63 | if self.pitch > 90.0: 64 | self.pitch = 90.0 65 | elif self.pitch < -90.0: 66 | self.pitch = -90.0 67 | # 68 | self.update_vectors() 69 | 70 | def zoomInOut(self, yoffset: float, zoomBound=45.0): 71 | "Zoom with camera" 72 | if self.zoom >= 1.0 and self.zoom <= zoomBound: 73 | self.zoom -= yoffset 74 | elif self.zoom <= 1.0: 75 | self.zoom = 1.0 76 | elif self.zoom >= zoomBound: 77 | self.zoom = zoomBound 78 | 79 | def setCameraWithVectors( 80 | self, 81 | position: dict, 82 | up: dict, 83 | front: dict, 84 | yaw: float, 85 | pitch: float, 86 | zoom: float, 87 | speed: float, 88 | sensitivity: float, 89 | ): 90 | "Set camera" 91 | self.check_coordinate_proc(position) 92 | self.check_coordinate_proc(up) 93 | self.check_coordinate_proc(front) 94 | self.position = position 95 | self.worldUp = up 96 | self.pitch = pitch 97 | self.yaw = yaw 98 | self.movementSpeed = speed 99 | self.movementSensitivity = sensitivity 100 | self.front = front 101 | self.zoom = zoom 102 | self.update_vectors() 103 | 104 | def setCameraWithFloatVals( 105 | self, 106 | posx: float, 107 | posy: float, 108 | posz: float, 109 | upx: float, 110 | upy: float, 111 | upz: float, 112 | yaw: float, 113 | pitch: float, 114 | speed: float, 115 | sensitivity: float, 116 | zoom: float, 117 | front: dict, 118 | ): 119 | "Set camera floats" 120 | self.check_coordinate_proc(front) 121 | self.position = {"x": posx, "y": posy, "z": posz} 122 | self.worldUp = {"x": upx, "y": upy, "z": upz} 123 | self.yaw = yaw 124 | self.pitch = pitch 125 | self.movementSpeed = speed 126 | self.movementSensitivity = sensitivity 127 | self.zoom = zoom 128 | self.front = front 129 | self.update_vectors() 130 | 131 | def getViewMatrix(self): 132 | "Obtain view matrix for camera" 133 | pos = (self.position["x"], self.position["y"], self.position["z"]) 134 | front = (self.front["x"], self.front["y"], self.front["z"]) 135 | wup = (self.worldUp["x"], self.worldUp["y"], self.worldUp["z"]) 136 | return computeLookAtPure(pos=pos, target=vec2vecAdd(pos, front), worldUp=wup) 137 | 138 | def __str__(self): 139 | "string representation" 140 | mess = "Camera: position {0},\n yaw: {1},\n pitch: {2},\n world up:{3}" 141 | mes = mess.format( 142 | str(self.position), str(self.yaw), str(self.pitch), str(self.worldUp) 143 | ) 144 | return mes 145 | 146 | 147 | class QtCamera(QtRigid3dObject, AbstractCamera): 148 | "An abstract camera for 3d movement in world" 149 | 150 | def __init__(self): 151 | "" 152 | super().__init__() 153 | # Camera attributes 154 | self.front = QVector3D(0.0, 0.0, -0.5) 155 | self.worldUp = QVector3D(0.0, 1.0, 0.0) 156 | 157 | # Euler Angles for rotation 158 | self.yaw = -90.0 159 | self.pitch = 0.0 160 | 161 | # camera options 162 | self.movementSpeed = 2.5 163 | self.movementSensitivity = 0.00001 164 | self.zoom = 45.0 165 | 166 | def lookAround(self, xoffset: float, yoffset: float, pitchBound: bool): 167 | "Look around with camera" 168 | xoffset *= self.movementSensitivity 169 | yoffset *= self.movementSensitivity 170 | self.yaw += xoffset 171 | self.pitch += yoffset 172 | 173 | if pitchBound: 174 | if self.pitch > 89.9: 175 | self.pitch = 89.9 176 | elif self.pitch < -89.9: 177 | self.pitch = -89.9 178 | # 179 | self.update_vectors() 180 | 181 | def zoomInOut(self, yoffset: float, zoomBound=45.0): 182 | "Zoom with camera" 183 | if self.zoom >= 1.0 and self.zoom <= zoomBound: 184 | self.zoom -= yoffset 185 | elif self.zoom <= 1.0: 186 | self.zoom = 1.0 187 | elif self.zoom >= zoomBound: 188 | self.zoom = zoomBound 189 | 190 | def getViewMatrix(self): 191 | "Obtain view matrix for camera" 192 | view = QMatrix4x4() 193 | view.lookAt(self.position, self.position + self.front, self.up) 194 | return view 195 | 196 | def setCameraWithVectors( 197 | self, 198 | position=QVector3D(0.0, 0.0, 0.0), 199 | worldUp=QVector3D(0.0, 1.0, 0.0), 200 | yaw=-90.0, 201 | pitch=0.0, 202 | zoom=45.0, 203 | speed=2.5, 204 | sensitivity=0.00001, 205 | ): 206 | "Set camera" 207 | self.check_coordinate_proc(position) 208 | self.check_coordinate_proc(worldUp) 209 | self.position = position 210 | self.worldUp = worldUp 211 | self.pitch = pitch 212 | self.yaw = yaw 213 | self.movementSpeed = speed 214 | self.movementSensitivity = sensitivity 215 | self.zoom = zoom 216 | self.update_vectors() 217 | 218 | def setCameraWithFloatVals( 219 | self, 220 | posx=0.0, 221 | posy=0.0, 222 | posz=0.0, 223 | upx=0.0, 224 | upy=1.0, 225 | upz=0.0, 226 | yaw=-90.0, 227 | pitch=0.0, 228 | zoom=45.0, 229 | speed=2.5, 230 | sensitivity=0.00001, 231 | ): 232 | "Set camera floats" 233 | self.position = QVector3D(posx, posy, posz) 234 | self.worldUp = QVector3D(upx, upy, upz) 235 | self.yaw = yaw 236 | self.pitch = pitch 237 | self.movementSpeed = speed 238 | self.movementSensitivity = sensitivity 239 | self.zoom = zoom 240 | self.update_vectors() 241 | 242 | def __str__(self): 243 | "string representation" 244 | mess = "Qt Camera: position {0}, yaw: {1}, pitch: {2}, world up:{3}" 245 | mes = mess.format( 246 | str(self.position), str(self.yaw), str(self.pitch), str(self.worldUp) 247 | ) 248 | return mes 249 | 250 | 251 | class FPSCameraQt(QtCamera): 252 | "FPS Camera based on qtcamera" 253 | 254 | def __init__(self): 255 | super().__init__() 256 | 257 | def move(self, direction: str, deltaTime: float): 258 | "Move camera in single axis" 259 | self.position = self.move2pos(direction, deltaTime) 260 | self.position.setY(0.0) # y val == 0 261 | -------------------------------------------------------------------------------- /ptmviewer/rgbptm_old.py: -------------------------------------------------------------------------------- 1 | # author: Kaan Eraslan 2 | # license: see, LICENSE 3 | # No warranties, see LICENSE 4 | import numpy as np 5 | 6 | from utils import normalize_1d_array 7 | from utils import ImageArray 8 | from utils import interpolateImage 9 | 10 | 11 | class RGBPTM: 12 | "Regroups methods on rgb ptm" 13 | 14 | def __init__(self, 15 | coeffarr: np.ndarray, 16 | image_height: int, 17 | image_width: int, 18 | scales: [float], 19 | biases: [int]): 20 | self.arr = np.copy(coeffarr) 21 | self.scales = scales 22 | self.biases = biases 23 | self.imheight = image_height 24 | self.imwidth = image_width 25 | 26 | def copy(self): 27 | "Copy self" 28 | return RGBPTM(coeffarr=np.copy(self.arr), 29 | image_height=self.imheight, 30 | image_width=self.imwidth, 31 | scales=self.scales, 32 | biases=self.biases) 33 | 34 | @property 35 | def imcoeffarr(self): 36 | "Image coefficients array" 37 | return self.arr.reshape((3, 38 | self.imheight * self.imwidth, 39 | 6)) 40 | 41 | @property 42 | def red_channel_coefficients(self): 43 | "Red channel coefficients array" 44 | return self.imcoeffarr[0, :, :] 45 | 46 | @property 47 | def green_channel_coefficients(self): 48 | "Green channel coefficients array" 49 | return self.imcoeffarr[1, :, :] 50 | 51 | @property 52 | def blue_channel_coefficients(self): 53 | "Blue channel coefficients array" 54 | return self.imcoeffarr[2, :, :] 55 | 56 | @property 57 | def red_light_dirU_vec(self): 58 | "Red light direction u vector" 59 | return self.get_light_dirU_vec(self.red_channel_coefficients) 60 | 61 | @property 62 | def red_light_dirV_vec(self): 63 | "Red light direction v vector" 64 | return self.get_light_dirV_vec(self.red_channel_coefficients) 65 | 66 | @property 67 | def red_channel_surface_normal(self): 68 | "Red channel surface normal" 69 | return self.form_surface_normal(self.red_light_dirU_vec, 70 | self.red_light_dirV_vec) 71 | 72 | @property 73 | def red_channel_pixel_values(self): 74 | "Get red channel pixel values" 75 | return self.get_channel_intensity(self.red_channel_coefficients) 76 | 77 | @property 78 | def red_channel_normalized_pixel_values(self): 79 | flat = self.red_channel_pixel_values.flatten() 80 | return normalize_1d_array(flat) 81 | 82 | @property 83 | def green_light_dirU_vec(self): 84 | "Green light direction u vector" 85 | return self.get_light_dirU_vec(self.green_channel_coefficients) 86 | 87 | @property 88 | def green_light_dirV_vec(self): 89 | "Green light direction v vector" 90 | return self.get_light_dirV_vec(self.green_channel_coefficients) 91 | 92 | @property 93 | def green_channel_surface_normal(self): 94 | "Green channel surface normal" 95 | return self.form_surface_normal(self.green_light_dirU_vec, 96 | self.green_light_dirV_vec) 97 | 98 | @property 99 | def green_channel_pixel_values(self): 100 | "Get green channel pixel values" 101 | return self.get_channel_intensity(self.green_channel_coefficients) 102 | 103 | @property 104 | def green_channel_normalized_pixel_values(self): 105 | flat = self.green_channel_pixel_values.flatten() 106 | return normalize_1d_array(flat) 107 | 108 | @property 109 | def blue_light_dirU_vec(self): 110 | "Blue light direction u vector" 111 | return self.get_light_dirU_vec(self.blue_channel_coefficients) 112 | 113 | @property 114 | def blue_light_dirV_vec(self): 115 | "Blue light direction v vector" 116 | return self.get_light_dirV_vec(self.blue_channel_coefficients) 117 | 118 | @property 119 | def blue_channel_surface_normal(self): 120 | "Green channel surface normal" 121 | return self.form_surface_normal(self.blue_light_dirU_vec, 122 | self.blue_light_dirV_vec) 123 | 124 | @property 125 | def blue_channel_pixel_values(self): 126 | "Get blue channel pixel values" 127 | return self.get_channel_intensity(self.blue_channel_coefficients) 128 | 129 | @property 130 | def blue_channel_normalized_pixel_values(self): 131 | flat = self.blue_channel_pixel_values.flatten() 132 | return normalize_1d_array(flat) 133 | 134 | @property 135 | def surface_normals(self): 136 | "Obtain surface normals" 137 | nshape = self.red_channel_surface_normal.shape 138 | normals = np.empty((3, *nshape), dtype=np.float32) 139 | normals[0, :] = self.red_channel_surface_normal 140 | normals[1, :] = self.green_channel_surface_normal 141 | normals[2, :] = self.blue_channel_surface_normal 142 | return normals 143 | 144 | @property 145 | def imarr(self): 146 | "Get image array" 147 | image = np.empty((self.imheight, self.imwidth, 3), dtype=np.float32) 148 | image[:, :, 0] = self.red_channel_pixel_values 149 | image[:, :, 1] = self.green_channel_pixel_values 150 | image[:, :, 2] = self.blue_channel_pixel_values 151 | # image = np.fliplr(image) # flip image in column wise 152 | imarr = ImageArray(image) 153 | imarr = interpolateImage(imarr) 154 | return imarr 155 | 156 | @property 157 | def image(self): 158 | "Get image" 159 | return np.fliplr(self.imarr.image) 160 | 161 | def get_light_dirU_vec(self, coeffarr: np.ndarray): 162 | """ 163 | Get light direction U vector using formula: 164 | l_u0 = (a_2 * a_4 - 2 * a_1 * a_3) / (4 * a_0 * a_1 - a_2**2) 165 | """ 166 | nomin = ( 167 | coeffarr[:, 2] * coeffarr[:, 4] 168 | ) - ( 169 | 2 * coeffarr[:, 1] * coeffarr[:, 3] 170 | ) 171 | denomin = ( 172 | 4 * coeffarr[:, 0] * coeffarr[:, 1] 173 | ) - (coeffarr[:, 2]**2) 174 | 175 | newcoeffarr = nomin / denomin 176 | return newcoeffarr 177 | 178 | def get_light_dirV_vec(self, coeffarr: np.ndarray): 179 | """ 180 | Get light direction U vector using formula: 181 | l_v0 = (a_2 * a_3 - 2 * a_0 * a_4) / (4 * a_0 * a_1 - a_2**2) 182 | """ 183 | nomin = ( 184 | coeffarr[:, 2] * coeffarr[:, 3] 185 | ) - ( 186 | 2 * coeffarr[:, 0] * coeffarr[:, 4] 187 | ) 188 | denomin = ( 189 | 4 * coeffarr[:, 0] * coeffarr[:, 1] 190 | ) - (coeffarr[:, 2]**2) 191 | newcoeffarr = nomin / denomin 192 | return newcoeffarr 193 | 194 | def form_light_direction_mat(self, luvec, lvvec): 195 | "Form the light direction matrice" 196 | l_mat = np.array([luvec**2, 197 | lvvec**2, 198 | lvvec * luvec, 199 | luvec, 200 | lvvec, 201 | np.ones_like(lvvec)], dtype=np.float) 202 | return l_mat.T 203 | 204 | def get_light_direction_matrix(self, coeffarr: np.ndarray): 205 | "Set light direction vector from coeffarr" 206 | luvec = self.get_light_dirU_vec(coeffarr) 207 | lvvec = self.get_light_dirV_vec(coeffarr) 208 | return self.form_light_direction_mat(luvec, lvvec) 209 | 210 | def get_channel_intensity(self, channel_coeffarr: np.ndarray): 211 | "Get channel intensity given channel coefficient array" 212 | arr = channel_coeffarr.reshape((-1, 6)) 213 | light_dir_mat = self.get_light_direction_matrix(arr) 214 | intensity = np.sum(arr * light_dir_mat, axis=1, dtype=np.float32) 215 | intensity = np.squeeze(intensity.reshape((self.imheight, self.imwidth, 216 | -1)) 217 | ) 218 | return intensity 219 | 220 | def form_surface_normal(self, luvec, 221 | lvvec): 222 | "Form surface normal matrice" 223 | normal = np.array( 224 | [luvec, 225 | lvvec, 226 | np.sqrt(1 - luvec**2 - lvvec**2) 227 | ], 228 | dtype=np.float) 229 | return np.transpose(normal, (1, 0)) 230 | 231 | def get_surface_normal(self, coeffarr): 232 | """ 233 | Surface normal 234 | """ 235 | luvec = self.get_light_dirU_vec(coeffarr) 236 | lvvec = self.get_light_dirV_vec(coeffarr) 237 | return self.form_surface_normal(luvec, lvvec) 238 | -------------------------------------------------------------------------------- /ptmviewer/utils/utils.py: -------------------------------------------------------------------------------- 1 | # author: Kaan Eraslan 2 | # license: see, LICENSE 3 | # No warranties, see LICENSE 4 | import numpy as np 5 | from PySide2.QtGui import QVector3D 6 | from PySide2.QtGui import QVector4D 7 | from PySide2.QtGui import QMatrix4x4 8 | 9 | from typing import Tuple, List, Dict 10 | import math 11 | 12 | 13 | def normalize_1d_array(arr): 14 | "Normalize 1d array" 15 | assert arr.ndim == 1 16 | result = None 17 | if np.linalg.norm(arr) == 0: 18 | result = arr 19 | else: 20 | result = arr / np.linalg.norm(arr) 21 | return result 22 | 23 | 24 | def normalize_3col_array(arr): 25 | "Normalize 3 column array" 26 | assert arr.shape[1] == 3 27 | assert arr.ndim == 2 28 | normal = np.copy(arr) 29 | normal[:, 0] = normalize_1d_array(normal[:, 0]) 30 | normal[:, 1] = normalize_1d_array(normal[:, 1]) 31 | normal[:, 2] = normalize_1d_array(normal[:, 2]) 32 | return normal 33 | 34 | 35 | def get_vector_dot(arr1, arr2): 36 | "Get vector dot product for 2 matrices" 37 | if arr1.shape != arr2.shape: 38 | raise ValueError("arr1 and arr2 shape should be same") 39 | newarr = np.sum(arr1 * arr2, axis=1, dtype=np.float32) 40 | return newarr 41 | 42 | 43 | def get_matrix_to_vector_dot(mat: np.ndarray, vec: np.ndarray): 44 | "Get vector dot for each segment of matrix" 45 | mshape = mat[0, :].shape 46 | if mshape != vec.shape: 47 | raise ValueError("Matrix vector shape should be same with vector shape") 48 | d1 = get_vector_dot(mat[0, :], vec) 49 | d2 = get_vector_dot(mat[1, :], vec) 50 | d3 = get_vector_dot(mat[2, :], vec) 51 | newmat = np.empty(mshape) 52 | newmat[:, 0] = d1 53 | newmat[:, 1] = d2 54 | newmat[:, 2] = d3 55 | return newmat 56 | 57 | 58 | def factor_3colmat_with_vec(mat: np.ndarray, vec: np.ndarray): 59 | "Factor matrix columns with vector" 60 | assert vec.ndim == 1 61 | assert mat.shape[1] == 3 62 | assert vec.shape[0] == mat.shape[0] 63 | vec = np.where(vec != 0, vec, 0.00001) # against zero divisions 64 | newmat = np.empty_like(mat) 65 | newmat[:, 0] = mat[:, 0] / vec 66 | newmat[:, 1] = mat[:, 1] / vec 67 | newmat[:, 2] = mat[:, 2] / vec 68 | return newmat 69 | 70 | 71 | def getDistancePoint2Array(apoint, coordarr): 72 | "Get distance between point1 and point2" 73 | yarr = coordarr[:, 0] # row nb 74 | xarr = coordarr[:, 1] # col nb 75 | xdist = (apoint.x - xarr) ** 2 76 | ydist = (apoint.y - yarr) ** 2 77 | return np.sqrt(xdist + ydist) 78 | 79 | 80 | def getInterpolationTable(arr: np.ndarray, mapRange: Tuple[float, float]) -> dict: 81 | "Interpolate given one dimensional array into given range output as a table" 82 | assert arr.ndim == 1 83 | newarr = np.interp(arr, (arr.min(), arr.max()), mapRange) 84 | return {arr[i]: newarr[i] for i in range(arr.size)} 85 | 86 | 87 | class ImageArray: 88 | "Image array have some additional properties besides np.ndarray" 89 | 90 | def __init__(self, image: np.ndarray): 91 | assert isinstance(image, np.ndarray) 92 | self.image = image 93 | 94 | @property 95 | def norm_coordinates(self): 96 | "Get normalized coordinates of the image pixels" 97 | # pdb.set_trace() 98 | rownb, colnb = self.image.shape[0], self.image.shape[1] 99 | norm = np.empty_like(self.coordinates, dtype=np.float32) 100 | norm[:, 0] = self.coordinates[:, 0] / rownb 101 | norm[:, 1] = self.coordinates[:, 1] / colnb 102 | return norm 103 | 104 | @property 105 | def norm_image(self): 106 | "Get normalized image with pixel values divided by 255" 107 | return self.image / 255.0 108 | 109 | @property 110 | def coordinates(self): 111 | "Coordinates of the image pixels" 112 | rownb, colnb = self.image.shape[:2] 113 | coords = [[(row, col) for col in range(colnb)] for row in range(rownb)] 114 | coordarray = np.array(coords) 115 | return coordarray.reshape((-1, 2)) 116 | 117 | @property 118 | def arrshape(self): 119 | "get array shape" 120 | return self.image.shape 121 | 122 | @property 123 | def flatarr(self): 124 | "get flattened array" 125 | return self.image.flatten() 126 | 127 | 128 | def interpolateImage(imarr: ImageArray): 129 | "Interpolate image array" 130 | imshape = imarr.image.shape 131 | newimage = imarr.image.flatten() 132 | newimage = np.uint8(np.interp(newimage, (newimage.min(), newimage.max()), (0, 255))) 133 | newimage = newimage.reshape(imshape) 134 | return ImageArray(newimage) 135 | 136 | 137 | def normalize_tuple(vec: tuple): 138 | "Normalize 1 d tuple" 139 | vecSum = sum([v ** 2 for v in vec]) 140 | if vecSum == 0: 141 | return vec 142 | else: 143 | return tuple([v / vecSum for v in vec]) 144 | 145 | 146 | def crossProduct(vec1, vec2): 147 | "take cross products of two vectors" 148 | assert len(vec1) == 3 and len(vec2) == 3 149 | vec3x = vec1[1] * vec2[2] - vec1[2] * vec2[1] 150 | vec3y = vec1[2] * vec2[0] - vec1[0] * vec2[2] 151 | vec3z = vec1[0] * vec2[1] - vec1[1] * vec2[0] 152 | return (vec3x, vec3y, vec3z) 153 | 154 | 155 | def vec2vecDot(vec1, vec2): 156 | "vector to vector dot product" 157 | assert len(vec1) == len(vec2) 158 | return tuple(sum(v1 * v2 for v1, v2 in zip(vec1, vec2))) 159 | 160 | 161 | def sliceCol(colInd: int, matrix: list): 162 | "slice column values from matrix" 163 | rownb = len(matrix) 164 | return [matrix[i, colInd] for i in range(rownb)] 165 | 166 | 167 | def set_column_mat(pos: int, arr: list, mat: list) -> list: 168 | "set column to matrix at given position" 169 | if len(mat) != len(arr): 170 | raise ValueError("Col height not equal to size of array to insert") 171 | for index, row in enumerate(mat): 172 | row[pos] = arr[index] 173 | return mat 174 | 175 | 176 | def mat2matDot(mat1: list, mat2: list) -> list: 177 | "Dot product in pure python" 178 | if len(mat1[0]) != len(mat2): 179 | raise ValueError("mat1 row size is not equal mat2 column size") 180 | colnb = len(mat1[0]) 181 | mat = [] 182 | for rown in range(len(mat1)): 183 | newmatRow = [] 184 | mat1Row = mat1[rown] 185 | for coln in range(colnb): 186 | mat2col = sliceCol(coln, mat2) 187 | newmatRow.append(vec2vecDot(mat1Row, mat2col)) 188 | mat.append(newmatRow) 189 | return mat 190 | 191 | 192 | def mat2vecDot(mat: list, vec: list) -> list: 193 | "dot product in pure python matrix to vector" 194 | if len(mat[0]) != len(vec): 195 | raise ValueError("Matrix vector shape should be same with vector shape") 196 | newmat = [[0 for i in range(len(mat[0]))] for k in range(len(mat))] 197 | for i, row in enumerate(mat): 198 | newrow = vec2vecDot(row, vec) 199 | newmat = set_column_mat(pos=i, arr=newrow, mat=newmat) 200 | return newmat 201 | 202 | 203 | def scalar2vecMult(vec, scalar): 204 | "scalar multiplication of a vector" 205 | return tuple([v * scalar for v in vec]) 206 | 207 | 208 | def vec2vecAdd(vec1, vec2): 209 | "vector to vector addition" 210 | assert len(vec1) == len(vec2) 211 | return tuple([vec1[i] + vec2[i] for i in range(len(vec1))]) 212 | 213 | 214 | def vec2vecSubs(vec1, vec2): 215 | "vector to vector subtraction" 216 | assert len(vec1) == len(vec2) 217 | return tuple([vec1[i] - vec2[i] for i in range(len(vec1))]) 218 | 219 | 220 | def computePerspectiveNp(fieldOfView: float, aspect: float, zNear: float, zFar: float): 221 | "Reproduces glm perspective function" 222 | assert aspect != 0 223 | assert zNear != zFar 224 | fieldOfViewRad = np.radians(fieldOfView) 225 | fieldHalfTan = np.tan(fieldOfViewRad / 2) 226 | # mat4 227 | result = np.zeros((4, 4), dtype=float) 228 | result[0, 0] = 1 / (aspect * fieldHalfTan) 229 | result[1, 1] = 1 / fieldHalfTan 230 | result[2, 2] = -(zFar + zNear) / (zFar - zNear) 231 | result[3, 2] = -1 232 | result[2, 3] = -(2 * zFar * zNear) / (zFar - zNear) 233 | return result 234 | 235 | 236 | def computePerspectiveQt(fieldOfView: float, aspect: float, zNear: float, zFar: float): 237 | "matrice" 238 | mat = QMatrix4x4(*[0.0 for i in range(16)]) 239 | return mat.perspective(fieldOfView, aspect, zNear, zFar) 240 | 241 | 242 | def computeLookAtPure(pos: tuple, target: tuple, worldUp: tuple) -> list: 243 | "" 244 | assert len(pos) == 3 and len(target) == 3 245 | assert len(worldUp) == 3 246 | zaxis = normalize_tuple(vec2vecSubs(pos, target)) 247 | 248 | # x axis 249 | normWorld = normalize_tuple(worldUp) 250 | xaxis = normalize_tuple(crossProduct(normWorld, zaxis)) 251 | yaxis = crossProduct(zaxis, xaxis) 252 | translation = [[1 for i in range(4)] for k in range(4)] 253 | translation[0][3] = -pos[0] 254 | translation[1][3] = -pos[1] # third col, second row 255 | translation[2][3] = -pos[2] 256 | 257 | rotation = [[1 for i in range(4)] for k in range(4)] 258 | rotation[0][0] = xaxis[0] 259 | rotation[0][1] = xaxis[1] 260 | rotation[0][2] = xaxis[2] 261 | rotation[1][0] = yaxis[0] 262 | rotation[1][1] = yaxis[1] 263 | rotation[1][2] = yaxis[2] 264 | rotation[2][0] = zaxis[0] 265 | rotation[2][1] = zaxis[1] 266 | rotation[2][2] = zaxis[2] 267 | return mat2matDot(translation, rotation) 268 | 269 | 270 | def computeLookAtMatrixNp( 271 | position: np.ndarray, target: np.ndarray, worldUp: np.ndarray 272 | ): 273 | "Compute a look at matrix for given position and target" 274 | assert position.ndim == 1 and target.ndim == 1 and worldUp.ndim == 1 275 | zaxis = normalize_1d_array(position - target) 276 | 277 | # positive xaxis at right 278 | xaxis = normalize_1d_array(np.cross(normalize_1d_array(worldUp), zaxis)) 279 | # camera up 280 | yaxis = np.cross(zaxis, xaxis) 281 | 282 | # compute translation matrix 283 | translation = np.ones((4, 4), dtype=np.float) 284 | translation[0, 3] = -position[0] # third col, first row 285 | translation[1, 3] = -position[1] # third col, second row 286 | translation[2, 3] = -position[2] 287 | 288 | # compute rotation matrix 289 | rotation = np.ones((4, 4), dtype=np.float) 290 | rotation[0, 0] = xaxis[0] 291 | rotation[0, 1] = xaxis[1] 292 | rotation[0, 2] = xaxis[2] 293 | rotation[1, 0] = yaxis[0] 294 | rotation[1, 1] = yaxis[1] 295 | rotation[1, 2] = yaxis[2] 296 | rotation[2, 0] = zaxis[0] 297 | rotation[2, 1] = zaxis[1] 298 | rotation[2, 2] = zaxis[2] 299 | 300 | return np.dot(translation, rotation) 301 | 302 | 303 | def computeLookAtMatrixQt(position: np.ndarray, target: np.ndarray, up: np.ndarray): 304 | "look at matrice" 305 | eye = QVector3D(position[0], position[1], position[2]) 306 | target = QVector3D(target[0], target[1], target[2]) 307 | upvec = QVector3D(up[0], up[1], up[2]) 308 | mat4 = QMatrix4x4() 309 | return mat4.lookAt(eye, target, upvec) 310 | 311 | 312 | def arr2vec(arr: np.ndarray): 313 | "convert array 2 vector" 314 | sqarr = np.squeeze(arr) 315 | assert sqarr.size == 4 316 | return QVector4D(sqarr[0], sqarr[1], sqarr[2], sqarr[3]) 317 | 318 | 319 | def arr2qmat(arr: np.ndarray): 320 | "array to matrix 4x4" 321 | assert arr.shape == (4, 4) 322 | mat4 = QMatrix4x4() 323 | for rowNb in range(arr.shape[0]): 324 | rowarr = arr[rowNb, :] 325 | rowvec = arr2vec(rowarr) 326 | mat4.setRow(rowNb, rowvec) 327 | # 328 | return mat4 329 | 330 | 331 | def move3dObjPure( 332 | direction: str, 333 | positionVector: Tuple[float, float, float], 334 | xaxis: Tuple[float, float, float], 335 | yaxis: Tuple[float, float, float], 336 | zaxis: Tuple[float, float, float], 337 | deltaTime: float, 338 | speed: float, 339 | availableMoves=["+z", "-z", "-x", "+x", "+y", "-y"], 340 | ): 341 | "" 342 | velocity = speed * deltaTime 343 | direction = direction.lower() 344 | if direction not in availableMoves: 345 | raise ValueError( 346 | "Unknown direction {0}, available moves are {1}".format( 347 | direction, availableMoves 348 | ) 349 | ) 350 | if direction == "+z": 351 | multip = scalar2vecMult(zaxis, velocity) 352 | positionVector = vec2vecAdd(positionVector, multip) 353 | elif direction == "-z": 354 | multip = scalar2vecMult(zaxis, velocity) 355 | positionVector = vec2vecSubs(positionVector, multip) 356 | elif direction == "+x": 357 | multip = scalar2vecMult(xaxis, velocity) 358 | positionVector = vec2vecAdd(positionVector, multip) 359 | elif direction == "-x": 360 | multip = scalar2vecMult(xaxis, velocity) 361 | positionVector = vec2vecSubs(positionVector, multip) 362 | elif direction == "+y": 363 | multip = scalar2vecMult(yaxis, velocity) 364 | positionVector = vec2vecAdd(positionVector, multip) 365 | elif direction == "-y": 366 | multip = scalar2vecMult(yaxis, velocity) 367 | positionVector = vec2vecSubs(positionVector, multip) 368 | 369 | return positionVector 370 | 371 | 372 | def move3dObjQt( 373 | direction: str, 374 | positionVector: QVector3D, 375 | xaxis: QVector3D, 376 | yaxis: QVector3D, 377 | zaxis: QVector3D, 378 | deltaTime: float, 379 | speed: float, 380 | availableMoves=["+x", "-x", "+y", "-y", "+z", "-z"], 381 | ): 382 | "" 383 | velocity = speed * deltaTime 384 | direction = direction.lower() 385 | if direction not in availableMoves: 386 | raise ValueError( 387 | "Unknown direction {0}, available moves are {1}".format( 388 | direction, availableMoves 389 | ) 390 | ) 391 | if direction == "+x": 392 | positionVector += xaxis * velocity 393 | elif direction == "-x": 394 | positionVector -= xaxis * velocity 395 | elif direction == "+y": 396 | positionVector += yaxis * velocity 397 | elif direction == "-y": 398 | positionVector -= yaxis * velocity 399 | elif direction == "+z": 400 | positionVector += zaxis * velocity 401 | elif direction == "-z": 402 | positionVector -= zaxis * velocity 403 | return positionVector 404 | 405 | 406 | def computeFrontRightPure(yaw: float, pitch: float, worldUp=(0.0, 1.0, 0.0)): 407 | "Compute front vector" 408 | yawRadian = math.radians(yaw) 409 | yawCos = math.cos(yawRadian) 410 | pitchRadian = math.radians(pitch) 411 | pitchCos = math.cos(pitchRadian) 412 | frontX = yawCos * pitchCos 413 | frontY = math.sin(pitchRadian) 414 | frontZ = math.sin(yawRadian) * pitchCos 415 | front = (frontX, frontY, frontZ) 416 | front = normalize_tuple(front) 417 | right = crossProduct(front, worldUp) 418 | right = normalize_tuple(right) 419 | up = crossProduct(right, front) 420 | up = normalize_tuple(up) 421 | return (front, right, up) 422 | 423 | 424 | def computeFrontRightQt(yaw: float, pitch: float, worldUp=QVector3D(0.0, 1.0, 0.0)): 425 | "" 426 | yawRadian = math.radians(yaw) 427 | yawCos = math.cos(yawRadian) 428 | pitchRadian = math.radians(pitch) 429 | pitchCos = math.cos(pitchRadian) 430 | frontX = yawCos * pitchCos 431 | frontY = math.sin(pitchRadian) 432 | frontZ = math.sin(yawRadian) * pitchCos 433 | front = QVector3D(frontX, frontY, frontZ) 434 | front.normalize() 435 | right = QVector3D.crossProduct(front, worldUp) 436 | right.normalize() 437 | up = QVector3D.crossProduct(right, front) 438 | up.normalize() 439 | return (front, right, up) 440 | -------------------------------------------------------------------------------- /ptmviewer/utils/obj3d.py: -------------------------------------------------------------------------------- 1 | """ 2 | A module for objects expressed 3d world 3 | """ 4 | 5 | import math 6 | from ptmviewer.utils.utils import mat2matDot 7 | from ptmviewer.utils.utils import mat2vecDot 8 | from ptmviewer.utils.utils import set_column_mat 9 | from ptmviewer.utils.utils import normalize_tuple 10 | from ptmviewer.utils.utils import vec2vecAdd 11 | from ptmviewer.utils.utils import vec2vecSubs 12 | from ptmviewer.utils.utils import scalar2vecMult 13 | from ptmviewer.utils.utils import crossProduct 14 | from ptmviewer.utils.utils import move3dObjPure 15 | from typing import Tuple, List, Dict 16 | from PySide2.QtGui import QVector3D 17 | from PySide2.QtGui import QMatrix4x4 18 | from PySide2.QtGui import QVector4D 19 | 20 | from abc import ABC, abstractmethod 21 | 22 | 23 | class AbstractRigid3dObject(ABC): 24 | "Abstract rigid 3d object" 25 | 26 | def __init__(self): 27 | self.position = {"x": 0.0, "y": 0.0, "z": 0.0} 28 | self.front = None 29 | self.worldUp = {"x": 0.0, "y": 1.0, "z": 0.0} 30 | # up with respect to center of the world 31 | self.up = None # with respect to the objects current position 32 | self.right = None # with respect to the objects current position 33 | # identity matrix for movement and rotation 34 | self.idmat = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]] 35 | self.modelmat = [[], [], [], []] 36 | 37 | # Euler angles for rotation 38 | self.yaw = -90.0 # rotation over z axis 39 | self.pitch = 0.0 # rotation over y axis 40 | self.roll = 0.0 # rotation over x axis 41 | 42 | # movement 43 | self.movementSensitivity = 0.001 44 | self.movementSpeed = 2.5 45 | self.availableMoves = ["+z", "-z", "+x", "-x", "+y", "-y"] 46 | 47 | @classmethod 48 | @abstractmethod 49 | def make_rotation_matrix(cls, axis: str, angle: float): 50 | raise NotImplementedError 51 | 52 | @property 53 | def z_axis_rotation_matrix(self): 54 | "rotation matrix over z axis" 55 | return self.make_rotation_matrix(axis="z", angle=self.yaw) 56 | 57 | @property 58 | def y_axis_rotation_matrix(self): 59 | "rotation matrix over y axis" 60 | return self.make_rotation_matrix(axis="y", angle=self.pitch) 61 | 62 | @property 63 | def x_axis_rotation_matrix(self): 64 | "rotation matrix over x axis" 65 | return self.make_rotation_matrix(axis="x", angle=self.roll) 66 | 67 | @property 68 | @abstractmethod 69 | def rotation_matrix(self): 70 | raise NotImplementedError 71 | 72 | @abstractmethod 73 | def compute_rotation(self, rotmat): 74 | raise NotImplementedError 75 | 76 | def rotate(self): 77 | "rotate object using rotation matrix" 78 | newpos = self.rotated_position() 79 | self.set_position(newpos) 80 | 81 | def rotated_position(self): 82 | return self.compute_rotation(self.rotation_matrix) 83 | 84 | def rotate_around(self, axis: str, angle: float): 85 | "rotate around given axis" 86 | ax = axis.lower() 87 | if ax == "x": 88 | rotation_matrix = self.make_rotation_matrix(axis="x", angle=angle) 89 | elif ax == "y": 90 | rotation_matrix = self.make_rotation_matrix(axis="y", angle=angle) 91 | elif ax == "z": 92 | rotation_matrix = self.make_rotation_matrix(axis="z", angle=angle) 93 | else: 94 | raise ValueError("unknown axis: " + axis + ". x, y, z are allowed.") 95 | newpos = self.compute_rotation(rotation_matrix) 96 | self.set_position(newpos) 97 | 98 | @abstractmethod 99 | def get_model_matrix(self): 100 | "get model matrix for object matrix 4x4" 101 | raise NotImplementedError 102 | 103 | @abstractmethod 104 | def update_vectors(self): 105 | raise NotImplementedError 106 | 107 | @abstractmethod 108 | def move(self, direction: str, deltaTime: float): 109 | raise NotImplementedError 110 | 111 | @abstractmethod 112 | def set_position(self, pos): 113 | raise NotImplementedError 114 | 115 | @abstractmethod 116 | def set_world_up(self, pos): 117 | raise NotImplementedError 118 | 119 | def check_angle(self, angle: float, angle_name: str): 120 | "check angle if it is in correct type and value" 121 | if isinstance(angle, float) is False: 122 | raise TypeError(angle_name + " value must be float: " + str(type(angle))) 123 | if angle < -360.0: 124 | raise ValueError( 125 | angle_name + " value can not be lower than -360: " + str(angle) 126 | ) 127 | if angle > 360.0: 128 | raise ValueError( 129 | angle_name + " value can not be higher than 360: " + str(angle) 130 | ) 131 | 132 | def set_yaw(self, val: float): 133 | "Set yaw value" 134 | self.check_angle(angle=val, angle_name="yaw") 135 | self.yaw = val 136 | self.update_vectors() 137 | 138 | def set_pitch(self, val: float): 139 | "Set pitch value" 140 | self.check_angle(angle=val, angle_name="pitch") 141 | self.pitch = val 142 | self.update_vectors() 143 | 144 | def set_roll(self, val: float): 145 | "Set roll value" 146 | self.check_angle(angle=val, angle_name="roll") 147 | self.roll = val 148 | self.update_vectors() 149 | 150 | def __str__(self): 151 | mess = "3d object at {0}, with yaw {1}, pitch {2}, " 152 | mess += "and roll {3}" 153 | return mess.format(self.position, self.yaw, self.pitch, self.roll) 154 | 155 | 156 | class PureRigid3dObject(AbstractRigid3dObject): 157 | "Basic rigid body in 3d world" 158 | 159 | def __init__(self): 160 | self.position = { 161 | "x": 0.0, 162 | "y": 0.0, 163 | "z": 0.0, 164 | } # object at the center of the world 165 | self.front = None 166 | self.worldUp = {"x": 0.0, "y": 1.0, "z": 0.0} 167 | # up with respect to center of the world 168 | self.up = None # with respect to the objects current position 169 | self.right = None # with respect to the objects current position 170 | # 171 | # Euler angles for rotation 172 | self.yaw = -90.0 # rotation over z axis 173 | self.pitch = 0.0 # rotation over y axis 174 | self.roll = 0.0 # rotation over x axis 175 | 176 | # movement 177 | self.movementSensitivity = 0.001 178 | self.movementSpeed = 2.5 179 | self.availableMoves = ["+z", "-z", "+x", "-x", "+y", "-y"] 180 | 181 | @classmethod 182 | def make_rotation_matrix(cls, axis: str, angle: float): 183 | "make a rotation matrix using angle and axis" 184 | ax = axis.lower() 185 | if ax == "x": 186 | return [ 187 | [1, 0, 0, 0], 188 | [0, math.cos(angle), -math.sin(angle), 0], 189 | [0, math.sin(angle), math.cos(angle), 0], 190 | [0, 0, 0, 1], 191 | ] 192 | elif ax == "y": 193 | return [ 194 | [math.cos(angle), 0, math.sin(angle), 0], 195 | [0, 1, 0, 0], 196 | [-math.sin(angle), 0, math.cos(angle), 0], 197 | [0, 0, 0, 1], 198 | ] 199 | elif ax == "z": 200 | return [ 201 | [math.cos(angle), -math.sin(angle), 0, 0], 202 | [math.sin(angle), math.cos(angle), 0, 0], 203 | [0, 0, 1, 0], 204 | [0, 0, 0, 1], 205 | ] 206 | else: 207 | raise ValueError("Unknown axis: " + axis + ". x, y, z available") 208 | 209 | @property 210 | def rotation_matrix(self): 211 | "rotation matrix" 212 | mat2 = mat2matDot( 213 | mat1=self.y_axis_rotation_matrix, mat2=self.x_axis_rotation_matrix 214 | ) 215 | return mat2matDot(mat1=self.z_axis_rotation_matrix, mat2=mat2) 216 | 217 | def compute_rotation(self, rotmat: List[List[float]]): 218 | "compute rotation for current coordinates" 219 | ps = self.position["x"], self.position["y"], self.position["z"] 220 | pos = list(ps) 221 | newpos = mat2vecDot(rotmat, pos) 222 | return {"x": newpos[0], "y": newpos[1], "z": newpos[2]} 223 | 224 | def get_model_matrix(self) -> list: 225 | "get model matrix" 226 | homcoord = self.position["x"], self.position["y"], self.position["z"], 1 227 | homocoord = list(homcoord) 228 | idmat = self.idmat.copy() 229 | idmat = set_column_mat(-1, homocoord, idmat) 230 | return idmat 231 | 232 | def update_vectors(self): 233 | "update front, up, right vectors" 234 | yawRadian = math.radians(self.yaw) 235 | yawCos = math.cos(yawRadian) 236 | pitchRadian = math.radians(self.pitch) 237 | pitchCos = math.cos(pitchRadian) 238 | frontX = yawCos * pitchCos 239 | frontY = math.sin(pitchRadian) 240 | frontZ = math.sin(yawRadian) * pitchCos 241 | self.front = (frontX, frontY, frontZ) 242 | self.front = normalize_tuple(self.front) 243 | self.right = crossProduct(self.front, self.worldUp) 244 | self.right = normalize_tuple(self.right) 245 | self.up = crossProduct(self.right, self.front) 246 | self.up = normalize_tuple(self.up) 247 | self.front = {"x": self.front[0], "y": self.front[1], "z": self.front[2]} 248 | self.right = {"x": self.right[0], "y": self.right[1], "z": self.right[2]} 249 | self.up = {"x": self.up[0], "y": self.up[1], "z": self.up[2]} 250 | 251 | @staticmethod 252 | def translateVec( 253 | axisVector: dict, positionVector: dict, velocity: float, isPlus=True 254 | ) -> dict: 255 | "Translate vector towards axis vector with velocity and position vector" 256 | atpl = tuple(axisVector.values()) 257 | ptpl = tuple(positionVector.values()) 258 | multip = scalar2vecMult(atpl, velocity) 259 | if isPlus: 260 | newPosTpl = vec2vecAdd(ptpl, multip) 261 | else: 262 | newPosTpl = vec2vecSubs(ptpl, multip) 263 | return {"x": newPosTpl[0], "y": newPosTpl[1], "z": newPosTpl[2]} 264 | 265 | def move2pos(self, direction: str, deltaTime: float): 266 | "move object to given direction" 267 | velocity = self.movementSpeed * deltaTime 268 | direction = direction.lower() 269 | if direction not in self.availableMoves: 270 | raise ValueError( 271 | "Unknown direction {0}, available moves are {1}".format( 272 | direction, self.availableMoves 273 | ) 274 | ) 275 | if direction == "+z": 276 | position = self.translateVec( 277 | axisVector=self.front, 278 | positionVector=self.position, 279 | velocity=velocity, 280 | isPlus=True, 281 | ) 282 | elif direction == "-z": 283 | position = self.translateVec( 284 | axisVector=self.front, 285 | positionVector=self.position, 286 | velocity=velocity, 287 | isPlus=False, 288 | ) 289 | elif direction == "+x": 290 | position = self.translateVec( 291 | axisVector=self.right, 292 | positionVector=self.position, 293 | velocity=velocity, 294 | isPlus=True, 295 | ) 296 | elif direction == "-x": 297 | position = self.translateVec( 298 | axisVector=self.right, 299 | positionVector=self.position, 300 | velocity=velocity, 301 | isPlus=False, 302 | ) 303 | elif direction == "+y": 304 | position = self.translateVec( 305 | axisVector=self.up, 306 | positionVector=self.position, 307 | velocity=velocity, 308 | isPlus=True, 309 | ) 310 | elif direction == "-y": 311 | position = self.translateVec( 312 | axisVector=self.up, 313 | positionVector=self.position, 314 | velocity=velocity, 315 | isPlus=False, 316 | ) 317 | 318 | return position 319 | 320 | def move(self, direction: str, deltaTime: float): 321 | "move object to its new position" 322 | self.position = self.move2pos(direction, deltaTime) 323 | self.update_vectors() 324 | 325 | def check_coordinate_proc(self, pos: dict): 326 | "check coordinate type and value" 327 | if not isinstance(pos, dict): 328 | raise TypeError("Given coordinates are not in type dict: " + str(type(pos))) 329 | pkeys = list(pos.keys()) 330 | if not all([True for k in pkeys if k in ["x", "y", "z"]]): 331 | mess = "Given coordinates do not have x, y, z." 332 | mess += " It has: " + str(pkeys) 333 | raise ValueError(mess) 334 | pvals = list(pos.values()) 335 | if not all([isinstance(v, float) for v in pvals]): 336 | raise TypeError("Given coordinates do not have proper type float") 337 | 338 | def set_world_up(self, wup: dict): 339 | "world up" 340 | self.check_coordinate_proc(wup) 341 | self.worldUp = wup 342 | self.update_vectors() 343 | 344 | def set_position(self, pos: dict): 345 | "set position" 346 | self.check_coordinate_proc(pos) 347 | self.position = pos 348 | self.update_vectors() 349 | 350 | 351 | class QtRigid3dObject(AbstractRigid3dObject): 352 | "Rigid 3d object with qt constructs" 353 | 354 | def __init__(self): 355 | super().__init__() 356 | self.position = QVector3D(0.0, 0.0, 0.0) 357 | self.front = None 358 | self.worldUp = QVector3D(0.0, 1.0, 0.0) 359 | self.up = None 360 | self.right = None 361 | self.idmat = QMatrix4x4() 362 | self.idmat.setToIdentity() 363 | 364 | @classmethod 365 | def rotate_matrix_on_axis(cls, axis: str, angle: float, mat: QMatrix4x4): 366 | "rotate matrix on given axis using angle" 367 | pass 368 | 369 | # overriding property 370 | @classmethod 371 | def make_rotation_matrix(cls, axis: str, angle: float): 372 | "make rotation matrix with given axis" 373 | ax = axis.lower() 374 | mat = QMatrix4x4() 375 | mat.setToIdentity() 376 | if ax == "x": 377 | mat.rotate(angle, QVector3D(1.0, 0.0, 0.0)) 378 | elif ax == "y": 379 | mat.rotate(angle, QVector3D(0.0, 1.0, 0.0)) 380 | elif ax == "z": 381 | mat.rotate(angle, QVector3D(0.0, 0.0, 1.0)) 382 | else: 383 | raise ValueError("Unknown axis: " + axis + ". x, y, z available") 384 | return mat 385 | 386 | @property 387 | def rotation_matrix(self): 388 | "rotation matrix" 389 | mat2 = self.y_axis_rotation_matrix * self.x_axis_rotation_matrix 390 | return self.z_axis_rotation_matrix * mat2 391 | 392 | def compute_rotation(self, rotmat: QMatrix4x4): 393 | "compute rotation" 394 | return rotmat * self.position 395 | 396 | def get_model_matrix(self): 397 | modelmat = QMatrix4x4() 398 | modelmat.setToIdentity() 399 | modelmat.translate(self.position) 400 | modelmat.rotate(self.roll, QVector3D(1.0, 0.0, 0.0)) 401 | modelmat.rotate(self.pitch, QVector3D(0.0, 1.0, 0.0)) 402 | modelmat.rotate(self.yaw, QVector3D(0.0, 0.0, 1.0)) 403 | return modelmat 404 | 405 | def update_vectors(self): 406 | "override base class" 407 | yawRadian = math.radians(self.yaw) 408 | yawCos = math.cos(yawRadian) 409 | pitchRadian = math.radians(self.pitch) 410 | pitchCos = math.cos(pitchRadian) 411 | frontX = yawCos * pitchCos 412 | frontY = math.sin(pitchRadian) 413 | frontZ = math.sin(yawRadian) * pitchCos 414 | self.front = QVector3D(frontX, frontY, frontZ) 415 | self.front.normalize() 416 | self.right = QVector3D.crossProduct(self.front, self.worldUp) 417 | self.right.normalize() 418 | self.up = QVector3D.crossProduct(self.right, self.front) 419 | self.up.normalize() 420 | 421 | def move2pos(self, direction: str, deltaTime: float): 422 | "compute new position in given direction" 423 | velocity = self.movementSpeed * deltaTime 424 | direction = direction.lower() 425 | pos = QVector3D() 426 | pos.setX(self.position.x()) 427 | pos.setY(self.position.y()) 428 | pos.setZ(self.position.z()) 429 | if direction not in self.availableMoves: 430 | raise ValueError( 431 | "Unknown direction {0}, available moves are {1}".format( 432 | direction, self.availableMoves 433 | ) 434 | ) 435 | if direction == "+z": 436 | pos += self.front * velocity 437 | elif direction == "-z": 438 | pos -= self.front * velocity 439 | elif direction == "+x": 440 | pos += self.right * velocity 441 | elif direction == "-x": 442 | pos -= self.right * velocity 443 | elif direction == "+y": 444 | pos += self.up * velocity 445 | elif direction == "-y": 446 | pos -= self.up * velocity 447 | return pos 448 | 449 | def move(self, direction: str, deltaTime: float): 450 | "move object to its new position" 451 | self.position = self.move2pos(direction, deltaTime) 452 | self.update_vectors() 453 | 454 | def check_coordinate_proc(self, pos: QVector3D): 455 | "check coordinate type and value" 456 | if not isinstance(pos, QVector3D): 457 | raise TypeError( 458 | "Given coordinates are not in type QVector3D: " + str(type(pos)) 459 | ) 460 | pvals = [pos.x(), pos.y(), pos.z()] 461 | if not all([isinstance(v, float) for v in pvals]): 462 | raise TypeError("Given coordinates do not have proper type float") 463 | 464 | def set_world_up(self, wup: QVector3D): 465 | "world up" 466 | self.check_coordinate_proc(wup) 467 | self.worldUp = wup 468 | self.update_vectors() 469 | 470 | def set_position(self, pos: QVector3D): 471 | "set position" 472 | self.check_coordinate_proc(pos) 473 | self.position = pos 474 | self.update_vectors() 475 | -------------------------------------------------------------------------------- /ptmviewer/interface/interface.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1278 10 | 901 11 | 12 | 13 | 14 | MainWindow 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | GL Viewer 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 0 42 | 0 43 | 1278 44 | 23 45 | 46 | 47 | 48 | 49 | 50 | 51 | 2 52 | 53 | 54 | 55 | 56 | 57 | 58 | File List 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | Load 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | Add File(s) 78 | 79 | 80 | 81 | 82 | 83 | 84 | Remove File(s) 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 2 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | Light Control 107 | 108 | 109 | 110 | 111 | 112 | 113 | Light Sources 114 | 115 | 116 | 117 | 118 | 119 | Ambient 120 | 121 | 122 | 123 | 124 | 125 | 126 | Diffuse 127 | 128 | 129 | true 130 | 131 | 132 | 133 | 134 | 135 | 136 | Specular 137 | 138 | 139 | false 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | Channel 154 | 155 | 156 | 157 | 158 | 159 | 160 | Intensity 161 | 162 | 163 | 164 | 165 | 166 | 167 | Coefficient 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | red 179 | 180 | 181 | 182 | 183 | 184 | 185 | 1.000000000000000 186 | 187 | 188 | 0.010000000000000 189 | 190 | 191 | 1.000000000000000 192 | 193 | 194 | 195 | 196 | 197 | 198 | 1.000000000000000 199 | 200 | 201 | 0.010000000000000 202 | 203 | 204 | 1.000000000000000 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | green 216 | 217 | 218 | 219 | 220 | 221 | 222 | 1.000000000000000 223 | 224 | 225 | 0.010000000000000 226 | 227 | 228 | 1.000000000000000 229 | 230 | 231 | 232 | 233 | 234 | 235 | 1.000000000000000 236 | 237 | 238 | 0.010000000000000 239 | 240 | 241 | 1.000000000000000 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | blue 253 | 254 | 255 | 256 | 257 | 258 | 259 | 1.000000000000000 260 | 261 | 262 | 0.010000000000000 263 | 264 | 265 | 1.000000000000000 266 | 267 | 268 | 269 | 270 | 271 | 272 | 1.000000000000000 273 | 274 | 275 | 0.010000000000000 276 | 277 | 278 | 1.000000000000000 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 2 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | Rotate 305 | 306 | 307 | 308 | 309 | 310 | Rotate Camera 311 | 312 | 313 | 314 | 315 | 316 | 317 | Rotate Light 318 | 319 | 320 | true 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | X axis 333 | 334 | 335 | 336 | 337 | 338 | 339 | Y axis 340 | 341 | 342 | 343 | 344 | 345 | 346 | Z axis 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | Angle 356 | 357 | 358 | 359 | 360 | 361 | 362 | -360.000000000000000 363 | 364 | 365 | 360.000000000000000 366 | 367 | 368 | 1.000000000000000 369 | 370 | 371 | 1.000000000000000 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 2 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | Move 398 | 399 | 400 | 401 | 402 | 403 | Move Light 404 | 405 | 406 | 407 | 408 | 409 | 410 | Move Camera 411 | 412 | 413 | true 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | X axis 430 | 431 | 432 | 433 | 434 | 435 | 436 | Y axis 437 | 438 | 439 | 440 | 441 | 442 | 443 | Z axis 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | + 455 | 456 | 457 | true 458 | 459 | 460 | 461 | 462 | 463 | 464 | - 465 | 466 | 467 | true 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 1 483 | 484 | 485 | 486 | 487 | 488 | 489 | Parameters 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | capture 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 1 509 | 510 | 511 | 512 | 513 | 514 | 515 | Transcribe 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | save 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 1 535 | 536 | 537 | 538 | 539 | 540 | 541 | Open GL Info 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 1 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | Shader 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | Shininess 583 | 584 | 585 | 586 | 587 | 588 | 589 | 1.000000000000000 590 | 591 | 592 | 300.000000000000000 593 | 594 | 595 | 0.100000000000000 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | -------------------------------------------------------------------------------- /ptmviewer/utils/window.py: -------------------------------------------------------------------------------- 1 | # window for showing widgets 2 | from PySide2 import QtWidgets, QtCore 3 | from PySide2.QtWidgets import QLabel 4 | from PySide2.QtWidgets import QVBoxLayout 5 | from PySide2.QtWidgets import QHBoxLayout 6 | from PySide2.QtWidgets import QWidget 7 | from PySide2.QtWidgets import QPushButton 8 | 9 | 10 | def createSlider(): 11 | slider = QtWidgets.QSlider(QtCore.Qt.Vertical) 12 | 13 | slider.setRange(0, 360 * 16) 14 | slider.setSingleStep(16) 15 | slider.setPageStep(15 * 16) 16 | slider.setTickInterval(15 * 16) 17 | slider.setTickPosition(QtWidgets.QSlider.TicksRight) 18 | return slider 19 | 20 | 21 | def createDSpinBox(smin: float, smax: float, 22 | step: float, setval=1.0): 23 | spinbox = QtWidgets.QDoubleSpinBox() 24 | spinbox.setRange(smin, smax) 25 | spinbox.setSingleStep(step) 26 | spinbox.setValue(setval) 27 | return spinbox 28 | 29 | 30 | class GLWindow(QtWidgets.QMainWindow): 31 | "Application window" 32 | 33 | def __init__(self, 34 | glwidget: QtWidgets.QOpenGLWidget, 35 | parent=None, 36 | ): 37 | super().__init__(parent) 38 | # OpenGL Widget 39 | self.glLayout = QVBoxLayout() 40 | self.glLabel = QLabel("OpenGL Widget") 41 | self.glWidget = glwidget() 42 | self.glLayout.addWidget(self.glLabel) 43 | self.glLayout.addWidget(self.glWidget) 44 | self.glLayout.setStretchFactor(self.glWidget, 1) 45 | self.glSection = QWidget() 46 | self.glSection.setLayout(self.glLayout) 47 | # Cube Controls 48 | self.labelx = QLabel("x") 49 | self.labely = QLabel("y") 50 | self.labelz = QLabel("z") 51 | self.xSlider = createSlider() 52 | self.ySlider = createSlider() 53 | self.zSlider = createSlider() 54 | # 55 | sliderLayoutV1 = QVBoxLayout() 56 | sliderLayoutV1.addWidget(self.labelx) 57 | sliderLayoutV1.addWidget(self.xSlider) 58 | # 59 | sliderLayoutV2 = QVBoxLayout() 60 | sliderLayoutV2.addWidget(self.labely) 61 | sliderLayoutV2.addWidget(self.ySlider) 62 | # 63 | sliderLayoutV3 = QVBoxLayout() 64 | sliderLayoutV3.addWidget(self.labelz) 65 | sliderLayoutV3.addWidget(self.zSlider) 66 | # 67 | sliderSection = QVBoxLayout() 68 | slidersLayout = QHBoxLayout() 69 | slidersTitle = QLabel('Rotate cubes') 70 | slidersLayout.addLayout(sliderLayoutV1) 71 | slidersLayout.addLayout(sliderLayoutV2) 72 | slidersLayout.addLayout(sliderLayoutV3) 73 | sliderSection.addWidget(slidersTitle) 74 | sliderSection.addLayout(slidersLayout) 75 | slidersWidget = QWidget() 76 | slidersWidget.setLayout(sliderSection) 77 | # Camera Controls 78 | # Rotate camera 79 | self.camX = createSlider() 80 | self.camY = createSlider() 81 | self.camX.setRange(-180, 180) 82 | self.camY.setRange(-180, 180) 83 | self.camX.setValue(0) 84 | self.camY.setValue(0) 85 | self.camlabelx = QLabel("x") 86 | self.camlabely = QLabel("y") 87 | self.camlabel = QLabel("Rotate camera") 88 | camV1 = QVBoxLayout() 89 | camV2 = QVBoxLayout() 90 | camV1.addWidget(self.camlabelx) 91 | camV1.addWidget(self.camX) 92 | camV2.addWidget(self.camlabely) 93 | camV2.addWidget(self.camY) 94 | cams = QHBoxLayout() 95 | cams.addLayout(camV1) 96 | cams.addLayout(camV2) 97 | camsWidget = QWidget() 98 | camsWidget.setLayout(cams) 99 | # cube and cam sliders 100 | 101 | camSection = QVBoxLayout() 102 | camSection.addWidget(self.camlabel) 103 | camSection.addWidget(camsWidget) 104 | camSection.setStretchFactor(camsWidget, 1) 105 | camSecWidget = QWidget() 106 | camSecWidget.setLayout(camSection) 107 | # 108 | camsCubes = QHBoxLayout() 109 | camsCubes.addWidget(slidersWidget) 110 | camsCubes.addWidget(camSecWidget) 111 | # 112 | buttonsTitel = QLabel("Move camera") 113 | buttonsLayoutH1 = QHBoxLayout() 114 | buttonsLayoutH2 = QHBoxLayout() 115 | buttonsLayoutV = QVBoxLayout() 116 | self.leftBtn = QtWidgets.QPushButton() 117 | self.rightBtn = QtWidgets.QPushButton() 118 | self.upBtn = QtWidgets.QPushButton() 119 | self.downBtn = QtWidgets.QPushButton() 120 | buttonsLayoutH1.addWidget(self.upBtn) 121 | buttonsLayoutH2.addWidget(self.leftBtn) 122 | buttonsLayoutH2.addWidget(self.downBtn) 123 | buttonsLayoutH2.addWidget(self.rightBtn) 124 | buttonsLayoutV.addWidget(buttonsTitel) 125 | buttonsLayoutV.addLayout(buttonsLayoutH1) 126 | buttonsLayoutV.addLayout(buttonsLayoutH2) 127 | buttonsLayoutV.addLayout(camsCubes) 128 | buttonsWidget = QWidget() 129 | buttonsWidget.setLayout(buttonsLayoutV) 130 | # 131 | self.leftBtn.setText("<") 132 | self.rightBtn.setText(">") 133 | self.upBtn.setText("^") 134 | self.downBtn.setText("v") 135 | self.leftBtn.setAutoRepeat(True) 136 | self.leftBtn.setAutoRepeatInterval(15) # repeat at x milisecond 137 | self.leftBtn.setAutoRepeatDelay(100) # start repeating after 138 | # x milisecond 139 | self.rightBtn.setAutoRepeat(True) 140 | self.rightBtn.setAutoRepeatDelay(100) 141 | self.rightBtn.setAutoRepeatInterval(15) 142 | # 143 | self.downBtn.setAutoRepeat(True) 144 | self.downBtn.setAutoRepeatDelay(100) 145 | self.downBtn.setAutoRepeatInterval(15) 146 | # 147 | self.upBtn.setAutoRepeat(True) 148 | self.upBtn.setAutoRepeatDelay(100) 149 | self.upBtn.setAutoRepeatInterval(15) 150 | # 151 | # Light controls 152 | # 153 | lightSection = QWidget() 154 | lightSectionLayout = QVBoxLayout() 155 | lightSectionLabel = QLabel("Light Controls") 156 | lightSectionWidgetsLayout = QVBoxLayout() 157 | # 158 | slidersLayout = QHBoxLayout() 159 | # light position 160 | self.lightPosWidget = QWidget() 161 | self.lightPosTitle = QLabel("light position") 162 | self.lightPosLayout = QVBoxLayout() 163 | lightPosWidgetsLayout = QHBoxLayout() 164 | # 165 | # light pos buttons 166 | self.lightPosForward = QPushButton() 167 | self.lightPosForward.setText("Forward") 168 | self.lightPosForward.setAutoRepeat(True) 169 | self.lightPosForward.setAutoRepeatInterval(15) 170 | self.lightPosForward.setAutoRepeatDelay(100) 171 | self.lightPosBackward = QPushButton() 172 | self.lightPosBackward.setText("Back") 173 | self.lightPosBackward.setAutoRepeat(True) 174 | self.lightPosBackward.setAutoRepeatInterval(15) 175 | self.lightPosBackward.setAutoRepeatDelay(100) 176 | self.lightPosRight = QPushButton() 177 | self.lightPosRight.setText("Right") 178 | self.lightPosRight.setAutoRepeat(True) 179 | self.lightPosRight.setAutoRepeatInterval(15) 180 | self.lightPosRight.setAutoRepeatDelay(100) 181 | self.lightPosLeft = QPushButton() 182 | self.lightPosLeft.setText("Left") 183 | self.lightPosLeft.setAutoRepeat(True) 184 | self.lightPosLeft.setAutoRepeatDelay(100) 185 | self.lightPosLeft.setAutoRepeatInterval(15) 186 | self.lightPosUp = QPushButton() 187 | self.lightPosUp.setText("Up") 188 | self.lightPosUp.setAutoRepeat(True) 189 | self.lightPosUp.setAutoRepeatDelay(100) 190 | self.lightPosUp.setAutoRepeatInterval(15) 191 | self.lightPosDown = QPushButton() 192 | self.lightPosDown.setText("Down") 193 | self.lightPosDown.setAutoRepeat(True) 194 | self.lightPosDown.setAutoRepeatDelay(100) 195 | self.lightPosDown.setAutoRepeatInterval(15) 196 | btnLV = QVBoxLayout() 197 | btnLV2 = QVBoxLayout() 198 | btnLH = QHBoxLayout() 199 | btnLV2.addWidget(self.lightPosForward) 200 | btnLV2.addWidget(self.lightPosBackward) 201 | btnLV.addWidget(self.lightPosUp) 202 | btnLV.addWidget(self.lightPosDown) 203 | btnLH.addWidget(self.lightPosLeft) 204 | btnLH.addLayout(btnLV2) 205 | btnLH.addLayout(btnLV) 206 | btnLH.addWidget(self.lightPosRight) 207 | # 208 | lightPosWidgetsLayout.addLayout(btnLH) 209 | self.lightPosLayout.addWidget(self.lightPosTitle) 210 | self.lightPosLayout.addLayout(lightPosWidgetsLayout) 211 | self.lightPosWidget.setLayout(self.lightPosLayout) 212 | slidersLayout.addWidget(self.lightPosWidget) 213 | # end light position 214 | # light rotation 215 | self.lightRotWidget = QWidget() 216 | self.lightRotTitle = QLabel("light rotation") 217 | self.lightRotLayout = QVBoxLayout() 218 | lightRotWidgetsLayout = QHBoxLayout() 219 | # 220 | self.lightRotXSlider = createSlider() 221 | self.lightRotXSlider.setRange(-180, 180) 222 | self.lightRotXSlider.setValue(0) 223 | self.lightRotXTitle = QLabel("x") 224 | RotxLayout = QVBoxLayout() 225 | RotxLayout.addWidget(self.lightRotXTitle) 226 | RotxLayout.addWidget(self.lightRotXSlider) 227 | # 228 | self.lightRotYSlider = createSlider() 229 | self.lightRotYSlider.setRange(-180, 180) 230 | self.lightRotYSlider.setValue(0) 231 | self.lightRotYTitle = QLabel("y") 232 | RotyLayout = QVBoxLayout() 233 | RotyLayout.addWidget(self.lightRotYTitle) 234 | RotyLayout.addWidget(self.lightRotYSlider) 235 | # 236 | self.lightRotZSlider = createSlider() 237 | self.lightRotZSlider.setRange(-180, 180) 238 | self.lightRotZSlider.setValue(0) 239 | self.lightRotZTitle = QLabel("z") 240 | RotzLayout = QVBoxLayout() 241 | RotzLayout.addWidget(self.lightRotZTitle) 242 | RotzLayout.addWidget(self.lightRotZSlider) 243 | # 244 | lightRotWidgetsLayout.addLayout(RotxLayout) 245 | lightRotWidgetsLayout.addLayout(RotyLayout) 246 | lightRotWidgetsLayout.addLayout(RotzLayout) 247 | # 248 | self.lightRotLayout.addWidget(self.lightRotTitle) 249 | self.lightRotLayout.addLayout(lightRotWidgetsLayout) 250 | self.lightRotWidget.setLayout(self.lightRotLayout) 251 | slidersLayout.addWidget(self.lightRotWidget) 252 | # end light rotation 253 | # 254 | spinsLayout = QVBoxLayout() 255 | # 256 | lightTypesLayout = QVBoxLayout() 257 | adsLayout = QHBoxLayout() 258 | # ambient light 259 | ambientWidget = QWidget() 260 | ambientLayout = QVBoxLayout() 261 | ambientTitle = QLabel("ambient") 262 | ambientWidgetsLayout = QVBoxLayout() 263 | # ambient red 264 | self.ambientIntensityRed = createDSpinBox(0.0, 1.0, 0.01, 1.0) 265 | self.ambientIntensityRedCoeff = createDSpinBox(0.0, 1.0, 0.05, 0.3) 266 | intenCoeff = QHBoxLayout() 267 | intenCoeff.addWidget(self.ambientIntensityRed) 268 | intenCoeff.addWidget(self.ambientIntensityRedCoeff) 269 | self.ambientRedLabel = QLabel("Red") 270 | channelLayout = QVBoxLayout() 271 | channelLayout.addWidget(self.ambientRedLabel) 272 | channelLayout.addLayout(intenCoeff) 273 | ambientWidgetsLayout.addLayout(channelLayout) 274 | # end ambient red 275 | # ambient green 276 | self.ambientIntensityGreen = createDSpinBox(0.0, 1.0, 0.01, 1.0) 277 | self.ambientIntensityGreenCoeff = createDSpinBox(0.0, 1.0, 0.05, 0.3) 278 | intenCoeff = QHBoxLayout() 279 | intenCoeff.addWidget(self.ambientIntensityGreen) 280 | intenCoeff.addWidget(self.ambientIntensityGreenCoeff) 281 | self.ambientGreenLabel = QLabel("Green") 282 | channelLayout = QVBoxLayout() 283 | channelLayout.addWidget(self.ambientGreenLabel) 284 | channelLayout.addLayout(intenCoeff) 285 | ambientWidgetsLayout.addLayout(channelLayout) 286 | # end ambient green 287 | # ambient blue 288 | self.ambientIntensityBlue = createDSpinBox(0.0, 1.0, 0.01, 1.0) 289 | self.ambientIntensityBlueCoeff = createDSpinBox(0.0, 1.0, 0.05, 0.3) 290 | intenCoeff = QHBoxLayout() 291 | intenCoeff.addWidget(self.ambientIntensityBlue) 292 | intenCoeff.addWidget(self.ambientIntensityBlueCoeff) 293 | self.ambientBlueLabel = QLabel("Blue") 294 | channelLayout = QVBoxLayout() 295 | channelLayout.addWidget(self.ambientBlueLabel) 296 | channelLayout.addLayout(intenCoeff) 297 | # end ambient blue 298 | ambientWidgetsLayout.addLayout(channelLayout) 299 | ambientLayout.addWidget(ambientTitle) 300 | ambientLayout.addLayout(ambientWidgetsLayout) 301 | ambientWidget.setLayout(ambientLayout) 302 | # 303 | adsLayout.addWidget(ambientWidget) 304 | # end ambient light 305 | # diffuse light 306 | diffuseWidget = QWidget() 307 | diffuseLayout = QVBoxLayout() 308 | diffuseTitle = QLabel("diffuse") 309 | diffuseWidgetsLayout = QVBoxLayout() 310 | # diffuse red 311 | self.diffuseIntensityRed = createDSpinBox(0.0, 1.0, 0.01, 1.0) 312 | self.diffuseIntensityRedCoeff = createDSpinBox(0.0, 1.0, 0.05) 313 | intenCoeff = QHBoxLayout() 314 | intenCoeff.addWidget(self.diffuseIntensityRed) 315 | intenCoeff.addWidget(self.diffuseIntensityRedCoeff) 316 | self.diffuseRedLabel = QLabel("Red") 317 | channelLayout = QVBoxLayout() 318 | channelLayout.addWidget(self.diffuseRedLabel) 319 | channelLayout.addLayout(intenCoeff) 320 | diffuseWidgetsLayout.addLayout(channelLayout) 321 | # end diffuse red 322 | # diffuse green 323 | self.diffuseIntensityGreen = createDSpinBox(0.0, 1.0, 0.01, 1.0) 324 | self.diffuseIntensityGreenCoeff = createDSpinBox(0.0, 1.0, 0.05, 0.5) 325 | intenCoeff = QHBoxLayout() 326 | intenCoeff.addWidget(self.diffuseIntensityGreen) 327 | intenCoeff.addWidget(self.diffuseIntensityGreenCoeff) 328 | self.diffuseGreenLabel = QLabel("Green") 329 | channelLayout = QVBoxLayout() 330 | channelLayout.addWidget(self.diffuseGreenLabel) 331 | channelLayout.addLayout(intenCoeff) 332 | diffuseWidgetsLayout.addLayout(channelLayout) 333 | # end diffuse green 334 | # diffuse blue 335 | self.diffuseIntensityBlue = createDSpinBox(0.0, 1.0, 0.01, 1.0) 336 | self.diffuseIntensityBlueCoeff = createDSpinBox(0.0, 1.0, 0.05, 0.5) 337 | intenCoeff = QHBoxLayout() 338 | intenCoeff.addWidget(self.diffuseIntensityBlue) 339 | intenCoeff.addWidget(self.diffuseIntensityBlueCoeff) 340 | self.diffuseBlueLabel = QLabel("Blue") 341 | channelLayout = QVBoxLayout() 342 | channelLayout.addWidget(self.diffuseBlueLabel) 343 | channelLayout.addLayout(intenCoeff) 344 | # end diffuse blue 345 | diffuseWidgetsLayout.addLayout(channelLayout) 346 | diffuseLayout.addWidget(diffuseTitle) 347 | diffuseLayout.addLayout(diffuseWidgetsLayout) 348 | diffuseWidget.setLayout(diffuseLayout) 349 | # 350 | adsLayout.addWidget(diffuseWidget) 351 | # end diffuse light 352 | # specular light 353 | specularWidget = QWidget() 354 | specularLayout = QVBoxLayout() 355 | specularTitle = QLabel("specular") 356 | specularWidgetsLayout = QVBoxLayout() 357 | # specular red 358 | self.specularIntensityRed = createDSpinBox(0.0, 1.0, 0.01, 1.0) 359 | self.specularIntensityRedCoeff = createDSpinBox(0.0, 1.0, 0.05, 1.0) 360 | intenCoeff = QHBoxLayout() 361 | intenCoeff.addWidget(self.specularIntensityRed) 362 | intenCoeff.addWidget(self.specularIntensityRedCoeff) 363 | self.specularRedLabel = QLabel("Red") 364 | channelLayout = QVBoxLayout() 365 | channelLayout.addWidget(self.specularRedLabel) 366 | channelLayout.addLayout(intenCoeff) 367 | specularWidgetsLayout.addLayout(channelLayout) 368 | # end specular red 369 | # specular green 370 | self.specularIntensityGreen = createDSpinBox(0.0, 1.0, 0.01, 1.0) 371 | self.specularIntensityGreenCoeff = createDSpinBox(0.0, 1.0, 0.05, 1.0) 372 | intenCoeff = QHBoxLayout() 373 | intenCoeff.addWidget(self.specularIntensityGreen) 374 | intenCoeff.addWidget(self.specularIntensityGreenCoeff) 375 | self.specularGreenLabel = QLabel("Green") 376 | channelLayout = QVBoxLayout() 377 | channelLayout.addWidget(self.specularGreenLabel) 378 | channelLayout.addLayout(intenCoeff) 379 | specularWidgetsLayout.addLayout(channelLayout) 380 | # end specular green 381 | # specular blue 382 | self.specularIntensityBlue = createDSpinBox(0.0, 1.0, 0.01, 1.0) 383 | self.specularIntensityBlueCoeff = createDSpinBox(0.0, 1.0, 0.05, 1.0) 384 | intenCoeff = QHBoxLayout() 385 | intenCoeff.addWidget(self.specularIntensityBlue) 386 | intenCoeff.addWidget(self.specularIntensityBlueCoeff) 387 | self.specularBlueLabel = QLabel("Blue") 388 | channelLayout = QVBoxLayout() 389 | channelLayout.addWidget(self.specularBlueLabel) 390 | channelLayout.addLayout(intenCoeff) 391 | # end specular blue 392 | specularWidgetsLayout.addLayout(channelLayout) 393 | specularLayout.addWidget(specularTitle) 394 | specularLayout.addLayout(specularWidgetsLayout) 395 | specularWidget.setLayout(specularLayout) 396 | # 397 | adsLayout.addWidget(specularWidget) 398 | spinsLayout.addLayout(adsLayout) 399 | # end specular light 400 | # attenuation 401 | attenuationWidget = QWidget() 402 | attenuationLayout = QVBoxLayout() 403 | attenuationTitle = QLabel("attenuation") 404 | attenuationWidgetsLayout = QHBoxLayout() 405 | # constant attenuation 406 | self.attenConstant = createDSpinBox(0.1, 1.0, 0.1) 407 | self.attenConstant.setValue(1.0) 408 | self.attenConstLabel = QLabel("constant") 409 | attenConstV = QVBoxLayout() 410 | attenConstV.addWidget(self.attenConstLabel) 411 | attenConstV.addWidget(self.attenConstant) 412 | attenuationWidgetsLayout.addLayout(attenConstV) 413 | # linear attenuation 414 | self.attenLinear = createDSpinBox(0.001, 10.0, 0.1, 0.7) 415 | self.attenLinearLabel = QLabel("linear") 416 | attenLinearV = QVBoxLayout() 417 | attenLinearV.addWidget(self.attenLinearLabel) 418 | attenLinearV.addWidget(self.attenLinear) 419 | attenuationWidgetsLayout.addLayout(attenLinearV) 420 | # quadratic attenuation 421 | self.attenQuadratic = createDSpinBox(0.001, 10.0, 1.8) 422 | self.attenQuadraticLabel = QLabel("quadratic") 423 | attenQuadraticV = QVBoxLayout() 424 | attenQuadraticV.addWidget(self.attenQuadraticLabel) 425 | attenQuadraticV.addWidget(self.attenQuadratic) 426 | attenuationWidgetsLayout.addLayout(attenQuadraticV) 427 | # 428 | attenuationLayout.addWidget(attenuationTitle) 429 | attenuationLayout.addLayout(attenuationWidgetsLayout) 430 | attenuationWidget.setLayout(attenuationLayout) 431 | # 432 | spinsLayout.addWidget(attenuationWidget) 433 | # end attenuation 434 | # angle and shininess 435 | otherWidget = QWidget() 436 | otherLayout = QHBoxLayout() 437 | # angle 438 | angLayout = QVBoxLayout() 439 | anglbl = QLabel("angle") 440 | self.angleSpin = createDSpinBox(1.0, 90.0, 1.0, 30.0) 441 | angLayout.addWidget(anglbl) 442 | angLayout.addWidget(self.angleSpin) 443 | # 444 | otherLayout.addLayout(angLayout) 445 | # end angle 446 | # shininess 447 | shinLayout = QVBoxLayout() 448 | shinlbl = QLabel("shininess") 449 | self.shininessSpin = createDSpinBox(0.01, 250.0, 0.01, 20.0) 450 | shinLayout.addWidget(shinlbl) 451 | shinLayout.addWidget(self.shininessSpin) 452 | # 453 | otherLayout.addLayout(shinLayout) 454 | # end shininess 455 | # cut off 456 | cutOffTitle = QLabel("Cut Off") 457 | cutOffLayout = QVBoxLayout() 458 | self.cutOffSpin = createDSpinBox(0.1, 520.0, 0.1) 459 | cutOffLayout.addWidget(cutOffTitle) 460 | cutOffLayout.addWidget(self.cutOffSpin) 461 | # 462 | otherLayout.addLayout(cutOffLayout) 463 | # end cut off 464 | otherWidget.setLayout(otherLayout) 465 | spinsLayout.addWidget(otherWidget) 466 | # end other widgets 467 | lightSectionWidgetsLayout.addLayout(spinsLayout) 468 | lightSectionWidgetsLayout.addLayout(slidersLayout) 469 | # 470 | lightSectionLayout.addWidget(lightSectionLabel) 471 | lightSectionLayout.addLayout(lightSectionWidgetsLayout) 472 | lightSection.setLayout(lightSectionLayout) 473 | # end light controls 474 | # 475 | mainLayout = QHBoxLayout() 476 | mainLayout.addWidget(self.glSection) 477 | mainLayout.addWidget(buttonsWidget) 478 | mainLayout.addWidget(lightSection) 479 | mainLayout.setStretchFactor(self.glSection, 1) 480 | self.mainwidget = QWidget() 481 | self.mainwidget.setLayout(mainLayout) 482 | self.setCentralWidget(self.mainwidget) 483 | self.setWindowTitle("PySide2 OpenGL Test Window") 484 | self.setMinimumSize(800, 600) 485 | 486 | def keyPressEvent(self, event): 487 | if event.key() == QtCore.Qt.Key_Escape: 488 | self.close() 489 | else: 490 | super().keyPressEvent(event) 491 | -------------------------------------------------------------------------------- /ptmviewer/utils/light.py: -------------------------------------------------------------------------------- 1 | # Author: Kaan Eraslan 2 | # purpose implements a light object 3 | 4 | 5 | import math 6 | from PySide2.QtGui import QVector3D 7 | from PySide2.QtGui import QVector4D 8 | from ptmviewer.utils.utils import normalize_tuple 9 | from ptmviewer.utils.utils import vec2vecDot 10 | from ptmviewer.utils.utils import crossProduct 11 | from ptmviewer.utils.utils import computeFrontRightPure 12 | from ptmviewer.utils.utils import computeFrontRightQt 13 | from ptmviewer.utils.obj3d import PureRigid3dObject 14 | from ptmviewer.utils.obj3d import QtRigid3dObject 15 | from ptmviewer.utils.obj3d import AbstractRigid3dObject 16 | 17 | from abc import ABC, abstractmethod 18 | 19 | 20 | class AbstractLightSource(ABC): 21 | def __init__(self): 22 | self.intensity = {} 23 | self.attenuation = {} 24 | self.coeffs = {} 25 | self.color = {} 26 | self.cutOff = math.cos(math.radians(12.5)) 27 | self.outerCutOff = math.cos(math.radians(15.5)) 28 | 29 | @abstractmethod 30 | def set_attenuation(self, attenuation): 31 | raise NotImplementedError 32 | 33 | @abstractmethod 34 | def set_color(self): 35 | raise NotImplementedError 36 | 37 | def check_intensity_coeff(self, val: float, valname: str): 38 | if not isinstance(val, float): 39 | raise TypeError( 40 | "Given " + valname + " is not of type float: ", str(type(val)) 41 | ) 42 | if not (val >= 0.0 and val <= 1.0): 43 | raise ValueError( 44 | "value " + valname + " not in given range 0.0 - 1.0: " + str(val) 45 | ) 46 | 47 | def check_notFloat_proc(self, val: float, name: str): 48 | if not isinstance(val, float): 49 | raise TypeError(name + " is not of type float: " + str(type(val))) 50 | 51 | @abstractmethod 52 | def set_channel_intensity(self, channel: str, val: float): 53 | raise NotImplementedError 54 | 55 | @abstractmethod 56 | def set_channel_coeff(self, channel: str, val: float): 57 | raise NotImplementedError 58 | 59 | def set_cut_off(self, val: float): 60 | "" 61 | self.check_notFloat_proc(val, "cut off") 62 | self.cutOff = math.cos(math.radians(val)) 63 | 64 | def set_outer_cut_off(self, val: float): 65 | "" 66 | self.check_notFloat_proc(val, "outer cut off") 67 | self.outerCutOff = math.cos(math.radians(val)) 68 | 69 | def get_coeff_average(self): 70 | "Get the average value of its coefficients" 71 | counter = 0 72 | for val in self.coeffs.values(): 73 | counter += val 74 | return counter / len(self.coeffs) 75 | 76 | 77 | class PureLightSource(AbstractLightSource): 78 | "A pure python light source implementation" 79 | 80 | def __init__( 81 | self, 82 | cutOff=12.5, 83 | outerCutOff=15.5, 84 | attenuation={"constant": 1.0, "linear": 0.7, "quadratic": 1.8}, 85 | intensity={"r": 1.0, "g": 1.0, "b": 1.0}, 86 | coeffs={"r": 1.0, "g": 1.0, "b": 1.0}, 87 | ): 88 | "" 89 | self.intensity = intensity 90 | self.coeffs = coeffs 91 | self.color = {} 92 | self.set_color() 93 | self.cutOff = math.cos(math.radians(cutOff)) 94 | self.outerCutOff = math.cos(math.radians(outerCutOff)) 95 | self.attenuation = attenuation 96 | self.attenVals = [ 97 | # data taken on 2019-08-30 from 98 | # https://learnopengl.com/Lighting/Light-casters 99 | # distance, attenConst, attenLin, attenQaud 100 | [7, 1.0, 0.14, 0.07], 101 | [13, 1.0, 0.35, 0.44], 102 | [20, 1.0, 0.22, 0.20], 103 | [32, 1.0, 0.14, 0.07], 104 | [50, 1.0, 0.09, 0.032], 105 | [65, 1.0, 0.07, 0.017], 106 | [100, 1.0, 0.045, 0.0075], 107 | [160, 1.0, 0.027, 0.0028], 108 | [200, 1.0, 0.022, 0.0019], 109 | [325, 1.0, 0.014, 0.0007], 110 | [600, 1.0, 0.007, 0.0002], 111 | [3250, 1.0, 0.0014, 0.000007], 112 | ] 113 | 114 | def setAttenuationByTableVals(self, index: int): 115 | "Set attenuation values by table" 116 | row = self.attenVals[index] 117 | self.attenuation["constant"] = row[1] 118 | self.attenuation["linear"] = row[2] 119 | self.attenuation["quadratic"] = row[3] 120 | 121 | def setAttenuationValuesByDistance(self, distance: float): 122 | "" 123 | self.attenVals.sort(key=lambda x: x[0]) 124 | maxdist = self.attenVals[-1][0] 125 | mindist = self.attenVals[0][0] 126 | if distance >= maxdist: 127 | self.setAttenuationByTableVals(-1) 128 | return 129 | if distance <= mindist: 130 | self.setAttenuationByTableVals(0) 131 | return 132 | for i, attenlst in enumerate(self.attenVals): 133 | dist = attenlst[0] 134 | if dist > distance: 135 | self.setAttenuationByTableVals(i) 136 | return 137 | 138 | def computeAttenuation4Distance(self, distance: float): 139 | "compute attenuation value for given distance" 140 | second = self.attenuation["linear"] * distance 141 | third = self.attenuation["quadratic"] * distance * distance 142 | return min(1, 1 / (self.attenuation["constant"] + second + third)) 143 | 144 | def set_attenuation(self, atten: dict): 145 | "set attenuation" 146 | constant = atten["constant"] 147 | linear = atten["linear"] 148 | quadratic = atten["quadratic"] 149 | self.check_notFloat_proc(constant, name="constant attenuation") 150 | self.check_notFloat_proc(linear, name="linear attenuation") 151 | self.check_notFloat_proc(quadratic, name="quadratic attenuation") 152 | self.attenuation["constant"] = constant 153 | self.attenuation["linear"] = linear 154 | self.attenuation["quadratic"] = quadratic 155 | 156 | def set_color(self): 157 | "Set color" 158 | self.color = {} 159 | for channel, val in self.intensity.items(): 160 | coeff = self.coeffs[channel] 161 | self.color[channel] = coeff * val 162 | 163 | @staticmethod 164 | def set_channel_val(channel: str, val: float): 165 | channel_d = {} 166 | name = channel.lower() 167 | if name == "red" or name == "r": 168 | channel_d["r"] = val 169 | elif name == "green" or name == "g": 170 | channel_d["g"] = val 171 | elif name == "blue" or name == "b": 172 | channel_d["b"] = val 173 | elif name == "alpha" or name == "a": 174 | channel_d["a"] = val 175 | else: 176 | mess = "Unrecognized channel name " + name 177 | mess += ", available channels are: " 178 | mess += "red, green, blue, alpha" 179 | raise ValueError(mess) 180 | return channel_d 181 | 182 | def set_channel_coeff(self, channel: str, val: float): 183 | "Set coefficients" 184 | self.check_intensity_coeff(val=val, valname=channel + " coefficient") 185 | # 186 | name = channel.lower() 187 | cdict = PureLightSource.set_channel_val(channel, val) 188 | self.coeffs.update(cdict) 189 | self.set_color() 190 | 191 | def set_channel_intensity(self, channel: str, val: float): 192 | "set channel intensity" 193 | self.check_intensity_coeff(val, channel + " intensity") 194 | # 195 | name = channel.lower() 196 | cdict = PureLightSource.set_channel_val(channel, val) 197 | self.intensity.update(cdict) 198 | self.set_color() 199 | 200 | def __str__(self): 201 | "" 202 | mess = "Light Source:\n position {0},\n direction {1},\n intensity {2}" 203 | mess += ",\n coefficients {3},\n color {4},\n cutOff value {5}" 204 | mess += ",\n outerCutOff value {9}, attenuation constant {6}," 205 | mess += "\n attenuation linear {7}" 206 | mess += ",\n attenuation quadratic {8}" 207 | return mess.format( 208 | str(self.position), 209 | str(self.direction), 210 | str(self.intensity), 211 | str(self.coeffs), 212 | str(self.color), 213 | str(self.cutOff), 214 | str(self.attenConst), 215 | str(self.attenLinear), 216 | str(self.attenQuad), 217 | str(self.outerCutOff), 218 | ) 219 | 220 | 221 | class QtLightSource(AbstractLightSource): 222 | "A light source" 223 | 224 | def __init__( 225 | self, 226 | intensity=QVector4D(1.0, 1.0, 1.0, 1.0), 227 | coefficients=QVector4D(1.0, 1.0, 1.0, 1.0), 228 | attenuation=QVector3D(1.0, 0.14, 0.07), 229 | cutOff=12.5, 230 | outerCutOff=15.0, 231 | ): 232 | "" 233 | self.color = QVector4D() 234 | self.intensity = intensity 235 | self.coeffs = coefficients 236 | self.set_color() 237 | self.cutOff = math.cos(math.radians(cutOff)) 238 | self.outerCutOff = math.cos(math.radians(outerCutOff)) 239 | self.attenuation = attenuation 240 | self.attenVals = [ 241 | # data taken on 2019-08-30 from 242 | # https://learnopengl.com/Lighting/Light-casters 243 | # distance, attenConst, attenLin, attenQaud 244 | [7, 1.0, 0.14, 0.07], 245 | [13, 1.0, 0.35, 0.44], 246 | [20, 1.0, 0.22, 0.20], 247 | [32, 1.0, 0.14, 0.07], 248 | [50, 1.0, 0.09, 0.032], 249 | [65, 1.0, 0.07, 0.017], 250 | [100, 1.0, 0.045, 0.0075], 251 | [160, 1.0, 0.027, 0.0028], 252 | [200, 1.0, 0.022, 0.0019], 253 | [325, 1.0, 0.014, 0.0007], 254 | [600, 1.0, 0.007, 0.0002], 255 | [3250, 1.0, 0.0014, 0.000007], 256 | ] 257 | 258 | def setAttenuationByTableVals(self, index: int): 259 | "Set attenuation values by table" 260 | row = self.attenVals[index] 261 | self.attenuation = QVector4D(row[1], row[2], row[3], 1.0) 262 | 263 | def setAttenuationByDistance(self, distance: float): 264 | "" 265 | self.attenVals.sort(key=lambda x: x[0]) 266 | maxdist = self.attenVals[-1][0] 267 | mindist = self.attenVals[0][0] 268 | if distance >= maxdist: 269 | self.setAttenuationByTableVals(-1) 270 | return 271 | if distance <= mindist: 272 | self.setAttenuationByTableVals(0) 273 | return 274 | for i, (dist, aconst, alin, aquad) in enumerate(self.attenVals): 275 | if dist > distance: 276 | self.setAttenuationByTableVals(i) 277 | return 278 | 279 | def check_notFloat_vec_proc(self, vec, vecname: str): 280 | "check whether all members of vectors are float" 281 | tpl = vec.toTuple() 282 | for t in tpl: 283 | self.check_notFloat_proc(t, name=vecname + " member") 284 | 285 | def set_attenuation(self, atten): 286 | "set attenuation" 287 | self.check_notFloat_vec_proc(atten, vecname="attenuation vector") 288 | self.attenuation = atten 289 | 290 | def set_color(self): 291 | "Set light source color using coeffs and intensities" 292 | if isinstance(self.intensity, QVector3D): 293 | self.color = QVector3D( 294 | self.intensity.x() * self.coeffs.x(), 295 | self.intensity.y() * self.coeffs.y(), 296 | self.intensity.z() * self.coeffs.z(), 297 | ) 298 | else: 299 | self.color = QVector4D( 300 | self.intensity.x() * self.coeffs.x(), 301 | self.intensity.y() * self.coeffs.y(), 302 | self.intensity.z() * self.coeffs.z(), 303 | self.intensity.w() * self.coeffs.w(), 304 | ) 305 | self.color = self.color.toVector3DAffine() 306 | 307 | def set_channel_val(self, channel: str, val: float, isIntensity=False): 308 | cvec = self.intensity if isIntensity else self.coeffs 309 | name = channel.lower() 310 | if name == "red" or name == "r": 311 | cvec.setX(val) 312 | elif name == "green" or name == "g": 313 | cvec.setY(val) 314 | elif name == "blue" or name == "b": 315 | cvec.setZ(val) 316 | elif name == "alpha" or name == "a": 317 | cvec.setW(val) 318 | else: 319 | mess = "Unrecognized channel name " + name 320 | mess += ", available channels are: " 321 | mess += "red, green, blue, alpha" 322 | raise ValueError(mess) 323 | self.set_color() 324 | return 325 | 326 | def set_channel_coeff(self, channel: str, val: float): 327 | "Set coefficient to given intesity" 328 | self.set_channel_val(channel=channel, val=val, isIntensity=False) 329 | 330 | def set_channel_intensity(self, channel: str, val: float): 331 | "Set coefficient to given intesity" 332 | self.set_channel_val(channel=channel, val=val, isIntensity=True) 333 | 334 | def get_coeff_average(self): 335 | "get average value for coefficients" 336 | counter = 0 337 | tplsize = 0 338 | for el in self.coeffs.toTuple(): 339 | tplsize += 1 340 | counter += el 341 | return counter / tplsize 342 | 343 | def fromPureLightSource(self, light: PureLightSource): 344 | "" 345 | self.set_channel_intensity(channel="r", val=light.intensity["r"]) 346 | self.set_channel_intensity(channel="g", val=light.intensity["g"]) 347 | self.set_channel_intensity(channel="b", val=light.intensity["b"]) 348 | atten = QVector3D() 349 | atten.setX(light.attenuation["constant"]) 350 | atten.setY(light.attenuation["linear"]) 351 | atten.setZ(light.attenuation["quadratic"]) 352 | self.set_attenuation(atten) 353 | self.set_channel_coeff(channel="r", val=light.coeffs["r"]) 354 | self.set_channel_coeff(channel="g", val=light.coeffs["g"]) 355 | self.set_channel_coeff(channel="b", val=light.coeffs["b"]) 356 | self.cutOff = light.cutOff 357 | 358 | def toPureLightSource(self): 359 | "" 360 | light = PureLightSource() 361 | light.setCutOff(self.cutOff) 362 | atten = {} 363 | atten["constant"] = self.attenuation.x() 364 | atten["linear"] = self.attenuation.y() 365 | atten["quadratic"] = self.attenuation.z() 366 | light.setAttenuation(atten) 367 | light.set_channel_coeff(channel="r", val=self.coeffs.x()) 368 | light.set_channel_coeff(channel="g", val=self.coeffs.y()) 369 | light.set_channel_coeff(channel="b", val=self.coeffs.z()) 370 | if isinstance(self.coeffs, QVector4D): 371 | light.set_channel_coeff(channel="a", val=self.coeffs.w()) 372 | light.set_channel_intensity(channel="r", val=self.intensity.x()) 373 | light.set_channel_intensity(channel="g", val=self.intensity.y()) 374 | light.set_channel_intensity(channel="b", val=self.intensity.z()) 375 | if isinstance(self.intensity, QVector4D): 376 | light.set_channel_intensity(channel="a", val=self.intensity.w()) 377 | return light 378 | 379 | 380 | class PureLambertianReflector: 381 | "Object that computes lambertian reflection" 382 | 383 | def __init__( 384 | self, 385 | lightSource: PureLightSource, 386 | objDiffuseReflectionCoefficientRed: float, 387 | objDiffuseReflectionCoefficientGreen: float, 388 | objDiffuseReflectionCoefficientBlue: float, 389 | surfaceNormal: (int, int, int), 390 | ): 391 | self.light = lightSource 392 | self.objR = objDiffuseReflectionCoefficientRed 393 | self.objG = objDiffuseReflectionCoefficientGreen 394 | self.objB = objDiffuseReflectionCoefficientBlue 395 | assert self.objR <= 1.0 and self.objR >= 0.0 396 | assert self.objG <= 1.0 and self.objG >= 0.0 397 | assert self.objB <= 1.0 and self.objB >= 0.0 398 | self.costheta = None 399 | assert len(surfaceNormal) == 3 400 | self.surfaceNormal = surfaceNormal 401 | self.setCosTheta() 402 | self.reflection = {} 403 | self.setLambertianReflection() 404 | 405 | def setCosTheta(self): 406 | "" 407 | lightDir = self.light.direction 408 | lightDir = (lightDir["x"], lightDir["y"], lightDir["z"]) 409 | normLight = normalize_tuple(lightDir) 410 | normSurf = normalize_tuple(self.surfaceNormal) 411 | self.costheta = vec2vecDot(normSurf, normLight) 412 | 413 | def setLambertianReflection(self): 414 | "compute reflection" 415 | red = self.light.intensity["r"] * self.objR * self.costheta 416 | green = self.light.intensity["g"] * self.objG * self.costheta 417 | blue = self.light.intensity["b"] * self.objB * self.costheta 418 | self.reflection["r"] = red 419 | self.reflection["g"] = green 420 | self.reflection["b"] = blue 421 | 422 | 423 | class PureLambertianReflectorAmbient(PureLambertianReflector): 424 | "Pure python implementation of lambertian reflector with ambient light" 425 | 426 | def __init__( 427 | self, 428 | lightSource: PureLightSource, 429 | ambientLight: PureLightSource, 430 | objDiffuseReflectionCoefficientRed: float, 431 | objDiffuseReflectionCoefficientGreen: float, 432 | objDiffuseReflectionCoefficientBlue: float, 433 | surfaceNormal: (int, int, int), 434 | ): 435 | super().__init__( 436 | lightSource, 437 | objDiffuseReflectionCoefficientRed, 438 | objDiffuseReflectionCoefficientGreen, 439 | objDiffuseReflectionCoefficientBlue, 440 | surfaceNormal, 441 | ) 442 | self.setCosTheta() 443 | self.setLambertianReflection() 444 | self.ambientLight = ambientLight 445 | 446 | def setLambertianReflectionWithAmbient(self): 447 | red = self.reflection["r"] + self.ambientLight.color["r"] 448 | green = self.reflection["g"] + self.ambientLight.color["g"] 449 | blue = self.reflection["b"] + self.ambientLight.color["b"] 450 | self.reflection = {"r": red, "g": green, "b": blue} 451 | 452 | 453 | class AbstractShaderLight: 454 | def __init__(self): 455 | self.attenuation = {"constant": 0.0, "linear": 0.0, "quadratic": 0.0} 456 | self.ambient = None 457 | self.diffuse = None 458 | self.specular = None 459 | self.availableLightSources = ["ambient", "diffuse", "specular"] 460 | self.cutOff = 0.0 461 | self.outerCutOff = 0.0 462 | 463 | def set_cut_off(self, val: float): 464 | "set cut off value to diffuse and specular" 465 | self.diffuse.set_cut_off(val) 466 | self.specular.set_cut_off(val) 467 | self.cutOff = self.specular.cutOff 468 | 469 | def set_attenuation(self, atten): 470 | self.diffuse.set_attenuation(atten) 471 | self.specular.set_attenuation(atten) 472 | self.attenuation = self.specular.attenuation 473 | 474 | def set_channel_intensity(self, channel: str, val: float, lsource="diffuse"): 475 | "channel intensity" 476 | if lsource not in self.availableLightSources: 477 | raise ValueError("Unavailable light source: " + lsource) 478 | if lsource == "diffuse": 479 | self.diffuse.set_channel_intensity(channel, val) 480 | elif lsource == "specular": 481 | self.specular.set_channel_intensity(channel, val) 482 | else: 483 | self.ambient.set_channel_intensity(channel, val) 484 | 485 | def set_channel_coeff(self, channel: str, val: float, lsource="diffuse"): 486 | "channel coefficient" 487 | if lsource not in self.availableLightSources: 488 | raise ValueError("Unavailable light source: " + lsource) 489 | if lsource == "diffuse": 490 | self.diffuse.set_channel_coeff(channel, val) 491 | elif lsource == "specular": 492 | self.specular.set_channel_coeff(channel, val) 493 | else: 494 | self.ambient.set_channel_coeff(channel, val) 495 | 496 | def set_outer_cut_off(self, val: float): 497 | "set cut off value to diffuse and specular" 498 | self.diffuse.set_outer_cut_off(val) 499 | self.specular.set_outer_cut_off(val) 500 | self.outerCutOff = self.specular.outerCutOff 501 | 502 | 503 | class PureShaderLight(AbstractShaderLight, PureRigid3dObject): 504 | "A Pure python shader light object for illumination" 505 | 506 | def __init__( 507 | self, 508 | position: dict, 509 | cutOff=12.5, 510 | outerCutOff=15.0, 511 | attenuation={"constant": 1.0, "linear": 0.7, "quadratic": 1.8}, 512 | ambient=PureLightSource(coeffs={"r": 0.3, "g": 0.3, "b": 0.3}), 513 | diffuse=PureLightSource(), 514 | specular=PureLightSource(), 515 | ): 516 | "" 517 | super().__init__() 518 | self.ambient = ambient 519 | self.diffuse = diffuse 520 | self.specular = specular 521 | self.set_position(position) 522 | self.set_attenuation(attenuation) 523 | self.set_cut_off(cutOff) 524 | self.set_outer_cut_off(outerCutOff) 525 | 526 | def get_specular_color(self): 527 | return { 528 | "r": self.specular.color["r"], 529 | "g": self.specular.color["g"], 530 | "b": self.specular.color["b"], 531 | } 532 | 533 | def get_ambient_color(self): 534 | return { 535 | "r": self.ambient.color["r"], 536 | "g": self.ambient.color["g"], 537 | "b": self.ambient.color["b"], 538 | } 539 | 540 | def get_diffuse_color(self): 541 | return { 542 | "r": self.diffuse.color["r"], 543 | "g": self.diffuse.color["g"], 544 | "b": self.diffuse.color["b"], 545 | } 546 | 547 | def __str__(self): 548 | "string representation" 549 | mess = "Shader Light:\n position {0},\n ambient {2}" 550 | mess += ",\n diffuse {3},\n specular {4},\n cut off {5}" 551 | mess += ",\n outerCutOff value {9}, attenuation values {6}," 552 | return mess.format( 553 | str(self.position), 554 | str(self.ambient), 555 | str(self.diffuse), 556 | str(self.specular), 557 | str(self.cutOff), 558 | str(self.outerCutOff), 559 | str(self.attenuation), 560 | ) 561 | 562 | 563 | class QtShaderLight(AbstractShaderLight, QtRigid3dObject): 564 | "Qt shader light object" 565 | 566 | def __init__( 567 | self, 568 | position=QVector3D(0.0, 1.0, 0.0), 569 | cutOff=12.5, 570 | outerCutOff=15.0, 571 | attenuation=QVector3D(1.0, 0.14, 1.8), 572 | ambient=QtLightSource(), 573 | diffuse=QtLightSource(), 574 | specular=QtLightSource(), 575 | ): 576 | "" 577 | QtRigid3dObject.__init__(self) 578 | AbstractShaderLight.__init__(self) 579 | 580 | # rigid 3d object 581 | self.set_position(position) 582 | 583 | # shader light 584 | self.ambient = ambient 585 | self.diffuse = diffuse 586 | self.specular = specular 587 | self.set_cut_off(cutOff) 588 | self.set_outer_cut_off(outerCutOff) 589 | self.set_attenuation(attenuation) 590 | 591 | -------------------------------------------------------------------------------- /ptmviewer/qtapp.py: -------------------------------------------------------------------------------- 1 | # author: Kaan Eraslan 2 | 3 | # Purpose: Application wrapper for ptm viewer 4 | 5 | from PySide2 import QtCore, QtGui, QtWidgets, QtOpenGL 6 | 7 | from PySide2.QtCore import QCoreApplication 8 | from PySide2.QtWidgets import QOpenGLWidget 9 | from PySide2.QtGui import QVector3D 10 | 11 | from PySide2.shiboken2 import VoidPtr 12 | 13 | import numpy as np 14 | import sys 15 | import os 16 | import json 17 | import pdb 18 | from PIL import Image, ImageQt 19 | 20 | from ptmviewer.interface.window import Ui_MainWindow 21 | from ptmviewer.glwidget import PtmLambertianGLWidget, PtmNormalMapGLWidget 22 | from ptmviewer.glwidget import PtmPerChannelNormalMapDirGLWidget 23 | from ptmviewer.glwidget import PtmPerChannelNormalMapPointGLWidget 24 | from ptmviewer.glwidget import PtmPerChannelNormalMapSpotGLWidget 25 | from ptmviewer.glwidget import PtmPerChannelNormalMapPhongGLWidget 26 | from ptmviewer.glwidget import PtmCoefficientShader 27 | from ptmviewer.rgbptm import RGBPTM 28 | from ptmviewer.utils.shaders import shaders 29 | 30 | 31 | class AppWindowInit(Ui_MainWindow): 32 | """ 33 | Initializes the image window 34 | made in qt designer 35 | """ 36 | 37 | def __init__(self): 38 | self.main_window = QtWidgets.QMainWindow() 39 | super().setupUi(self.main_window) 40 | 41 | # adding basic defaults to here 42 | # Main Window Events 43 | self.main_window.setWindowTitle("Python PTM Viewer") 44 | self.main_window.closeEvent = self.closeApp 45 | self.closeShort = QtWidgets.QShortcut( 46 | QtGui.QKeySequence("ctrl+w"), self.main_window 47 | ) 48 | self.closeShort.activated.connect(self.closeKey) 49 | # make menu 50 | self.toolbar = self.main_window.addToolBar("Widgets") 51 | self.createToolbar() 52 | 53 | def prepDockWidget(self, txt: str, keyseq: str, dockWidget): 54 | "prepare docker widget" 55 | dockAct = dockWidget.toggleViewAction() 56 | dockAct.setShortcut(QtGui.QKeySequence(keyseq)) 57 | dockAct.setText(txt) 58 | dockAct.setToolTip(keyseq) 59 | self.toolbar.addAction(dockAct) 60 | return dockAct 61 | 62 | def createToolbar(self): 63 | "create tool bar" 64 | self.fact = self.prepDockWidget( 65 | txt="file list", keyseq="ctrl+f", dockWidget=self.fileListDock 66 | ) 67 | # self.toolbar.addAction(self.fact) 68 | self.cact = self.prepDockWidget( 69 | txt="color controller", keyseq="ctrl+z", dockWidget=self.colorDock 70 | ) 71 | self.sact = self.prepDockWidget( 72 | txt="shader controller", keyseq="ctrl+s", dockWidget=self.shaderDock 73 | ) 74 | # self.toolbar.addAction(self.cact) 75 | self.ract = self.prepDockWidget( 76 | txt="rotation controller", keyseq="ctrl+r", dockWidget=self.rotateDock 77 | ) 78 | # self.toolbar.addAction(self.ract) 79 | self.mact = self.prepDockWidget( 80 | txt="move controller", keyseq="ctrl+m", dockWidget=self.moveDock 81 | ) 82 | # self.toolbar.addAction(self.mact) 83 | self.pact = self.prepDockWidget( 84 | txt="parameter viewer", keyseq="ctrl+p", dockWidget=self.paramsDock 85 | ) 86 | 87 | # self.toolbar.addAction(self.pact) 88 | self.tact = self.prepDockWidget( 89 | txt="transcriber", keyseq="ctrl+t", dockWidget=self.transcribeDock 90 | ) 91 | 92 | # self.toolbar.addAction(self.tact) 93 | self.gact = self.prepDockWidget( 94 | txt="opengl info viewer", keyseq="ctrl+g", dockWidget=self.glInfoDock 95 | ) 96 | # self.toolbar.addAction(self.gact) 97 | 98 | 99 | class AppWindowGLEvents(AppWindowInit): 100 | def get_intensity_values(self) -> dict: 101 | "get intensity values from spin boxes" 102 | ambient = self.viewerWidget.lamp.ambient.intensity 103 | diffuse = self.viewerWidget.lamp.diffuse.intensity 104 | specular = self.viewerWidget.lamp.specular.intensity 105 | return { 106 | "intensities": { 107 | "ambient": ambient, 108 | "diffuse": diffuse, 109 | "specular": specular, 110 | } 111 | } 112 | 113 | def get_coefficient_values(self): 114 | "get coefficient values" 115 | ambient = self.viewerWidget.lamp.ambient.coeffs 116 | diffuse = self.viewerWidget.lamp.diffuse.coeffs 117 | specular = self.viewerWidget.lamp.specular.coeffs 118 | return { 119 | "coefficients": { 120 | "ambient": ambient, 121 | "diffuse": diffuse, 122 | "specular": specular, 123 | } 124 | } 125 | 126 | def get_lamp_parameters(self) -> dict: 127 | "get lamp parameters" 128 | pos = self.viewerWidget.lamp.position 129 | position = {"position": {"x": pos.x(), "y": pos.y(), "z": pos.z()}} 130 | frt = self.viewerWidget.lamp.front 131 | front = {"front": {"x": frt.x(), "y": frt.y(), "z": frt.z()}} 132 | upl = self.viewerWidget.lamp.up 133 | up = {"up": {"x": upl.x(), "y": upl.y(), "z": upl.z()}} 134 | rgt = self.viewerWidget.lamp.right 135 | right = {"right": {"x": rgt.x(), "y": rgt.y(), "z": rgt.z()}} 136 | yaw = self.viewerWidget.lamp.yaw 137 | pitch = self.viewerWidget.lamp.pitch 138 | roll = self.viewerWidget.lamp.roll 139 | angles = {"angles": {"yaw": yaw, "pitch": pitch, "roll": roll}} 140 | atten = self.viewerWidget.lamp.attenuation 141 | attenuation = { 142 | "attenuation": { 143 | "constant": atten.x(), 144 | "linear": atten.y(), 145 | "quadratic": atten.z(), 146 | } 147 | } 148 | params = {} 149 | params.update(position) 150 | params.update(front) 151 | params.update(up) 152 | params.update(right) 153 | params.update(angles) 154 | params.update(attenuation) 155 | params["lightCutOff"] = self.viewerWidget.lamp.cutOff 156 | params["lightOuterCutOff"] = self.viewerWidget.lamp.outerCutOff 157 | return params 158 | 159 | def get_camera_parameters(self) -> dict: 160 | "get camera parameters" 161 | pos = self.viewerWidget.camera.position 162 | position = {"position": {"x": pos.x(), "y": pos.y(), "z": pos.z()}} 163 | frt = self.viewerWidget.camera.front 164 | front = {"front": {"x": frt.x(), "y": frt.y(), "z": frt.z()}} 165 | upl = self.viewerWidget.camera.up 166 | up = {"up": {"x": upl.x(), "y": upl.y(), "z": upl.z()}} 167 | rgt = self.viewerWidget.camera.right 168 | right = {"right": {"x": rgt.x(), "y": rgt.y(), "z": rgt.z()}} 169 | yaw = self.viewerWidget.camera.yaw 170 | pitch = self.viewerWidget.camera.pitch 171 | roll = self.viewerWidget.camera.roll 172 | viewMat = self.viewerWidget.camera.getViewMatrix() 173 | angles = {"angles": {"yaw": yaw, "pitch": pitch, "roll": roll}} 174 | params = {} 175 | params["zoom"] = self.viewerWidget.camera.zoom 176 | params["view_matrix"] = repr(viewMat) 177 | params.update(position) 178 | params.update(front) 179 | params.update(up) 180 | params.update(right) 181 | params.update(angles) 182 | return params 183 | 184 | def get_shader_parameters(self) -> dict: 185 | "get shader parameters" 186 | params = {} 187 | shininess = self.shinSpin.value() 188 | lampShader = self.viewerWidget.lampShaderName 189 | objectShader = self.viewerWidget.objectShaderName 190 | params["light-source-shader"] = shaders[lampShader] 191 | params["object-shader"] = shaders[objectShader] 192 | params["shininess"] = shininess 193 | return params 194 | 195 | 196 | class AppWindowLightControlEvents(AppWindowGLEvents): 197 | "Light Control events" 198 | 199 | def set_intensity_coefficient(self, val: float, channel: str, isIntensity=False): 200 | "set intensity" 201 | if self.ambientRBtn.isChecked(): 202 | if isIntensity: 203 | self.viewerWidget.change_lamp_ambient_intensity( 204 | channel=channel, val=val 205 | ) 206 | else: 207 | self.viewerWidget.change_lamp_ambient_coefficient( 208 | channel=channel, val=val 209 | ) 210 | 211 | elif self.diffuseRBtn.isChecked(): 212 | if isIntensity: 213 | self.viewerWidget.change_lamp_diffuse_intensity( 214 | channel=channel, val=val 215 | ) 216 | else: 217 | self.viewerWidget.change_lamp_diffuse_coefficient( 218 | channel=channel, val=val 219 | ) 220 | 221 | elif self.specularRBtn.isChecked(): 222 | if isIntensity: 223 | self.viewerWidget.change_lamp_specular_intensity( 224 | channel=channel, val=val 225 | ) 226 | else: 227 | self.viewerWidget.change_lamp_specular_coefficient( 228 | channel=channel, val=val 229 | ) 230 | 231 | else: 232 | return 233 | 234 | def set_red_intensity(self): 235 | redval = self.intensityR.value() 236 | self.set_intensity_coefficient(val=redval, channel="red", isIntensity=True) 237 | 238 | def set_green_intensity(self): 239 | greenval = self.intensityG.value() 240 | self.set_intensity_coefficient(val=greenval, channel="green", isIntensity=True) 241 | 242 | def set_blue_intensity(self): 243 | blueval = self.intensityB.value() 244 | self.set_intensity_coefficient(val=blueval, channel="blue", isIntensity=True) 245 | 246 | def set_red_coefficient(self): 247 | redval = self.coefficientR.value() 248 | self.set_intensity_coefficient(val=redval, channel="red", isIntensity=False) 249 | 250 | def set_green_coefficient(self): 251 | greenval = self.coefficientG.value() 252 | self.set_intensity_coefficient(val=greenval, channel="green", isIntensity=False) 253 | 254 | def set_blue_coefficient(self): 255 | blueval = self.coefficientB.value() 256 | self.set_intensity_coefficient(val=blueval, channel="blue", isIntensity=False) 257 | 258 | 259 | class AppWindowRotateControl(AppWindowLightControlEvents): 260 | "Rotate Control" 261 | 262 | def get_axes(self) -> list: 263 | zaxis = self.rotZCbox.isChecked() 264 | yaxis = self.rotYCbox.isChecked() 265 | xaxis = self.rotXCbox.isChecked() 266 | rotaxes = [] 267 | if zaxis: 268 | rotaxes.append("z") 269 | if yaxis: 270 | rotaxes.append("y") 271 | if xaxis: 272 | rotaxes.append("x") 273 | return rotaxes 274 | 275 | def set_angles(self): 276 | "Set euler angles either to yaw, pitch and roll" 277 | rotation_axes = self.get_axes() 278 | angle = self.angleSpin.value() 279 | self.viewerWidget.set_rotate_axes(rotation_axes) 280 | if self.rotCamRBtn.isChecked(): 281 | self.viewerWidget.set_euler_angles_to_camera(angle) 282 | elif self.rotLightRBtn.isChecked(): 283 | self.viewerWidget.set_euler_angle_to_lamp(angle) 284 | 285 | 286 | class AppWindowMoveControl(AppWindowRotateControl): 287 | "Move window" 288 | 289 | def move_light_camera(self, isUp=False): 290 | "" 291 | zaxis = self.moveZCBox.isChecked() 292 | yaxis = self.moveYCBox.isChecked() 293 | xaxis = self.moveXCBox.isChecked() 294 | zdir = "+z" if isUp else "-z" 295 | ydir = "+y" if isUp else "-y" 296 | xdir = "+x" if isUp else "-x" 297 | if self.moveLightRBtn.isChecked(): 298 | if zaxis: 299 | self.viewerWidget.move_light(zdir) 300 | if yaxis: 301 | self.viewerWidget.move_light(ydir) 302 | if xaxis: 303 | self.viewerWidget.move_light(xdir) 304 | elif self.moveCameraRBtn.isChecked(): 305 | if zaxis: 306 | self.viewerWidget.move_camera(zdir) 307 | if yaxis: 308 | self.viewerWidget.move_camera(ydir) 309 | if xaxis: 310 | self.viewerWidget.move_camera(xdir) 311 | 312 | def move_up_light_camera(self): 313 | self.move_light_camera(isUp=True) 314 | 315 | def move_down_light_camera(self): 316 | self.move_light_camera(isUp=False) 317 | 318 | 319 | class AppWindowFinal(AppWindowMoveControl): 320 | "Final window" 321 | 322 | def __init__(self): 323 | super().__init__() 324 | self.ptmfiles = {} 325 | 326 | # dock widgets 327 | 328 | ## params widget 329 | ### Available buttons 330 | self.captureBtn.clicked.connect(self.captureParams) 331 | 332 | ## transcribe widget 333 | self.saveBtn.clicked.connect(self.saveNotes) 334 | 335 | ## move widget 336 | ### Available buttons 337 | # self.moveCameraRBtn.toggled.connect(lambda x: x) 338 | # self.moveLightRBtn.toggled.connect(lambda x: x) 339 | # self.moveXCBox.stateChanged.connect(lambda x: x) 340 | # self.moveYCBox.stateChanged.connect(lambda x: x) 341 | # self.moveZCBox.stateChanged.connect(lambda x: x) 342 | self.moveUp.clicked.connect(self.move_up_light_camera) 343 | self.moveDown.clicked.connect(self.move_down_light_camera) 344 | ## rotate widget 345 | ### Available buttons 346 | # self.rotCamRBtn.toggled.connect(lambda x: x) 347 | # self.rotLightRBtn.toggled.connect(lambda x: x) 348 | self.angleSpin.valueChanged.connect(self.set_angles) 349 | ## color widget 350 | ### Available buttons 351 | # self.ambientRBtn.toggled.connect(lambda x: x) 352 | # self.diffuseRBtn.toggled.connect(lambda x: x) 353 | # self.specularRBtn.toggled.connect(lambda x: x) 354 | self.intensityR.valueChanged.connect(self.set_red_intensity) 355 | self.intensityG.valueChanged.connect(self.set_green_intensity) 356 | self.intensityB.valueChanged.connect(self.set_blue_intensity) 357 | self.coefficientR.valueChanged.connect(self.set_red_coefficient) 358 | self.coefficientG.valueChanged.connect(self.set_green_coefficient) 359 | self.coefficientB.valueChanged.connect(self.set_blue_coefficient) 360 | 361 | ## file list widget 362 | ### Available buttons 363 | self.addFile.clicked.connect(self.browseFolder) 364 | self.addFile.setShortcut("ctrl+o") 365 | 366 | self.loadFile.clicked.connect(self.loadPtm) 367 | self.loadFile.setShortcut("ctrl+l") 368 | 369 | self.removeFile.clicked.connect(self.removeItems) 370 | ## shader widget 371 | 372 | # viewer widget, opengl widgets with different shaders 373 | self.availableGlWidgets = { 374 | "Lambertian": PtmLambertianGLWidget, 375 | "SingleNormalMap": PtmNormalMapGLWidget, 376 | "PerChannelPhong": PtmPerChannelNormalMapPhongGLWidget, 377 | "PerChannelNormalMapDir": PtmPerChannelNormalMapDirGLWidget, 378 | "PerChannelNormalMapPoint": PtmPerChannelNormalMapPointGLWidget, 379 | # "PerChannelNormalMapSpot": PtmPerChannelNormalMapSpotGLWidget, 380 | } 381 | wnames = [k for k in self.availableGlWidgets.keys()] 382 | self.shaderCombo.addItems(wnames) 383 | self.shaderCombo.setEditable(False) 384 | self.shaderCombo.setCurrentText(wnames[0]) 385 | # self.shaderCombo.currentTextChanged.connect(self.loadPtm) 386 | self.cleanViewWidget = QtWidgets.QWidget(self.main_window) 387 | width = self.viewerWidget.width() 388 | height = self.viewerWidget.height() 389 | self.cleanViewWidget.resize(width, height) 390 | # 391 | maindir = os.curdir 392 | ptmdir = os.path.join(maindir, "ptmviewer") 393 | assetdir = os.path.join(ptmdir, "assets") 394 | self.jsondir = os.path.join(assetdir, "jsons") 395 | self.notedir = os.path.join(assetdir, "notes") 396 | 397 | # Ptm related stuff 398 | def loadPtm(self): 399 | "load ptm file into gl widget" 400 | self.viewerWidget.close() 401 | if isinstance(self.viewerWidget, PtmNormalMapGLWidget): 402 | self.viewerWidget.cleanUpGL() 403 | # 404 | citem = self.fileList.currentItem() 405 | cindex = self.fileList.indexFromItem(citem) 406 | ptmobj = self.ptmfiles[cindex] 407 | ptm = RGBPTM(ptmobj["path"]) 408 | # vertices, indices = ptm.getVerticesAndSizeArr() 409 | glchoice = self.shaderCombo.currentText() 410 | # 411 | self.runGlPipeline(glchoice, ptm) 412 | info = self.viewerWidget.getGLInfo() 413 | self.glInfoBrowser.clear() 414 | self.glInfoBrowser.setPlainText(info) 415 | 416 | def replaceViewerWidget(self, glwidget): 417 | "replace viewer widget with the given instantiated glwidget" 418 | self.viewerLayout.replaceWidget(self.viewerWidget, glwidget) 419 | self.viewerWidget = glwidget 420 | 421 | def runGlPipeline(self, glchoice: str, ptm): 422 | "run gl pipeline using given gl choice" 423 | if glchoice == "Lambertian": 424 | self.runLambertianPipeLine(glchoice, ptm) 425 | elif glchoice == "SingleNormalMap": 426 | self.runSingleNormalMapPipeLine(glchoice, ptm) 427 | elif glchoice == "PerChannelPhong": 428 | self.runPerChannelNormalMapsPipeline(glchoice, ptm) 429 | elif glchoice == "PerChannelNormalMapDir": 430 | self.runPerChannelNormalMapsPipeline(glchoice, ptm) 431 | elif glchoice == "PerChannelNormalMapPoint": 432 | self.runPerChannelNormalMapsPipeline(glchoice, ptm) 433 | elif glchoice == "PerChannelNormalMapSpot": 434 | self.runPerChannelNormalMapsPipeline(glchoice, ptm) 435 | elif glchoice == "CoefficientShader": 436 | self.runRGBCoeffShaderPipeline(glchoice, ptm) 437 | 438 | def runLambertianPipeLine(self, glchoice: str, ptm): 439 | "run lambertian pipeline" 440 | image = ptm.getImage() 441 | imqt = ImageQt.ImageQt(image) 442 | glwidget = self.availableGlWidgets[glchoice](imqt) 443 | self.replaceViewerWidget(glwidget) 444 | 445 | def runSingleNormalMapPipeLine(self, glchoice: str, ptm): 446 | "run single normal map pipeline" 447 | image = ptm.getImage() 448 | imqt = ImageQt.ImageQt(image) 449 | nmaps = ptm.getNormalMaps() 450 | nmap = nmaps[0] 451 | nmapqt = ImageQt.ImageQt(nmap) 452 | glwidget = self.availableGlWidgets[glchoice](imqt, nmapqt) 453 | self.replaceViewerWidget(glwidget) 454 | 455 | def runPerChannelNormalMapsPipeline(self, glchoice: str, ptm): 456 | "run per channel normal map pipeline" 457 | image = ptm.getImage() 458 | imqt = ImageQt.ImageQt(image) 459 | nmaps = ptm.getNormalMaps() 460 | nmaps = [ImageQt.ImageQt(nmap) for nmap in nmaps] 461 | glwidget = self.availableGlWidgets[glchoice](imqt, nmaps) 462 | self.replaceViewerWidget(glwidget) 463 | 464 | def runRGBCoeffShaderPipeline(self, glchoice: str, ptm): 465 | "pipeline for rgb coefficient shader" 466 | vertices, vertexNb = ptm.getNbVertices() 467 | glwidget = self.availableGlWidgets[glchoice](vertices, vertexNb) 468 | self.replaceViewerWidget(glwidget) 469 | 470 | def getParams(self) -> dict: 471 | "Get parameters from widgets" 472 | light_params = self.get_lamp_parameters() 473 | camera_params = self.get_camera_parameters() 474 | parameters = {} 475 | parameters["light"] = self.get_lamp_parameters() 476 | parameters["camera"] = self.get_camera_parameters() 477 | parameters["shader"] = self.get_shader_parameters() 478 | return parameters 479 | 480 | def updateParamBrowser(self): 481 | params = self.getParams() 482 | self.paramBrowser.clear() 483 | self.paramBrowser.setPlainText(str(params)) 484 | 485 | def captureParams(self): 486 | params = self.getParams() 487 | params["image-format"] = "PNG" 488 | params["image-encoding"] = "hex" 489 | screen = self.viewerWidget.grabFramebuffer() 490 | barr = QtCore.QByteArray() 491 | qbfr = QtCore.QBuffer(barr) 492 | qbfr.open(QtCore.QIODevice.WriteOnly) 493 | screen.save(qbfr, params["image-format"]) 494 | params["image"] = barr.data().hex() 495 | fileName = QtWidgets.QFileDialog.getSaveFileName( 496 | self.centralwidget, "Save Parameters", self.jsondir, "Json Files (*.json)" 497 | ) 498 | fpath = fileName[0] 499 | if fpath: 500 | with open(fpath, "w", encoding="utf-8", newline="\n") as fd: 501 | json.dump(params, fd, ensure_ascii=False, indent=2) 502 | 503 | def saveNotes(self): 504 | "Save notes in the transcription dock widget" 505 | text = self.transcribeEdit.toPlainText() 506 | fileName = QtWidgets.QFileDialog.getSaveFileName( 507 | self.centralwidget, "Save Notes", self.notedir, "Text Files (*.txt)" 508 | ) 509 | fpath = fileName[0] 510 | if fpath: 511 | with open(fpath, "w", encoding="utf-8", newline="\n") as fd: 512 | fd.write(text) 513 | 514 | def set_shininess(self): 515 | shininess = self.shinSpin.value() 516 | self.viewerWidget.change_shininess(val=shininess) 517 | 518 | ### Standard Gui Elements ### 519 | 520 | def showInterface(self): 521 | "Show the interface" 522 | self.main_window.show() 523 | 524 | def browseFolder(self): 525 | "Import ptm files from folder using file dialog" 526 | self.fileList.clear() 527 | fdir = QtWidgets.QFileDialog.getOpenFileNames( 528 | self.centralwidget, 529 | "Select PTM files", 530 | "ptmviewer/assets/ptms", 531 | "PTMs (*.ptm)", 532 | ) 533 | if fdir: 534 | for fname in fdir[0]: 535 | ptmitem = QtWidgets.QListWidgetItem(self.fileList) 536 | itemname = os.path.basename(fname) 537 | ptmitem.setText(itemname) 538 | ptmobj = {} 539 | ptmobj["path"] = fname 540 | ptmobj["name"] = itemname 541 | ptmobj["index"] = self.fileList.indexFromItem(ptmitem) 542 | self.ptmfiles[ptmobj["index"]] = ptmobj 543 | self.fileList.sortItems() 544 | 545 | def removeItems(self): 546 | "remove items from list" 547 | items = self.fileList.selectedItems() 548 | if not items: 549 | return 550 | for item in items: 551 | index = self.fileList.indexFromItem(item) 552 | itemRow = self.fileList.row(item) 553 | self.ptmfiles.pop(index) 554 | self.fileList.takeItem(itemRow) 555 | 556 | def closeApp(self, event): 557 | "Close application" 558 | reply = QtWidgets.QMessageBox.question( 559 | self.centralwidget, 560 | "Message", 561 | "Are you sure to quit?", 562 | QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, 563 | QtWidgets.QMessageBox.No, 564 | ) 565 | 566 | if reply == QtWidgets.QMessageBox.Yes: 567 | event.accept() 568 | sys.exit(0) 569 | else: 570 | event.ignore() 571 | # 572 | return 573 | 574 | def closeKey(self): 575 | sys.exit(0) 576 | 577 | 578 | if __name__ == "__main__": 579 | app = QtWidgets.QApplication(sys.argv) 580 | window = AppWindowFinal() 581 | window.showInterface() 582 | sys.exit(app.exec_()) 583 | -------------------------------------------------------------------------------- /ptmviewer/interface/window.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'interface.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.9.2 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | from PySide2 import QtCore, QtGui, QtWidgets 10 | 11 | class Ui_MainWindow(object): 12 | def setupUi(self, MainWindow): 13 | MainWindow.setObjectName("MainWindow") 14 | MainWindow.resize(1278, 901) 15 | self.centralwidget = QtWidgets.QWidget(MainWindow) 16 | self.centralwidget.setObjectName("centralwidget") 17 | self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.centralwidget) 18 | self.horizontalLayout_4.setObjectName("horizontalLayout_4") 19 | self.horizontalLayout_2 = QtWidgets.QHBoxLayout() 20 | self.horizontalLayout_2.setObjectName("horizontalLayout_2") 21 | self.viewerLayout = QtWidgets.QVBoxLayout() 22 | self.viewerLayout.setObjectName("viewerLayout") 23 | self.label_2 = QtWidgets.QLabel(self.centralwidget) 24 | self.label_2.setObjectName("label_2") 25 | self.viewerLayout.addWidget(self.label_2) 26 | self.viewerWidget = QtWidgets.QOpenGLWidget(self.centralwidget) 27 | self.viewerWidget.setObjectName("viewerWidget") 28 | self.viewerLayout.addWidget(self.viewerWidget) 29 | self.viewerLayout.setStretch(1, 1) 30 | self.horizontalLayout_2.addLayout(self.viewerLayout) 31 | self.horizontalLayout_2.setStretch(0, 1) 32 | self.horizontalLayout_4.addLayout(self.horizontalLayout_2) 33 | MainWindow.setCentralWidget(self.centralwidget) 34 | self.menubar = QtWidgets.QMenuBar(MainWindow) 35 | self.menubar.setGeometry(QtCore.QRect(0, 0, 1278, 23)) 36 | self.menubar.setObjectName("menubar") 37 | MainWindow.setMenuBar(self.menubar) 38 | self.statusbar = QtWidgets.QStatusBar(MainWindow) 39 | self.statusbar.setObjectName("statusbar") 40 | MainWindow.setStatusBar(self.statusbar) 41 | self.fileListDock = QtWidgets.QDockWidget(MainWindow) 42 | self.fileListDock.setObjectName("fileListDock") 43 | self.dockWidgetContents = QtWidgets.QWidget() 44 | self.dockWidgetContents.setObjectName("dockWidgetContents") 45 | self.verticalLayout_8 = QtWidgets.QVBoxLayout(self.dockWidgetContents) 46 | self.verticalLayout_8.setObjectName("verticalLayout_8") 47 | self.label_3 = QtWidgets.QLabel(self.dockWidgetContents) 48 | self.label_3.setObjectName("label_3") 49 | self.verticalLayout_8.addWidget(self.label_3) 50 | self.fileList = QtWidgets.QListWidget(self.dockWidgetContents) 51 | self.fileList.setObjectName("fileList") 52 | self.verticalLayout_8.addWidget(self.fileList) 53 | self.loadFile = QtWidgets.QPushButton(self.dockWidgetContents) 54 | self.loadFile.setObjectName("loadFile") 55 | self.verticalLayout_8.addWidget(self.loadFile) 56 | self.horizontalLayout_13 = QtWidgets.QHBoxLayout() 57 | self.horizontalLayout_13.setObjectName("horizontalLayout_13") 58 | self.addFile = QtWidgets.QPushButton(self.dockWidgetContents) 59 | self.addFile.setObjectName("addFile") 60 | self.horizontalLayout_13.addWidget(self.addFile) 61 | self.removeFile = QtWidgets.QPushButton(self.dockWidgetContents) 62 | self.removeFile.setObjectName("removeFile") 63 | self.horizontalLayout_13.addWidget(self.removeFile) 64 | self.verticalLayout_8.addLayout(self.horizontalLayout_13) 65 | self.fileListDock.setWidget(self.dockWidgetContents) 66 | MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.fileListDock) 67 | self.colorDock = QtWidgets.QDockWidget(MainWindow) 68 | self.colorDock.setObjectName("colorDock") 69 | self.dockWidgetContents_2 = QtWidgets.QWidget() 70 | self.dockWidgetContents_2.setObjectName("dockWidgetContents_2") 71 | self.verticalLayout_11 = QtWidgets.QVBoxLayout(self.dockWidgetContents_2) 72 | self.verticalLayout_11.setObjectName("verticalLayout_11") 73 | self.verticalLayout_18 = QtWidgets.QVBoxLayout() 74 | self.verticalLayout_18.setObjectName("verticalLayout_18") 75 | self.verticalLayout_16 = QtWidgets.QVBoxLayout() 76 | self.verticalLayout_16.setObjectName("verticalLayout_16") 77 | self.label_17 = QtWidgets.QLabel(self.dockWidgetContents_2) 78 | self.label_17.setObjectName("label_17") 79 | self.verticalLayout_16.addWidget(self.label_17) 80 | self.groupBox_3 = QtWidgets.QGroupBox(self.dockWidgetContents_2) 81 | self.groupBox_3.setObjectName("groupBox_3") 82 | self.horizontalLayout_5 = QtWidgets.QHBoxLayout(self.groupBox_3) 83 | self.horizontalLayout_5.setObjectName("horizontalLayout_5") 84 | self.ambientRBtn = QtWidgets.QRadioButton(self.groupBox_3) 85 | self.ambientRBtn.setObjectName("ambientRBtn") 86 | self.horizontalLayout_5.addWidget(self.ambientRBtn) 87 | self.diffuseRBtn = QtWidgets.QRadioButton(self.groupBox_3) 88 | self.diffuseRBtn.setChecked(True) 89 | self.diffuseRBtn.setObjectName("diffuseRBtn") 90 | self.horizontalLayout_5.addWidget(self.diffuseRBtn) 91 | self.specularRBtn = QtWidgets.QRadioButton(self.groupBox_3) 92 | self.specularRBtn.setChecked(False) 93 | self.specularRBtn.setObjectName("specularRBtn") 94 | self.horizontalLayout_5.addWidget(self.specularRBtn) 95 | self.verticalLayout_16.addWidget(self.groupBox_3) 96 | self.horizontalLayout_10 = QtWidgets.QHBoxLayout() 97 | self.horizontalLayout_10.setObjectName("horizontalLayout_10") 98 | self.verticalLayout_21 = QtWidgets.QVBoxLayout() 99 | self.verticalLayout_21.setObjectName("verticalLayout_21") 100 | self.label_9 = QtWidgets.QLabel(self.dockWidgetContents_2) 101 | self.label_9.setObjectName("label_9") 102 | self.verticalLayout_21.addWidget(self.label_9) 103 | self.label_10 = QtWidgets.QLabel(self.dockWidgetContents_2) 104 | self.label_10.setObjectName("label_10") 105 | self.verticalLayout_21.addWidget(self.label_10) 106 | self.label_16 = QtWidgets.QLabel(self.dockWidgetContents_2) 107 | self.label_16.setObjectName("label_16") 108 | self.verticalLayout_21.addWidget(self.label_16) 109 | self.horizontalLayout_10.addLayout(self.verticalLayout_21) 110 | self.verticalLayout_12 = QtWidgets.QVBoxLayout() 111 | self.verticalLayout_12.setObjectName("verticalLayout_12") 112 | self.label_6 = QtWidgets.QLabel(self.dockWidgetContents_2) 113 | self.label_6.setObjectName("label_6") 114 | self.verticalLayout_12.addWidget(self.label_6) 115 | self.intensityR = QtWidgets.QDoubleSpinBox(self.dockWidgetContents_2) 116 | self.intensityR.setMaximum(1.0) 117 | self.intensityR.setSingleStep(0.01) 118 | self.intensityR.setProperty("value", 1.0) 119 | self.intensityR.setObjectName("intensityR") 120 | self.verticalLayout_12.addWidget(self.intensityR) 121 | self.coefficientR = QtWidgets.QDoubleSpinBox(self.dockWidgetContents_2) 122 | self.coefficientR.setMaximum(1.0) 123 | self.coefficientR.setSingleStep(0.01) 124 | self.coefficientR.setProperty("value", 1.0) 125 | self.coefficientR.setObjectName("coefficientR") 126 | self.verticalLayout_12.addWidget(self.coefficientR) 127 | self.verticalLayout_12.setStretch(1, 1) 128 | self.horizontalLayout_10.addLayout(self.verticalLayout_12) 129 | self.verticalLayout_13 = QtWidgets.QVBoxLayout() 130 | self.verticalLayout_13.setObjectName("verticalLayout_13") 131 | self.label_14 = QtWidgets.QLabel(self.dockWidgetContents_2) 132 | self.label_14.setObjectName("label_14") 133 | self.verticalLayout_13.addWidget(self.label_14) 134 | self.intensityG = QtWidgets.QDoubleSpinBox(self.dockWidgetContents_2) 135 | self.intensityG.setMaximum(1.0) 136 | self.intensityG.setSingleStep(0.01) 137 | self.intensityG.setProperty("value", 1.0) 138 | self.intensityG.setObjectName("intensityG") 139 | self.verticalLayout_13.addWidget(self.intensityG) 140 | self.coefficientG = QtWidgets.QDoubleSpinBox(self.dockWidgetContents_2) 141 | self.coefficientG.setMaximum(1.0) 142 | self.coefficientG.setSingleStep(0.01) 143 | self.coefficientG.setProperty("value", 1.0) 144 | self.coefficientG.setObjectName("coefficientG") 145 | self.verticalLayout_13.addWidget(self.coefficientG) 146 | self.horizontalLayout_10.addLayout(self.verticalLayout_13) 147 | self.verticalLayout_14 = QtWidgets.QVBoxLayout() 148 | self.verticalLayout_14.setObjectName("verticalLayout_14") 149 | self.label_15 = QtWidgets.QLabel(self.dockWidgetContents_2) 150 | self.label_15.setObjectName("label_15") 151 | self.verticalLayout_14.addWidget(self.label_15) 152 | self.intensityB = QtWidgets.QDoubleSpinBox(self.dockWidgetContents_2) 153 | self.intensityB.setMaximum(1.0) 154 | self.intensityB.setSingleStep(0.01) 155 | self.intensityB.setProperty("value", 1.0) 156 | self.intensityB.setObjectName("intensityB") 157 | self.verticalLayout_14.addWidget(self.intensityB) 158 | self.coefficientB = QtWidgets.QDoubleSpinBox(self.dockWidgetContents_2) 159 | self.coefficientB.setMaximum(1.0) 160 | self.coefficientB.setSingleStep(0.01) 161 | self.coefficientB.setProperty("value", 1.0) 162 | self.coefficientB.setObjectName("coefficientB") 163 | self.verticalLayout_14.addWidget(self.coefficientB) 164 | self.horizontalLayout_10.addLayout(self.verticalLayout_14) 165 | self.verticalLayout_16.addLayout(self.horizontalLayout_10) 166 | self.verticalLayout_18.addLayout(self.verticalLayout_16) 167 | self.verticalLayout_11.addLayout(self.verticalLayout_18) 168 | self.colorDock.setWidget(self.dockWidgetContents_2) 169 | MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.colorDock) 170 | self.rotateDock = QtWidgets.QDockWidget(MainWindow) 171 | self.rotateDock.setObjectName("rotateDock") 172 | self.dockWidgetContents_3 = QtWidgets.QWidget() 173 | self.dockWidgetContents_3.setObjectName("dockWidgetContents_3") 174 | self.verticalLayout_19 = QtWidgets.QVBoxLayout(self.dockWidgetContents_3) 175 | self.verticalLayout_19.setObjectName("verticalLayout_19") 176 | self.verticalLayout_9 = QtWidgets.QVBoxLayout() 177 | self.verticalLayout_9.setObjectName("verticalLayout_9") 178 | self.groupBox_2 = QtWidgets.QGroupBox(self.dockWidgetContents_3) 179 | self.groupBox_2.setObjectName("groupBox_2") 180 | self.horizontalLayout_14 = QtWidgets.QHBoxLayout(self.groupBox_2) 181 | self.horizontalLayout_14.setObjectName("horizontalLayout_14") 182 | self.rotCamRBtn = QtWidgets.QRadioButton(self.groupBox_2) 183 | self.rotCamRBtn.setObjectName("rotCamRBtn") 184 | self.horizontalLayout_14.addWidget(self.rotCamRBtn) 185 | self.rotLightRBtn = QtWidgets.QRadioButton(self.groupBox_2) 186 | self.rotLightRBtn.setChecked(True) 187 | self.rotLightRBtn.setObjectName("rotLightRBtn") 188 | self.horizontalLayout_14.addWidget(self.rotLightRBtn) 189 | self.verticalLayout_9.addWidget(self.groupBox_2) 190 | self.horizontalLayout_7 = QtWidgets.QHBoxLayout() 191 | self.horizontalLayout_7.setObjectName("horizontalLayout_7") 192 | self.rotXCbox = QtWidgets.QCheckBox(self.dockWidgetContents_3) 193 | self.rotXCbox.setObjectName("rotXCbox") 194 | self.horizontalLayout_7.addWidget(self.rotXCbox) 195 | self.rotYCbox = QtWidgets.QCheckBox(self.dockWidgetContents_3) 196 | self.rotYCbox.setObjectName("rotYCbox") 197 | self.horizontalLayout_7.addWidget(self.rotYCbox) 198 | self.rotZCbox = QtWidgets.QCheckBox(self.dockWidgetContents_3) 199 | self.rotZCbox.setObjectName("rotZCbox") 200 | self.horizontalLayout_7.addWidget(self.rotZCbox) 201 | self.verticalLayout_6 = QtWidgets.QVBoxLayout() 202 | self.verticalLayout_6.setObjectName("verticalLayout_6") 203 | self.angleLbl = QtWidgets.QLabel(self.dockWidgetContents_3) 204 | self.angleLbl.setObjectName("angleLbl") 205 | self.verticalLayout_6.addWidget(self.angleLbl) 206 | self.angleSpin = QtWidgets.QDoubleSpinBox(self.dockWidgetContents_3) 207 | self.angleSpin.setMinimum(-360.0) 208 | self.angleSpin.setMaximum(360.0) 209 | self.angleSpin.setSingleStep(1.0) 210 | self.angleSpin.setProperty("value", 1.0) 211 | self.angleSpin.setObjectName("angleSpin") 212 | self.verticalLayout_6.addWidget(self.angleSpin) 213 | self.horizontalLayout_7.addLayout(self.verticalLayout_6) 214 | self.verticalLayout_9.addLayout(self.horizontalLayout_7) 215 | self.verticalLayout_19.addLayout(self.verticalLayout_9) 216 | self.rotateDock.setWidget(self.dockWidgetContents_3) 217 | MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.rotateDock) 218 | self.moveDock = QtWidgets.QDockWidget(MainWindow) 219 | self.moveDock.setObjectName("moveDock") 220 | self.dockWidgetContents_4 = QtWidgets.QWidget() 221 | self.dockWidgetContents_4.setObjectName("dockWidgetContents_4") 222 | self.verticalLayout_22 = QtWidgets.QVBoxLayout(self.dockWidgetContents_4) 223 | self.verticalLayout_22.setObjectName("verticalLayout_22") 224 | self.verticalLayout_7 = QtWidgets.QVBoxLayout() 225 | self.verticalLayout_7.setObjectName("verticalLayout_7") 226 | self.horizontalLayout_9 = QtWidgets.QHBoxLayout() 227 | self.horizontalLayout_9.setObjectName("horizontalLayout_9") 228 | self.groupBox = QtWidgets.QGroupBox(self.dockWidgetContents_4) 229 | self.groupBox.setObjectName("groupBox") 230 | self.horizontalLayout_12 = QtWidgets.QHBoxLayout(self.groupBox) 231 | self.horizontalLayout_12.setObjectName("horizontalLayout_12") 232 | self.moveLightRBtn = QtWidgets.QRadioButton(self.groupBox) 233 | self.moveLightRBtn.setObjectName("moveLightRBtn") 234 | self.horizontalLayout_12.addWidget(self.moveLightRBtn) 235 | self.moveCameraRBtn = QtWidgets.QRadioButton(self.groupBox) 236 | self.moveCameraRBtn.setChecked(True) 237 | self.moveCameraRBtn.setObjectName("moveCameraRBtn") 238 | self.horizontalLayout_12.addWidget(self.moveCameraRBtn) 239 | self.horizontalLayout_9.addWidget(self.groupBox) 240 | self.verticalLayout_7.addLayout(self.horizontalLayout_9) 241 | self.horizontalLayout = QtWidgets.QHBoxLayout() 242 | self.horizontalLayout.setObjectName("horizontalLayout") 243 | self.horizontalLayout_3 = QtWidgets.QHBoxLayout() 244 | self.horizontalLayout_3.setObjectName("horizontalLayout_3") 245 | self.moveXCBox = QtWidgets.QCheckBox(self.dockWidgetContents_4) 246 | self.moveXCBox.setObjectName("moveXCBox") 247 | self.horizontalLayout_3.addWidget(self.moveXCBox) 248 | self.moveYCBox = QtWidgets.QCheckBox(self.dockWidgetContents_4) 249 | self.moveYCBox.setObjectName("moveYCBox") 250 | self.horizontalLayout_3.addWidget(self.moveYCBox) 251 | self.moveZCBox = QtWidgets.QCheckBox(self.dockWidgetContents_4) 252 | self.moveZCBox.setObjectName("moveZCBox") 253 | self.horizontalLayout_3.addWidget(self.moveZCBox) 254 | self.horizontalLayout.addLayout(self.horizontalLayout_3) 255 | self.verticalLayout_3 = QtWidgets.QVBoxLayout() 256 | self.verticalLayout_3.setObjectName("verticalLayout_3") 257 | self.moveUp = QtWidgets.QPushButton(self.dockWidgetContents_4) 258 | self.moveUp.setAutoRepeat(True) 259 | self.moveUp.setObjectName("moveUp") 260 | self.verticalLayout_3.addWidget(self.moveUp) 261 | self.moveDown = QtWidgets.QPushButton(self.dockWidgetContents_4) 262 | self.moveDown.setAutoRepeat(True) 263 | self.moveDown.setObjectName("moveDown") 264 | self.verticalLayout_3.addWidget(self.moveDown) 265 | self.horizontalLayout.addLayout(self.verticalLayout_3) 266 | self.verticalLayout_7.addLayout(self.horizontalLayout) 267 | self.verticalLayout_22.addLayout(self.verticalLayout_7) 268 | self.moveDock.setWidget(self.dockWidgetContents_4) 269 | MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.moveDock) 270 | self.paramsDock = QtWidgets.QDockWidget(MainWindow) 271 | self.paramsDock.setObjectName("paramsDock") 272 | self.dockWidgetContents_7 = QtWidgets.QWidget() 273 | self.dockWidgetContents_7.setObjectName("dockWidgetContents_7") 274 | self.verticalLayout_10 = QtWidgets.QVBoxLayout(self.dockWidgetContents_7) 275 | self.verticalLayout_10.setObjectName("verticalLayout_10") 276 | self.label_4 = QtWidgets.QLabel(self.dockWidgetContents_7) 277 | self.label_4.setObjectName("label_4") 278 | self.verticalLayout_10.addWidget(self.label_4) 279 | self.paramBrowser = QtWidgets.QTextBrowser(self.dockWidgetContents_7) 280 | self.paramBrowser.setObjectName("paramBrowser") 281 | self.verticalLayout_10.addWidget(self.paramBrowser) 282 | self.captureBtn = QtWidgets.QPushButton(self.dockWidgetContents_7) 283 | self.captureBtn.setObjectName("captureBtn") 284 | self.verticalLayout_10.addWidget(self.captureBtn) 285 | self.paramsDock.setWidget(self.dockWidgetContents_7) 286 | MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(1), self.paramsDock) 287 | self.transcribeDock = QtWidgets.QDockWidget(MainWindow) 288 | self.transcribeDock.setObjectName("transcribeDock") 289 | self.dockWidgetContents_8 = QtWidgets.QWidget() 290 | self.dockWidgetContents_8.setObjectName("dockWidgetContents_8") 291 | self.verticalLayout_15 = QtWidgets.QVBoxLayout(self.dockWidgetContents_8) 292 | self.verticalLayout_15.setObjectName("verticalLayout_15") 293 | self.label_5 = QtWidgets.QLabel(self.dockWidgetContents_8) 294 | self.label_5.setObjectName("label_5") 295 | self.verticalLayout_15.addWidget(self.label_5) 296 | self.transcribeEdit = QtWidgets.QPlainTextEdit(self.dockWidgetContents_8) 297 | self.transcribeEdit.setObjectName("transcribeEdit") 298 | self.verticalLayout_15.addWidget(self.transcribeEdit) 299 | self.saveBtn = QtWidgets.QPushButton(self.dockWidgetContents_8) 300 | self.saveBtn.setObjectName("saveBtn") 301 | self.verticalLayout_15.addWidget(self.saveBtn) 302 | self.transcribeDock.setWidget(self.dockWidgetContents_8) 303 | MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(1), self.transcribeDock) 304 | self.glInfoDock = QtWidgets.QDockWidget(MainWindow) 305 | self.glInfoDock.setObjectName("glInfoDock") 306 | self.dockWidgetContents_5 = QtWidgets.QWidget() 307 | self.dockWidgetContents_5.setObjectName("dockWidgetContents_5") 308 | self.verticalLayout_20 = QtWidgets.QVBoxLayout(self.dockWidgetContents_5) 309 | self.verticalLayout_20.setObjectName("verticalLayout_20") 310 | self.label_8 = QtWidgets.QLabel(self.dockWidgetContents_5) 311 | self.label_8.setObjectName("label_8") 312 | self.verticalLayout_20.addWidget(self.label_8) 313 | self.glInfoBrowser = QtWidgets.QTextBrowser(self.dockWidgetContents_5) 314 | self.glInfoBrowser.setObjectName("glInfoBrowser") 315 | self.verticalLayout_20.addWidget(self.glInfoBrowser) 316 | self.glInfoDock.setWidget(self.dockWidgetContents_5) 317 | MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(1), self.glInfoDock) 318 | self.shaderDock = QtWidgets.QDockWidget(MainWindow) 319 | self.shaderDock.setObjectName("shaderDock") 320 | self.dockWidgetContents_9 = QtWidgets.QWidget() 321 | self.dockWidgetContents_9.setObjectName("dockWidgetContents_9") 322 | self.verticalLayout_17 = QtWidgets.QVBoxLayout(self.dockWidgetContents_9) 323 | self.verticalLayout_17.setObjectName("verticalLayout_17") 324 | self.horizontalLayout_11 = QtWidgets.QHBoxLayout() 325 | self.horizontalLayout_11.setObjectName("horizontalLayout_11") 326 | self.verticalLayout_2 = QtWidgets.QVBoxLayout() 327 | self.verticalLayout_2.setObjectName("verticalLayout_2") 328 | self.label_13 = QtWidgets.QLabel(self.dockWidgetContents_9) 329 | self.label_13.setObjectName("label_13") 330 | self.verticalLayout_2.addWidget(self.label_13) 331 | self.shaderCombo = QtWidgets.QComboBox(self.dockWidgetContents_9) 332 | self.shaderCombo.setCurrentText("") 333 | self.shaderCombo.setObjectName("shaderCombo") 334 | self.verticalLayout_2.addWidget(self.shaderCombo) 335 | self.horizontalLayout_11.addLayout(self.verticalLayout_2) 336 | self.verticalLayout = QtWidgets.QVBoxLayout() 337 | self.verticalLayout.setObjectName("verticalLayout") 338 | self.label = QtWidgets.QLabel(self.dockWidgetContents_9) 339 | self.label.setObjectName("label") 340 | self.verticalLayout.addWidget(self.label) 341 | self.shinSpin = QtWidgets.QDoubleSpinBox(self.dockWidgetContents_9) 342 | self.shinSpin.setMinimum(1.0) 343 | self.shinSpin.setMaximum(300.0) 344 | self.shinSpin.setSingleStep(0.1) 345 | self.shinSpin.setObjectName("shinSpin") 346 | self.verticalLayout.addWidget(self.shinSpin) 347 | self.verticalLayout.setStretch(1, 1) 348 | self.horizontalLayout_11.addLayout(self.verticalLayout) 349 | self.verticalLayout_17.addLayout(self.horizontalLayout_11) 350 | self.shaderDock.setWidget(self.dockWidgetContents_9) 351 | MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(1), self.shaderDock) 352 | 353 | self.retranslateUi(MainWindow) 354 | QtCore.QMetaObject.connectSlotsByName(MainWindow) 355 | 356 | def retranslateUi(self, MainWindow): 357 | _translate = QtCore.QCoreApplication.translate 358 | MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) 359 | self.label_2.setText(_translate("MainWindow", "GL Viewer")) 360 | self.label_3.setText(_translate("MainWindow", "File List")) 361 | self.loadFile.setText(_translate("MainWindow", "Load")) 362 | self.addFile.setText(_translate("MainWindow", "Add File(s)")) 363 | self.removeFile.setText(_translate("MainWindow", "Remove File(s)")) 364 | self.label_17.setText(_translate("MainWindow", "Light Control")) 365 | self.groupBox_3.setTitle(_translate("MainWindow", "Light Sources")) 366 | self.ambientRBtn.setText(_translate("MainWindow", "Ambient")) 367 | self.diffuseRBtn.setText(_translate("MainWindow", "Diffuse")) 368 | self.specularRBtn.setText(_translate("MainWindow", "Specular")) 369 | self.label_9.setText(_translate("MainWindow", "Channel")) 370 | self.label_10.setText(_translate("MainWindow", "Intensity")) 371 | self.label_16.setText(_translate("MainWindow", "Coefficient")) 372 | self.label_6.setText(_translate("MainWindow", "red")) 373 | self.label_14.setText(_translate("MainWindow", "green")) 374 | self.label_15.setText(_translate("MainWindow", "blue")) 375 | self.groupBox_2.setTitle(_translate("MainWindow", "Rotate")) 376 | self.rotCamRBtn.setText(_translate("MainWindow", "Rotate Camera")) 377 | self.rotLightRBtn.setText(_translate("MainWindow", "Rotate Light")) 378 | self.rotXCbox.setText(_translate("MainWindow", "X axis")) 379 | self.rotYCbox.setText(_translate("MainWindow", "Y axis")) 380 | self.rotZCbox.setText(_translate("MainWindow", "Z axis")) 381 | self.angleLbl.setText(_translate("MainWindow", "Angle")) 382 | self.groupBox.setTitle(_translate("MainWindow", "Move")) 383 | self.moveLightRBtn.setText(_translate("MainWindow", "Move Light")) 384 | self.moveCameraRBtn.setText(_translate("MainWindow", "Move Camera")) 385 | self.moveXCBox.setText(_translate("MainWindow", "X axis")) 386 | self.moveYCBox.setText(_translate("MainWindow", "Y axis")) 387 | self.moveZCBox.setText(_translate("MainWindow", "Z axis")) 388 | self.moveUp.setText(_translate("MainWindow", "+")) 389 | self.moveDown.setText(_translate("MainWindow", "-")) 390 | self.label_4.setText(_translate("MainWindow", "Parameters")) 391 | self.captureBtn.setText(_translate("MainWindow", "capture")) 392 | self.label_5.setText(_translate("MainWindow", "Transcribe")) 393 | self.saveBtn.setText(_translate("MainWindow", "save")) 394 | self.label_8.setText(_translate("MainWindow", "Open GL Info")) 395 | self.label_13.setText(_translate("MainWindow", "Shader")) 396 | self.label.setText(_translate("MainWindow", "Shininess")) 397 | 398 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------