-- Decals example. -- This sample demonstrates: -- - Performing a raycast to the octree and adding a decal to the hit location -- - Defining a Cursor UI element which stays inside the window and can be shown/hidden -- - Marking suitable (large) objects as occluders for occlusion culling -- - Displaying renderer debug geometry to see the effect of occlusion require "LuaScripts/Utilities/Sample" function Start() -- Execute the common startup for samples SampleStart() -- Create the scene content CreateScene() -- Create the UI content CreateUI() -- Setup the viewport for displaying the scene SetupViewport() -- Set the mouse mode to use in the sample SampleInitMouseMode(MM_RELATIVE) -- Hook up to the frame update and render post-update events SubscribeToEvents() end function CreateScene() scene_ = Scene() -- Create octree, use default volume (-1000, -1000, -1000) to (1000, 1000, 1000) -- Also create a DebugRenderer component so that we can draw debug geometry scene_:CreateComponent("Octree") scene_:CreateComponent("DebugRenderer") -- Create scene node & StaticModel component for showing a static plane local planeNode = scene_:CreateChild("Plane") planeNode.scale = Vector3(100.0, 1.0, 100.0) local planeObject = planeNode:CreateComponent("StaticModel") planeObject.model = cache:GetResource("Model", "Models/Plane.mdl") planeObject.material = cache:GetResource("Material", "Materials/StoneTiled.xml") -- Create a Zone component for ambient lighting & fog control local zoneNode = scene_:CreateChild("Zone") local zone = zoneNode:CreateComponent("Zone") zone.boundingBox = BoundingBox(-1000.0, 1000.0) zone.ambientColor = Color(0.15, 0.15, 0.15) zone.fogColor = Color(0.5, 0.5, 0.7) zone.fogStart = 100.0 zone.fogEnd = 300.0 -- Create a directional light to the world. Enable cascaded shadows on it local lightNode = scene_:CreateChild("DirectionalLight") lightNode.direction = Vector3(0.6, -1.0, 0.8) local light = lightNode:CreateComponent("Light") light.lightType = LIGHT_DIRECTIONAL light.castShadows = true light.shadowBias = BiasParameters(0.00025, 0.5) -- Set cascade splits at 10, 50 and 200 world units, fade shadows out at 80% of maximum shadow distance light.shadowCascade = CascadeParameters(10.0, 50.0, 200.0, 0.0, 0.8) -- Create some mushrooms local NUM_MUSHROOMS = 240 for i = 1, NUM_MUSHROOMS do local mushroomNode = scene_:CreateChild("Mushroom") mushroomNode.position = Vector3(Random(90.0) - 45.0, 0.0, Random(90.0) - 45.0) mushroomNode.rotation = Quaternion(0.0, Random(360.0), 0.0) mushroomNode:SetScale(0.5 + Random(2.0)) local mushroomObject = mushroomNode:CreateComponent("StaticModel") mushroomObject.model = cache:GetResource("Model", "Models/Mushroom.mdl") mushroomObject.material = cache:GetResource("Material", "Materials/Mushroom.xml") mushroomObject.castShadows = true end -- Create randomly sized boxes. If boxes are big enough, make them occluders. Occluders will be software rasterized before -- rendering to a low-resolution depth-only buffer to test the objects in the view frustum for visibility local NUM_BOXES = 20 for i = 1, NUM_BOXES do local boxNode = scene_:CreateChild("Box") local size = 1.0 + Random(10.0) boxNode.position = Vector3(Random(80.0) - 40.0, size * 0.5, Random(80.0) - 40.0) boxNode:SetScale(size) local boxObject = boxNode:CreateComponent("StaticModel") boxObject.model = cache:GetResource("Model", "Models/Box.mdl") boxObject.material = cache:GetResource("Material", "Materials/Stone.xml") boxObject.castShadows = true if size >= 3.0 then boxObject.occluder = true end end -- Create the camera. Limit far clip distance to match the fog cameraNode = scene_:CreateChild("Camera") local camera = cameraNode:CreateComponent("Camera") camera.farClip = 300.0 -- Set an initial position for the camera scene node above the plane cameraNode.position = Vector3(0.0, 5.0, 0.0) end function CreateUI() -- Create a Cursor UI element because we want to be able to hide and show it at will. When hidden, the mouse cursor will -- control the camera, and when visible, it will point the raycast target local style = cache:GetResource("XMLFile", "UI/DefaultStyle.xml") local cursor = ui.root:CreateChild("Cursor") cursor:SetStyleAuto(style) ui.cursor = cursor -- Set starting position of the cursor at the rendering window center cursor:SetPosition(graphics.width / 2, graphics.height / 2) -- Construct new Text object, set string to display and font to use local instructionText = ui.root:CreateChild("Text") instructionText.text = "Use WASD keys to move\n".. "LMB to paint decals, RMB to rotate view\n".. "Space to toggle debug geometry\n".. "7 to toggle occlusion culling" instructionText:SetFont(cache:GetResource("Font", "Fonts/Anonymous Pro.ttf"), 15) -- The text has multiple rows. Center them in relation to each other instructionText.textAlignment = HA_CENTER -- Position the text relative to the screen center instructionText.horizontalAlignment = HA_CENTER instructionText.verticalAlignment = VA_CENTER instructionText:SetPosition(0, ui.root.height / 4) end function SetupViewport() -- Set up a viewport to the Renderer subsystem so that the 3D scene can be seen local viewport = Viewport:new(scene_, cameraNode:GetComponent("Camera")) renderer:SetViewport(0, viewport) end function SubscribeToEvents() -- Subscribe HandleUpdate() function for processing update events SubscribeToEvent("Update", "HandleUpdate") -- Subscribe HandlePostRenderUpdate() function for processing the post-render update event, during which we request -- debug geometry SubscribeToEvent("PostRenderUpdate", "HandlePostRenderUpdate") end function MoveCamera(timeStep) input.mouseVisible = input.mouseMode ~= MM_RELATIVE mouseDown = input:GetMouseButtonDown(MOUSEB_RIGHT) -- Override the MM_RELATIVE mouse grabbed settings, to allow interaction with UI input.mouseGrabbed = mouseDown -- Right mouse button controls mouse cursor visibility: hide when pressed ui.cursor.visible = not mouseDown -- Do not move if the UI has a focused element (the console) if ui.focusElement ~= nil then return end -- Movement speed as world units per second local MOVE_SPEED = 20.0 -- Mouse sensitivity as degrees per pixel local MOUSE_SENSITIVITY = 0.1 -- Use this frame's mouse motion to adjust camera node yaw and pitch. Clamp the pitch between -90 and 90 degrees -- Only move the camera when the cursor is hidden if not ui.cursor.visible then local mouseMove = input.mouseMove yaw = yaw + MOUSE_SENSITIVITY * mouseMove.x pitch = pitch + MOUSE_SENSITIVITY * mouseMove.y pitch = Clamp(pitch, -90.0, 90.0) -- Construct new orientation for the camera scene node from yaw and pitch. Roll is fixed to zero cameraNode.rotation = Quaternion(pitch, yaw, 0.0) end -- Read WASD keys and move the camera scene node to the corresponding direction if they are pressed if input:GetKeyDown(KEY_W) then cameraNode:Translate(Vector3(0.0, 0.0, 1.0) * MOVE_SPEED * timeStep) end if input:GetKeyDown(KEY_S) then cameraNode:Translate(Vector3(0.0, 0.0, -1.0) * MOVE_SPEED * timeStep) end if input:GetKeyDown(KEY_A) then cameraNode:Translate(Vector3(-1.0, 0.0, 0.0) * MOVE_SPEED * timeStep) end if input:GetKeyDown(KEY_D) then cameraNode:Translate(Vector3(1.0, 0.0, 0.0) * MOVE_SPEED * timeStep) end -- Toggle debug geometry with space if input:GetKeyPress(KEY_SPACE) then drawDebug = not drawDebug end -- Paint decal with the left mousebutton cursor must be visible if ui.cursor.visible and input:GetMouseButtonPress(MOUSEB_LEFT) then PaintDecal() end end function PaintDecal() local hitPos, hitDrawable = Raycast(250.0) if hitDrawable ~= nil then -- Check if target scene node already has a DecalSet component. If not, create now local targetNode = hitDrawable.node local decal = targetNode:GetComponent("DecalSet") if decal == nil then decal = targetNode:CreateComponent("DecalSet") decal.material = cache:GetResource("Material", "Materials/UrhoDecal.xml") end -- Add a square decal to the decal set using the geometry of the drawable that was hit, orient it to face the camera, -- use full texture UV's (0,0) to (1,1). Note that if we create several decals to a large object (such as the ground -- plane) over a large area using just one DecalSet component, the decals will all be culled as one unit. If that is -- undesirable, it may be necessary to create more than one DecalSet based on the distance decal:AddDecal(hitDrawable, hitPos, cameraNode.rotation, 0.5, 1.0, 1.0, Vector2(0.0, 0.0), Vector2(1.0, 1.0)) end end function Raycast(maxDistance) local hitPos = nil local hitDrawable = nil local pos = ui.cursorPosition -- Check the cursor is visible and there is no UI element in front of the cursor if (not ui.cursor.visible) or (ui:GetElementAt(pos, true) ~= nil) then return nil, nil end local camera = cameraNode:GetComponent("Camera") local cameraRay = camera:GetScreenRay(pos.x / graphics.width, pos.y / graphics.height) -- Pick only geometry objects, not eg. zones or lights, only get the first (closest) hit local octree = scene_:GetComponent("Octree") local result = octree:RaycastSingle(cameraRay, RAY_TRIANGLE, maxDistance, DRAWABLE_GEOMETRY) if result.drawable ~= nil then return result.position, result.drawable end return nil, nil end function HandleUpdate(eventType, eventData) -- Take the frame time step, which is stored as a float local timeStep = eventData["TimeStep"]:GetFloat() -- Move the camera, scale movement with time step MoveCamera(timeStep) end function HandlePostRenderUpdate(eventType, eventData) -- If draw debug mode is enabled, draw viewport debug geometry. Disable depth test so that we can see the effect of occlusion if drawDebug then renderer:DrawDebugGeometry(false) end end -- Create XML patch instructions for screen joystick layout specific to this sample app function GetScreenJoystickPatchString() return "" .. " " .. " Paint" .. " " .. " " .. " " .. " " .. " " .. " " .. " " .. " Debug" .. " " .. " " .. " " .. " " .. " " .. " " .. "" end