├── src ├── __init__.py ├── base │ ├── __init__.py │ └── msg_queue.py ├── core │ ├── __init__.py │ ├── telephoto.dat │ ├── telephoto.250mm.dat │ ├── dgauss.50mm.dat │ ├── dgauss.dat │ ├── fisheye.10mm.dat │ ├── fisheye.dat │ ├── wide.dat │ ├── wide.22mm.dat │ ├── test.py │ ├── renderer_utils.py │ ├── renderer.py │ └── realistic.py └── gui │ ├── __init__.py │ ├── lens_preset.py │ ├── app.py │ ├── widget.py │ └── lens_designer.py ├── .gitignore ├── feature.jpg ├── setup.py └── README.md /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/base/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/gui/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__ 2 | .vim 3 | dist -------------------------------------------------------------------------------- /feature.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yslib/Cameray/HEAD/feature.jpg -------------------------------------------------------------------------------- /src/core/telephoto.dat: -------------------------------------------------------------------------------- 1 | # SIGLER Super achromate telephoto, EFL=254mm, F/5.6 2 | # MLD, Page 175 3 | # radius axpos N aperture 4 | 21.851 0 1.529 19.0 5 | -34.546 5.008 1.599 17.8 6 | 108.705 1.502 1.0 16.6 7 | 0 1.127 0 16.2 8 | -12.852 26.965 1.613 12.6 9 | 19.813 1.502 1.603 13.4 10 | -20.378 5.008 1.0 14.8 -------------------------------------------------------------------------------- /src/core/telephoto.250mm.dat: -------------------------------------------------------------------------------- 1 | # SIGLER Super achromate telephoto, EFL=254mm, F/5.6" 2 | # MLD, Page 175" 3 | # Scaled to 250 mm from 100 mm 4 | 54.6275 12.52 1.529 47.5 5 | -86.365 3.755 1.599 44.5 6 | 271.7625 2.8175 1 41.5 7 | 0 67.4125 0 40.5 8 | -32.13 3.755 1.613 31.5 9 | 49.5325 12.52 1.603 33.5 10 | -50.945 0 1 37 11 | -------------------------------------------------------------------------------- /src/core/dgauss.50mm.dat: -------------------------------------------------------------------------------- 1 | # D-GAUSS F/2 22deg HFOV 2 | # US patent 2,673,491 Tronnier" 3 | # Moden Lens Design, p.312" 4 | # Scaled to 50 mm from 100 mm 5 | # radius axpos N aperture 6 | 29.475 3.76 1.67 25.2 7 | 84.83 0.12 1 25.2 8 | 19.275 4.025 1.67 23 9 | 40.77 3.275 1.699 23 10 | 12.75 5.705 1 18 11 | 0 4.5 0 17.1 12 | -14.495 1.18 1.603 17 13 | 40.77 6.065 1.658 20 14 | -20.385 0.19 1 20 15 | 437.065 3.22 1.717 20 16 | -39.73 0 1 20 17 | -------------------------------------------------------------------------------- /src/core/dgauss.dat: -------------------------------------------------------------------------------- 1 | # D-GAUSS F/2 22deg HFOV 2 | # US patent 2,673,491 Tronnier 3 | # Moden Lens Design, p.312 4 | # radius axpos N aperture 5 | 58.950 0.000 1.670 50.4 6 | 169.660 7.520 1.0 50.4 7 | 38.550 0.240 1.670 46.0 8 | 81.540 8.050 1.699 46.0 9 | 25.500 6.550 1.0 36.0 10 | 0 11.410 0 34.2 11 | -28.990 9.000 1.603 34.0 12 | 81.540 2.360 1.658 40.0 13 | -40.770 12.130 1.0 40.0 14 | 874.130 0.380 1.717 40.0 15 | -79.460 6.440 1.0 40.0 -------------------------------------------------------------------------------- /src/base/msg_queue.py: -------------------------------------------------------------------------------- 1 | import queue 2 | import functools 3 | 4 | _msg_queue = queue.Queue(50) 5 | 6 | _main_loop_coroutine = [] 7 | 8 | def msg(func): 9 | @functools.wraps(func) 10 | def wrapper(*args, **kwargs): 11 | global _msg_queue 12 | _msg_queue.put(lambda:func(*args,**kwargs)) 13 | return wrapper 14 | 15 | 16 | def get_msg_queue(): 17 | return _msg_queue 18 | 19 | def get_coroutine(): 20 | return _main_loop_coroutine 21 | -------------------------------------------------------------------------------- /src/core/fisheye.10mm.dat: -------------------------------------------------------------------------------- 1 | # Muller 16mm/f4 155.9FOV fisheye lens 2 | # MLD p164 3 | # Scaled to 10 mm from 100 mm 4 | # radius sep n aperture 5 | 30.2249 0.8335 1.62 30.34 6 | 11.3931 7.4136 1 20.68 7 | 75.2019 1.0654 1.639 17.8 8 | 8.3349 11.1549 1 13.42 9 | 9.5882 2.0054 1.654 9.02 10 | 43.8677 5.3895 1 8.14 11 | 0 1.4163 0 6.08 12 | 29.4541 2.1934 1.517 5.96 13 | -5.2265 0.9714 1.805 5.84 14 | -14.2884 0.0627 1 5.96 15 | -22.3726 0.94 1.673 5.96 16 | -15.0404 0 1 6.52 17 | -------------------------------------------------------------------------------- /src/core/fisheye.dat: -------------------------------------------------------------------------------- 1 | # Muller 16mm/f4 155.9FOV fisheye lens 2 | # MLD p164 3 | # radius axpos N aperture 4 | 302.249 0 1.620 303.4 5 | 113.931 8.335 1.0 206.8 6 | 752.019 74.136 1.639 178.0 7 | 83.349 10.654 1.0 134.2 8 | 95.882 111.549 1.654 90.2 9 | 438.677 20.054 1.0 81.4 10 | 0 53.895 0 60.8 11 | 294.541 14.163 1.517 59.6 12 | -52.265 21.934 1.805 58.4 13 | -142.884 9.714 1.0 59.6 14 | -223.726 0.627 1.673 59.6 15 | -150.404 9.400 1.0 65.2 -------------------------------------------------------------------------------- /src/core/wide.dat: -------------------------------------------------------------------------------- 1 | # Wide-angle (38-degree) lens. Nakamura. 2 | # MLD, p. 360 3 | # radius axpos N aperture 4 | 163.579 0 1.540 107.8 5 | 53.169 5.529 1.0 81.8 6 | 59.487 45.435 1.772 56.2 7 | -102.877 23.301 1.617 44.6 8 | 322.991 8.042 1.0 41.6 9 | 0 3.720 0.0 39.8 10 | -43.572 10.353 1.617 37.2 11 | -51.312 11.057 1.0 41.6 12 | -758.075 0.523 1.713 48.4 13 | -34.505 14.073 1.805 52.0 14 | -76.210 6.031 1.0 55.8 15 | -35.013 18.094 1.617 61.0 16 | -54.424 5.529 1.0 81.8 -------------------------------------------------------------------------------- /src/core/wide.22mm.dat: -------------------------------------------------------------------------------- 1 | # Wide-angle (38-degree) lens. Nakamura. 2 | # MLD, p. 360" 3 | # Scaled to 22 mm from 100 mm 4 | # radius sep n aperture 5 | 35.98738 1.21638 1.54 23.716 6 | 11.69718 9.9957 1 17.996 7 | 13.08714 5.12622 1.772 12.364 8 | -22.63294 1.76924 1.617 9.812 9 | 71.05802 0.8184 1 9.152 10 | 0 2.27766 0 8.756 11 | -9.58584 2.43254 1.617 8.184 12 | -11.28864 0.11506 1 9.152 13 | -166.7765 3.09606 1.713 10.648 14 | -7.5911 1.32682 1.805 11.44 15 | -16.7662 3.98068 1 12.276 16 | -7.70286 1.21638 1.617 13.42 17 | -11.97328 0 1 17.996 18 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( name='cameray', 4 | install_requires=[ 5 | 'networkx', 6 | 'taichi', 7 | 'numpy', 8 | 'dearpygui', 9 | 'dearpygui_ext' 10 | ], 11 | setup_requires=[], 12 | python_requires='>=3.6, <=3.9', 13 | version='0.0.2', 14 | 15 | author='Shuoliu Yang', 16 | 17 | author_email='visysl@outlook.com', 18 | 19 | url='https://github.com/yslib/Cameray', 20 | 21 | package_dir={'':'src'}, 22 | packages=find_packages(where='src'), 23 | 24 | entry_points={ 25 | 'console_scripts':[ 26 | 'cameray = gui.app:main' 27 | ] 28 | 29 | }, 30 | 31 | classifiers = [ 32 | # https://pypi.org/pypi?%3Aaction=list_classifiers 33 | 'Development Status :: 3 - Alpha', 34 | 'Intended Audience :: End Users/Desktop', 35 | 'Topic :: Software Development :: Build Tools', 36 | 'License :: OSI Approved :: MIT License', 37 | 'Programming Language :: Python :: 3.6', 38 | 'Programming Language :: Python :: 3.7', 39 | 'Programming Language :: Python :: 3.8', 40 | 'Programming Language :: Python :: 3.8', 41 | ] 42 | ) 43 | -------------------------------------------------------------------------------- /src/gui/lens_preset.py: -------------------------------------------------------------------------------- 1 | 2 | wide22 = [ 3 | # curvature radius, thickness, index of refraction, aperture diameter 4 | [35.98738, 1.21638, 1.54, 23.716], 5 | [11.69718, 9.9957, 1, 17.996], 6 | [13.08714, 5.12622, 1.772, 12.364], 7 | [22.63294, 1.76924, 1.617, 9.812], 8 | [71.05802, 0.8184, 1, 9.152], 9 | [0, 2.27766,0, 8.756], 10 | [9.58584,2.43254,1.617,8.184], 11 | [11.28864,0.11506,1,9.152], 12 | [166.7765,3.09606,1.713,10.648], 13 | [7.5911,1.32682,1.805,11.44], 14 | [16.7662,3.98068,1,12.276], 15 | [7.70286,1.21638,1.617,13.42], 16 | [11.97328,10,1,17.996] 17 | ] 18 | dgauss50 = [ 19 | [29.475,3.76,1.67,25.2], 20 | [84.83,0.12,1,25.2], 21 | [19.275,4.025,1.67,23], 22 | [40.77,3.275,1.699,23], 23 | [12.75,5.705,1,18], 24 | [0,4.5,0,17.1], 25 | [-14.495,1.18,1.603,17], 26 | [40.77,6.065,1.658,20], 27 | [-20.385,0.19,1,20], 28 | [437.065,3.22,1.717,20], 29 | [-39.73,5.0,1,20] 30 | ] 31 | telephoto=[ 32 | [21.851,0 ,1.529,19.0], 33 | [-34.546,5.008,1.599,17.8], 34 | [108.705,1.502,1.0,16.6], 35 | [ 0,1.127,0,16.2], 36 | [-12.852,26.965,1.613,12.6], 37 | [19.813,1.502,1.603,13.4], 38 | [-20.378,5.008,1.0,14.8] 39 | ] 40 | telephoto250=[ 41 | [54.6275,12.52,1.529,47.5], 42 | [-86.365,3.755,1.599,44.5], 43 | [271.7625,2.8175,1,41.5], 44 | [0,67.4125,0,40.5], 45 | [-32.13,3.755,1.613,31.5], 46 | [49.5325,12.52,1.603,33.5], 47 | [-50.945,0,1,37] 48 | ] 49 | 50 | lens_data={ 51 | 'dgauss50':dgauss50, 52 | 'wide22':wide22, 53 | 'telephoto':telephoto, 54 | 'telephoto250':telephoto250 55 | } -------------------------------------------------------------------------------- /src/core/test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import taichi as ti 3 | from realistic import RealisticCamera 4 | ti.init(arch=ti.gpu) 5 | 6 | pos = [10000.0,10000.0,10000.0] # mm 7 | center = [0.0,0.0,0.0] 8 | world_up = [0.0,1.0,0.0] 9 | resolution = [400, 400] 10 | cam = RealisticCamera(resolution, pos, center, world_up) 11 | 12 | pos = [10.0,10.0,10.0 ,1.0] 13 | center = [0.0,0.0,0.0, 1.0] 14 | up = [0.0,1.0, 0.0,1.0] 15 | 16 | 17 | v = ti.Vector([0.0,0.0,0.0,0.0]) 18 | v = ti.Vector([*pos]) 19 | matr = ti.Matrix([pos,pos,pos,pos]) 20 | 21 | @ti.kernel 22 | def test_focal_length(): 23 | print('front z: ', cam.front_z()) 24 | print('lens z: ', cam.rear_z()) 25 | 26 | for i in ti.static(range(3)): 27 | x = 0.1 + i * 0.1 28 | so = ti.Vector([x, 0.0, 2000.0]) 29 | sd = ti.Vector([0.0, 0.0, -1.0]) 30 | fo = ti.Vector([x, 0.0, cam.rear_z() - 1.0]) 31 | fd = ti.Vector([0.0,0.0,1.0]) 32 | ok1, o1, d1 = cam.gen_ray_from_scene(so, sd) 33 | ok2, o2, d2 = cam.gen_ray_from_film(fo, fd) 34 | tf = -o1.x / d1.x 35 | tf2 = -o2.x / d2.x 36 | 37 | fz1,pz1, fz2, pz2 = cam.compute_thick_lens_approximation() 38 | print('fz1, pz1, fz2, pz2: ', fz1, pz1, fz2, pz2) 39 | print('first focal length, second focal length', cam.get_focal_length()) 40 | 41 | @ti.kernel 42 | def test_pupils(): 43 | cam.refocus(1200) 44 | cam.recompute_exit_pupil() 45 | 46 | # test_pupils() 47 | 48 | cam.refocus(10000.0) 49 | cam.recompute_exit_pupil() 50 | 51 | @ti.kernel 52 | def gen_ray_test(): 53 | cam.vignet[None] = 0 54 | for i in range(600): 55 | for j in range(400): 56 | weight, ray = cam.gen_ray_of(i, j) 57 | if weight > 0.0: 58 | print(i, j) 59 | 60 | gen_ray_test() 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cameray 2 | --- 3 | 4 | **Cameray** is a lens editor and simulator for fun. It's could be used for studying an optics system of DSLR in an interactive way. But the project is in a very early version. The program is still crash-prone and also lack of many realistic camera features now. 5 | 6 | 7 | ![](./feature.jpg) 8 | 9 | Usage 10 | --- 11 | 12 | Just clone the repo and go to the root directory and run 13 | 14 | You can install it by pip using: 15 | 16 | ```shell 17 | python -m pip install cameray 18 | ``` 19 | 20 | and then enter 21 | 22 | ```shell 23 | cameray 24 | ``` 25 | to run it. 26 | 27 | Or, just clone the repo and go to the root directory and run 28 | 29 | ```shell 30 | python -m pip install setuptools 31 | python setup.py sdist 32 | python setup.py install 33 | ``` 34 | and enter 35 | 36 | ```shell 37 | cameray 38 | ``` 39 | 40 | to run it. 41 | 42 | Credits 43 | --- 44 | 45 | - [Taichi][1] is the core of the simulator. It's a powerful programming language embedded in Python for high-performance numerical computations. Without the help of it, this project can not be implemented in such an efficient way in Python. **(The ray-tracing part of the project is from Taichi's cornell box example now. A general renderer should replace it in the near future.)** 46 | 47 | - [DearPyGui][2], an easy-to-use Python GUI framework based on [ImGUI][3]. Besides the native ImGUI widgets, it also wraps many other 3rd party ImGUI plugins (Imnodes). It's convenient to provide a friendly GUI for Cameray. 48 | 49 | Dependencies 50 | --- 51 | 52 | **Cameray** only could be run in Python 3.7/3.7/3.8/3.9 because Taichi only support these versions. 53 | 54 | [1]: (https://github.com/taichi-dev/taichi) 55 | [2]: (https://github.com/hoffstadt/DearPyGui) 56 | [3]: (https://github.com/ocornut/imgui) 57 | 58 | 59 | Roadmap 60 | --- 61 | 62 | - [ ] Stablity 63 | 64 | - [ ] A more realistic camera model considering general optical spectrum 65 | 66 | - [ ] General renderer 67 | 68 | - [ ] A more detail 2D camera illustration. (More kinds of rays and lens data) 69 | 70 | - [ ] Undo/Redo for Editor -------------------------------------------------------------------------------- /src/gui/app.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | from typing import Callable, Dict, Any, List, Tuple 4 | import configparser 5 | 6 | sys.path.append('..') 7 | import dearpygui.dearpygui as dpg 8 | from base.msg_queue import get_msg_queue, msg, get_coroutine 9 | from gui.lens_designer import LensDesignerWidget 10 | 11 | CMR_CONFIG_FILE_PATH = r'' 12 | # CMR_FONT_FILE_PATH = r'C:\Windows\Fonts\msyh.ttc' 13 | PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) 14 | 15 | 16 | msgqueue = get_msg_queue() 17 | coroutine = get_coroutine() 18 | 19 | class App: 20 | def __init__(self) -> None: 21 | self._setup_init() 22 | self._setup_uuid() 23 | self._setup_style() 24 | self._setup_window() 25 | self._setup_viewport() 26 | 27 | def _on_app_close(self, s,a,u): 28 | dpg.delete_item(s) 29 | self._app_config.write(open(CMR_CONFIG_FILE_PATH,'a')) 30 | print('_on_app_close') 31 | 32 | def _setup_style(self): 33 | with dpg.font_registry(): 34 | # Change your font here 35 | # dpg.add_font(CMR_FONT_FILE_PATH, 18, default_font=True) 36 | pass 37 | 38 | def _setup_init(self): 39 | self._app_config = configparser.ConfigParser() 40 | self._app_config.read(CMR_CONFIG_FILE_PATH) 41 | 42 | def _setup_uuid(self): 43 | self._gui_id_app:int = dpg.generate_uuid() 44 | self._lense_designer_widget:LensDesignerWidget = None 45 | 46 | def _gui_viewport_resize_event(self, sender, a, u): 47 | """ 48 | Keep the root widget fill up the viewport 49 | """ 50 | dpg.set_item_height(self._gui_id_app, a[3]) 51 | dpg.set_item_width(self._gui_id_app, a[2]) 52 | 53 | def _setup_viewport(self): 54 | if not dpg.is_viewport_created(): 55 | icon = PROJECT_DIR+'/icon.png' 56 | vp = dpg.create_viewport(small_icon=icon,title='Cameray', large_icon=icon,width=1920,height=1080) 57 | dpg.set_viewport_resize_callback(lambda a, b:self._gui_viewport_resize_event(a, b, self._gui_id_app)) 58 | dpg.setup_dearpygui(viewport=vp) 59 | dpg.show_viewport(vp) 60 | dpg.set_viewport_title(title='Cameray') 61 | # dpg.set_viewport_decorated(False) 62 | dpg.set_viewport_resizable(False) 63 | 64 | def _setup_window(self): 65 | 66 | with dpg.window(label="Cameray",id=self._gui_id_app, 67 | on_close=self._on_app_close, 68 | pos=(0, 0), 69 | no_title_bar=True, 70 | no_move=True, 71 | no_resize=True): 72 | 73 | self._lense_designer_widget:LensDesignerWidget = LensDesignerWidget(self._gui_id_app) 74 | 75 | def _window_resize_callback(self, s,a,u): 76 | pass 77 | 78 | def show(self): 79 | global coroutine 80 | while(dpg.is_dearpygui_running()): 81 | while not msgqueue.empty(): 82 | event = msgqueue.get() 83 | if callable(event): 84 | cr = event() 85 | if not cr: 86 | continue 87 | try: 88 | next(cr) 89 | except StopIteration: 90 | continue 91 | coroutine.append(cr) 92 | 93 | if coroutine: 94 | copy_cr = coroutine 95 | coroutine = [] 96 | news = [] 97 | for cr in copy_cr: 98 | try: 99 | cr.send(0) 100 | news.append(cr) 101 | except StopIteration: 102 | continue 103 | coroutine.extend(news) 104 | 105 | dpg.render_dearpygui_frame() 106 | dpg.cleanup_dearpygui() 107 | 108 | 109 | def main(): 110 | App().show() 111 | 112 | 113 | if __name__ == '__main__': 114 | main() -------------------------------------------------------------------------------- /src/gui/widget.py: -------------------------------------------------------------------------------- 1 | import dearpygui.dearpygui as dpg 2 | from dearpygui_ext.logger import mvLogger 3 | from typing import Callable, Any 4 | 5 | _logger = None 6 | 7 | def init_global_logger(parent:int): 8 | global _logger 9 | if _logger is None: 10 | _logger = mvLogger(parent) 11 | 12 | def get_logger()->mvLogger: 13 | return _logger 14 | 15 | 16 | def _config(sender, keyword, user_data): 17 | widget_type = dpg.get_item_type(sender) 18 | items = user_data 19 | 20 | if widget_type == "mvAppItemType::mvRadioButton": 21 | value = True 22 | 23 | else: 24 | keyword = dpg.get_item_label(sender) 25 | value = dpg.get_value(sender) 26 | 27 | if isinstance(user_data, list): 28 | for item in items: 29 | dpg.configure_item(item, **{keyword: value}) 30 | else: 31 | dpg.configure_item(items, **{keyword: value}) 32 | 33 | class Widget: 34 | def __init__(self,*, parent:int,callback:Callable[[Any],None]=None): 35 | self._widget_id:int = None 36 | self._parent_id:int = parent 37 | self._block = False 38 | self._callback = callback 39 | 40 | def widget(self)->int: 41 | return self._widget_id 42 | 43 | def parent(self)->int: 44 | return self._parent_id 45 | 46 | def __hash__(self): 47 | return self._widget_id 48 | 49 | def property_changed(self, s, a, u): 50 | self._invoke_update(sender=s, app_data=a, user_data=u) 51 | 52 | def block_callback(self, block): 53 | self._block = block 54 | 55 | def _invoke_update(self, *args, **kwargs): 56 | not self._block and callable(self._callback) and self._callback(*args, **kwargs) 57 | 58 | def callback(self): 59 | return self._callback 60 | 61 | def delete(self): 62 | self._widget_id and dpg.delete_item(self._widget_id) 63 | self._widget_id = None 64 | 65 | def __del__(self): 66 | self.delete() 67 | 68 | class AttributeValueType: 69 | ATTRI_FLOAT = 0 70 | ATTRI_FLOATX = 1 71 | ATTRI_INT = 2 72 | ATTRI_BOOL = 3 73 | 74 | def PropertyWidget(name:str, property_type:int, min_value:Any=None, max_value:Any=None, width=20,height=10,size=4): 75 | storage_name = '_' + name 76 | @property 77 | def prop(self:Widget): 78 | return dpg.get_value(getattr(self, storage_name)) 79 | 80 | @prop.setter 81 | def prop(self:Widget, value): 82 | if not hasattr(self, storage_name): 83 | if property_type == AttributeValueType.ATTRI_FLOAT: 84 | item_id = dpg.add_input_float( 85 | label=name, 86 | default_value=value, 87 | min_value=min_value, 88 | max_value=max_value, 89 | width=width, 90 | parent=self.widget(), 91 | callback=self.property_changed) 92 | elif property_type == AttributeValueType.ATTRI_FLOATX: 93 | item_id = dpg.add_input_floatx( 94 | label=name, 95 | default_value=value, 96 | min_value=min_value, 97 | max_value=max_value, 98 | width=width, 99 | parent=self.widget(), 100 | size=size, 101 | callback=self.property_changed) 102 | elif property_type == AttributeValueType.ATTRI_INT: 103 | item_id = dpg.add_input_int( 104 | label=name, 105 | default_value=value, 106 | min_value=min_value, 107 | max_value=max_value, 108 | width=width, 109 | parent=self.widget(), 110 | callback=self.property_changed) 111 | elif property_type == AttributeValueType.ATTRI_BOOL: 112 | item_id = dpg.add_checkbox(label=name,parent=self.widget(),default_value=value,callback=self.property_changed) 113 | setattr(self, storage_name, item_id) 114 | else: 115 | item_id = getattr(self, storage_name) 116 | dpg.set_value(item_id, value) 117 | return prop 118 | -------------------------------------------------------------------------------- /src/core/renderer_utils.py: -------------------------------------------------------------------------------- 1 | import taichi as ti 2 | import math 3 | 4 | eps = 1e-4 5 | inf = 1e10 6 | 7 | 8 | @ti.func 9 | def out_dir(n): 10 | u = ti.Vector([1.0, 0.0, 0.0]) 11 | if ti.abs(n[1]) < 1 - 1e-3: 12 | u = n.cross(ti.Vector([0.0, 1.0, 0.0])).normalized() 13 | v = n.cross(u) 14 | phi = 2 * math.pi * ti.random(ti.f32) 15 | r = ti.random(ti.f32) 16 | ay = ti.sqrt(r) 17 | ax = ti.sqrt(1 - r) 18 | return ax * (ti.cos(phi) * u + ti.sin(phi) * v) + ay * n 19 | 20 | 21 | @ti.func 22 | def reflect(d, n): 23 | # Assuming |d| and |n| are normalized 24 | return d - 2.0 * d.dot(n) * n 25 | 26 | 27 | @ti.func 28 | def refract(d, n, ni_over_nt): 29 | # Assuming |d| and |n| are normalized 30 | has_r, rd = 0, d 31 | dt = d.dot(n) 32 | discr = 1.0 - ni_over_nt * ni_over_nt * (1.0 - dt * dt) 33 | if discr > 0.0: 34 | has_r = 1 35 | rd = (ni_over_nt * (d - n * dt) - n * ti.sqrt(discr)).normalized() 36 | else: 37 | rd *= 0.0 38 | return has_r, rd 39 | 40 | 41 | @ti.func 42 | def ray_aabb_intersection(box_min, box_max, o, d): 43 | intersect = 1 44 | 45 | near_int = -inf 46 | far_int = inf 47 | 48 | for i in ti.static(range(3)): 49 | if d[i] == 0: 50 | if o[i] < box_min[i] or o[i] > box_max[i]: 51 | intersect = 0 52 | else: 53 | i1 = (box_min[i] - o[i]) / d[i] 54 | i2 = (box_max[i] - o[i]) / d[i] 55 | 56 | new_far_int = ti.max(i1, i2) 57 | new_near_int = ti.min(i1, i2) 58 | 59 | far_int = ti.min(new_far_int, far_int) 60 | near_int = ti.max(new_near_int, near_int) 61 | 62 | if near_int > far_int: 63 | intersect = 0 64 | return intersect, near_int, far_int 65 | 66 | 67 | # (T + x d)(T + x d) = r * r 68 | # T*T + 2Td x + x^2 = r * r 69 | # x^2 + 2Td x + (T * T - r * r) = 0 70 | 71 | refine = True 72 | 73 | 74 | @ti.func 75 | def intersect_sphere(pos, d, center, radius): 76 | T = pos - center 77 | A = 1.0 78 | B = 2.0 * T.dot(d) 79 | C = T.dot(T) - radius * radius 80 | delta = B * B - 4.0 * A * C 81 | dist = inf 82 | hit_pos = ti.Vector([0.0, 0.0, 0.0]) 83 | 84 | if delta > -1e-4: 85 | delta = ti.max(delta, 0) 86 | sdelta = ti.sqrt(delta) 87 | ratio = 0.5 / A 88 | ret1 = ratio * (-B - sdelta) 89 | dist = ret1 90 | if ti.static(refine): 91 | if dist < inf: 92 | # refinement 93 | old_dist = dist 94 | new_pos = pos + d * dist 95 | T = new_pos - center 96 | A = 1.0 97 | B = 2.0 * T.dot(d) 98 | C = T.dot(T) - radius * radius 99 | delta = B * B - 4 * A * C 100 | if delta > 0: 101 | sdelta = ti.sqrt(delta) 102 | ratio = 0.5 / A 103 | ret1 = ratio * (-B - sdelta) + old_dist 104 | if ret1 > 0: 105 | dist = ret1 106 | hit_pos = new_pos + ratio * (-B - sdelta) * d 107 | else: 108 | pass 109 | #ret2 = ratio * (-B + sdelta) + old_dist 110 | #if ret2 > 0: 111 | # dist = ret2 112 | # hit_pos = new_pos + ratio * (-B + sdelta) * d 113 | else: 114 | dist = inf 115 | 116 | return dist, hit_pos 117 | 118 | 119 | @ti.func 120 | def ray_plane_intersect(pos, d, pt_on_plane, norm): 121 | dist = inf 122 | hit_pos = ti.Vector([0.0, 0.0, 0.0]) 123 | denom = d.dot(norm) 124 | if abs(denom) > eps: 125 | dist = norm.dot(pt_on_plane - pos) / denom 126 | hit_pos = pos + d * dist 127 | return dist, hit_pos 128 | 129 | 130 | @ti.func 131 | def point_aabb_distance2(box_min, box_max, o): 132 | p = ti.Vector([0.0, 0.0, 0.0]) 133 | for i in ti.static(range(3)): 134 | p[i] = ti.max(ti.min(o[i], box_max[i]), box_min[i]) 135 | return (p - o).norm_sqr() 136 | 137 | 138 | @ti.func 139 | def sphere_aabb_intersect(box_min, box_max, o, radius): 140 | return point_aabb_distance2(box_min, box_max, o) < radius * radius 141 | 142 | 143 | @ti.func 144 | def sphere_aabb_intersect_motion(box_min, box_max, o1, o2, radius): 145 | lo = 0.0 146 | hi = 1.0 147 | while lo + 1e-5 < hi: 148 | m1 = 2 * lo / 3 + hi / 3 149 | m2 = lo / 3 + 2 * hi / 3 150 | d1 = point_aabb_distance2(box_min, box_max, (1 - m1) * o1 + m1 * o2) 151 | d2 = point_aabb_distance2(box_min, box_max, (1 - m2) * o1 + m2 * o2) 152 | if d2 > d1: 153 | hi = m2 154 | else: 155 | lo = m1 156 | 157 | return point_aabb_distance2(box_min, box_max, 158 | (1 - lo) * o1 + lo * o2) < radius * radius 159 | 160 | 161 | @ti.func 162 | def inside(p, c, r): 163 | return (p - c).norm_sqr() <= r * r 164 | 165 | 166 | @ti.func 167 | def inside_left(p, c, r): 168 | return inside(p, c, r) and p[0] < c[0] 169 | 170 | 171 | @ti.func 172 | def inside_right(p, c, r): 173 | return inside(p, c, r) and p[0] > c[0] 174 | 175 | 176 | def Vector2(x, y): 177 | return ti.Vector([x, y]) 178 | 179 | 180 | @ti.func 181 | def inside_taichi(p_): 182 | p = p_ 183 | ret = -1 184 | if not inside(p, Vector2(0.50, 0.50), 0.52): 185 | if ret == -1: 186 | ret = 0 187 | if not inside(p, Vector2(0.50, 0.50), 0.495): 188 | if ret == -1: 189 | ret = 1 190 | p = Vector2(0.5, 0.5) + (p - Vector2(0.5, 0.5)) 191 | if inside(p, Vector2(0.50, 0.25), 0.08): 192 | if ret == -1: 193 | ret = 1 194 | if inside(p, Vector2(0.50, 0.75), 0.08): 195 | if ret == -1: 196 | ret = 0 197 | if inside(p, Vector2(0.50, 0.25), 0.25): 198 | if ret == -1: 199 | ret = 0 200 | if inside(p, Vector2(0.50, 0.75), 0.25): 201 | if ret == -1: 202 | ret = 1 203 | if p[0] < 0.5: 204 | if ret == -1: 205 | ret = 1 206 | else: 207 | if ret == -1: 208 | ret = 0 209 | return ret 210 | -------------------------------------------------------------------------------- /src/core/renderer.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from numpy.lib.type_check import real 3 | import taichi as ti 4 | import time 5 | import math 6 | import numpy as np 7 | from .renderer_utils import ray_aabb_intersection, intersect_sphere, ray_plane_intersect, reflect, refract 8 | from .realistic import RealisticCamera 9 | 10 | ti.init(arch=ti.gpu) 11 | res = (600, 800) 12 | color_buffer = ti.Vector.field(3, dtype=ti.f32, shape=res) 13 | # color_buffer = ti.Vector.field(3, dtype=ti.f32) 14 | 15 | # ti.root.dense(ti.ji,res).place(color_buffer) 16 | 17 | count_var = ti.field(ti.i32, shape=(1, )) 18 | 19 | max_ray_depth = 10 20 | eps = 1e-4 21 | inf = 1e10 22 | fov = 0.8 23 | 24 | camera_pos = ti.Vector([0.0, 1.8, 10.0]) 25 | 26 | mat_none = 0 27 | mat_lambertian = 1 28 | mat_specular = 2 29 | mat_glass = 3 30 | mat_light = 4 31 | 32 | light_y_pos = 30.0 - eps 33 | light_x_min_pos = -0.25 34 | light_x_range = 5 35 | light_z_min_pos = 100.0 36 | light_z_range = 12 37 | light_area = light_x_range * light_z_range 38 | light_min_pos = ti.Vector([light_x_min_pos, light_y_pos, light_z_min_pos]) 39 | light_max_pos = ti.Vector([ 40 | light_x_min_pos + light_x_range, light_y_pos, 41 | light_z_min_pos + light_z_range 42 | ]) 43 | light_color = ti.Vector(list(np.array([0.9, 0.85, 0.7]))) 44 | light_normal = ti.Vector([0.0, -1.0, 0.0]) 45 | 46 | # No absorbtion, integrates over a unit hemisphere 47 | lambertian_brdf = 1.0 / math.pi 48 | # diamond! 49 | refr_idx = 2.4 50 | 51 | # right near sphere 52 | sp1 = [0.0, 2.0, 225.0] 53 | sp1_center = ti.Vector(sp1) 54 | sp1_radius = 2.0 55 | # left far sphere 56 | sp2_center = ti.Vector([-0.28, 0.55, 0.8]) 57 | sp2_radius = 0.32 58 | 59 | 60 | 61 | pos = [0.0, 3.0, 240.0] # mm 62 | center = [0.0,0.0,0.0] 63 | world_up = [0.0, 1.0, 0.0] 64 | real_cam = RealisticCamera(pos, center, world_up) 65 | 66 | def make_box_transform_matrices(): 67 | rad = math.pi / 10.0 68 | c, s = math.cos(rad), math.sin(rad) 69 | rot = np.array([[c, 0, s, 0], [0, 1, 0, 0], [-s, 0, c, 0], [0, 0, 0, 1]]) 70 | translate = np.array([ 71 | [1, 0, 0, -0.7], 72 | [0, 1, 0, 0], 73 | [0, 0, 1, 0.7], 74 | [0, 0, 0, 1], 75 | ]) 76 | m = translate @ rot 77 | m_inv = np.linalg.inv(m) 78 | m_inv_t = np.transpose(m_inv) 79 | return ti.Matrix(m_inv), ti.Matrix(m_inv_t) 80 | 81 | 82 | # left box 83 | box_min = ti.Vector([-4.0, -1.0, 15.0]) 84 | box_max = ti.Vector([4.0, 30.0, 25.00]) 85 | box_m_inv, box_m_inv_t = make_box_transform_matrices() 86 | 87 | 88 | @ti.func 89 | def intersect_light(pos, d, tmax): 90 | hit, t, _ = ray_aabb_intersection(light_min_pos, light_max_pos, pos, d) 91 | if hit and 0 < t < tmax: 92 | hit = 1 93 | else: 94 | hit = 0 95 | t = inf 96 | return hit, t 97 | 98 | 99 | @ti.func 100 | def ray_aabb_intersection2(box_min, box_max, o, d): 101 | # Compared to ray_aabb_intersection2(), this also returns the normal of 102 | # the nearest t. 103 | intersect = 1 104 | 105 | near_t = -inf 106 | far_t = inf 107 | near_face = 0 108 | near_is_max = 0 109 | 110 | for i in ti.static(range(3)): 111 | if d[i] == 0: 112 | if o[i] < box_min[i] or o[i] > box_max[i]: 113 | intersect = 0 114 | else: 115 | i1 = (box_min[i] - o[i]) / d[i] 116 | i2 = (box_max[i] - o[i]) / d[i] 117 | 118 | new_far_t = max(i1, i2) 119 | new_near_t = min(i1, i2) 120 | new_near_is_max = i2 < i1 121 | 122 | far_t = min(new_far_t, far_t) 123 | if new_near_t > near_t: 124 | near_t = new_near_t 125 | near_face = int(i) 126 | near_is_max = new_near_is_max 127 | 128 | near_norm = ti.Vector([0.0, 0.0, 0.0]) 129 | if near_t > far_t: 130 | intersect = 0 131 | if intersect: 132 | # TODO: Issue#1004... 133 | if near_face == 0: 134 | near_norm[0] = -1 + near_is_max * 2 135 | elif near_face == 1: 136 | near_norm[1] = -1 + near_is_max * 2 137 | else: 138 | near_norm[2] = -1 + near_is_max * 2 139 | 140 | return intersect, near_t, far_t, near_norm 141 | 142 | 143 | @ti.func 144 | def mat_mul_point(m, p): 145 | hp = ti.Vector([p[0], p[1], p[2], 1.0]) 146 | hp = m @ hp 147 | hp /= hp[3] 148 | return ti.Vector([hp[0], hp[1], hp[2]]) 149 | 150 | 151 | @ti.func 152 | def mat_mul_vec(m, v): 153 | hv = ti.Vector([v[0], v[1], v[2], 0.0]) 154 | hv = m @ hv 155 | return ti.Vector([hv[0], hv[1], hv[2]]) 156 | 157 | 158 | @ti.func 159 | def ray_aabb_intersection2_transformed(box_min, box_max, o, d): 160 | # Transform the ray to the box's local space 161 | obj_o = mat_mul_point(box_m_inv, o) 162 | obj_d = mat_mul_vec(box_m_inv, d) 163 | intersect, near_t, _, near_norm = ray_aabb_intersection2( 164 | box_min, box_max, obj_o, obj_d) 165 | if intersect and 0 < near_t: 166 | # Transform the normal in the box's local space to world space 167 | near_norm = mat_mul_vec(box_m_inv_t, near_norm) 168 | else: 169 | intersect = 0 170 | return intersect, near_t, near_norm 171 | 172 | 173 | @ti.func 174 | def intersect_scene(pos, ray_dir): 175 | closest, normal = inf, ti.Vector.zero(ti.f32, 3) 176 | c, mat = ti.Vector.zero(ti.f32, 3), mat_none 177 | 178 | # right near sphere 179 | cur_dist, hit_pos = intersect_sphere(pos, ray_dir, sp1_center, sp1_radius) 180 | if 0 < cur_dist < closest: 181 | closest = cur_dist 182 | normal = (hit_pos - sp1_center).normalized() 183 | c, mat = ti.Vector([1.0, 1.0, 1.0]), mat_glass 184 | # left box 185 | hit, cur_dist, pnorm = ray_aabb_intersection2_transformed( 186 | box_min, box_max, pos, ray_dir) 187 | if hit and 0 < cur_dist < closest: 188 | closest = cur_dist 189 | normal = pnorm 190 | c, mat = ti.Vector([0.8, 0.5, 0.4]), mat_specular 191 | 192 | # left 193 | pnorm = ti.Vector([1.0, 0.0, 0.0]) 194 | cur_dist, _ = ray_plane_intersect(pos, ray_dir, ti.Vector([-40.0, 0.0, 195 | 0.0]), pnorm) 196 | if 0 < cur_dist < closest: 197 | closest = cur_dist 198 | normal = pnorm 199 | c, mat = ti.Vector([0.65, 0.05, 0.05]), mat_lambertian 200 | # right 201 | pnorm = ti.Vector([-1.0, 0.0, 0.0]) 202 | cur_dist, _ = ray_plane_intersect(pos, ray_dir, ti.Vector([40.0, 0.0, 0.0]), 203 | pnorm) 204 | if 0 < cur_dist < closest: 205 | closest = cur_dist 206 | normal = pnorm 207 | c, mat = ti.Vector([0.12, 0.45, 0.15]), mat_lambertian 208 | # bottom 209 | gray = ti.Vector([0.93, 0.93, 0.93]) 210 | pnorm = ti.Vector([0.0, 1.0, 0.0]) 211 | cur_dist, _ = ray_plane_intersect(pos, ray_dir, ti.Vector([0.0, -1.0, 0.0]), 212 | pnorm) 213 | if 0 < cur_dist < closest: 214 | closest = cur_dist 215 | normal = pnorm 216 | c, mat = gray, mat_lambertian 217 | # top 218 | pnorm = ti.Vector([0.0, -1.0, 0.0]) 219 | cur_dist, _ = ray_plane_intersect(pos, ray_dir, ti.Vector([0.0, 30.0, 0.0]), 220 | pnorm) 221 | if 0 < cur_dist < closest: 222 | closest = cur_dist 223 | normal = pnorm 224 | c, mat = gray, mat_lambertian 225 | # far 226 | pnorm = ti.Vector([0.0, 0.0, 1.0]) 227 | cur_dist, _ = ray_plane_intersect(pos, ray_dir, ti.Vector([0.0, 0.0, 0.0]), 228 | pnorm) 229 | if 0 < cur_dist < closest: 230 | closest = cur_dist 231 | normal = pnorm 232 | c, mat = gray, mat_lambertian 233 | # light 234 | hit_l, cur_dist = intersect_light(pos, ray_dir, closest) 235 | if hit_l and 0 < cur_dist < closest: 236 | # technically speaking, no need to check the second term 237 | closest = cur_dist 238 | normal = light_normal 239 | c, mat = light_color, mat_light 240 | 241 | return closest, normal, c, mat 242 | 243 | 244 | @ti.func 245 | def visible_to_light(pos, ray_dir): 246 | a, b, c, mat = intersect_scene(pos, ray_dir) 247 | return mat == mat_light 248 | 249 | 250 | @ti.func 251 | def dot_or_zero(n, l): 252 | return max(0.0, n.dot(l)) 253 | 254 | 255 | @ti.func 256 | def mis_power_heuristic(pf, pg): 257 | # Assume 1 sample for each distribution 258 | f = pf**2 259 | g = pg**2 260 | return f / (f + g) 261 | 262 | 263 | @ti.func 264 | def compute_area_light_pdf(pos, ray_dir): 265 | hit_l, t = intersect_light(pos, ray_dir, inf) 266 | pdf = 0.0 267 | if hit_l: 268 | l_cos = light_normal.dot(-ray_dir) 269 | if l_cos > eps: 270 | tmp = ray_dir * t 271 | dist_sqr = tmp.dot(tmp) 272 | pdf = dist_sqr / (light_area * l_cos) 273 | return pdf 274 | 275 | 276 | @ti.func 277 | def compute_brdf_pdf(normal, sample_dir): 278 | return dot_or_zero(normal, sample_dir) / math.pi 279 | 280 | 281 | @ti.func 282 | def sample_area_light(hit_pos, pos_normal): 283 | # sampling inside the light area 284 | x = ti.random() * light_x_range + light_x_min_pos 285 | z = ti.random() * light_z_range + light_z_min_pos 286 | on_light_pos = ti.Vector([x, light_y_pos, z]) 287 | return (on_light_pos - hit_pos).normalized() 288 | 289 | 290 | @ti.func 291 | def sample_brdf(normal): 292 | # cosine hemisphere sampling 293 | # first, uniformly sample on a disk (r, theta) 294 | r, theta = 0.0, 0.0 295 | sx = ti.random() * 2.0 - 1.0 296 | sy = ti.random() * 2.0 - 1.0 297 | if sx >= -sy: 298 | if sx > sy: 299 | # first region 300 | r = sx 301 | div = abs(sy / r) 302 | if sy > 0.0: 303 | theta = div 304 | else: 305 | theta = 7.0 + div 306 | else: 307 | # second region 308 | r = sy 309 | div = abs(sx / r) 310 | if sx > 0.0: 311 | theta = 1.0 + sx / r 312 | else: 313 | theta = 2.0 + sx / r 314 | else: 315 | if sx <= sy: 316 | # third region 317 | r = -sx 318 | div = abs(sy / r) 319 | if sy > 0.0: 320 | theta = 3.0 + div 321 | else: 322 | theta = 4.0 + div 323 | else: 324 | # fourth region 325 | r = -sy 326 | div = abs(sx / r) 327 | if sx < 0.0: 328 | theta = 5.0 + div 329 | else: 330 | theta = 6.0 + div 331 | # Malley's method 332 | u = ti.Vector([1.0, 0.0, 0.0]) 333 | if abs(normal[1]) < 1 - eps: 334 | u = normal.cross(ti.Vector([0.0, 1.0, 0.0])) 335 | v = normal.cross(u) 336 | 337 | theta = theta * math.pi * 0.25 338 | costt, sintt = ti.cos(theta), ti.sin(theta) 339 | xy = (u * costt + v * sintt) * r 340 | zlen = ti.sqrt(max(0.0, 1.0 - xy.dot(xy))) 341 | return xy + zlen * normal 342 | 343 | 344 | @ti.func 345 | def sample_direct_light(hit_pos, hit_normal, hit_color): 346 | direct_li = ti.Vector([0.0, 0.0, 0.0]) 347 | fl = lambertian_brdf * hit_color * light_color 348 | light_pdf, brdf_pdf = 0.0, 0.0 349 | # sample area light 350 | to_light_dir = sample_area_light(hit_pos, hit_normal) 351 | if to_light_dir.dot(hit_normal) > 0: 352 | light_pdf = compute_area_light_pdf(hit_pos, to_light_dir) 353 | brdf_pdf = compute_brdf_pdf(hit_normal, to_light_dir) 354 | if light_pdf > 0 and brdf_pdf > 0: 355 | l_visible = visible_to_light(hit_pos, to_light_dir) 356 | if l_visible: 357 | w = mis_power_heuristic(light_pdf, brdf_pdf) 358 | nl = dot_or_zero(to_light_dir, hit_normal) 359 | direct_li += fl * w * nl / light_pdf 360 | # sample brdf 361 | brdf_dir = sample_brdf(hit_normal) 362 | brdf_pdf = compute_brdf_pdf(hit_normal, brdf_dir) 363 | if brdf_pdf > 0: 364 | light_pdf = compute_area_light_pdf(hit_pos, brdf_dir) 365 | if light_pdf > 0: 366 | l_visible = visible_to_light(hit_pos, brdf_dir) 367 | if l_visible: 368 | w = mis_power_heuristic(brdf_pdf, light_pdf) 369 | nl = dot_or_zero(brdf_dir, hit_normal) 370 | direct_li += fl * w * nl / brdf_pdf 371 | return direct_li 372 | 373 | 374 | @ti.func 375 | def schlick(cos, eta): 376 | r0 = (1.0 - eta) / (1.0 + eta) 377 | r0 = r0 * r0 378 | return r0 + (1 - r0) * ((1.0 - cos)**5) 379 | 380 | 381 | @ti.func 382 | def sample_ray_dir(indir, normal, hit_pos, mat): 383 | u = ti.Vector([0.0, 0.0, 0.0]) 384 | pdf = 1.0 385 | if mat == mat_lambertian: 386 | u = sample_brdf(normal) 387 | pdf = max(eps, compute_brdf_pdf(normal, u)) 388 | elif mat == mat_specular: 389 | u = reflect(indir, normal) 390 | elif mat == mat_glass: 391 | cos = indir.dot(normal) 392 | ni_over_nt = refr_idx 393 | outn = normal 394 | if cos > 0.0: 395 | outn = -normal 396 | cos = refr_idx * cos 397 | else: 398 | ni_over_nt = 1.0 / refr_idx 399 | cos = -cos 400 | has_refr, refr_dir = refract(indir, outn, ni_over_nt) 401 | refl_prob = 1.0 402 | if has_refr: 403 | refl_prob = schlick(cos, refr_idx) 404 | if ti.random() < refl_prob: 405 | u = reflect(indir, normal) 406 | else: 407 | u = refr_dir 408 | return u.normalized(), pdf 409 | 410 | 411 | stratify_res = 5 412 | inv_stratify = 1.0 / 5.0 413 | 414 | 415 | @ti.kernel 416 | def taichi_render(): 417 | for u, v in color_buffer: 418 | weight, r = real_cam.gen_ray_of(v, res[0]-u) 419 | if weight <= 0.0: 420 | continue 421 | ray_dir = r[1] 422 | pos = r[0] 423 | 424 | acc_color = ti.Vector([0.0, 0.0, 0.0]) 425 | throughput = ti.Vector([1.0, 1.0, 1.0]) 426 | depth = 0 427 | while depth < max_ray_depth: 428 | closest, hit_normal, hit_color, mat = intersect_scene(pos, ray_dir) 429 | if mat == mat_none: 430 | break 431 | 432 | hit_pos = pos + closest * ray_dir 433 | hit_light = (mat == mat_light) 434 | if hit_light: 435 | acc_color += throughput * light_color 436 | break 437 | elif mat == mat_lambertian: 438 | acc_color += throughput * sample_direct_light( 439 | hit_pos, hit_normal, hit_color) 440 | 441 | depth += 1 442 | ray_dir, pdf = sample_ray_dir(ray_dir, hit_normal, hit_pos, mat) 443 | pos = hit_pos + 1e-4 * ray_dir 444 | if mat == mat_lambertian: 445 | throughput *= lambertian_brdf * hit_color * dot_or_zero( 446 | hit_normal, ray_dir) / pdf 447 | else: 448 | throughput *= hit_color 449 | color_buffer[u, v] += weight*acc_color 450 | 451 | 452 | # gui = ti.GUI('Realistic camera', res) 453 | last_t = time.time() 454 | i = 0 455 | 456 | real_cam.refocus(4.5) 457 | # real_cam.refocus(np.linalg.norm(np.array(sp1) - real_cam.get_position())) 458 | real_cam.recompute_exit_pupil() 459 | 460 | 461 | class Renderer: 462 | def __init__(self): 463 | self.camera = None 464 | self._iter = 0 465 | 466 | def render(self): 467 | taichi_render() 468 | 469 | def clear(self): 470 | color_buffer.from_numpy(np.zeros(res)) 471 | self._iter = 0 472 | 473 | def var(self): 474 | pass 475 | 476 | def refocus(self, depth): 477 | global real_cam 478 | real_cam.refocus(depth) 479 | 480 | def recompute_exit_pupil(self): 481 | global real_cam 482 | real_cam.recompute_exit_pupil() 483 | 484 | def get_camera(self): 485 | global real_cam 486 | return real_cam 487 | 488 | def get_color_buffer_to_numpy(self): 489 | return color_buffer.to_numpy() 490 | 491 | 492 | # while gui.running: 493 | # taichi_render() 494 | # interval = 2000 495 | # if i % interval == 0 and i > 0: 496 | # img = color_buffer.to_numpy() * (1 / (i + 1)) 497 | # img = np.sqrt(img / img.mean() * 0.24) 498 | # var = np.var(img) 499 | # if var < 0.11790: 500 | # ti.imwrite(img, 'output.png') 501 | # break 502 | # print("{:.2f} samples/s ({} iters, var={})".format( 503 | # interval / (time.time() - last_t), i, var)) 504 | # last_t = time.time() 505 | # gui.set_image(img) 506 | # gui.show() 507 | # i += 1 508 | -------------------------------------------------------------------------------- /src/core/realistic.py: -------------------------------------------------------------------------------- 1 | import math 2 | import taichi as ti 3 | import numpy as np 4 | from typing import List, Dict 5 | from .renderer_utils import refract 6 | 7 | max_elements = 30 8 | max_draw_rays = 20 9 | pupil_interval_count = 64 10 | eps = 1e-5 11 | inf = 9999999.0 12 | 13 | def convert_raw_data_from_dict(raw_data:List[List[float]]): 14 | """ 15 | Args: raw_data 16 | for example: 17 | [ 18 | # curvature radius, thickness, index of refraction, aperture diameter 19 | [29.475,3.76,1.67,25.2], 20 | [84.83,0.12,1,25.2], 21 | [19.275,4.025,1.67,23], 22 | [40.77,3.275,1.699,23], 23 | [12.75,5.705,1,18], 24 | [0,4.5,0,17.1], 25 | [-14.495,1.18,1.603,17], 26 | [40.77,6.065,1.658,20], 27 | [-20.385,0.19,1,20], 28 | [437.065,3.22,1.717,20], 29 | [-39.73,5.0,1,20] 30 | ] 31 | """ 32 | dict_data = [] 33 | for elem in raw_data: 34 | dict_data.append( 35 | {'curvature_radius':elem[0], 36 | 'thickness':elem[1], 37 | 'eta':elem[2], 38 | 'aperture_diameter':elem[3] 39 | }) 40 | return dict_data 41 | 42 | def convert_dict_data_from_raw(dict_data:List[Dict[str, List[float]]]): 43 | raw_data = [] 44 | for elem_dict in dict_data: 45 | raw_data.append([ 46 | elem_dict.get('curvature_radius', 0), 47 | elem_dict.get('thickness', 0), 48 | elem_dict.get('eta', 0), 49 | elem_dict.get('aperture_diameter', 0) 50 | ]) 51 | return raw_data 52 | 53 | @ti.func 54 | def lerp(val, begin ,end): 55 | return begin * (1.0 - val) + val * end 56 | 57 | @ti.func 58 | def bound_union_with(bmin, bmax, pos): 59 | return ti.min(bmin, pos), ti.max(bmax, pos) 60 | 61 | @ti.func 62 | def make_bound2(): 63 | return ti.Vector([inf, inf]), ti.Vector([-inf, -inf]) 64 | 65 | @ti.func 66 | def inside_aabb(bmin, bmax, pos): 67 | return all(bmin <= pos) and all(pos <= bmax) 68 | 69 | 70 | 71 | @ti.data_oriented 72 | class RealisticCamera: 73 | def __init__(self, camera_pos, center, world_up): 74 | self.vignet = ti.field(ti.i32, shape=()) 75 | 76 | self.curvature_radius = ti.field(ti.f32) 77 | self.thickness = ti.field(ti.f32) 78 | self.eta = ti.field(ti.f32) 79 | self.aperture_radius = ti.field(ti.f32) 80 | self.exitPupilBoundMin = ti.Vector.field(2, dtype=ti.f32) 81 | self.exitPupilBoundMax = ti.Vector.field(2, dtype=ti.f32) 82 | self.draw_rays = ti.Vector.field(3, dtype=ti.f32) 83 | 84 | ti.root.dense(ti.i, (max_elements, )).place(self.curvature_radius, self.thickness, self.eta, self.aperture_radius) 85 | ti.root.dense(ti.i, (pupil_interval_count, )).place(self.exitPupilBoundMin, self.exitPupilBoundMax) 86 | ti.root.dense(ti.ij, (max_draw_rays, max_elements + 2)).place(self.draw_rays) 87 | 88 | self.camera2world_point = ti.Matrix([[1.0,0.0,0.0,0.0],[0.0,1.0,0.0,0.0],[0.0,0.0,1.0,0.0],[0.0,0.0,0.0,1.0]]) 89 | self.camera2world_vec = ti.Matrix([[1.0,0.0,0.0],[0.0,1.0,0.0],[0.0,0.0,1.0]]) 90 | 91 | self.shutter = 0.1 92 | self.film_width = 36 93 | self.film_height = 24 94 | self.film_diagnal = math.sqrt(self.film_height * self.film_height + self.film_width * self.film_width) 95 | self.pixel_width = 800 96 | self.pixel_height = 600 97 | self.camera_pos = np.array(camera_pos) 98 | 99 | self._elem_count = 0 100 | self.load_lens_data([]) 101 | self.set_camera(camera_pos, center, world_up) 102 | 103 | def get_resolution(self): 104 | return [self.pixel_width, self.pixel_height] 105 | 106 | def get_position(self): 107 | return self.camera_pos 108 | 109 | def set_camera(self, eye, center, world_up): 110 | """ 111 | setup camera2world_pos transformation 112 | """ 113 | eye = np.array(eye) 114 | center = np.array(center) 115 | world_up = np.array(world_up) 116 | 117 | self.camera_pos = eye 118 | 119 | def normalize(v): 120 | norm = np.linalg.norm(v) 121 | return v / norm if norm > 0.0 else np.array([0.0,0.0,0.0]) 122 | 123 | direction = normalize(center - eye) 124 | right = normalize(np.cross(direction, world_up)) 125 | up = normalize(np.cross(right, direction)) 126 | 127 | self.camera2world_point = ti.Matrix([ 128 | [1.0/1000.0,0.0,0.0,0.0], 129 | [0.0,1.0/1000.0,0.0,0.0], 130 | [0.0,0.0,1.0/1000.0,0.0], 131 | [0.0,0.0,0.0,1.0] 132 | ]) @ ti.Matrix([ 133 | [right[0], up[0], direction[0], 1000.0 * eye[0]], 134 | [right[1], up[1], direction[1], 1000.0 * eye[1]], 135 | [right[2], up[2], direction[2], 1000.0 * eye[2]], 136 | [0.0,0.0,0.0,1.0]]) 137 | 138 | self.camera2world_vec = ti.Matrix([ 139 | [right[0], up[0], direction[0]], 140 | [right[1], up[1], direction[1]], 141 | [right[2], up[2], direction[2]] 142 | ]) 143 | 144 | def get_element_count(self): 145 | return self._elem_count 146 | 147 | def load_lens_data(self, lenses:List[List[float]]=[[]]): 148 | wide22 = [ 149 | # curvature radius, thickness, index of refraction, aperture diameter 150 | [35.98738, 1.21638, 1.54, 23.716], 151 | [11.69718, 9.9957, 1, 17.996], 152 | [13.08714, 5.12622, 1.772, 12.364], 153 | [22.63294, 1.76924, 1.617, 9.812], 154 | [71.05802, 0.8184, 1, 9.152], 155 | [0, 2.27766,0, 8.756], 156 | [9.58584,2.43254,1.617,8.184], 157 | [11.28864,0.11506,1,9.152], 158 | [166.7765,3.09606,1.713,10.648], 159 | [7.5911,1.32682,1.805,11.44], 160 | [16.7662,3.98068,1,12.276], 161 | [7.70286,1.21638,1.617,13.42], 162 | [11.97328,10,1,17.996] 163 | ] 164 | dgauss50 = [ 165 | [29.475,3.76,1.67,25.2], 166 | [84.83,0.12,1,25.2], 167 | [19.275,4.025,1.67,23], 168 | [40.77,3.275,1.699,23], 169 | [12.75,5.705,1,18], 170 | [0,4.5,0,17.1], 171 | [-14.495,1.18,1.603,17], 172 | [40.77,6.065,1.658,20], 173 | [-20.385,0.19,1,20], 174 | [437.065,3.22,1.717,20], 175 | [-39.73,5.0,1,20] 176 | ] 177 | telephoto=[ 178 | [21.851,0 ,1.529,19.0], 179 | [-34.546,5.008,1.599,17.8], 180 | [108.705,1.502,1.0,16.6], 181 | [ 0,1.127,0,16.2], 182 | [-12.852,26.965,1.613,12.6], 183 | [19.813,1.502,1.603,13.4], 184 | [-20.378,5.008,1.0,14.8] 185 | ] 186 | telephoto250=[ 187 | [54.6275,12.52,1.529,47.5], 188 | [-86.365,3.755,1.599,44.5], 189 | [271.7625,2.8175,1,41.5], 190 | [0,67.4125,0,40.5], 191 | [-32.13,3.755,1.613,31.5], 192 | [49.5325,12.52,1.603,33.5], 193 | [-50.945,0,1,37] 194 | ] 195 | 196 | lenses = lenses if lenses else dgauss50 197 | self._elem_count = len(lenses) 198 | self._lenses_data = lenses.copy() 199 | for _ in range(max(0, max_elements - self._elem_count)): 200 | lenses.append([0 for j in range(4)]) 201 | a = np.array(lenses).transpose() 202 | self.curvature_radius.from_numpy(a[0]) 203 | self.thickness.from_numpy(a[1]) 204 | self.eta.from_numpy(a[2]) 205 | self.aperture_radius.from_numpy(a[3]/2.0) 206 | 207 | def get_lenses_data(self): 208 | a = self.curvature_radius.to_numpy()[0:self._elem_count] 209 | b = self.thickness.to_numpy()[0:self._elem_count] 210 | c = self.eta.to_numpy()[0:self._elem_count] 211 | d = self.aperture_radius.to_numpy()[0:self._elem_count] * 2.0 212 | return np.stack((a,b,c,d)).transpose() 213 | 214 | @ti.func 215 | def camera_2_world(self, o, d): 216 | """ 217 | Transform the ray from camera space to world space 218 | 219 | o: origin of the ray 220 | d: direction of the ray 221 | """ 222 | wo = self.camera2world_point @ ti.Vector([o.x, o.y, o.z, 1.0]) 223 | wd = self.camera2world_vec @ d 224 | return ti.Vector([wo.x,wo.y,wo.z]), wd 225 | 226 | @ti.func 227 | def rear_z(self): 228 | return self.thickness[self._elem_count - 1] 229 | 230 | @ti.func 231 | def front_z(self): 232 | z = 0.0 233 | for i in self.thickness: 234 | z += self.thickness[i] 235 | return z 236 | 237 | @ti.func 238 | def rear_radius(self): 239 | return self.curvature_radius[self._elem_count - 1] 240 | 241 | @ti.func 242 | def rear_aperture(self): 243 | return self.aperture_radius[self._elem_count - 1] 244 | 245 | @ti.kernel 246 | def recompute_exit_pupil(self): 247 | """ 248 | pre-process exit pupil of the lens system 249 | """ 250 | 251 | rearZ = self.rear_z() 252 | if rearZ <= 0.0: 253 | print('Not focus') 254 | rearRadius = self.rear_aperture() 255 | samples = 1024 * 1024 256 | half = 2.0 * rearRadius 257 | proj_bmin, proj_bmax = ti.Vector([-half, -half]), ti.Vector([half, half]) 258 | for i in range(pupil_interval_count): 259 | r0 = ti.cast(i, ti.f32) / pupil_interval_count * self.film_diagnal / 2.0 260 | r1 = ti.cast(i + 1, ti.f32) / pupil_interval_count * self.film_diagnal / 2.0 261 | bmin, bmax = make_bound2() 262 | count = 0 263 | for j in range(samples): 264 | u, v= ti.random(), ti.random() 265 | film_pos = ti.Vector([lerp(ti.cast(j, ti.f32)/samples, r0, r1), 0.0, 0.0]) 266 | x, y = lerp(u, -half, half), lerp(v, -half, half) 267 | lens_pos = ti.Vector([x, y, rearZ]) 268 | if inside_aabb(bmin, bmax, ti.Vector([x, y])): 269 | ti.atomic_add(count, 1) 270 | else: 271 | ok, _, _ = self.gen_ray_from_film(film_pos, (lens_pos - film_pos).normalized()) 272 | if ok: 273 | bmin, bmax = bound_union_with(bmin,bmax, ti.Vector([x, y])) 274 | ti.atomic_add(count, 1) 275 | 276 | if count == 0: 277 | bmin, bmax = proj_bmin, proj_bmax 278 | 279 | # extents pupil bound 280 | delta = 2 * (proj_bmax - proj_bmin).norm() / ti.sqrt(samples) 281 | bmin -= delta 282 | bmax += delta 283 | 284 | self.exitPupilBoundMin[i] = bmin 285 | self.exitPupilBoundMax[i] = bmax 286 | 287 | @ti.func 288 | def sample_exit_pupil(self, film_pos, uv): 289 | """ 290 | filme_pos: sampled point on the film 291 | uv: 2d sample 292 | 293 | Returns the sample point in the lenses space and the pupil area 294 | """ 295 | 296 | r = film_pos.norm() 297 | index = ti.cast(ti.min(r / self.film_diagnal * 2.0 * pupil_interval_count, pupil_interval_count - 1), ti.i32) 298 | bmin, bmax = self.exitPupilBoundMin[index], self.exitPupilBoundMax[index] 299 | area = (bmax - bmin).dot(ti.Vector([1.0, 1.0])) 300 | sampled = lerp(uv, bmin, bmax) 301 | sint = film_pos.y / r if abs(r) >= eps else 0.0 302 | cost = film_pos.x / r if abs(r) >= eps else 1.0 303 | return ti.Vector( 304 | [ 305 | cost * sampled.x - sint * sampled.y, 306 | sint * sampled.x + cost * sampled.y, 307 | self.rear_z() 308 | ]), area 309 | 310 | @ti.func 311 | def gen_ray(self, film_uv, lens_uv): 312 | 313 | """ 314 | film_uv: samples on film 315 | lens_uv: samples on lens 316 | 317 | Returns: 318 | weight: non-zero if exit ray exists otherwise returns 0 319 | (ro, rd): exit ray 320 | film_pos: sampled point on film 321 | """ 322 | extent = ti.Vector([self.film_width, self.film_height]) 323 | film_pos_xy = lerp(film_uv, ti.Vector([0.0, 0.0]), extent) 324 | 325 | film_pos_xy = ti.Vector([film_pos_xy.x - self.film_width/2.0, self.film_height /2.0 - film_pos_xy.y]) 326 | lens_pos, area = self.sample_exit_pupil(film_pos_xy, lens_uv) 327 | film_pos = ti.Vector([film_pos_xy.x, film_pos_xy.y, 0.0]) 328 | o, d = film_pos, lens_pos - film_pos 329 | exit, out_o ,out_d = self.gen_ray_from_film(o, d) 330 | 331 | weight = 0.0 332 | if not exit: 333 | self.vignet[None] += 1 334 | 335 | if exit: 336 | cost = out_d.z 337 | cos4t = cost * cost * cost * cost 338 | weight = self.shutter * cos4t * area /(self.rear_z() * self.rear_z()) 339 | pos = lerp(film_uv, ti.Vector([0.0, 0.0]), ti.Vector([self.pixel_width, self.pixel_height])) 340 | return weight, self.camera_2_world(out_o, out_d), pos 341 | 342 | @ti.func 343 | def gen_ray_of(self, px, py): 344 | """ 345 | px, py: pixel position of final image 346 | """ 347 | lens_uv = ti.Vector([ti.random(), ti.random()]) 348 | u, v = ti.cast(px, ti.f32) / self.pixel_width, ti.cast(py, ti.f32) / self.pixel_height 349 | film_uv = ti.Vector([u, v]) 350 | 351 | extent = ti.Vector([self.film_width, self.film_height]) 352 | film_pos_xy = lerp(film_uv, ti.Vector([0.0, 0.0]), extent) 353 | 354 | film_pos_xy = ti.Vector([film_pos_xy.x - self.film_width/2.0, self.film_height /2.0 - film_pos_xy.y]) 355 | lens_pos, area = self.sample_exit_pupil(film_pos_xy, lens_uv) 356 | film_pos = ti.Vector([film_pos_xy.x, film_pos_xy.y, 0.0]) 357 | o, d = film_pos, lens_pos - film_pos 358 | exit, out_o ,out_d = self.gen_ray_from_film(o, d) 359 | 360 | weight = 0.0 361 | if not exit: 362 | self.vignet[None] += 1 363 | 364 | if exit: 365 | cost = out_d.z 366 | cos4t = cost * cost * cost * cost 367 | weight = self.shutter * cos4t * area /(self.rear_z() * self.rear_z()) 368 | 369 | return weight, self.camera_2_world(out_o, out_d) 370 | 371 | @ti.func 372 | def compute_cardinal_points(self, in_ro, out_ro, out_rd): 373 | """ 374 | Returns the z coordinate of the principal plane the the focal point 375 | (fz, pz) in the lenses space 376 | note: input vectors are in camera space 377 | """ 378 | tf = -out_ro.x / out_rd.x 379 | tp = (in_ro.x - out_ro.x) / out_rd.x 380 | return -(out_ro + out_rd * tf).z, -(out_ro + out_rd * tp).z 381 | 382 | 383 | @ti.func 384 | def compute_thick_lens_approximation(self): 385 | """ 386 | Returns the focal length and the z of principal plane 387 | return fz1, pz1, fz2, pz2 in lense space 388 | """ 389 | 390 | x = self.film_diagnal * 0.001 391 | so = ti.Vector([x, 0.0, self.front_z() + 1.0]) 392 | sd = ti.Vector([0.0, 0.0, -1.0]) 393 | fo = ti.Vector([x, 0.0, self.rear_z() - 1.0]) 394 | fd = ti.Vector([0.0, 0.0, 1.0]) 395 | ok1, o1, d1 = self.gen_ray_from_scene(so, sd) 396 | ok2, o2, d2 = self.gen_ray_from_film(fo, fd) 397 | assert ok1 == True and ok2 == True 398 | fz, pz = self.compute_cardinal_points(so, o1, d1) 399 | fz1, pz1 = self.compute_cardinal_points(fo, o2, d2) 400 | assert fz1 < pz1 and pz < fz 401 | return fz, pz, fz1, pz1 402 | 403 | @ti.func 404 | def get_focal_length(self): 405 | fz, pz ,fz1 ,pz1 = self.compute_thick_lens_approximation() 406 | return fz - pz, pz1 - fz1 407 | 408 | @ti.func 409 | def focus_thick_camera(self, focus_distance): 410 | """ 411 | focus_distance > 0 412 | """ 413 | fz1, pz1, fz2, pz2 = self.compute_thick_lens_approximation() 414 | f = fz1 - pz1 415 | assert f > 0 416 | z = -focus_distance 417 | delta = 0.5 * (pz2 - z + pz1 - ti.sqrt((pz2 - z - pz1)*(pz2-z-4*f-pz1) )) 418 | return self.thickness[self._elem_count - 1] + delta 419 | 420 | @ti.kernel 421 | def refocus(self, focus_distance:ti.f32): 422 | rf = self.focus_thick_camera(focus_distance * 1000.0) 423 | self.thickness[self._elem_count - 1] = rf 424 | 425 | @ti.func 426 | def intersect_with_sphere(self, 427 | center, 428 | radius, 429 | ro, 430 | rd): 431 | """ 432 | center: z depth of lens sphere center 433 | """ 434 | 435 | o = ro - ti.Vector([0.0, 0.0, center]) 436 | ok = True 437 | 438 | # translate the sphere to original 439 | A = rd.x * rd.x + rd.y * rd.y + rd.z * rd.z 440 | B = 2 * ( rd.x * o.x + rd.y * o.y + rd.z * o.z) 441 | C = o.x * o.x + o.y * o.y + o.z * o.z - radius * radius 442 | delta = B * B - 4 * A * C 443 | if delta < 0: 444 | ok = False 445 | 446 | root_delta = ti.sqrt(delta) 447 | t = 0.0 448 | n = ti.Vector([0.0,0.0,0.0]) 449 | 450 | if ok: 451 | q = 0.0 452 | if B < 0: 453 | q = -0.5 * (B - root_delta) 454 | else: 455 | q = -0.5 * (B + root_delta) 456 | t0, t1 = q/A , C/q 457 | if t0 > t1: 458 | t0, t1 = t1, t0 459 | 460 | # t0, t1 = (-B - root_delta) / (2 * A), (-B + root_delta) / (2 * A) 461 | 462 | closer = (rd.z > 0) ^ (radius < 0) 463 | t = ti.min(t0, t1) if closer else ti.max(t0, t1) 464 | if t < 0: 465 | ok = False 466 | if ok: 467 | n = (o + t * rd).normalized() 468 | n = -n if n.dot(-rd) < 0.0 else n 469 | 470 | return ok, t, n 471 | 472 | 473 | @ti.func 474 | def gen_ray_from_scene(self, ori, dir): 475 | ro, rd = ti.Vector([ti.cast(ori.x,ti.f32), ori.y, -ori.z]), ti.Vector([ti.cast(dir.x,ti.f32), dir.y, -dir.z]).normalized() 476 | elemZ = -self.front_z() 477 | ok = True 478 | t = 0.0 479 | n = ti.Vector([0.0,0.0,0.0]) 480 | for _ in range(1): # force the inner loop serialized so that break could be used 481 | for i in range(self._elem_count): 482 | is_stop = self.curvature_radius[i] == 0.0 483 | if is_stop: 484 | t = (elemZ - ro.z) / rd.z 485 | else: 486 | radius = self.curvature_radius[i] 487 | centerZ = elemZ + radius 488 | isect, t, n = self.intersect_with_sphere(centerZ, radius, ro, rd) 489 | if not isect: 490 | ok = False 491 | break 492 | 493 | assert t > 0.0 494 | 495 | hit = ro + rd * t 496 | r = hit.x * hit.x + hit.y * hit.y 497 | if r > self.aperture_radius[i] * self.aperture_radius[i]: # out of the element aperture 498 | ok = False 499 | break 500 | ro = ti.Vector([hit.x, hit.y, hit.z]) 501 | 502 | if not is_stop: 503 | # refracted by lens 504 | etaI = 1.0 if i == 0 or self.eta[i - 1] == 0.0 else self.eta[i - 1] 505 | etaT = self.eta[i] if self.eta[i] != 0.0 else 1.0 506 | rd.normalized() 507 | has_r, d = refract(rd, n, etaI/etaT) 508 | if not has_r: 509 | ok = False 510 | break 511 | rd = ti.Vector([d.x, d.y, d.z]) 512 | 513 | elemZ += self.thickness[i] 514 | 515 | return ok, ti.Vector([ro.x, ro.y, -ro.z]), ti.Vector([rd.x, rd.y, -rd.z]).normalized() 516 | 517 | @ti.func 518 | def gen_ray_from_film(self, ori, dir): 519 | """ 520 | Input ray is the initial ray sampled from film to the rear lens element. 521 | Returns True and the output ray if the ray could be pass the lens system 522 | or returns False 523 | """ 524 | ro, rd = ti.Vector([ori.x, ori.y, -ori.z]), ti.Vector([dir.x, dir.y, -dir.z]).normalized() 525 | elemZ = 0.0 526 | ok = True 527 | t = 0.0 528 | n = ti.Vector([0.0,0.0,0.0]) 529 | for _ in range(1): # force the inner loop serialized so that break could be allowed 530 | for ii in range(self._elem_count): 531 | i = self._elem_count - ii - 1 532 | elemZ -= self.thickness[i] 533 | is_stop = self.curvature_radius[i] == 0.0 534 | if is_stop: 535 | if rd.z >= 0.0: 536 | ok = False 537 | break 538 | t = (elemZ - ro.z) / rd.z 539 | else: 540 | radius = self.curvature_radius[i] 541 | centerZ = elemZ + radius 542 | isect, t, n = self.intersect_with_sphere(centerZ, radius, ro, rd) 543 | if not isect: 544 | ok = False 545 | break 546 | 547 | assert t > 0.0 548 | hit = ro + rd * t 549 | r = hit.x * hit.x + hit.y * hit.y 550 | if r > self.aperture_radius[i] * self.aperture_radius[i]: # out of the element aperture 551 | ok = False 552 | break 553 | 554 | ro = ti.Vector([hit.x, hit.y, hit.z]) 555 | 556 | if not is_stop: 557 | # refracted by lens 558 | etaI = self.eta[i] 559 | etaT = self.eta[i - 1] if i > 0 and self.eta[i - 1] != 0.0 else 1.0 # the outer of 0-th element is air, whose eta is 1.0 560 | # rd.normalized() 561 | has_r, d = refract(rd, n, etaI/etaT) 562 | if not has_r: 563 | ok = False 564 | break 565 | rd = ti.Vector([d.x, d.y, d.z]) 566 | 567 | return ok, ti.Vector([ro.x, ro.y, -ro.z]), ti.Vector([rd.x, rd.y, -rd.z]).normalized() 568 | 569 | @ti.kernel 570 | def gen_draw_rays_from_film(self): 571 | """ 572 | draw the bound ray 573 | """ 574 | r = self.aperture_radius[self._elem_count - 1] 575 | step = 0.01 576 | count = ti.cast(r / step,ti.i32) 577 | for j in range(1): 578 | for i in range(count): 579 | y = r - i * step 580 | ori, dir = ti.Vector([0.0, 0.0, 0.0]), ti.Vector([y, 0.0, self.rear_z()]) 581 | ok, a, b = self.gen_ray_from_film(ori, dir) 582 | if ok: 583 | self.draw_ray_from_film(ori, dir, 0) 584 | break 585 | 586 | @ti.kernel 587 | def gen_draw_rays_from_scene(self): 588 | pass 589 | 590 | @ti.func 591 | def draw_ray_from_film(self, ori, dir, ind): 592 | ro, rd = ti.Vector([ori.x, ori.y, -ori.z]), ti.Vector([dir.x, dir.y, -dir.z]).normalized() 593 | elemZ = 0.0 594 | ok = True 595 | t = 0.0 596 | n = ti.Vector([0.0,0.0,0.0]) 597 | for _ in range(1): # force the inner loop serialized so that break could be allowed 598 | for ii in range(self._elem_count): 599 | i = self._elem_count - ii - 1 600 | elemZ -= self.thickness[i] 601 | is_stop = self.curvature_radius[i] == 0.0 602 | if is_stop: 603 | if rd.z >= 0.0: 604 | ok = False 605 | break 606 | t = (elemZ - ro.z) / rd.z 607 | else: 608 | radius = self.curvature_radius[i] 609 | centerZ = elemZ + radius 610 | isect, t, n = self.intersect_with_sphere(centerZ, radius, ro, rd) 611 | if not isect: 612 | ok = False 613 | break 614 | 615 | hit = ro + rd * t 616 | r = hit.x * hit.x + hit.y * hit.y 617 | if r > self.aperture_radius[i] * self.aperture_radius[i]: # out of the element aperture 618 | ok = False 619 | break 620 | 621 | self.draw_rays[ind, ii] = ro 622 | 623 | ro = ti.Vector([hit.x, hit.y, hit.z]) 624 | 625 | if not is_stop: 626 | # refracted by lens 627 | etaI = self.eta[i] 628 | etaT = self.eta[i - 1] if i > 0 and self.eta[i - 1] != 0.0 else 1.0 # the outer of 0-th element is air, whose eta is 1.0 629 | has_r, d = refract(rd, n, etaI/etaT) 630 | if not has_r: 631 | ok = False 632 | break 633 | rd = ti.Vector([d.x, d.y, d.z]) 634 | 635 | self.draw_rays[ind, self._elem_count] = ro 636 | self.draw_rays[ind, self._elem_count + 1] = ro + rd * 20.0 637 | # return ok, ti.Vector([ro.x, ro.y, -ro.z]), ti.Vector([rd.x, rd.y, -rd.z]).normalized() 638 | 639 | @ti.func 640 | def draw_ray_from_scene(self, ori, dir, ind): 641 | ro, rd = ti.Vector([ti.cast(ori.x,ti.f32), ori.y, -ori.z]), ti.Vector([ti.cast(dir.x,ti.f32), dir.y, -dir.z]).normalized() 642 | elemZ = -self.front_z() 643 | ok = True 644 | t = 0.0 645 | n = ti.Vector([0.0,0.0,0.0]) 646 | for _ in range(1): # force the inner loop serialized so that break could be used 647 | for i in range(self._elem_count): 648 | is_stop = self.curvature_radius[i] == 0.0 649 | if is_stop: 650 | t = (elemZ - ro.z) / rd.z 651 | else: 652 | radius = self.curvature_radius[i] 653 | centerZ = elemZ + radius 654 | isect, t, n = self.intersect_with_sphere(centerZ, radius, ro, rd) 655 | if not isect: 656 | ok = False 657 | break 658 | 659 | assert t > 0.0 660 | 661 | hit = ro + rd * t 662 | r = hit.x * hit.x + hit.y * hit.y 663 | if r > self.aperture_radius[i] * self.aperture_radius[i]: # out of the element aperture 664 | ok = False 665 | break 666 | 667 | self.draw_rays[ind, i] = ro 668 | ro = ti.Vector([hit.x, hit.y, hit.z]) 669 | 670 | if not is_stop: 671 | # refracted by lens 672 | etaI = 1.0 if i == 0 or self.eta[i - 1] == 0.0 else self.eta[i - 1] 673 | etaT = self.eta[i] if self.eta[i] != 0.0 else 1.0 674 | rd.normalized() 675 | has_r, d = refract(rd, n, etaI/etaT) 676 | if not has_r: 677 | ok = False 678 | break 679 | rd = ti.Vector([d.x, d.y, d.z]) 680 | 681 | elemZ += self.thickness[i] 682 | 683 | self.draw_rays[ind, self._elem_count] = ro 684 | self.draw_rays[ind, self._elem_count + 1] = ro + rd * 20.0 685 | 686 | def get_ray_points(self): 687 | return self.draw_rays.to_numpy() 688 | -------------------------------------------------------------------------------- /src/gui/lens_designer.py: -------------------------------------------------------------------------------- 1 | import math 2 | import dearpygui.dearpygui as dpg 3 | import taichi as ti 4 | 5 | from . import lens_preset 6 | from typing import List, Any, Callable, Dict 7 | from base.msg_queue import msg 8 | from gui.widget import Widget, PropertyWidget, AttributeValueType 9 | import numpy as np 10 | import networkx as nx 11 | 12 | from core.renderer import real_cam, color_buffer, taichi_render 13 | 14 | class LensCanvasWidget(Widget): 15 | def __init__(self, *, parent: int, film_height=24.0, callback:Callable[[None],None]=None): 16 | super().__init__(parent=parent, callback=callback) 17 | height = 400 18 | with dpg.child(parent=parent,autosize_x=True,no_scrollbar=True,height=400) as self._widget_id: 19 | with dpg.drawlist(label='lenses',parent=self._widget_id) as self._drawlist_id: 20 | self.film_height = film_height 21 | self.axis_y = int(height / 2.0) 22 | self.origin_z = 0 23 | self.scale = 5.0 24 | self.lense_length = 0.0 25 | self.max_radius = 0.0 26 | self.world_matrix = np.array([[1.0,0.0,0.0], [0.0,1.0 ,0.0],[0.0,0.0,1.0]]) 27 | self.screen_matrix = np.array([[1.0,0.0,0.0], [0.0,1.0 ,0.0],[0.0,0.0,1.0]]) 28 | 29 | # self._drawlist_id = self.widget() 30 | 31 | def drawlist(self): 32 | return self._drawlist_id 33 | 34 | def _setup_transform(self,scale: float, origin_z:float, axis_y:float): 35 | """ 36 | Updates transform matrix whenever the lense size or canvas size changes 37 | """ 38 | self.world_matrix = np.array([ 39 | [scale,0.0,0.0], 40 | [0.0,scale,0.0], 41 | [0.0,0.0,1.0] 42 | ]) 43 | 44 | self.screen_matrix = np.array([ 45 | [-1, -0, origin_z], 46 | [0, -1, axis_y], 47 | [0,0,1] 48 | ]) 49 | 50 | self.world_to_screen = self.screen_matrix @ self.world_matrix 51 | 52 | def _world_to_screen(self, point: List[float]): 53 | return (self.world_to_screen @ np.array([point[0], point[1], 1.0]))[0:2] 54 | 55 | def _draw_frame(self, padding = 5): 56 | # rect = dpg.get_item_rect_size(self.drawlist()) 57 | rect = (dpg.get_item_width(self.drawlist()), dpg.get_item_height(self.drawlist())) 58 | poly = [ 59 | [padding,padding], # topleft 60 | [rect[0] - padding, padding], # topright 61 | [rect[0]- padding, rect[1] - padding], # bottomright 62 | [padding,rect[1] - padding] # bottomleft 63 | ] 64 | dpg.draw_polyline(poly, parent=self.drawlist(),closed=True) 65 | 66 | def _update_canvas(self, lense_length:float, lense_radius:float): 67 | """ 68 | Updates whenever the lense size or canvas size changes 69 | """ 70 | w, h = dpg.get_item_width(self.drawlist()), dpg.get_item_height(self.drawlist()) 71 | rect = (w, h) 72 | self.scale = min(rect[0] / lense_radius, rect[1] / lense_length) 73 | self.axis_y = rect[1] / 2.0 74 | self.origin_z = rect[0]/2.0 + lense_length * self.scale / 2.0 75 | self._setup_transform(self.scale, self.origin_z, self.axis_y) 76 | 77 | def _draw_impl(self, lenses, lense_length, lense_radius): 78 | self._update_canvas(lense_length, lense_radius) 79 | self.clear_drawinglist() 80 | self._draw_frame() 81 | z = lense_length 82 | # draw lense groups 83 | for i in range(len(lenses)): 84 | is_stop = lenses[i][0] == 0.0 85 | r = lenses[i][3]/2.0 86 | if is_stop: 87 | self._draw_aperture_stop(z, r, color=[255.0, 255.0, 0.0], thickness=4.0) 88 | else: 89 | a, b = self._draw_arch(z, lenses[i][0], min(min(6.0, lenses[i][3]), abs(lenses[i][0]))) 90 | if i > 0 and lenses[i - 1][2] != 1 and lenses[i-1][2] != 0: 91 | self._draw_line(a, first) 92 | self._draw_line(b, last) # draw connection between element surface 93 | first, last = a, b 94 | 95 | z -= lenses[i][1] 96 | 97 | self._draw_axis() 98 | self._draw_film() 99 | 100 | def draw_lenses(self, lenses:np.ndarray): 101 | 102 | # workaround 103 | rect = dpg.get_item_rect_size(self.widget()) 104 | dpg.configure_item(self.drawlist(), width=rect[0] - 20,height=rect[1] - 20) 105 | #### 106 | 107 | length = 0.0 108 | max_radius = 0.0 109 | for i in range(len(lenses)): 110 | thickness = lenses[i][1] 111 | if math.isnan(thickness): 112 | continue 113 | else: 114 | length += lenses[i][1] 115 | max_radius = max(max_radius, lenses[i][3] / 2.0) 116 | 117 | if length == 0.0 or max_radius == 0.0: 118 | return 119 | 120 | self.lense_length = length 121 | self.max_radius = max_radius 122 | 123 | 124 | self._draw_impl(lenses, length, max_radius) 125 | 126 | def draw_bound_rays(self, rays:np.array, count:int, color=[255.0,255.0,255.0,255.0], thickness=1.0): 127 | points = [] 128 | point1 = [] 129 | for i in range(count): 130 | p = rays[0][i] 131 | p0 = self._world_to_screen([-p[2],p[0]]) 132 | p1 = self._world_to_screen([-p[2],-p[0]]) 133 | points.append(p0) 134 | point1.append(p1) 135 | 136 | dpg.draw_polyline(points, parent=self.drawlist(), color=color, thickness=thickness) 137 | dpg.draw_polyline(point1, parent=self.drawlist(), color=color, thickness=thickness) 138 | 139 | def _draw_line(self, p0:List[float], p1:List[float], color=[255.0,255.0,255.0], thickness=1.0): 140 | dpg.draw_line(self._world_to_screen(p0), self._world_to_screen(p1), color=color, thickness=thickness,parent=self.drawlist()) 141 | 142 | def _draw_axis(self): 143 | p0 = self._world_to_screen([self.lense_length + 10,0]) 144 | p1 = self._world_to_screen([-10,0]) 145 | dpg.draw_line(p0, p1, parent=self.drawlist()) 146 | 147 | def _draw_film(self, color=[255.0,255.0,255.0], thickness = 4.0): 148 | p0 = self._world_to_screen([0, self.film_height / 2]) 149 | p1 = self._world_to_screen([0, -self.film_height/2]) 150 | dpg.draw_line(p0, p1, color=color, thickness=thickness, parent=self.drawlist()) 151 | 152 | def _draw_aperture_stop(self, z:float, aperture_radius:float, color=[255.0,255.0,255.0], thickness=2.0): 153 | p0 = self._world_to_screen([z, aperture_radius + 10]) 154 | p1 = self._world_to_screen([z, aperture_radius]) 155 | p2 = self._world_to_screen([z, -aperture_radius]) 156 | p3 = self._world_to_screen([z, -aperture_radius - 10]) 157 | dpg.draw_line(p0, p1, color=color, thickness=thickness, parent=self.drawlist()) 158 | dpg.draw_line(p2, p3, color=color, thickness=thickness, parent=self.drawlist()) 159 | 160 | def _draw_arch(self, z: float, curvature_radius:float, aperture_radius:float, color=[255.0,255.0,255.0], thickness=1.0): 161 | """ 162 | There is no built-in arch drawing API in DearPyGui. So arch is done with 163 | segmented by polylines. 164 | 165 | Returns the two end points of the arch 166 | """ 167 | center = z - curvature_radius 168 | if abs(curvature_radius) < 1e-5: 169 | return [z, 0.0], [z, 0.0] 170 | half = math.asin(aperture_radius/curvature_radius) 171 | min_theta = -2 * half 172 | max_theta = 2 * half 173 | seg_count = 30 174 | points = [] 175 | first = [] 176 | last = [] 177 | for i in range(seg_count): 178 | t = i * 1.0 / seg_count 179 | theta = min_theta * (1.0 - t) + t * max_theta 180 | p0 = (center + curvature_radius * math.cos(theta)) 181 | p1 = curvature_radius * math.sin(theta) 182 | if i == 0: 183 | first = [p0, p1] 184 | elif i == seg_count - 1: 185 | last = [p0, p1] 186 | points.append(self._world_to_screen([p0, p1])) 187 | dpg.draw_polyline(points=points,parent=self.drawlist(),color=color,thickness=thickness) 188 | return first, last 189 | 190 | def clear_drawinglist(self): 191 | dpg.delete_item(self.drawlist(), children_only=True) 192 | 193 | 194 | class LensSurface(Widget): 195 | def __init__(self, parent: int, callback:Callable[[None],None]=None): 196 | super().__init__(parent=parent,callback=callback) 197 | 198 | class LensSphereSurface(LensSurface): 199 | curvature_radius = PropertyWidget('Curvature Radius', AttributeValueType.ATTRI_FLOAT,-1000,1100.0,100) 200 | thickness = PropertyWidget('Thickness', AttributeValueType.ATTRI_FLOAT,0.0,1000.0,100) 201 | eta = PropertyWidget('Eta', AttributeValueType.ATTRI_FLOAT,0.0,100.0,100) 202 | aperture_radius = PropertyWidget('Aperture Radius', AttributeValueType.ATTRI_FLOAT,0.0,1000.0,100) 203 | def __init__(self, parent: int, callback:Callable[[None],None]): 204 | super().__init__(parent=parent,callback=callback) 205 | with dpg.tree_node(label='SphereElement',parent=parent) as self._widget_id: 206 | self.curvature_radius = 0.0 207 | self.thickness = 0.0 208 | self.eta = 0.0 209 | self.aperture_radius = 0.0 210 | 211 | def dump(self): 212 | return [ self.curvature_radius,self.thickness,self.eta,self.aperture_radius] 213 | 214 | def property_changed(self, s, a, u): 215 | self._invoke_update() 216 | 217 | def load(self, data:List[float]= [0.0,0.0,0.0,0.0]): 218 | self.curvature_radius = data[0] 219 | self.thickness = data[1] 220 | self.eta = data[2] 221 | self.aperture_radius = data[3] 222 | 223 | 224 | 225 | class WidgetNode(Widget): 226 | def __init__(self,*, name:str, parent:int, callback:Callable[[Any],Any]=None): 227 | super(WidgetNode, self).__init__(parent=parent, callback=callback) 228 | self._attri_dict:Dict[str,Any] = {} 229 | self._callback = callback 230 | self._input_attr = None 231 | self._output_attr = None 232 | with dpg.node(label=name,parent=parent, user_data=self) as self._widget_id: 233 | pass 234 | 235 | def add_attribute(self, attri_name:str, attri_type:int): 236 | if attri_name not in self._attri_dict.keys(): 237 | with dpg.node_attribute(label=attri_name, attribute_type=attri_type, parent=self.widget(),user_data=self.widget()) as attri: 238 | self._attri_dict[attri_name] = (attri, {}) 239 | return attri 240 | return None 241 | 242 | def get_attribute(self, attri_name:str): 243 | return self._attri_dict.get(attri_name, (None, {}))[0] 244 | 245 | def remove_attribute(self, attri_name): 246 | if attri_name in self._attri_dict.keys(): 247 | dpg.delete_item(self._attri_dict[attri_name][0]) 248 | 249 | def add_value(self,*,attri_name:str, 250 | value_name:str, 251 | value_type:int, 252 | default_value:Any, 253 | size:int=4, 254 | callback:Callable[[Any], Any]=None): 255 | 256 | attri_id = self.get_attribute(attri_name) 257 | attri_id, value_dict = self._attri_dict.get(attri_name, (None, {})) 258 | if attri_id is None: 259 | print('No corresponding attribute :', attri_name) 260 | return 261 | 262 | width = 100 263 | 264 | if value_name in value_dict.keys(): 265 | print(value_name, 'has already existed in attribute ', attri_name) 266 | return None 267 | else: 268 | if value_type == AttributeValueType.ATTRI_FLOAT: 269 | value_id = dpg.add_input_float(label=value_name, callback=callback,default_value=default_value,parent=attri_id,width=width) 270 | elif value_type == AttributeValueType.ATTRI_FLOATX: 271 | value_id = dpg.add_input_floatx(label=value_name, callback=callback,default_value=default_value,size=size, parent=attri_id,width=width) 272 | elif value_type == AttributeValueType.ATTRI_INT: 273 | value_id = dpg.add_input_int(label=value_name,callback=callback,default_value=default_value, parent=attri_id,width=width) 274 | value_dict[value_name] = value_id 275 | 276 | return value_id 277 | 278 | 279 | def get_attri_value_item_id(self,attri_name:str, value_name:str): 280 | return self._attri_dict.get(attri_name, (-1, {}))[1].get(value_name, None) 281 | 282 | def get_value(self, attri_name:str, value_name:str): 283 | item_id = self.get_attri_value_item_id(attri_name=attri_name,value_name=value_name) 284 | if item_id is not None: 285 | return dpg.get_value(item_id) 286 | 287 | print('No such value: ', value_name, ' of ', attri_name) 288 | return None 289 | 290 | def set_value(self, attri_name:str, value_name:str, value:Any): 291 | item_id = self.get_attri_value_item_id(attri_name=attri_name,value_name=value_name) 292 | if item_id is not None: 293 | dpg.configure_item(item=item_id, value=Any) 294 | return 295 | print('No such value: ', value_name, ' of ', attri_name) 296 | 297 | 298 | class MidNode(WidgetNode): 299 | def __init__(self, *, name: str, parent: int, callback: Callable[[Any], Any]): 300 | super().__init__(name=name, parent=parent, callback=callback) 301 | self._input_end = self.add_attribute('input', dpg.mvNode_Attr_Input) 302 | self._output_end = self.add_attribute('output', dpg.mvNode_Attr_Output) 303 | 304 | def input_end(self): 305 | return self._input_end 306 | 307 | def output_end(self): 308 | return self._output_end 309 | 310 | 311 | class OutputNode(WidgetNode): 312 | def __init__(self, *, name: str, parent: int, callback: Callable[[Any], Any]): 313 | super().__init__(name=name, parent=parent, callback=callback) 314 | self._output_end = self.add_attribute('output', dpg.mvNode_Attr_Output) 315 | 316 | def output_end(self): 317 | return self._output_end 318 | 319 | class InputNode(WidgetNode): 320 | def __init__(self, *, name: str, parent: int, callback: Callable[[Any], Any]): 321 | super().__init__(name=name, parent=parent, callback=callback) 322 | self._input_end = self.add_attribute('input', dpg.mvNode_Attr_Input) 323 | 324 | def input_end(self): 325 | return self._input_end 326 | 327 | class SceneNodeParam(Widget): 328 | focus_depth = PropertyWidget(name='FocusDepth', property_type=AttributeValueType.ATTRI_FLOAT,min_value=0.0, max_value=10000.0,width=100) 329 | enable_focus = PropertyWidget(name='EnableFocus', property_type=AttributeValueType.ATTRI_BOOL, width=100) 330 | def __init__(self, *, parent: int, callback: Callable[[Any], None]): 331 | super().__init__(parent=parent, callback=callback) 332 | self._widget_id = parent 333 | self.focus_depth = 1.0 334 | self.enable_focus = True 335 | 336 | class SceneNode(OutputNode): 337 | def __init__(self,parent:int, value_update_callback:Callable[[Any], None]=None): 338 | super().__init__(name='Scene',parent=parent,callback=value_update_callback) 339 | attri = self.add_attribute(attri_name='focus depth',attri_type=dpg.mvNode_Attr_Static) 340 | self._param = SceneNodeParam(parent=attri,callback=self.callback()) 341 | 342 | def get_focus_depth(self): 343 | return self._param.focus_depth 344 | 345 | def get_focus_state(self): 346 | return self._param.enable_focus 347 | 348 | class FilmNodeParam(Widget): 349 | film_size = PropertyWidget(name='FilmSize', property_type=AttributeValueType.ATTRI_FLOATX,min_value=0.0,max_value=1000.0,size=2,width=100) 350 | render_window = PropertyWidget(name='RenderWindow', property_type=AttributeValueType.ATTRI_BOOL, width=100) 351 | def __init__(self, *, parent: int, callback: Callable[[Any], None]): 352 | super().__init__(parent=parent, callback=callback) 353 | self._widget_id = parent 354 | self.film_size = (36.00,24.00) 355 | self.render_window = True 356 | self._image = ImageViewer(parent=self.widget()) 357 | self._enable_render = True 358 | 359 | if self._enable_render: 360 | self._render() 361 | 362 | 363 | @msg 364 | def _render(self): 365 | i = 0 366 | color_buffer.from_numpy(np.zeros((800, 600, 3))) 367 | last_t = 0.0 368 | while True: 369 | taichi_render() 370 | interval = 50 371 | if i % interval == 0 and i > 0: 372 | img = color_buffer.to_numpy() * (1 / (i + 1)) 373 | img = np.sqrt(img / img.mean() * 0.24) 374 | # var = np.var(img) 375 | # img = img * 255.0 376 | # Image.fromarray(img.astype('uint8')).convert('RGB').save('output.jpg') 377 | self._image.from_numpy(img) 378 | 379 | i+=1 380 | if self._enable_render: 381 | yield 382 | else: 383 | break 384 | return 385 | 386 | def property_changed(self, s, a, u): 387 | ''' 388 | Filters the RenderWindow property changes 389 | ''' 390 | if s == getattr(self, '_RenderWindow'): 391 | dpg.configure_item(self._image.widget(), show=a) 392 | self._enable_render = a 393 | if self._enable_render: 394 | self._render() 395 | return super().property_changed(s, a, u) 396 | 397 | 398 | class ImageViewer(Widget): 399 | def __init__(self, parent): 400 | super().__init__(parent=parent) 401 | with dpg.child(parent=parent, label='Image',height=600,width=800) as self._widget_id: 402 | self._texture_container = dpg.add_texture_registry(label='Texture') 403 | self._width:int = 0 404 | self._height:int = 0 405 | self._texture_id:int = None 406 | 407 | def _release_texture(self): 408 | if self._texture_id is not None: 409 | dpg.delete_item(self._texture_id) 410 | self._texture_id = None 411 | 412 | def _set_or_recreate_texture(self, width, height, norm_rbga:List[float]): 413 | if width <= 0 or height <= 0: 414 | return 415 | if not self.valid() or self._width != width or self._height != height: 416 | self._release_texture() 417 | rect = dpg.get_item_rect_size(self.parent()) 418 | self._texture_id = dpg.add_dynamic_texture(width, height, norm_rbga, parent=self._texture_container) 419 | dpg.add_image(self._texture_id,parent=self.widget(), width=rect[0], height=rect[1]) 420 | self._width = width 421 | self._height = height 422 | return 423 | dpg.set_value(self._texture_id, norm_rbga) 424 | 425 | def set_image_norm_rgba(self,width:int, height:int, rgba:List[float]): 426 | """ 427 | each channel of rgba is range from [0 1] 428 | """ 429 | self._set_or_recreate_texture(width,height,rgba) 430 | 431 | def from_numpy(self, data:np.ndarray): 432 | shape = data.shape 433 | norm_rgba = [] 434 | if shape[2] > 4: 435 | return 436 | if shape[2] == 3: 437 | rgba = np.concatenate((data, np.ones((shape[0],shape[1], 1),dtype=np.float32)), axis=2) 438 | self.set_image_norm_rgba(shape[1],shape[0],rgba.flatten()) 439 | elif shape[2] == 4: 440 | self.set_image_norm_rgba(shape[1],shape[0],data.flatten()) 441 | elif shape[2] == 2 or shape[2] == 1: 442 | pass 443 | 444 | def to_numpy(self)->np.ndarray: 445 | raise NotImplementedError 446 | 447 | def width(self)->int: 448 | return self._width 449 | 450 | def height(self)->int: 451 | return self._height 452 | 453 | def valid(self)->bool: 454 | return self._width > 0 and self._height > 0 and self._texture_id != None 455 | 456 | 457 | class FilmNode(InputNode): 458 | def __init__(self, parent:int,value_update_callback:Callable[[Any], None]=None): 459 | super().__init__(name='Film',parent=parent,callback=value_update_callback) 460 | attri = self.add_attribute(attri_name='film size',attri_type=dpg.mvNode_Attr_Static) 461 | self._param = FilmNodeParam(parent=attri,callback=self.callback()) 462 | 463 | def get_film_size(self): 464 | return self._param.film_size 465 | 466 | def get_keep_rendering(self): 467 | return self._param.render_window 468 | 469 | class ApertureSurface(LensSurface): 470 | thickness = PropertyWidget('Thickness', AttributeValueType.ATTRI_FLOAT,0.0,100.0,100) 471 | aperture_radius = PropertyWidget('Aperture Radius', AttributeValueType.ATTRI_FLOAT,0.0,100.0,100) 472 | def __init__(self, parent: int, callback:Callable[[None],None]): 473 | super().__init__(parent=parent,callback=callback) 474 | self._widget_id = parent 475 | self.thickness = 0.0 476 | self.aperture_radius = 0.0 477 | 478 | def dump(self): 479 | return [ 0.0,self.thickness, 0.0 ,self.aperture_radius] 480 | 481 | def load(self, data:List[float]= [0.0,0.0,0.0,0.0]): 482 | self.thickness = data[1] 483 | self.aperture_radius = data[3] 484 | 485 | class ApertureStop(MidNode): 486 | def __init__(self, *, parent: int,value_update_callback:Callable[[Any], None]=None): 487 | super().__init__(name='Aperture Stop', parent=parent, callback=value_update_callback) 488 | self.static_attri = self.add_attribute('Aperture', attri_type=dpg.mvNode_Attr_Static) 489 | self.aperture_surface = ApertureSurface(self.static_attri, self.callback()) 490 | 491 | def get_surface(self, ind:int): 492 | return self.aperture_surface 493 | 494 | def get_surface_count(self): 495 | return 1 496 | 497 | class LensSurfaceGroup(MidNode): 498 | def __init__(self,*,name:str,parent:int, update_callback:Callable[[Any], Any]=None): 499 | super().__init__(name=name, parent=parent,callback=update_callback) 500 | self._lense_surface_group:List[LensSurface] = [] 501 | self._surface_data_value_id:List[int] = [] 502 | self.input_attri_item_id = self.add_attribute('surface count', attri_type=dpg.mvNode_Attr_Static) 503 | 504 | self.count_attri = self.add_value(attri_name='surface count', 505 | value_name="Surface Count", 506 | value_type=AttributeValueType.ATTRI_INT, 507 | default_value=0, 508 | callback=lambda s,a,u:self._update_surface(int(a))) 509 | 510 | 511 | def _update_surface(self, count): 512 | """ 513 | """ 514 | cur_count = len(self._lense_surface_group) 515 | print('surface count changed: ',count, cur_count) 516 | 517 | delta = count - cur_count 518 | if delta > 0: 519 | for _ in range(delta): 520 | surf = LensSphereSurface(self.input_attri_item_id, callback=self.callback()) 521 | self._lense_surface_group.append(surf) 522 | elif delta < 0: 523 | for item in self._lense_surface_group[count:]: 524 | item.delete() 525 | del self._lense_surface_group[count:] 526 | 527 | self._invoke_update() 528 | 529 | def load(self, raw_group_data: List[List[float]]): 530 | self._clear_surface_data() 531 | surfs = [] 532 | for s in raw_group_data: 533 | surf = LensSphereSurface(self.input_attri_item_id, callback=self.callback()) 534 | surf.load(s) 535 | surfs.append(surf) 536 | 537 | self._lense_surface_group = surfs 538 | dpg.set_value(self.count_attri, len(self._lense_surface_group)) 539 | self._invoke_update() 540 | 541 | def get_surface(self, ind:int): 542 | return self._lense_surface_group[ind] 543 | 544 | def get_surface_count(self): 545 | return len(self._lense_surface_group) 546 | 547 | def _clear_surface_data(self): 548 | for surf in self._lense_surface_group: 549 | surf.delete() 550 | self._lense_surface_group = [] 551 | 552 | def clear_surface_data(self): 553 | """ 554 | Invode update signal 555 | """ 556 | self._clear_surface_data() 557 | self._invoke_update() 558 | 559 | @staticmethod 560 | def create_sphere_lense_group(*, name, parent, group_data:List[List[float]], callback=None): 561 | group = LensSurfaceGroup(name=name, parent=parent,update_callback=callback) 562 | group.block_callback(True) 563 | group.load(group_data) 564 | group.block_callback(False) 565 | return group 566 | 567 | 568 | class EditorEventType: 569 | EVENT_NODE_ADD= 0x000 570 | EVENT_NODE_DELETE = 0x001 571 | EVENT_NODE_UPDATE = 0x002 572 | EVENT_NODE_DELETE_ALL = 0x003 573 | 574 | EVENT_LINK_ADD = 0x200 575 | EVENT_LINK_DELETE = 0x201 576 | 577 | 578 | EVENT_SURFACE_ADD = 0x300 579 | EVENT_SURFACE_DELETE = 0x301 580 | EVENT_SURFACE_ATTRIBUTE_CHANGED = 0x302 581 | 582 | EVENT_UPDATE_ALL = 0xFFF 583 | 584 | 585 | class ToolBar(Widget): 586 | def __init__(self, *, parent: int, callback: Callable[[Any], None]): 587 | super().__init__(parent=parent, callback=callback) 588 | self.add_node_button = dpg.add_button(label='Add Node') 589 | dpg.add_same_line() 590 | self.remove_node_button = dpg.add_button(label='Remove Nodes') 591 | dpg.add_same_line() 592 | self.remove_link_button = dpg.add_button(label='Remove Links') 593 | dpg.add_same_line() 594 | self.clear_all_button = dpg.add_button(label='Clear') 595 | dpg.add_same_line() 596 | self.save_button = dpg.add_button(label='Save') 597 | dpg.add_same_line() 598 | self.open_button = dpg.add_button(label='Open') 599 | dpg.add_same_line() 600 | self.preset_combo = dpg.add_combo(list(lens_preset.lens_data.keys()),label='Lens Preset',width=150) 601 | dpg.add_same_line() 602 | self.auto_arrange = dpg.add_button(label='Auto Arrange') 603 | 604 | 605 | class LensEditorWidget(Widget): 606 | def __init__(self,*,update_callback:Callable[[Any],None], parent: int): 607 | super().__init__(parent=parent, callback=update_callback) 608 | self._lense_data:List[Dict[str, List[float]]]= [] 609 | self._valid_lenses = False 610 | self._editor_id = -1 611 | 612 | with dpg.group(horizontal=False,parent=parent) as self._widget_id: 613 | self._toolbar = ToolBar(parent=self._widget_id,callback=self.callback()) 614 | with dpg.node_editor(parent=self._widget_id,callback=self._link_add_callback, delink_callback=self._link_delete_callback, height = 800) as self._editor_id: 615 | pass 616 | dpg.configure_item(self._toolbar.add_node_button,callback = lambda s,a,u:self.add_lens_group(LensSurfaceGroup(name='LensGroup', parent=self._editor_id, update_callback=self.callback()))) 617 | dpg.configure_item(self._toolbar.clear_all_button,callback = lambda s,a,u:self.clear_lense_group()) 618 | dpg.configure_item(self._toolbar.preset_combo,callback = lambda s,a,u:self.set_lense_data(lens_preset.lens_data.get(a,[]))) 619 | dpg.configure_item(self._toolbar.remove_node_button,callback = lambda s,a,u:self.remove_selected_nodes()) 620 | dpg.configure_item(self._toolbar.remove_link_button,callback = lambda s,a,u:self.remove_selected_links()) 621 | dpg.configure_item(self._toolbar.auto_arrange,callback = lambda s,a,u:self.auto_arrange()) 622 | 623 | # self._editor_id = self.widget() 624 | self.widget_g = nx.Graph() 625 | self.attri_g = nx.Graph() 626 | 627 | self._add_default_node() 628 | 629 | def _link_add_callback(self, sender:int, app_data:Any, user_data:Any): 630 | """ 631 | input_end 632 | output_end 633 | """ 634 | self._add_link_impl(app_data[0], app_data[1]) 635 | self._invoke_update(event=EditorEventType.EVENT_LINK_ADD) 636 | 637 | def _link_delete_callback(self, sender:int, app_data:Any, user_data:Any): 638 | """ 639 | app_data: edge item 640 | """ 641 | self._remove_link_impl(app_data) 642 | self._invoke_update(event=EditorEventType.EVENT_LINK_DELETE) 643 | 644 | def _add_link_impl(self, output_end, input_end): 645 | output_node, input_node = dpg.get_item_user_data(output_end), dpg.get_item_user_data(input_end) 646 | deleted_attri_edge = [] 647 | deleted_node_edge = [] 648 | for nbr, datadict in self.attri_g.adj[input_end].items(): 649 | deleted_attri_edge.append((nbr, input_end)) 650 | edge_item = datadict.get('edge_item', None) 651 | if edge_item: 652 | dpg.delete_item(edge_item) 653 | adj_output_node = dpg.get_item_user_data(nbr) 654 | deleted_node_edge.append((adj_output_node, input_node)) 655 | 656 | self.attri_g.remove_edges_from(deleted_attri_edge) 657 | self.widget_g.remove_edges_from(deleted_node_edge) 658 | 659 | link = dpg.add_node_link(output_end, input_end, parent=self._editor_id, user_data={'input_node':input_node,'output_node':output_node,'input_end':input_end,'output_end':output_end}) 660 | self.widget_g.add_edge(output_node, input_node, edge_item=link) 661 | self.attri_g.add_edge(output_end, input_end, edge_item=link) 662 | 663 | def _remove_link_impl(self, edge_item): 664 | udata = dpg.get_item_user_data(edge_item) 665 | input_node, output_node = udata['input_node'], udata['output_node'] 666 | self.widget_g.remove_edge(output_node,input_node) 667 | self.attri_g.remove_edge(udata['output_end'],udata['input_end']) 668 | dpg.delete_item(edge_item) 669 | 670 | def _add_node_impl(self, lens_group_node): 671 | self.widget_g.add_node(lens_group_node.widget()) 672 | if isinstance(lens_group_node, SceneNode): 673 | self.attri_g.add_node(lens_group_node.output_end()) 674 | elif isinstance(lens_group_node, FilmNode): 675 | self.attri_g.add_node(lens_group_node.input_end()) 676 | else: 677 | self.attri_g.add_node(lens_group_node.input_end()) 678 | self.attri_g.add_node(lens_group_node.output_end()) 679 | 680 | def _remove_selected_nodes_impl(self): 681 | """ 682 | TODO:: 683 | 684 | dpg.get_selected_nodes may hava a potential BUG 685 | 686 | """ 687 | print("_remove_selected_nodes_impl::dpg.get_selected_nodes may hava a potential BUG") 688 | selected_nodes = dpg.get_selected_nodes(self._editor_id) 689 | for x in selected_nodes: 690 | self._remove_node_impl(dpg.get_item_user_data(x)) 691 | 692 | def _remove_selected_links_impl(self): 693 | selected_links = dpg.get_selected_links(self._editor_id) 694 | for link in selected_links: 695 | self._remove_link_impl(link) 696 | 697 | def _remove_node_impl(self, lens_group_node): 698 | item = lens_group_node.widget() 699 | self.widget_g.remove_node(item) 700 | attri_ends = [] 701 | if isinstance(lens_group_node, SceneNode): 702 | attri_ends.append(lens_group_node.output_end()) 703 | elif isinstance(lens_group_node, FilmNode): 704 | attri_ends.append(lens_group_node.input_end()) 705 | else: 706 | attri_ends.append(lens_group_node.input_end()) 707 | attri_ends.append(lens_group_node.output_end()) 708 | 709 | deleted_edge = [] 710 | for end in attri_ends: 711 | for nbr, datadict in self.attri_g.adj[end].items(): 712 | edge_item = datadict['edge_item'] 713 | deleted_edge.append((nbr, end)) 714 | dpg.delete_item(edge_item) 715 | 716 | self.attri_g.remove_edges_from(deleted_edge) 717 | lens_group_node.delete() 718 | 719 | def _remove_all_node_impl(self): 720 | list(map(lambda x: self._remove_node_impl(dpg.get_item_user_data(x)), list(self.widget_g.nodes))) 721 | 722 | def _add_default_node(self): 723 | self._scene_node = SceneNode(self._editor_id, self.callback()) 724 | self._film_node = FilmNode(self._editor_id, self.callback()) 725 | self._add_node_impl(self._scene_node) 726 | self._add_node_impl(self._film_node) 727 | 728 | def add_lens_group(self, node:LensSurfaceGroup): 729 | self._add_node_impl(node) 730 | self._invoke_update(event=EditorEventType.EVENT_NODE_ADD) 731 | 732 | def remove_lense_group(self, nw:Widget): 733 | self._remove_node_impl(nw) 734 | self._invoke_update(event=EditorEventType.EVENT_NODE_DELETE) 735 | 736 | def remove_selected_nodes(self): 737 | self._remove_selected_nodes_impl() 738 | self._invoke_update(event=EditorEventType.EVENT_NODE_DELETE) 739 | 740 | def remove_selected_links(self): 741 | self._remove_selected_links_impl() 742 | self._invoke_update(event=EditorEventType.EVENT_LINK_DELETE) 743 | 744 | def clear_lense_group(self): 745 | self._remove_all_node_impl() 746 | self._add_default_node() 747 | self._invoke_update(event=EditorEventType.EVENT_NODE_DELETE) 748 | 749 | def auto_arrange(self): 750 | rect = dpg.get_item_rect_size(self._editor_id) 751 | all_nodes = self.get_lense_group() 752 | s = len(all_nodes) 753 | if s < 2: 754 | return 755 | 756 | s = int(s / 2.0) 757 | w_interval = rect[0] / (s + 2) 758 | h_interval = rect[1] / 2.0 759 | for i in range(len(all_nodes)): 760 | y = (i % 2) * h_interval + 100 761 | x = (i / 2) * w_interval + 100 762 | dpg.configure_item(all_nodes[i].widget(),pos=(x,y)) 763 | 764 | 765 | def get_lense_group(self): 766 | res = list(nx.all_simple_paths(self.widget_g, self._scene_node.widget(), self._film_node.widget())) 767 | ret = [] 768 | if len(res) > 0: 769 | ret = list(map(lambda x: dpg.get_item_user_data(x), res[0])) 770 | return ret 771 | 772 | def get_lense_data(self): 773 | groups = self.get_lense_group() 774 | lense_data = [] 775 | for g in range(1, len(groups) - 1): # excludes scene and film node 776 | c = groups[g].get_surface_count() 777 | for ind in range(c): 778 | surf = groups[g].get_surface(ind) 779 | lense_data.append(surf.dump()) 780 | return lense_data 781 | 782 | def set_lense_data(self, lense_data:List[List[float]]): 783 | self._remove_all_node_impl() 784 | self._add_default_node() 785 | 786 | # parse lense data 787 | stack = [] 788 | output_end = self._scene_node.output_end() 789 | for surf in lense_data: 790 | if surf[2] != 1.0 and surf[0] != 0: # surface begin 791 | stack.append(surf) 792 | elif surf[2] == 1.0 and surf[0] != 0: # surface end 793 | stack.append(surf) 794 | lense_group = LensSurfaceGroup.create_sphere_lense_group(name='LensGroup',parent=self._editor_id,group_data=stack, callback=self.callback()) 795 | self._add_node_impl(lense_group) 796 | input_end = lense_group.input_end() 797 | self._add_link_impl(output_end=output_end,input_end=input_end) 798 | output_end = lense_group.output_end() 799 | stack = [] 800 | elif surf[0] == 0.0 and surf[2] == 0.0: # aperture surface 801 | aperture = ApertureStop(parent=self._editor_id,value_update_callback=self.callback()) 802 | aperture.block_callback(True) 803 | aperture.get_surface(0).load(surf) 804 | self._add_node_impl(aperture) 805 | input_end = aperture.input_end() 806 | self._add_link_impl(output_end=output_end,input_end=input_end) 807 | output_end = aperture.output_end() 808 | aperture.block_callback(False) 809 | 810 | if len(stack) > 0: 811 | print('Wrong lense data') 812 | 813 | self._add_link_impl(output_end=output_end, input_end=self._film_node.input_end()) 814 | # closed 815 | # parse data end 816 | 817 | self.auto_arrange() 818 | self._invoke_update(event=EditorEventType.EVENT_NODE_UPDATE) 819 | 820 | def get_film_size(self): 821 | return self._film_node.get_film_size() 822 | 823 | def get_focus_depth(self): 824 | return self._scene_node.get_focus_depth() 825 | 826 | def get_focus_state(self): 827 | return self._scene_node.get_focus_state() 828 | 829 | def get_keep_rendering(self): 830 | return self._film_node.get_keep_rendering() 831 | 832 | 833 | 834 | 835 | class LensDesignerWidget(Widget): 836 | 837 | def __init__(self, parent: int): 838 | super().__init__(parent=parent) 839 | with dpg.group(parent=parent,horizontal=False) as self._widget_id: 840 | self._lense_canvas: LensCanvasWidget = LensCanvasWidget(parent=self.widget(),callback=self._canvas_update) 841 | self._node_editor: LensEditorWidget = LensEditorWidget(update_callback=self._editor_update, parent=self.widget()) 842 | 843 | pos = [0.0, 3.0, 24.0] 844 | center = [0.0, 0.0, 0.0] 845 | world_up = [0.0, 1.0, 0.0] 846 | # self.camera = RealisticCamera(pos, center, world_up) 847 | self._paint_canvas() 848 | 849 | 850 | def _editor_update(self, *args, **kwargs): 851 | """ 852 | Update Realistic camera here 853 | """ 854 | lense_data = self._node_editor.get_lense_data() 855 | self._canvas_update(lense_data = lense_data) 856 | 857 | def _canvas_update(self, *args, **kwargs): 858 | self._update_camera(kwargs['lense_data']) 859 | 860 | def _paint_canvas(self): 861 | ray_points = real_cam.get_ray_points() 862 | new_lense_data = real_cam.get_lenses_data() 863 | self._lense_canvas.draw_lenses(np.array(new_lense_data)) 864 | self._lense_canvas.draw_bound_rays(ray_points, real_cam.get_element_count() + 2, color=[0, 0, 255]) 865 | 866 | 867 | @msg 868 | def _update_camera(self, lense_data): 869 | real_cam.load_lens_data(lense_data) 870 | self._node_editor.get_focus_state() and real_cam.refocus(self._node_editor.get_focus_depth()) 871 | if self._node_editor.get_keep_rendering(): 872 | # real_cam.recompute_exit_pupil() 873 | color_buffer.from_numpy(np.zeros((800, 600, 3))) 874 | real_cam.gen_draw_rays_from_film() 875 | self._paint_canvas() --------------------------------------------------------------------------------