From af2bd70e523fe2bf933e8dfe0640304a8e4ceae2 Mon Sep 17 00:00:00 2001 From: Lubos Lenco Date: Thu, 17 Aug 2017 14:37:04 +0200 Subject: [PATCH] Crude ies lamp support --- Assets/ies/load_ies.py | 172 ++++++++++++++++++ .../deferred_light/deferred_light.frag.glsl | 9 + Shaders/deferred_light/deferred_light.json | 5 + Shaders/std/ies.glsl | 37 ++-- blender/arm/make_world.py | 4 + blender/arm/props.py | 1 + blender/arm/props_ui.py | 1 + 7 files changed, 214 insertions(+), 15 deletions(-) create mode 100644 Assets/ies/load_ies.py diff --git a/Assets/ies/load_ies.py b/Assets/ies/load_ies.py new file mode 100644 index 00000000..ed47c220 --- /dev/null +++ b/Assets/ies/load_ies.py @@ -0,0 +1,172 @@ +# IES parser based on: +# https://github.com/tobspr/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. + +import re +import os +import math +import bpy +import random + +def load(filepath): + global _vertical_angles + global _horizontal_angles + global _candela_values + KEYWORD_REGEX = re.compile(r"\[([A-Za-z0-8_-]+)\](.*)") + + 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" + ] + + with open(filepath, "r") as handle: + lines = handle.readlines() + + lines = [i.strip() for i in lines] + + # Parse version header + first_line = lines.pop(0) + if first_line not in PROFILES: + raise "Unsupported Profile: " + first_line + + # Extracts the keywords + keywords = {} + while lines: + line = lines.pop(0) + if not line.startswith("["): + if line != "TILT=NONE": + continue + lines.insert(0, line) + break + else: + match = KEYWORD_REGEX.match(line) + if match: + key, val = match.group(1, 2) + keywords[key.strip()] = val.strip() + else: + raise "Invalid keyword line: " + line + + # Next line should be TILT=NONE according to the spec + if lines.pop(0) != "TILT=NONE": + raise "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 "Only 1 Lamp supported!" + + # Extract various properties + lumen_per_lamp = read_float() + candela_multiplier = read_float() + num_vertical_angles = read_int() + num_horizontal_angles = read_int() + + if num_vertical_angles < 1 or num_horizontal_angles < 1: + raise "Invalid of vertical/horizontal angles!" + + photometric_type = read_int() + 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 "Invalid unit type" + + width = read_float() + length = read_float() + height = read_float() + ballast_factor = read_float() + future_use = read_float() + input_watts = read_float() + + _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] + generate_texture() + +def generate_texture(): + tex = bpy.data.images.new("iestexture", width=128, height=128, float_buffer=True) # R16 + resolution_vertical = 128 + resolution_horizontal = 128 + + for vert in range(resolution_vertical): + for horiz in range(resolution_horizontal): + vert_angle = vert / (resolution_vertical - 1.0) + vert_angle = math.cos(vert_angle * math.pi) * 90.0 + 90.0 + horiz_angle = horiz / (resolution_horizontal - 1.0) * 360.0 + candela = get_candela_value(vert_angle, horiz_angle) + x = vert + y = horiz + i = x + y * resolution_horizontal + tex.pixels[i * 4] = candela + tex.pixels[i * 4 + 1] = candela + tex.pixels[i * 4 + 2] = candela + tex.pixels[i * 4 + 3] = 1.0 + +def get_candela_value(vertical_angle, horizontal_angle): + # Assume a dataset without horizontal angles + return get_vertical_candela_value(0, vertical_angle) + +def get_vertical_candela_value(horizontal_angle_idx, vertical_angle): + if vertical_angle < 0.0: + return 0.0 + + if vertical_angle > _vertical_angles[len(_vertical_angles) - 1]: + return 0.0 + + for vertical_index in range(1, len(_vertical_angles)): + curr_angle = _vertical_angles[vertical_index] + if curr_angle > vertical_angle: + prev_angle = _vertical_angles[vertical_index - 1] + prev_value = get_candela_value_from_index(vertical_index - 1, horizontal_angle_idx) + curr_value = get_candela_value_from_index(vertical_index, horizontal_angle_idx) + lerp = (vertical_angle - prev_angle) / (curr_angle - prev_angle) + assert lerp >= 0.0 and lerp <= 1.0 + return curr_value * lerp + prev_value * (1.0 - lerp) + return 0.0 + +def get_candela_value_from_index(vertical_angle_idx, horizontal_angle_idx): + index = vertical_angle_idx + horizontal_angle_idx * len(_vertical_angles) + return _candela_values[index] + +filepath = "/Users/lubos/Desktop/ies/JellyFish.ies" +load(filepath) diff --git a/Shaders/deferred_light/deferred_light.frag.glsl b/Shaders/deferred_light/deferred_light.frag.glsl index b31bb3b3..acc4b0a8 100644 --- a/Shaders/deferred_light/deferred_light.frag.glsl +++ b/Shaders/deferred_light/deferred_light.frag.glsl @@ -7,6 +7,9 @@ precision mediump float; #include "../compiled.glsl" #include "../std/brdf.glsl" #include "../std/math.glsl" +#ifdef _LampIES +#include "../std/ies.glsl" +#endif #ifdef _VoxelGIDirect #include "../std/conetrace.glsl" #endif @@ -51,6 +54,9 @@ uniform sampler2D gbuffer1; #ifdef _DFRS //!uniform sampler3D sdftex; #endif +#ifdef _LampIES + //!uniform sampler2D texIES; +#endif uniform mat4 invVP; uniform mat4 LWVP; @@ -167,6 +173,9 @@ void main() { // Per-light #ifndef _NoLampFalloff visibility *= attenuate(distance(p, lightPos)); +#endif +#ifdef _LampIES + visibility *= iesAttenuation(-l); #endif if (lightType == 2) { // Spot float spotEffect = dot(lightDir, l); diff --git a/Shaders/deferred_light/deferred_light.json b/Shaders/deferred_light/deferred_light.json index 4ceaa42e..a7dadb40 100755 --- a/Shaders/deferred_light/deferred_light.json +++ b/Shaders/deferred_light/deferred_light.json @@ -46,6 +46,11 @@ "link": "_lampColorTexture", "ifdef": ["_LampColTex"] }, + { + "name": "texIES", + "link": "_iesTexture", + "ifdef": ["_LampIES"] + }, { "name": "shadowsBias", "link": "_lampShadowsBias" diff --git a/Shaders/std/ies.glsl b/Shaders/std/ies.glsl index 2764a4b3..a7e9f124 100644 --- a/Shaders/std/ies.glsl +++ b/Shaders/std/ies.glsl @@ -1,25 +1,32 @@ -sampler2D texIES; +uniform sampler2D texIES; -// https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf -float iesAttenuation(vec3 L, ShadowLightInfo light) { +float iesAttenuation(vec3 l) { + + const float PI = 3.1415926535; + // https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf // Sample direction into light space - vec3 iesSampleDirection = mul(light.worldToLight , -L); + // vec3 iesSampleDirection = mul(light.worldToLight , -L); // Cartesian to spherical // Texture encoded with cos( phi ), scale from -1 - >1 to 0 - >1 - float phiCoord = (iesSampleDirection.z * 0.5f) + 0.5f; - float theta = atan2 (iesSampleDirection.y , iesSampleDirection .x); - float thetaCoord = theta * (1.0 / (PI * 2.0)); - float iesProfileScale = texture(texIES, vec2(thetaCoord, phiCoord)).r; - return iesProfileScale; + // float phiCoord = (iesSampleDirection.z * 0.5f) + 0.5f; + // float theta = atan2 (iesSampleDirection.y , iesSampleDirection .x); + // float thetaCoord = theta * (1.0 / (PI * 2.0)); + // float iesProfileScale = texture(texIES, vec2(thetaCoord, phiCoord)).r; + // return iesProfileScale; // 1D texture - vec3 pl = normalize(p - lightPos); - float f = asin(dot(pl, l)) / PI + 0.5; - return texture(texIES, vec2(f, 0.0)).r; + // vec3 pl = normalize(p - lightPos); + // float f = asin(dot(pl, l)) / PI + 0.5; + // return texture(texIES, vec2(f, 0.0)).r; // 1D texture - float cosTheta = dot(lightToPos, lightDir); - float angle = acos(cosTheta) * (1.0 / PI); - return texture(texIES, vec2(angle, 0.0), 0.0).r; + // float cosTheta = dot(lightToPos, lightDir); + // float angle = acos(cosTheta) * (1.0 / PI); + // return texture(texIES, vec2(angle, 0.0), 0.0).r; + + // Based on https://github.com/tobspr/RenderPipeline + float hor = acos(l.z) / PI; + float vert = atan(l.x, l.y) * (1.0 / (PI * 2.0)) + 0.5; + return texture(texIES, vec2(hor, vert)).r; } diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index 94e496d8..9c92c3f9 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -83,6 +83,10 @@ def build_node_tree(world): if wrd.generate_lamp_texture != '': bpy.data.worlds['Arm'].world_defs += '_LampColTex' + if wrd.generate_lamp_ies_texture != '': + bpy.data.worlds['Arm'].world_defs += '_LampIES' + assets.add_embedded_data('iestexture.png') + if not wrd.generate_lamp_falloff: bpy.data.worlds['Arm'].world_defs += '_NoLampFalloff' diff --git a/blender/arm/props.py b/blender/arm/props.py index 03467043..0d952e65 100755 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -535,6 +535,7 @@ def init_properties(): ('Uncharted', 'Uncharted', 'Uncharted')], name='Tonemap', description='Tonemapping operator', default='Filmic', update=assets.invalidate_shader_cache) bpy.types.World.generate_lamp_texture = bpy.props.StringProperty(name="Lamp Texture", default="") + bpy.types.World.generate_lamp_ies_texture = bpy.props.StringProperty(name="Lamp IES Texture", default="") bpy.types.World.generate_lens_texture = bpy.props.StringProperty(name="Lens Texture", default="") bpy.types.World.generate_lamp_falloff = bpy.props.BoolProperty(name="Lamp Falloff", default=True, update=assets.invalidate_shader_cache) bpy.types.World.generate_fisheye = bpy.props.BoolProperty(name="Fish Eye", default=False, update=assets.invalidate_shader_cache) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 61f7196f..074248bf 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -376,6 +376,7 @@ class WorldPropsPanel(bpy.types.Panel): layout.prop(wrd, 'generate_fisheye') layout.prop(wrd, 'generate_vignette') layout.prop(wrd, 'generate_lamp_texture') + layout.prop(wrd, 'generate_lamp_ies_texture') layout.prop(wrd, 'generate_lens_texture') layout.prop(wrd, 'generate_lamp_falloff')