Changeset View
Changeset View
Standalone View
Standalone View
intern/cycles/render/light.cpp
| /* | /* | ||||
| * Copyright 2011-2013 Blender Foundation | * Copyright 2011-2013 Blender Foundation | ||||
| * | * | ||||
| * Licensed under the Apache License, Version 2.0 (the "License"); | * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| * you may not use this file except in compliance with the License. | * you may not use this file except in compliance with the License. | ||||
| * You may obtain a copy of the License at | * You may obtain a copy of the License at | ||||
| * | * | ||||
| * http://www.apache.org/licenses/LICENSE-2.0 | * http://www.apache.org/licenses/LICENSE-2.0 | ||||
| * | * | ||||
| * Unless required by applicable law or agreed to in writing, software | * Unless required by applicable law or agreed to in writing, software | ||||
| * distributed under the License is distributed on an "AS IS" BASIS, | * distributed under the License is distributed on an "AS IS" BASIS, | ||||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| * See the License for the specific language governing permissions and | * See the License for the specific language governing permissions and | ||||
| * limitations under the License. | * limitations under the License. | ||||
| */ | */ | ||||
| #include "render/light.h" | |||||
| #include "device/device.h" | #include "device/device.h" | ||||
| #include "render/background.h" | #include "render/background.h" | ||||
| #include "render/film.h" | #include "render/film.h" | ||||
| #include "render/graph.h" | #include "render/graph.h" | ||||
| #include "render/integrator.h" | #include "render/integrator.h" | ||||
| #include "render/light.h" | |||||
| #include "render/mesh.h" | #include "render/mesh.h" | ||||
| #include "render/nodes.h" | #include "render/nodes.h" | ||||
| #include "render/object.h" | #include "render/object.h" | ||||
| #include "render/scene.h" | #include "render/scene.h" | ||||
| #include "render/shader.h" | #include "render/shader.h" | ||||
| #include "render/stats.h" | #include "render/stats.h" | ||||
| #include "integrator/shader_eval.h" | |||||
| #include "util/util_foreach.h" | #include "util/util_foreach.h" | ||||
| #include "util/util_hash.h" | #include "util/util_hash.h" | ||||
| #include "util/util_logging.h" | #include "util/util_logging.h" | ||||
| #include "util/util_path.h" | #include "util/util_path.h" | ||||
| #include "util/util_progress.h" | #include "util/util_progress.h" | ||||
| #include "util/util_task.h" | #include "util/util_task.h" | ||||
| CCL_NAMESPACE_BEGIN | CCL_NAMESPACE_BEGIN | ||||
| static void shade_background_pixels(Device *device, | static void shade_background_pixels(Device *device, | ||||
| DeviceScene *dscene, | DeviceScene *dscene, | ||||
| int width, | int width, | ||||
| int height, | int height, | ||||
| vector<float3> &pixels, | vector<float3> &pixels, | ||||
| Progress &progress) | Progress &progress) | ||||
| { | { | ||||
| /* create input */ | /* Needs to be up to data for attribute access. */ | ||||
| device_vector<uint4> d_input(device, "background_input", MEM_READ_ONLY); | device->const_copy_to("__data", &dscene->data, sizeof(dscene->data)); | ||||
| device_vector<float4> d_output(device, "background_output", MEM_READ_WRITE); | |||||
| const int size = width * height; | |||||
| pixels.resize(size); | |||||
| uint4 *d_input_data = d_input.alloc(width * height); | /* Evaluate shader on device. */ | ||||
| ShaderEval shader_eval(device, progress); | |||||
| shader_eval.eval( | |||||
| SHADER_EVAL_BACKGROUND, | |||||
| size, | |||||
| [&](device_vector<KernelShaderEvalInput> &d_input) { | |||||
| /* Fill coordinates for shading. */ | |||||
| KernelShaderEvalInput *d_input_data = d_input.data(); | |||||
| for (int y = 0; y < height; y++) { | for (int y = 0; y < height; y++) { | ||||
| for (int x = 0; x < width; x++) { | for (int x = 0; x < width; x++) { | ||||
| float u = (x + 0.5f) / width; | float u = (x + 0.5f) / width; | ||||
| float v = (y + 0.5f) / height; | float v = (y + 0.5f) / height; | ||||
| uint4 in = make_uint4(__float_as_int(u), __float_as_int(v), 0, 0); | KernelShaderEvalInput in; | ||||
| in.object = OBJECT_NONE; | |||||
| in.prim = PRIM_NONE; | |||||
| in.u = u; | |||||
| in.v = v; | |||||
| d_input_data[x + y * width] = in; | d_input_data[x + y * width] = in; | ||||
| } | } | ||||
| } | } | ||||
| /* compute on device */ | return size; | ||||
| d_output.alloc(width * height); | }, | ||||
| d_output.zero_to_device(); | [&](device_vector<float4> &d_output) { | ||||
| d_input.copy_to_device(); | /* Copy output to pixel buffer. */ | ||||
| device->const_copy_to("__data", &dscene->data, sizeof(dscene->data)); | |||||
| DeviceTask main_task(DeviceTask::SHADER); | |||||
| main_task.shader_input = d_input.device_pointer; | |||||
| main_task.shader_output = d_output.device_pointer; | |||||
| main_task.shader_eval_type = SHADER_EVAL_BACKGROUND; | |||||
| main_task.shader_x = 0; | |||||
| main_task.shader_w = width * height; | |||||
| main_task.num_samples = 1; | |||||
| main_task.get_cancel = function_bind(&Progress::get_cancel, &progress); | |||||
| /* disabled splitting for now, there's an issue with multi-GPU mem_copy_from */ | |||||
| list<DeviceTask> split_tasks; | |||||
| main_task.split(split_tasks, 1, 128 * 128); | |||||
| foreach (DeviceTask &task, split_tasks) { | |||||
| device->task_add(task); | |||||
| device->task_wait(); | |||||
| d_output.copy_from_device(task.shader_x, 1, task.shader_w); | |||||
| } | |||||
| d_input.free(); | |||||
| float4 *d_output_data = d_output.data(); | float4 *d_output_data = d_output.data(); | ||||
| pixels.resize(width * height); | |||||
| for (int y = 0; y < height; y++) { | for (int y = 0; y < height; y++) { | ||||
| for (int x = 0; x < width; x++) { | for (int x = 0; x < width; x++) { | ||||
| pixels[y * width + x].x = d_output_data[y * width + x].x; | pixels[y * width + x].x = d_output_data[y * width + x].x; | ||||
| pixels[y * width + x].y = d_output_data[y * width + x].y; | pixels[y * width + x].y = d_output_data[y * width + x].y; | ||||
| pixels[y * width + x].z = d_output_data[y * width + x].z; | pixels[y * width + x].z = d_output_data[y * width + x].z; | ||||
| } | } | ||||
| } | } | ||||
| }); | |||||
| d_output.free(); | |||||
| } | } | ||||
| /* Light */ | /* Light */ | ||||
| NODE_DEFINE(Light) | NODE_DEFINE(Light) | ||||
| { | { | ||||
| NodeType *type = NodeType::add("light", create); | NodeType *type = NodeType::add("light", create); | ||||
| Show All 24 Lines | NODE_DEFINE(Light) | ||||
| SOCKET_FLOAT(spot_angle, "Spot Angle", M_PI_4_F); | SOCKET_FLOAT(spot_angle, "Spot Angle", M_PI_4_F); | ||||
| SOCKET_FLOAT(spot_smooth, "Spot Smooth", 0.0f); | SOCKET_FLOAT(spot_smooth, "Spot Smooth", 0.0f); | ||||
| SOCKET_TRANSFORM(tfm, "Transform", transform_identity()); | SOCKET_TRANSFORM(tfm, "Transform", transform_identity()); | ||||
| SOCKET_BOOLEAN(cast_shadow, "Cast Shadow", true); | SOCKET_BOOLEAN(cast_shadow, "Cast Shadow", true); | ||||
| SOCKET_BOOLEAN(use_mis, "Use Mis", false); | SOCKET_BOOLEAN(use_mis, "Use Mis", false); | ||||
| SOCKET_BOOLEAN(use_camera, "Use Camera", true); | |||||
| SOCKET_BOOLEAN(use_diffuse, "Use Diffuse", true); | SOCKET_BOOLEAN(use_diffuse, "Use Diffuse", true); | ||||
| SOCKET_BOOLEAN(use_glossy, "Use Glossy", true); | SOCKET_BOOLEAN(use_glossy, "Use Glossy", true); | ||||
| SOCKET_BOOLEAN(use_transmission, "Use Transmission", true); | SOCKET_BOOLEAN(use_transmission, "Use Transmission", true); | ||||
| SOCKET_BOOLEAN(use_scatter, "Use Scatter", true); | SOCKET_BOOLEAN(use_scatter, "Use Scatter", true); | ||||
| SOCKET_INT(samples, "Samples", 1); | |||||
| SOCKET_INT(max_bounces, "Max Bounces", 1024); | SOCKET_INT(max_bounces, "Max Bounces", 1024); | ||||
| SOCKET_UINT(random_id, "Random ID", 0); | SOCKET_UINT(random_id, "Random ID", 0); | ||||
| SOCKET_BOOLEAN(is_shadow_catcher, "Shadow Catcher", true); | |||||
| SOCKET_BOOLEAN(is_portal, "Is Portal", false); | SOCKET_BOOLEAN(is_portal, "Is Portal", false); | ||||
| SOCKET_BOOLEAN(is_enabled, "Is Enabled", true); | SOCKET_BOOLEAN(is_enabled, "Is Enabled", true); | ||||
| SOCKET_NODE(shader, "Shader", Shader::get_node_type()); | SOCKET_NODE(shader, "Shader", Shader::get_node_type()); | ||||
| return type; | return type; | ||||
| } | } | ||||
| Light::Light() : Node(get_node_type()) | Light::Light() : Node(get_node_type()) | ||||
| { | { | ||||
| dereference_all_used_nodes(); | dereference_all_used_nodes(); | ||||
| } | } | ||||
| void Light::tag_update(Scene *scene) | void Light::tag_update(Scene *scene) | ||||
| { | { | ||||
| if (is_modified()) { | if (is_modified()) { | ||||
| scene->light_manager->tag_update(scene, LightManager::LIGHT_MODIFIED); | scene->light_manager->tag_update(scene, LightManager::LIGHT_MODIFIED); | ||||
| if (samples_is_modified()) { | |||||
| scene->integrator->tag_update(scene, Integrator::LIGHT_SAMPLES_MODIFIED); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| bool Light::has_contribution(Scene *scene) | bool Light::has_contribution(Scene *scene) | ||||
| { | { | ||||
| if (strength == zero_float3()) { | if (strength == zero_float3()) { | ||||
| return false; | return false; | ||||
| } | } | ||||
| if (is_portal) { | if (is_portal) { | ||||
| return false; | return false; | ||||
| } | } | ||||
| if (light_type == LIGHT_BACKGROUND) { | if (light_type == LIGHT_BACKGROUND) { | ||||
| return true; | return true; | ||||
| } | } | ||||
| return (shader) ? shader->has_surface_emission : scene->default_light->has_surface_emission; | return (shader) ? shader->has_surface_emission : scene->default_light->has_surface_emission; | ||||
| } | } | ||||
| /* Light Manager */ | /* Light Manager */ | ||||
| LightManager::LightManager() | LightManager::LightManager() | ||||
| { | { | ||||
| update_flags = UPDATE_ALL; | update_flags = UPDATE_ALL; | ||||
| need_update_background = true; | need_update_background = true; | ||||
| use_light_visibility = false; | |||||
| last_background_enabled = false; | last_background_enabled = false; | ||||
| last_background_resolution = 0; | last_background_resolution = 0; | ||||
| } | } | ||||
| LightManager::~LightManager() | LightManager::~LightManager() | ||||
| { | { | ||||
| foreach (IESSlot *slot, ies_slots) { | foreach (IESSlot *slot, ies_slots) { | ||||
| delete slot; | delete slot; | ||||
| ▲ Show 20 Lines • Show All 147 Lines • ▼ Show 20 Lines | foreach (Object *object, scene->objects) { | ||||
| } | } | ||||
| /* Sum area. */ | /* Sum area. */ | ||||
| Mesh *mesh = static_cast<Mesh *>(object->get_geometry()); | Mesh *mesh = static_cast<Mesh *>(object->get_geometry()); | ||||
| bool transform_applied = mesh->transform_applied; | bool transform_applied = mesh->transform_applied; | ||||
| Transform tfm = object->get_tfm(); | Transform tfm = object->get_tfm(); | ||||
| int object_id = j; | int object_id = j; | ||||
| int shader_flag = 0; | int shader_flag = 0; | ||||
| if (!(object->get_visibility() & PATH_RAY_CAMERA)) { | |||||
| shader_flag |= SHADER_EXCLUDE_CAMERA; | |||||
| } | |||||
| if (!(object->get_visibility() & PATH_RAY_DIFFUSE)) { | if (!(object->get_visibility() & PATH_RAY_DIFFUSE)) { | ||||
| shader_flag |= SHADER_EXCLUDE_DIFFUSE; | shader_flag |= SHADER_EXCLUDE_DIFFUSE; | ||||
| use_light_visibility = true; | |||||
| } | } | ||||
| if (!(object->get_visibility() & PATH_RAY_GLOSSY)) { | if (!(object->get_visibility() & PATH_RAY_GLOSSY)) { | ||||
| shader_flag |= SHADER_EXCLUDE_GLOSSY; | shader_flag |= SHADER_EXCLUDE_GLOSSY; | ||||
| use_light_visibility = true; | |||||
| } | } | ||||
| if (!(object->get_visibility() & PATH_RAY_TRANSMIT)) { | if (!(object->get_visibility() & PATH_RAY_TRANSMIT)) { | ||||
| shader_flag |= SHADER_EXCLUDE_TRANSMIT; | shader_flag |= SHADER_EXCLUDE_TRANSMIT; | ||||
| use_light_visibility = true; | |||||
| } | } | ||||
| if (!(object->get_visibility() & PATH_RAY_VOLUME_SCATTER)) { | if (!(object->get_visibility() & PATH_RAY_VOLUME_SCATTER)) { | ||||
| shader_flag |= SHADER_EXCLUDE_SCATTER; | shader_flag |= SHADER_EXCLUDE_SCATTER; | ||||
| use_light_visibility = true; | } | ||||
| if (!(object->get_is_shadow_catcher())) { | |||||
| shader_flag |= SHADER_EXCLUDE_SHADOW_CATCHER; | |||||
| } | } | ||||
| size_t mesh_num_triangles = mesh->num_triangles(); | size_t mesh_num_triangles = mesh->num_triangles(); | ||||
| for (size_t i = 0; i < mesh_num_triangles; i++) { | for (size_t i = 0; i < mesh_num_triangles; i++) { | ||||
| int shader_index = mesh->get_shader()[i]; | int shader_index = mesh->get_shader()[i]; | ||||
| Shader *shader = (shader_index < mesh->get_used_shaders().size()) ? | Shader *shader = (shader_index < mesh->get_used_shaders().size()) ? | ||||
| static_cast<Shader *>(mesh->get_used_shaders()[shader_index]) : | static_cast<Shader *>(mesh->get_used_shaders()[shader_index]) : | ||||
| scene->default_surface; | scene->default_surface; | ||||
| ▲ Show 20 Lines • Show All 107 Lines • ▼ Show 20 Lines | if (kintegrator->use_direct_light) { | ||||
| kintegrator->use_lamp_mis = use_lamp_mis; | kintegrator->use_lamp_mis = use_lamp_mis; | ||||
| /* bit of an ugly hack to compensate for emitting triangles influencing | /* bit of an ugly hack to compensate for emitting triangles influencing | ||||
| * amount of samples we get for this pass */ | * amount of samples we get for this pass */ | ||||
| kfilm->pass_shadow_scale = 1.0f; | kfilm->pass_shadow_scale = 1.0f; | ||||
| if (kintegrator->pdf_triangles != 0.0f) | if (kintegrator->pdf_triangles != 0.0f) | ||||
| kfilm->pass_shadow_scale *= 0.5f; | kfilm->pass_shadow_scale /= 0.5f; | ||||
| if (num_background_lights < num_lights) | if (num_background_lights < num_lights) | ||||
| kfilm->pass_shadow_scale *= (float)(num_lights - num_background_lights) / (float)num_lights; | kfilm->pass_shadow_scale /= (float)(num_lights - num_background_lights) / (float)num_lights; | ||||
| /* CDF */ | /* CDF */ | ||||
| dscene->light_distribution.copy_to_device(); | dscene->light_distribution.copy_to_device(); | ||||
| /* Portals */ | /* Portals */ | ||||
| if (num_portals > 0) { | if (num_portals > 0) { | ||||
| kbackground->portal_offset = light_index; | kbackground->portal_offset = light_index; | ||||
| kbackground->num_portals = num_portals; | kbackground->num_portals = num_portals; | ||||
| ▲ Show 20 Lines • Show All 250 Lines • ▼ Show 20 Lines | foreach (Light *light, scene->lights) { | ||||
| Shader *shader = (light->shader) ? light->shader : scene->default_light; | Shader *shader = (light->shader) ? light->shader : scene->default_light; | ||||
| int shader_id = scene->shader_manager->get_shader_id(shader); | int shader_id = scene->shader_manager->get_shader_id(shader); | ||||
| int max_bounces = light->max_bounces; | int max_bounces = light->max_bounces; | ||||
| float random = (float)light->random_id * (1.0f / (float)0xFFFFFFFF); | float random = (float)light->random_id * (1.0f / (float)0xFFFFFFFF); | ||||
| if (!light->cast_shadow) | if (!light->cast_shadow) | ||||
| shader_id &= ~SHADER_CAST_SHADOW; | shader_id &= ~SHADER_CAST_SHADOW; | ||||
| if (!light->use_camera) { | |||||
| shader_id |= SHADER_EXCLUDE_CAMERA; | |||||
| } | |||||
| if (!light->use_diffuse) { | if (!light->use_diffuse) { | ||||
| shader_id |= SHADER_EXCLUDE_DIFFUSE; | shader_id |= SHADER_EXCLUDE_DIFFUSE; | ||||
| use_light_visibility = true; | |||||
| } | } | ||||
| if (!light->use_glossy) { | if (!light->use_glossy) { | ||||
| shader_id |= SHADER_EXCLUDE_GLOSSY; | shader_id |= SHADER_EXCLUDE_GLOSSY; | ||||
| use_light_visibility = true; | |||||
| } | } | ||||
| if (!light->use_transmission) { | if (!light->use_transmission) { | ||||
| shader_id |= SHADER_EXCLUDE_TRANSMIT; | shader_id |= SHADER_EXCLUDE_TRANSMIT; | ||||
| use_light_visibility = true; | |||||
| } | } | ||||
| if (!light->use_scatter) { | if (!light->use_scatter) { | ||||
| shader_id |= SHADER_EXCLUDE_SCATTER; | shader_id |= SHADER_EXCLUDE_SCATTER; | ||||
| use_light_visibility = true; | } | ||||
| if (!light->is_shadow_catcher) { | |||||
| shader_id |= SHADER_EXCLUDE_SHADOW_CATCHER; | |||||
| } | } | ||||
| klights[light_index].type = light->light_type; | klights[light_index].type = light->light_type; | ||||
| klights[light_index].samples = light->samples; | |||||
| klights[light_index].strength[0] = light->strength.x; | klights[light_index].strength[0] = light->strength.x; | ||||
| klights[light_index].strength[1] = light->strength.y; | klights[light_index].strength[1] = light->strength.y; | ||||
| klights[light_index].strength[2] = light->strength.z; | klights[light_index].strength[2] = light->strength.z; | ||||
| if (light->light_type == LIGHT_POINT) { | if (light->light_type == LIGHT_POINT) { | ||||
| shader_id &= ~SHADER_AREA_LIGHT; | shader_id &= ~SHADER_AREA_LIGHT; | ||||
| float radius = light->size; | float radius = light->size; | ||||
| Show All 35 Lines | foreach (Light *light, scene->lights) { | ||||
| else if (light->light_type == LIGHT_BACKGROUND) { | else if (light->light_type == LIGHT_BACKGROUND) { | ||||
| uint visibility = scene->background->get_visibility(); | uint visibility = scene->background->get_visibility(); | ||||
| shader_id &= ~SHADER_AREA_LIGHT; | shader_id &= ~SHADER_AREA_LIGHT; | ||||
| shader_id |= SHADER_USE_MIS; | shader_id |= SHADER_USE_MIS; | ||||
| if (!(visibility & PATH_RAY_DIFFUSE)) { | if (!(visibility & PATH_RAY_DIFFUSE)) { | ||||
| shader_id |= SHADER_EXCLUDE_DIFFUSE; | shader_id |= SHADER_EXCLUDE_DIFFUSE; | ||||
| use_light_visibility = true; | |||||
| } | } | ||||
| if (!(visibility & PATH_RAY_GLOSSY)) { | if (!(visibility & PATH_RAY_GLOSSY)) { | ||||
| shader_id |= SHADER_EXCLUDE_GLOSSY; | shader_id |= SHADER_EXCLUDE_GLOSSY; | ||||
| use_light_visibility = true; | |||||
| } | } | ||||
| if (!(visibility & PATH_RAY_TRANSMIT)) { | if (!(visibility & PATH_RAY_TRANSMIT)) { | ||||
| shader_id |= SHADER_EXCLUDE_TRANSMIT; | shader_id |= SHADER_EXCLUDE_TRANSMIT; | ||||
| use_light_visibility = true; | |||||
| } | } | ||||
| if (!(visibility & PATH_RAY_VOLUME_SCATTER)) { | if (!(visibility & PATH_RAY_VOLUME_SCATTER)) { | ||||
| shader_id |= SHADER_EXCLUDE_SCATTER; | shader_id |= SHADER_EXCLUDE_SCATTER; | ||||
| use_light_visibility = true; | |||||
| } | } | ||||
| } | } | ||||
| else if (light->light_type == LIGHT_AREA) { | else if (light->light_type == LIGHT_AREA) { | ||||
| float3 axisu = light->axisu * (light->sizeu * light->size); | float3 axisu = light->axisu * (light->sizeu * light->size); | ||||
| float3 axisv = light->axisv * (light->sizev * light->size); | float3 axisv = light->axisv * (light->sizev * light->size); | ||||
| float area = len(axisu) * len(axisv); | float area = len(axisu) * len(axisv); | ||||
| if (light->round) { | if (light->round) { | ||||
| area *= -M_PI_4_F; | area *= -M_PI_4_F; | ||||
| ▲ Show 20 Lines • Show All 133 Lines • ▼ Show 20 Lines | void LightManager::device_update(Device *device, | ||||
| VLOG(1) << "Total " << scene->lights.size() << " lights."; | VLOG(1) << "Total " << scene->lights.size() << " lights."; | ||||
| /* Detect which lights are enabled, also determines if we need to update the background. */ | /* Detect which lights are enabled, also determines if we need to update the background. */ | ||||
| test_enabled_lights(scene); | test_enabled_lights(scene); | ||||
| device_free(device, dscene, need_update_background); | device_free(device, dscene, need_update_background); | ||||
| use_light_visibility = false; | |||||
| device_update_points(device, dscene, scene); | device_update_points(device, dscene, scene); | ||||
| if (progress.get_cancel()) | if (progress.get_cancel()) | ||||
| return; | return; | ||||
| device_update_distribution(device, dscene, scene, progress); | device_update_distribution(device, dscene, scene, progress); | ||||
| if (progress.get_cancel()) | if (progress.get_cancel()) | ||||
| return; | return; | ||||
| if (need_update_background) { | if (need_update_background) { | ||||
| device_update_background(device, dscene, scene, progress); | device_update_background(device, dscene, scene, progress); | ||||
| if (progress.get_cancel()) | if (progress.get_cancel()) | ||||
| return; | return; | ||||
| } | } | ||||
| device_update_ies(dscene); | device_update_ies(dscene); | ||||
| if (progress.get_cancel()) | if (progress.get_cancel()) | ||||
| return; | return; | ||||
| scene->film->set_use_light_visibility(use_light_visibility); | |||||
| update_flags = UPDATE_NONE; | update_flags = UPDATE_NONE; | ||||
| need_update_background = false; | need_update_background = false; | ||||
| } | } | ||||
| void LightManager::device_free(Device *, DeviceScene *dscene, const bool free_background) | void LightManager::device_free(Device *, DeviceScene *dscene, const bool free_background) | ||||
| { | { | ||||
| dscene->light_distribution.free(); | dscene->light_distribution.free(); | ||||
| dscene->lights.free(); | dscene->lights.free(); | ||||
| ▲ Show 20 Lines • Show All 138 Lines • Show Last 20 Lines | |||||