""" RenderPipeline Copyright (c) 2014-2016 tobspr Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ # Disable the unused variable warning - this occurs since we read a lot more # properties from the IES file than we actually need # pylint: disable=unused-variable from __future__ import print_function import re from panda3d.core import PTAFloat, Filename, SamplerState, VirtualFileSystem from panda3d.core import get_model_path from direct.stdpy.file import open, join, isfile from six.moves import range # pylint: disable=import-error from rpcore.native import IESDataset from rpcore.image import Image from rpcore.rpobject import RPObject class InvalidIESProfileException(Exception): """ Exception which is thrown when an error occurs during loading an IES Profile """ class IESProfileLoader(RPObject): """ Loader class to load .IES files and create an IESDataset from it. It generates a LUT for each loaded ies profile which is used by the lighting pipeline later on. """ # Supported IES Profiles PROFILES = [ "IESNA:LM-63-1986", "IESNA:LM-63-1991", "IESNA91", "IESNA:LM-63-1995", "IESNA:LM-63-2002", "ERCO Leuchten GmbH BY: ERCO/LUM650/8701", "ERCO Leuchten GmbH" ] # Regexp for extracting keywords KEYWORD_REGEX = re.compile(r"\[([A-Za-z0-8_-]+)\](.*)") def __init__(self, pipeline): RPObject.__init__(self) self._pipeline = pipeline self._entries = [] self._max_entries = 32 self._create_storage() def _create_storage(self): """ Internal method to create the storage for the profile dataset textures """ self._storage_tex = Image.create_3d("IESDatasets", 512, 512, self._max_entries, "R16") self._storage_tex.set_minfilter(SamplerState.FT_linear) self._storage_tex.set_magfilter(SamplerState.FT_linear) self._storage_tex.set_wrap_u(SamplerState.WM_clamp) self._storage_tex.set_wrap_v(SamplerState.WM_repeat) self._storage_tex.set_wrap_w(SamplerState.WM_clamp) self._pipeline.stage_mgr.inputs["IESDatasetTex"] = self._storage_tex self._pipeline.stage_mgr.defines["MAX_IES_PROFILES"] = self._max_entries def load(self, filename): """ Loads a profile from a given filename and returns the internal used index which can be assigned to a light.""" # Make sure the user can load profiles directly from the ies profile folder data_path = join("/$$rp/data/ies_profiles/", filename) if isfile(data_path): filename = data_path # Make filename unique fname = Filename.from_os_specific(filename) if not VirtualFileSystem.get_global_ptr().resolve_filename( fname, get_model_path().get_value(), "ies"): self.error("Could not resolve", filename) return -1 fname = fname.get_fullpath() # Check for cache entries if fname in self._entries: return self._entries.index(fname) # Check for out of bounds if len(self._entries) >= self._max_entries: # TODO: Could remove unused profiles here or regenerate texture self.warn("Cannot load IES Profile, too many loaded! (Maximum: 32)") # Try loading the dataset, and see what happes try: dataset = self._load_and_parse_file(fname) except InvalidIESProfileException as msg: self.warn("Failed to load profile from", filename, ":", msg) return -1 if not dataset: return -1 # Dataset was loaded successfully, now copy it dataset.generate_dataset_texture_into(self._storage_tex, len(self._entries)) self._entries.append(fname) return len(self._entries) - 1 def _load_and_parse_file(self, pth): """ Loads a .IES file from a given filename, returns an IESDataset which is used by the load function later on. """ self.debug("Loading ies profile from", pth) try: with open(pth, "r") as handle: lines = handle.readlines() except IOError as msg: self.error("Failed to open", pth, ":", msg) return None lines = [i.strip() for i in lines] # Parse version header self._check_version_header(lines.pop(0)) # Parse arbitrary amount of keywords keywords = self._extract_keywords(lines) # noqa # Next line should be TILT=NONE according to the spec if lines.pop(0) != "TILT=NONE": raise InvalidIESProfileException("Expected TILT=NONE line, but none found!") # From now on, lines do not matter anymore, instead everything is # space seperated new_parts = (' '.join(lines)).replace(",", " ").split() def read_int(): return int(new_parts.pop(0)) def read_float(): return float(new_parts.pop(0)) # Amount of Lamps if read_int() != 1: raise InvalidIESProfileException("Only 1 Lamp supported!") # Extract various properties lumen_per_lamp = read_float() # noqa candela_multiplier = read_float() # noqa num_vertical_angles = read_int() num_horizontal_angles = read_int() if num_vertical_angles < 1 or num_horizontal_angles < 1: raise InvalidIESProfileException("Invalid of vertical/horizontal angles!") photometric_type = read_int() # noqa unit_type = read_int() # Check for a correct unit type, should be 1 for meters and 2 for feet if unit_type not in [1, 2]: raise InvalidIESProfileException("Invalid unit type") width = read_float() # noqa length = read_float() # noqa height = read_float() # noqa ballast_factor = read_float() # noqa future_use = read_float() # Unused field for future usage # noqa input_watts = read_float() # noqa # Read vertical angles vertical_angles = [read_float() for i in range(num_vertical_angles)] horizontal_angles = [read_float() for i in range(num_horizontal_angles)] candela_values = [] candela_scale = 0.0 for i in range(num_horizontal_angles): vertical_data = [read_float() for i in range(num_vertical_angles)] candela_scale = max(candela_scale, max(vertical_data)) candela_values += vertical_data # Rescale values, divide by maximum candela_values = [i / candela_scale for i in candela_values] if len(new_parts) != 0: self.warn("Unhandled data at file-end left:", new_parts) # Dont abort here, some formats like those from ERCO Leuchten GmbH # have an END keyword, just ignore everything after the data was # read in. dataset = IESDataset() dataset.set_vertical_angles(self._list_to_pta(vertical_angles)) dataset.set_horizontal_angles(self._list_to_pta(horizontal_angles)) dataset.set_candela_values(self._list_to_pta(candela_values)) # Testing code to write out the LUT # from panda3d.core import Texture # tex = Texture("temp") # tex.setup_3d_texture(512, 512, 1, Image.T_float, Image.F_r16) # dataset.generate_dataset_texture_into(tex, 0) # tex.write("generated.png") return dataset def _list_to_pta(self, list_values): """ Converts a list to a PTAFloat """ pta = PTAFloat.empty_array(len(list_values)) for i, val in enumerate(list_values): pta[i] = val return pta def _check_version_header(self, first_line): """ Checks if the IES version header is correct and the specified IES version is supported """ if first_line not in self.PROFILES: raise InvalidIESProfileException("Unsupported Profile: " + first_line) def _extract_keywords(self, lines): """ Extracts the keywords from a list of lines, and removes all lines containing keywords """ keywords = {} while lines: line = lines.pop(0) if not line.startswith("["): # Special format used by some IES files, indicates end of properties # By just checking for the tilt keyword instead of validating each line, # we can read even malformed lines, like those from ERCO Leuchten GmbH if line != "TILT=NONE": continue # No keyword, we popped the line already tho, so append it again lines.insert(0, line) return keywords else: # Try matching the keywords match = self.KEYWORD_REGEX.match(line) if match: key, val = match.group(1, 2) keywords[key.strip()] = val.strip() else: raise InvalidIESProfileException("Invalid keyword line: " + line) return keywords