Simple 3D Model Renderer
A simple 3D scene renderer for Love2D 11.3. Support simple lighting.
The goal of this project is to easily and quickly create and render 3D geometric scenes through code. On this basis, to support complex 3D model rendering.
Features
- Create model from
.obj
model file - Create basic model:
plane
,cycle
,box
,cylinder
,sphere
. perspective
andorthogonal
camera.- Project Love2D drawing to 3D world.
- Simple render scene with light and shadow.
- Simple PBR
- Custom shader pass
- Support generate mesh by path points
Example
More see examples code.
Installation
Copy src
to your project.
Copy CPML to your project. And make sure able to require 'cpml'
Usage
local MR = require 'model_renderer'
-- Create model from obj file or basic shape
local ground = MR.model.new_plane(2000, 2000)
local model = MR.model.load('3d.obj')
local box = MR.model.new_box(50)
local sphere = MR.model.new_sphere(30)
local cylinder = MR.model.new_cylinder(30, 100)
local renderer, scene, camera
function love.load()
-- Initalize render, scene and camera
renderer = MR.renderer.new()
scene = MR.scene.new()
camera = MR.camera.new()
local pos = { 1000, 2000, 1000 }
local color = { 500000, 500000, 500000 }
local linear, quadratic = 0, 1
scene:add_light(pos, color, linear, quadratic)
scene.ambient_color = { 0.05, 0.05, 0.05 }
ground:set_opts({ instance_usage = 'static' })
ground:set_instances({
{ coord = { -1000, 0, -1000 }, albedo = { 0, 0.9, 0 }, physics = { roughness = 1, metallic = 0 } }
})
end
function love.draw()
local w, h = love.graphics.getDimensions()
local hw, hh = w * 0.5, h * 0.5
-- Set camera projection and view, and apply camera for renderer
camera:orthogonal(-hw, hw, hh, -hh, -500, 2000)
camera:look_at(0, 0, 0, math.rad(60), 0, 0)
renderer:apply_camera(camera)
local ts = love.timer.getTime()
-- Add some model to scene
scene:add_model(ground) -- static model
-- dynamic model instances: coord, angle, scale, albedo, physics attributes { roughness, metallic }
scene:add_model(model, { 0, 0, 0 }, { 0, math.sin(ts) * math.pi * 2, 0 }, 10, { 0, 1, 0, 1 }, { 0.5, 0.5 })
scene:add_model(model,
{ math.sin(ts) * 100, 0, math.cos(ts) * 100 },
{ 0, math.rad(45), 0 }, 10, { 1, 0, 0, 1 }, { 0.5, 0.5 }
)
local angle = { 0, ts % (math.pi * 2), 0 }
scene:add_model(box, { -300, 0, 0 }, angle)
scene:add_model(sphere, { -300, 0, 300 }, angle)
scene:add_model(cylinder, { 300, 0, 300 }, angle)
love.graphics.clear(0.5, 0.5, 0.5)
-- Render and clean scene models
renderer:render(scene:build())
scene:clean_model()
end
See examples folder for more.
Functions
Model
- MR.model.new(vertices, texture, opts): new a custom model form vertices. vertex: { x, y, z, tex_x, tex_y, normal_x, normal_y, normal_z }. opts is the same as
model:set_opts
MR.model.new(vertices)
MR.model.new(vertices, opts)
MR.model.new(vertices, texture)
-- with vertex map
MR.model.new({ v1, v2, v3, ..., vertex_map = { vi1, vi2, vi3, ... }, texture, opts)
- MR.model.load(path): load a model from
.obj
file - MR.model.new_plane(width, height, ox, oy, oz)
- MR.model.new_circle(radius, segments, ox, oy, oz)
- MR.model.new_box(xlen, ylen, zlen, ox, oy, oz)
- MR.model.new_cylinder(radius, height, segments, ox, oy, oz)
- MR.model.new_sphere(radius_x, radius_y, radius_z, segments, ox, oy, oz)
- MR.model.new_complex({ { 'circle', radius, segmetns }, { 'sphere', 30, 20, 30, 16, 0, 10, 0 }, ... })
- MR.model.new_path_mesh({ { x, y, z, scale_x, scale_y, roll, link_segment_dist = 10 or nil }, ... }, { roll = 0, poly_segment = 4, close_path = false }): create mesh according the path
- Model:set_texture(texture): image or canvas
- Model:set_opts(opts).
-- default value
model:set_opts({
write_depth = true,
face_culling = 'back', -- 'back', 'front', 'none'
transparent = false, -- model is transparent or not
instance_usage = 'dynamic', -- see love2d SpriteBatchUsage. dynamic, static, stream.
mesh_format = {
{ 'VertexPosition', 'float', 3 },
{ 'VertexTexCoord', 'float', 2 },
{ 'VertexNormal', 'float', 3 },
},
instance_mesh_format = {
{ 'ModelPos', 'float', 3 },
{ 'ModelAngle', 'float', 3 },
{ 'ModelScale', 'float', 3 },
{ 'ModelAlbedo', 'byte', 4 },
{ 'ModelPhysics', 'byte', 4 },
},
instance_attrs_parser = function(attrs) end, -- to parse attrs for model:set_instances
ext_pass_id = 0, -- for extend vertex_code and pixel_code
}
})
Note:
mesh_format
andinstance_mesh_format
only allow set byMR.model.new
- Model:set_instances(attrs): { { coord = vec3, rotation = vec3, scale = number or vec3, albedo = rgb or rgba, physics = vec2 or { roughness = v, metallic = v } }, ... }. Set instances for render. build attributes and call
model:set_raw_instances(raw_transforms)
to attach.
model:set_instances({
{ coord = { x, y, z } }, -- instance 1
{ coord = { x = x, y = y, z = z }, physics = { roughness = 0.5, metallic = 0.2 } }, -- instance 2
{ coord = Cpml.vec3(x, y, z), physics = { 0.5, 0.2 } }, -- instance 3, it's the same as instance 3
{ coord = { x, y, z }, rotation = { x = 1, y = 2, z = 3 }, albedo = { 0.5, 0.5, 0 } }, -- instance 4
{ coord = { x, y, z }, rotation = { 1, 2, 3 }, albedo = { r = 0.5, g = 0.5, b = 0 } }, -- instance 5, it's the same as instance 4
{ coord = { x, y, z }, albedo = { r = 0.5, g = 0.5, b = 0, a = 1 } }, -- for now, alpha is not well supported
{ coord = { x, y, z }, scale = 2 }, -- instance 7
{ coord = { x, y, z }, scale = { 2, 2, 2 } }, -- it's the same as instance 7
{ coord = { x, y, z }, scale = { x = 2, y = 2, z = 2 } }, -- it's the same as instance 7
})
- Model:set_raw_instances(raw_attrs): { { x, y, z, rotate_x, rotate_y, rotate_z, scale_x, scale_y, scale_z, albedo_r, albedo_g, albedo_b, albedo_a, roughness, metallic }, ... }. Set intances for render, it will create(if not created or vertices_count < #raw_attrs) a mesh to save all instances data and attach to the model.
- Model:clone(): clone model without instances data
Renderer
MR.renderer
is fast for small instances and light.
- Renderer.new(options) return a new instance
local opts = {
vertex_code = 'void vertex_pass(inout vec4 world_pos, inout vec3 normal) { }',
pixel_code = [[
void pixel_pass(
inout vec3 world_pos, inout vec3 normal, inout vec4 albedo,
inout float roughness, inout float metallic
) { }
]],
macros = {
XXX = 1, -- #define XXX 1
}
}
local renderer = MR.renderer.new(default_opts)
-
renderer:apply_camera(camera_instance): the camera must initialized projection and view. fetch all camera attributes and apply to renderer.
-
renderer:render(scene_desc, time, draw_to_screen): Must call
renderer:apply_camera
before render. uselove.timer.getTime()
if not give a time. Default value of draw_to_screen istrue
. Return result canvas -
renderer:draw_to_screen(): draw to render result to screen
renderer:render({
model = { model1, model2, ... },
transparent_model = { model1, model2, ... },
lights = {
pos = { light1_pos, light2_pos, ... },
color = { light1_color, light2_color, ... },
linear = { light1_linear, light2_linear, ... }
quadratic = { light1_q, light2_q, ... }
},
sun_dir = { x, y, z },
sun_color = { r, g, b },
ambient_color = { r, g, b },
})
-- or
renderer:render(scene_instance:build())
-- equal
renderer:render(scene_instance:build(), nil, false)
renderer:draw_to_screen()
- renderer:attach(...): attach the result canvas, depth buffer and camera. Can draw some 2d object with depth. all arguments pass to
camear:attach
- renderer:detach(): detach the result canvas
renderer:render(scene, nil, false)
renderer:attach(plane_transform)
love.graphics.circle('fill', 100, 100, 10)
renderer:detach()
renderer:draw_to_screen()
Renderer Attributes
- renderer.render_shadow: boolean, change it to enable/disable shadow.
- renderer.skybox: a cubeimage texture. A demo in
examples/pbr.lua
.
Scene
It is optional, you can also manually build scene description for renderer.
-
MR.scene.new() return scene instance
-
scene:add_model(model, coord, angle, scale, albedo, physics): add a model to scene. Coord is required, other is optional
- coord: { x, y, z } or { x = x, y = y, z = z }
- angle: { x, y, z } or { x = x, y = y, z = z }, defualt { 0, 0, 0 }
- scale: { x, y, z } or { x = x, y = y, z = z }, default { 1, 1, 1 }
- albedo: { r, g, b, a } or { r = r, g = g, b = b, a = a }, now alpha is unused.
- physics: { roughness, metallic } or { roughness = 0.5, metallic = 0.5 }. value 0.0-1.0
-
scene:add_light(pos, color, linear, quadratic): add point light source.
-
scene:set_lights(lights): Set point light sources. In the shader, max lights limit is 32.
scene:set_lights({
{ pos = { 10, 10, 10 }, color = { 10000, 10000, 0 } },
{ pos = { 10, 10, 10 }, color = { 10000, 10000, 0 }, linear = 0, quadratic = 1 }, -- same as first
...
})
- scene:clean_model(): reset scene, remove all models from scene.
- scene:clean(): reset scene, remove all models and lights from scene.
- scene:build(): build scene for renderer.
renderer:render(scene:build())
. Automatically apply all transforms to models bymodel:set_instances
Camera
It is optional, you can also manually set all camera attributes for renderer.
- MR.camera.new() return camera instance
- camera:perspective(fovy, aspect, near, far) create perspective projection for this camera
- camera:orthogonal(left, right, top, bottom, near, far) create orthogonal projection for this camera
- camera:move_to(x, y, z, rx, ry, rz) move camera to the position, set camera angle and update view
- camera:look_at(x, y, z, rx, ry, rz) look at the position use the specified angle and update view
- camera:project(point, viewport) project the world point to screen. point: Cpml.vec3 or { x = x, y = y, z = z }. viewport: { ox, oy, w, h }
- camera:unproject(screen_x, screen_y, viewport, plane) unproject the screen point to world. viewport: { ox, oy, w, h }. plane: { position = vec3, normal = vec3 }, default is { position = vec3(0, 0, 0), normal = vec3(0, 1, 0) }
- camera:attach(plane_transform) project love 2D drawing to 3D world. the
plane_transform
is a matrix to transform the 2d plane. plane_transform is optional, default just rotatemath.pi * 0.5
based on x(transformy
of 2D to z of3D
). The function will calllove.graphics.setShader
- camera:detach()
Util
Some helper functions.
MR.util.send_uniform(shader, name, ...)
: check has uniform before sending uniform.
-- equal to
if shader:hasUniform(name) then
shader:send(k, ...)
end
MR.util.compute_face_normal(vertex1, vertex2, vertex3)
: return normal { x, y, z }MR.util.generate_vertices(vertices, faces, build_cb)
: generate model vertices from vertices and face
-- auto compute face normal
MR.util.generate_vertices(
{{ 0, 0, 0 }, { 1, 0, 0 }, { 1, 0, 1 }, { 0, 0, 1 }},
{{ 1, 2, 3 }, { 2, 3, 4 }}
)
-- use custom vertex normal
MR.util.generate_vertices(
{{ x, y, z }, { x2, y2, z2 }, { x3, y3, z3 }, { x4, y4, z4 }},
{
{ { 1, vn = { 0, 1 ,0 } }, { 2, vn = { 0, 1, 0 } }, { 3, vn = { 0, 1, 0 } } },
{ 2, 3, 4 } -- auto compute face normal
}
)
-- build custom vertex
MR.util.generate_vertices(
{{ 0, 0, 0 }, { 1, 0, 0 }, { 1, 0, 1 }, { 0, 0, 1 }},
{{ 1, 2, 3 }, { 2, 3, 4 }},
function(vertex, normal)
return { vertex[1], vertex[2], vertex[3], normal[1], normal[2], normal[3], unpack(vertex, 4) }
end
)
TODO
- Better shadow(CSM or other).
- SSAO for forward renderer
- Build better geometry mesh. (vertices, normal or texture coord)
- More 3D geometry shapes
- Better Blend transparent object
- More support for model file(mtl, tex and more)
- Support Normal, Albedo, Metallic, Roughness and AO texture map
References
- LearnOpenGL
- LOVEPBR
- 3DreamEngine It's a library with more functions