Barrels and boxes
Shoot alternating barrels and boxes into a physics scene. Click the canvas to aim, use the left button for one shot or hold the right button for automatic fire. Escape releases the mouse.
Each projectile pairs a physics body with a child model. Interpolation keeps motion smooth, and the oldest projectile is removed after the limit of 64.
local natiny = require("natiny")
local demo = require("common.modules.demo")
local BG = { 21 / 255, 30 / 255, 44 / 255 } -- #151E2C
-- The deferred scene needs an explicit window background: its full-screen
-- composite otherwise turns untouched G-buffer pixels black.
local COMPOSITE = [[
attribute vec3 position;
attribute vec2 texcoord0;
varying vec2 uv;
uniform mat4 mtx_worldviewproj;
uniform sampler2D hdr;
uniform sampler2D g_albedo;
uniform sampler2D g_ao;
uniform sampler2D g_position;
uniform vec4 tone_params;
uniform vec4 background_color;
void vertex()
{
uv = texcoord0;
POSITION = mtx_worldviewproj * vec4(position, 1.0);
}
void fragment()
{
vec3 color = textureLod(hdr, uv, 0.0).rgb;
vec3 albedo = textureLod(g_albedo, uv, 0.0).rgb;
float ao = textureLod(g_ao, uv, 0.0).r;
color *= mix(1.0, ao, tone_params.z);
color += albedo * tone_params.y * ao;
color *= tone_params.x;
color = color / (color + vec3(1.0));
color = pow(color, vec3(1.0 / 2.2));
float geometry = step(0.5, textureLod(g_position, uv, 0.0).a);
COLOR = vec4(mix(background_color.rgb, color, geometry), 1.0);
}
]]
local function inline_shader(source)
local resource = natiny.resource.from_data(source)
local shader = natiny.shader.load(resource)
natiny.resource.destroy(resource)
return shader
end
if not natiny.backend.init(arg and arg[1] or natiny.backend.AUTO) then
os.exit(1)
end
if not natiny.physics.init() then
os.exit(1)
end
--////// rendering pipeline
local function set_fullscreen_state(material)
natiny.material.set_state(material, { blend = natiny.material.BLEND_NONE, depth = natiny.material.DEPTH_OFF })
end
-- Every file the pipeline is made of comes from the shell, which already
-- holds the bytes by the time this runs.
local function create_rendering_pipeline(width, height, options)
local light = options.light
local shadow_options = options.shadow
local ao = options.ao
local materials = {
geometry = natiny.material.create(natiny.shader.load(demo:resource("data/examples/barrels_and_boxes/shaders/gbuffer.nsl")), "scene"),
shadow = natiny.material.create(natiny.shader.load(demo:resource("data/examples/barrels_and_boxes/shaders/shadow.nsl"))),
ssao = natiny.material.create(natiny.shader.load(demo:resource("data/examples/barrels_and_boxes/shaders/ssao.nsl"))),
light = natiny.material.create(natiny.shader.load(demo:resource("data/examples/barrels_and_boxes/shaders/light.nsl"))),
composite = natiny.material.create(inline_shader(COMPOSITE)),
}
local blur_shader = natiny.shader.load(demo:resource("data/examples/barrels_and_boxes/shaders/ssao_blur.nsl"))
materials.blur_x = natiny.material.create(blur_shader)
materials.blur_y = natiny.material.create(blur_shader)
local roughness_texture = natiny.texture.create(demo:resource(options.roughness_texture))
natiny.material.set_state(materials.geometry, { blend = natiny.material.BLEND_NONE })
natiny.material.set_state(materials.shadow, { blend = natiny.material.BLEND_NONE, cull = natiny.material.CULL_NONE })
for _, material in ipairs({ materials.ssao, materials.blur_x, materials.blur_y, materials.light, materials.composite }) do
set_fullscreen_state(material)
end
natiny.material.set_texture(materials.geometry, "roughness_map", roughness_texture)
natiny.material.set_constant(materials.geometry, "surface_params", options.metallic, 0, 0, 0)
natiny.material.set_constant(materials.ssao, "ao_params", ao.radius, ao.strength, ao.bias, math.tan(math.rad(options.camera_fov) * 0.5))
natiny.material.set_constant(materials.ssao, "ao_curve", ao.curve, 0, 0, 0)
natiny.material.set_constant(materials.blur_x, "blur_params", 1, 0, ao.blur_tolerance, 0)
natiny.material.set_constant(materials.blur_y, "blur_params", 0, 1, ao.blur_tolerance, 0)
natiny.material.set_constant(materials.light, "light_pos", light.x, light.y, light.z, 0)
natiny.material.set_constant(materials.light, "light_color", light.r, light.g, light.b, light.power)
natiny.material.set_constant(materials.light, "shadow_params", shadow_options.normal_bias, shadow_options.depth_bias, 1 / shadow_options.face_size, 0)
natiny.material.set_constant(materials.composite, "tone_params", options.tone.exposure, options.tone.ambient, options.tone.ao_visibility, 0)
natiny.material.set_constant(materials.composite, "background_color",
BG[1], BG[2], BG[3], 1)
local shadow = natiny.surface.create(shadow_options.face_size * 6, shadow_options.face_size, natiny.surface.RGBA16F, true)
local shadow_cameras = {}
local face_angles = {
{ 0, -90, 0 }, { 0, 90, 0 },
{ 90, 0, 0 }, { -90, 0, 0 },
{ 0, 180, 0 }, { 0, 0, 0 },
}
for face, angles in ipairs(face_angles) do
local camera = natiny.camera.create()
natiny.camera.set_fov(camera, 90)
natiny.camera.set_clip(camera, shadow_options.near_clip, shadow_options.far_clip)
natiny.entity.set_position(camera, natiny.entity.SPACE_WORLD, light.x, light.y, light.z)
natiny.entity.set_rotation(camera, natiny.entity.SPACE_WORLD, angles[1], angles[2], angles[3])
shadow_cameras[face] = camera
end
local function ao_size(w, h)
return math.max(1, math.floor(w / ao.resolution_scale)),
math.max(1, math.floor(h / ao.resolution_scale))
end
local ao_width, ao_height = ao_size(width, height)
local pipeline = {
width = width,
height = height,
material = materials.geometry,
roughness_texture = roughness_texture,
albedo = natiny.surface.create(width, height, natiny.surface.RGBA8, true),
normal = natiny.surface.create(width, height, natiny.surface.RGBA16F, false),
position = natiny.surface.create(width, height, natiny.surface.RGBA16F, false),
lighting = natiny.surface.create(width, height, natiny.surface.RGBA16F, false),
occlusion = natiny.surface.create(ao_width, ao_height, natiny.surface.RGBA16F, false),
occlusion_blur = natiny.surface.create(ao_width, ao_height, natiny.surface.RGBA16F, false),
}
function pipeline:connect()
for name, target in pairs({ g_albedo = self.albedo, g_normal = self.normal, g_position = self.position, g_shadow = shadow }) do
natiny.material.set_texture(materials.light, name, natiny.surface.get_texture(target))
end
natiny.material.set_texture(materials.ssao, "g_normal", natiny.surface.get_texture(self.normal))
natiny.material.set_texture(materials.ssao, "g_position", natiny.surface.get_texture(self.position))
natiny.material.set_constant(materials.ssao, "ao_source", self.width, self.height, 1 / self.width, 1 / self.height)
natiny.material.set_texture(materials.blur_x, "g_ao", natiny.surface.get_texture(self.occlusion))
natiny.material.set_texture(materials.blur_y, "g_ao", natiny.surface.get_texture(self.occlusion_blur))
for name, target in pairs({ hdr = self.lighting, g_albedo = self.albedo,
g_ao = self.occlusion, g_position = self.position }) do
natiny.material.set_texture(materials.composite, name, natiny.surface.get_texture(target))
end
end
function pipeline:resize(new_width, new_height)
if new_width == self.width and new_height == self.height then return end
self.width, self.height = new_width, new_height
for _, target in ipairs({ self.albedo, self.normal, self.position, self.lighting }) do
natiny.surface.resize(target, new_width, new_height)
end
local new_ao_width, new_ao_height = ao_size(new_width, new_height)
natiny.surface.resize(self.occlusion, new_ao_width, new_ao_height)
natiny.surface.resize(self.occlusion_blur, new_ao_width, new_ao_height)
self:connect()
end
function pipeline:draw_shadows(draw_scene)
natiny.surface.bind(shadow, function()
natiny.render.clear(1000, 0, 0, 1)
natiny.material.bind(materials.shadow, function()
for face, camera in ipairs(shadow_cameras) do
natiny.render.viewport_begin((face - 1) * shadow_options.face_size, 0, shadow_options.face_size, shadow_options.face_size)
natiny.camera.bind(camera, draw_scene)
natiny.render.viewport_end()
end
end)
end)
end
function pipeline:render(camera, draw_scene)
self:draw_shadows(draw_scene)
natiny.surface.bind(self.albedo, 0)
natiny.surface.bind(self.normal, 1)
natiny.surface.bind(self.position, 2)
natiny.render.clear(0, 0, 0, 0)
natiny.camera.bind(camera, draw_scene)
natiny.surface.unbind()
natiny.surface.bind(self.occlusion, function()
natiny.material.bind(materials.ssao, natiny.render.cover)
end)
natiny.surface.bind(self.occlusion_blur, function()
natiny.material.bind(materials.blur_x, natiny.render.cover)
end)
natiny.surface.bind(self.occlusion, function()
natiny.material.bind(materials.blur_y, natiny.render.cover)
end)
natiny.surface.bind(self.lighting, function()
natiny.render.clear(0, 0, 0, 1)
natiny.material.bind(materials.light, natiny.render.cover)
end)
natiny.material.bind(materials.composite, natiny.render.cover)
end
pipeline:connect()
return pipeline
end
--////// demo scene
local WINDOW = { width = 1280, height = 720 }
local CAMERA = {
x = 0, y = 1.6, z = 0,
fov = 65,
sensitivity = 0.15,
}
local LIGHT = {
x = 0, y = 2.5, z = 0,
r = 1.0, g = 0.92, b = 0.82,
power = 1000,
}
local SHOT = {
spawn_distance = 0.8,
impulse = 24,
friction = 0.6,
restitution = 0.25,
mass = 20,
limit = 64,
auto_fire_interval = 0.12,
}
local BARREL = { radius = 0.534 / 2, height = 0.858 }
local BOX = { size = 0.5 }
local RENDERING = {
camera_fov = CAMERA.fov,
light = LIGHT,
roughness_texture = "data/examples/barrels_and_boxes/textures/roughness.png",
metallic = 0,
shadow = {
face_size = 256,
near_clip = 0.05,
far_clip = 20,
normal_bias = 0.03,
depth_bias = 0.02,
},
ao = {
resolution_scale = 4,
radius = 0.5,
strength = 3.0,
bias = 0.1,
curve = 2.0,
blur_tolerance = 0.05,
},
tone = {
exposure = 1.4,
ambient = 0.12,
ao_visibility = 1.0,
},
}
-- Six shaders, a roughness map and three models: enough that the wait is
-- worth showing, which is what the shell is for.
local ASSETS = {
"data/examples/barrels_and_boxes/shaders/gbuffer.nsl",
"data/examples/barrels_and_boxes/shaders/shadow.nsl",
"data/examples/barrels_and_boxes/shaders/ssao.nsl",
"data/examples/barrels_and_boxes/shaders/ssao_blur.nsl",
"data/examples/barrels_and_boxes/shaders/light.nsl",
"data/examples/barrels_and_boxes/textures/roughness.png",
"data/examples/barrels_and_boxes/models/demo_scene.glb",
"data/common/models/barrel.glb",
"data/common/models/box_01.glb",
}
local window = natiny.window.create("Natiny", WINDOW.width, WINDOW.height)
natiny.window.set_scale_mode(window, natiny.window.SCALE_MODE_STRETCH)
-- All of these are made out of the files above, so they are made in the
-- build below rather than here.
local pipeline, scene, barrel_mesh, box_mesh, projectile_types
local camera = natiny.camera.create()
local yaw, pitch = 0, 0
natiny.camera.set_fov(camera, CAMERA.fov)
natiny.entity.set_position(camera, natiny.entity.SPACE_WORLD, CAMERA.x, CAMERA.y, CAMERA.z)
-- Turning only while the pointer is locked. Unlocked, the cursor is a
-- cursor: it stops at the edge of the window, so a turn would stop with
-- it, and it belongs to whatever is behind the window anyway.
local function update_camera(captured)
if captured then
yaw = yaw - natiny.input.get_mouse_x_speed() * CAMERA.sensitivity
pitch = pitch - natiny.input.get_mouse_y_speed() * CAMERA.sensitivity
pitch = math.max(-89, math.min(89, pitch))
natiny.entity.set_rotation(camera, natiny.entity.SPACE_LOCAL, pitch, yaw, 0)
end
local ry, rx = math.rad(yaw), math.rad(pitch)
return -math.sin(ry) * math.cos(rx),
math.sin(rx),
-math.cos(ry) * math.cos(rx)
end
local projectiles = {}
local next_projectile = 1
local function shoot_projectile(dx, dy, dz)
local projectile = projectile_types[next_projectile]
local body = projectile.create_body()
local model = natiny.model.create(projectile.mesh, pipeline.material)
next_projectile = next_projectile % #projectile_types + 1
natiny.entity.set_position(body, natiny.entity.SPACE_WORLD, CAMERA.x + dx * SHOT.spawn_distance, CAMERA.y + dy * SHOT.spawn_distance, CAMERA.z + dz * SHOT.spawn_distance)
natiny.entity.set_parent(model, body)
natiny.entity.set_position(model, natiny.entity.SPACE_LOCAL, 0, projectile.model_y, 0)
natiny.physics.set_material(body, SHOT.friction, SHOT.restitution, SHOT.mass)
natiny.physics.apply_impulse(body, dx * SHOT.impulse, dy * SHOT.impulse, dz * SHOT.impulse)
natiny.physics.set_interpolation(body, true)
projectiles[#projectiles + 1] = { body = body, model = model }
if #projectiles > SHOT.limit then
local oldest = table.remove(projectiles, 1)
natiny.model.destroy(oldest.model)
natiny.physics.destroy(oldest.body)
end
end
local function draw_scene()
natiny.render.tag("scene")
end
local MOUSE_LEFT = natiny.input.MOUSE_BUTTON_LEFT
local MOUSE_RIGHT = natiny.input.MOUSE_BUTTON_RIGHT
local auto_fire_cooldown = 0
local function frame(dt)
natiny.window.bind(window, function()
local captured = natiny.input.get_mouse_captured()
demo:set_user_text(captured
and "Mouse: look | LMB: shoot | Hold RMB: automatic fire | Esc: free the mouse"
or "Click to take the mouse")
-- The click that takes the mouse does not also fire: it is the click
-- the browser wants before it will grant a pointer lock at all, and a
-- barrel spawned by it would come as a surprise. Escape gives the
-- cursor back -- the engine does that itself, and on both platforms,
-- because in the browser it has no say in the matter.
if not captured then
if natiny.input.get_mouse_button_pressed(MOUSE_LEFT) then
natiny.input.set_mouse_capture(true)
end
auto_fire_cooldown = 0
end
local dx, dy, dz = update_camera(captured)
if captured then
if natiny.input.get_mouse_button_pressed(MOUSE_LEFT) then
shoot_projectile(dx, dy, dz)
end
if natiny.input.get_mouse_button_down(MOUSE_RIGHT) then
auto_fire_cooldown = auto_fire_cooldown - dt
if auto_fire_cooldown <= 0 then
shoot_projectile(dx, dy, dz)
auto_fire_cooldown = SHOT.auto_fire_interval
end
else
auto_fire_cooldown = 0
end
end
pipeline:resize(natiny.window.get_width(window), natiny.window.get_height(window))
pipeline:render(camera, draw_scene)
end)
end
-- Everything above is either a number or a function; on_loaded is where the
-- files turn into the scene.
local function build_scene()
pipeline = create_rendering_pipeline(
natiny.window.get_width(window), natiny.window.get_height(window), RENDERING)
local scene_mesh = natiny.mesh.load(demo:resource("data/examples/barrels_and_boxes/models/demo_scene.glb"))
scene = {
model = natiny.model.create(scene_mesh, pipeline.material),
body = natiny.physics.mesh(scene_mesh),
}
barrel_mesh = natiny.mesh.load(demo:resource("data/common/models/barrel.glb"))
box_mesh = natiny.mesh.load(demo:resource("data/common/models/box_01.glb"))
projectile_types = {
{
mesh = barrel_mesh,
create_body = function() return natiny.physics.cylinder(BARREL.radius, BARREL.height) end,
model_y = -BARREL.height / 2,
},
{
mesh = box_mesh,
create_body = function() return natiny.physics.cube(BOX.size, BOX.size, BOX.size) end,
model_y = 0,
},
}
end
demo:run({
window = window,
title = "Barrels and boxes",
resources = ASSETS,
on_loaded = build_scene,
loop = frame,
})
natiny.backend.shutdown()