VolumetricLight = {} g_output = nil g_mesh = nil g_material_input = nil g_placement = nil g_transform = nil g_light_pos = nil g_target_pos = nil g_subdiv_level = nil setupMesh = function(mesh, subdiv_level) local face_count = 0 local subd_value_node = subdiv_level:getInputNode(octane.P_INPUT) if subd_value_node ~= nil then face_count = subd_value_node:getAttribute(octane.A_VALUE) end face_count = face_count[1] + 4 local vertices = {} local vertsPerPoly = {} local polyVertexIndices = {} local polyMaterialIndices = {} -- divide up 2pi into face_count chunks local rad = (math.pi * 2.0) / face_count -- this will make our top of the tetrahedron table.insert(vertices, {0, 0, 0}) table.insert(vertices, {1, -1, 0}) local bottom_indices = { 1 } -- make the sides of the cone for i = 1, face_count-1, 1 do local prev_ix = #vertices - 1 local x = math.cos(rad * i) local z = math.sin(rad * i) local coord = {x, -1, z} local ix = #vertices table.insert(vertices, coord) table.insert(vertsPerPoly, 3) table.insert(polyMaterialIndices, 0) table.insert(polyVertexIndices, ix) table.insert(polyVertexIndices, prev_ix) table.insert(polyVertexIndices, 0) table.insert(bottom_indices, ix) end table.insert(vertsPerPoly, 3) table.insert(polyMaterialIndices, 0) table.insert(polyVertexIndices, 1) table.insert(polyVertexIndices, #vertices-1) table.insert(polyVertexIndices, 0) -- stitch together the bottom of the cone table.insert(vertsPerPoly, face_count+1) table.insert(polyMaterialIndices, 0) for k,v in pairs(bottom_indices) do table.insert(polyVertexIndices, v) end table.insert(polyVertexIndices, 1) local radius = 0.1 -- make the emitter plane's vertices table.insert(vertices, {radius, -radius, 0}) local emitter_indices = { #vertices-1 } for i = 1, face_count-1, 1 do local prev_ix = #vertices - 1 local x = radius * math.cos(rad * i) local z = radius * math.sin(rad * i) local coord = {x, -radius, z} local ix = #vertices table.insert(vertices, coord) table.insert(emitter_indices, ix) end -- stitch together the emitter plane table.insert(vertsPerPoly, face_count+1) table.insert(polyMaterialIndices, 1) for k,v in pairs(emitter_indices) do table.insert(polyVertexIndices, v) end table.insert(polyVertexIndices, emitter_indices[1]) --[[ -- setup a tetrahedron local vertices = { -- A (0) { -1, -1, 1 }, -- B (1) { 1, -1, 1 }, -- C (2) { 1, -1, -1 }, -- D (3) { -1, -1, -1 }, -- E (4) { 0, 0, 0 } } local vertsPerPoly = { 3, 3, 3, 3, 4 } local polyVertexIndices = { 0, 1, 4, 1, 2, 4, 2, 3, 4, 3, 0, 4, 3, 2, 1, 0 -- bottom of tetrahedron } ]]-- mesh:setAttribute(octane.A_VERTICES, vertices, false) mesh:setAttribute(octane.A_VERTICES_PER_POLY, vertsPerPoly, false) mesh:setAttribute(octane.A_POLY_VERTEX_INDICES, polyVertexIndices, false) mesh:setAttribute(octane.A_POLY_MATERIAL_INDICES , polyMaterialIndices , false) mesh:evaluate() end setupTransform = function(transform, light_pos, target_pos, cone_size) local light = light_pos:getInputNode(octane.P_INPUT) local target = target_pos:getInputNode(octane.P_INPUT) local cone = cone_size:getInputNode(octane.P_INPUT) if light == nil or target == nil or cone == nil then octane.common.print("Light, target positions and cone size must be given.") return end local P = light:getAttribute(octane.A_VALUE) local T = target:getAttribute(octane.A_VALUE) local S = cone:getAttribute(octane.A_VALUE)[1] local PT = octane.vec.sub(T, P) local y_scale = octane.vec.length(PT) local xz_scale = S local mat = octane.matrix.getIdentity() -- now setup the basis for the light local a = octane.vec.normalized(PT) local c = octane.vec.cross(a, {0,1,0}) local b = octane.vec.cross(c, a) -- scale the vector pointing towards the target a = octane.vec.scale(a, y_scale) c = octane.vec.scale(c, xz_scale) b = octane.vec.scale(b, xz_scale) local basis = { {b[1], -a[1], c[1], P[1]}, {b[2], -a[2], c[2], P[2]}, {b[3], -a[3], c[3], P[3]}, } mat = octane.matrix.mul(mat, basis) transform:setAttribute(octane.A_TRANSFORM, mat, true) end VolumetricLight.onInit = function(self, graph) local inputs = graph:setInputLinkers { { type = octane.PT_MATERIAL, label = "Light medium", defaultNodeType = octane.NT_MAT_DIFFUSE }, { type = octane.PT_MATERIAL, label = "Emitter material", defaultNodeType = octane.NT_MAT_DIFFUSE }, { type = octane.PT_FLOAT, label = "Light position", defaultNodeType = octane.NT_FLOAT }, { type = octane.PT_FLOAT, label = "Light target", defaultNodeType = octane.NT_FLOAT }, { type = octane.PT_FLOAT, label = "Cone size", defaultNodeType = octane.NT_FLOAT }, { type = octane.PT_INT, label = "Subdiv level", defaultNodeType = octane.NT_INT } } local outputs = graph:setOutputLinkers { { type = octane.PT_GEOMETRY, label = "Light volume", defaultNodeType = octane.NT_OUT_GEOMETRY } } g_output = outputs[1] g_material_input = inputs[1] g_material2_input = inputs[2] g_light_pos = inputs[3] g_target_pos = inputs[4] g_cone_size = inputs[5] g_subdiv_level = inputs[6] end VolumetricLight.onEvaluate = function(self, graph) -- make the mesh and connect it to the input linker if g_mesh ~= nil then g_mesh:destroy() end g_mesh = octane.node.create { type = octane.NT_GEO_MESH } g_mesh:setAttribute(octane.A_MATERIAL_NAMES, {"Material", "Emitter material"}, true) g_mesh:connectToIx(1, g_material_input) g_mesh:connectToIx(2, g_material2_input) setupMesh(g_mesh, g_subdiv_level) -- make the placement node and connect it to our mesh if g_placement ~= nil then g_placement:destroy() end g_placement = octane.node.create { type = octane.NT_GEO_PLACEMENT } g_placement:connectTo(octane.P_GEOMETRY, g_mesh) -- make the transform and connect to the placement if g_transform ~= nil then g_transform:destroy() end g_transform = octane.node.create { type = octane.NT_TRANSFORM_VALUE } g_placement:connectTo(octane.P_TRANSFORM, g_transform) setupTransform(g_transform, g_light_pos, g_target_pos, g_cone_size) g_output:connectToIx(1, g_placement) end return VolumetricLight