require 'honey.std' local paused = true local balls = {} local function formatize(f) return function(fmt, ...) f(string.format(fmt, ...)) end end local printf = formatize(print) local log = honey.log log.fatal = formatize(log.fatal) log.error = formatize(log.error) log.warn = formatize(log.warn) log.info = formatize(log.info) log.debug = formatize(log.debug) log.trace = formatize(log.trace) log.info( "honey v%d.%d.%d -- %s", honey.version.major, honey.version.minor, honey.version.patch, honey.version.string ) local glfw = honey.glfw local gl = honey.gl local Vec3 = honey.Vec3 local Mat4 = honey.Mat4 local Quaternion = honey.Quaternion local ecs = honey.ecs local systems = honey.standardSystems local ode = honey.ode local nvg = honey.nvg -- initialize honey local window = honey.init() -- setup vector graphics local vg = nvg.CreateContext() local november = nvg.CreateFont(vg, "November", "assets/november.regular.ttf") local vw, vh = 640, 480 -- create camera matrices local camera = { view=Mat4():identity():translate(Vec3{0, 0, -30}), projection=Mat4():perspective(math.rad(45), 640/480, 0.1, 100), } -- setup ecs local level = ecs.Level() level:addSystem(systems.transformCascade) level:addSystem(systems.renderCam(camera)) level:addSystem(systems.update) -- physics system local world = ode.WorldCreate() local space = ode.HashSpaceCreate(ode.Space0) level:addSystem{ setup=function(self) self.time = 0 self.world=world self.space=space ode.WorldSetGravity(self.world, 0, -100, 0) ode.WorldSetCFM(self.world, 1e-5) self.contactgroup = ode.JointGroupCreate(200) self.__gc = honey.util.gc_canary(function() ode.WorldDestroy(self.world) ode.SpaceDestroy(self.space) end) end, filter = ecs.Filter.AND{"transform", "collisionShape", "physicsBody"}, preUpdate = function(self) self.dt = nil if self.time > 0.01 then self.time = self.time - 0.01 ode.SpaceCollide(self.space, function(o1, o2) local b1 = ode.GeomGetBody(o1) local b2 = ode.GeomGetBody(o2) local contact = ode.CreateContact{ surface = { mode = ode.ContactBounce + ode.ContactSoftCFM, mu = ode.Infinity, bounce = 0.90, bounce_vel = 0.1, soft_cfm = 0.001, }, } local collisions = ode.Collide(o1, o2, 1) if #collisions > 0 then local e1 = ode.GeomGetData(o1) local e2 = ode.GeomGetData(o2) ode.ContactSetGeom(contact, collisions[1]) local joint = ode.JointCreateContact(self.world, self.contactgroup, contact) ode.JointAttach(joint, b1, b2) end end) ode.WorldQuickStep(self.world, 0.01) ode.JointGroupEmpty(self.contactgroup) end end, update = function(self, entity, dt) if not self.dt then self.dt = dt self.time = self.time + dt end local x, y, z = ode.BodyGetPosition(entity.physicsBody) local d, a, b, c = ode.BodyGetQuaternion(entity.physicsBody) entity.transform:identity():translate(Vec3{x, y, z}):mul(Quaternion{a, b, c, d}:toMat4()) end, onRemoveEntity = function(self, id, entity) print("remove", id) ode.GeomDestroy(entity.collisionShape) ode.BodyDestroy(entity.physicsBody) end, } -- create shader local shader = honey.Shader{ vertexFile = "vertex.glsl", fragmentFile = "fragment.glsl", } -- load models local plane = honey.mesh.loadFile("assets/plane.obj")[1] local tetra = honey.mesh.loadFile("assets/tetrahedron.obj")[1] local cube = honey.mesh.loadFile("assets/cube.obj")[1] local octa = honey.mesh.loadFile("assets/octahedron.obj")[1] local dodeca = honey.mesh.loadFile("assets/dodecahedron.obj")[1] local icosa = honey.mesh.loadFile("assets/icosahedron.obj")[1] -- update function for each entity function updateTransform(self, dt) self.transform:rotateY(0.3 * math.pi * dt) self.transform:rotateX(0.1 * math.pi * dt) end -- create entities function growLine(prev, depth) if depth == 0 then return prev end local entity = { transform=Mat4():identity():translate(Vec3{2, 0, 0}), parent=false, mesh=octa, shader=shader, update=updateTransform, } prev.parent = entity level:addEntity(prev) return growLine(entity, depth-1) end local leaf = { transform=Mat4():identity():translate(Vec3{2, 0, 0}), parent=false, mesh=tetra, shader=shader, } local root = growLine(leaf, 24) root.update = function(self, dt) self.transform:rotateY(0.2 * math.pi * dt) end level:addEntity(root) local groundPlane = { transform=Mat4():identity():translate(Vec3{0, -2, 0}):scale(Vec3{10, 10, 10}), parent=false, mesh=plane, shader=shader, } level:addEntity(groundPlane) local function createNewBall() local ball = { transform=Mat4():identity(), parent=false, mesh=icosa, shader=shader, collisionShape=ode.CreateSphere(space, 1.0), physicsBody=ode.BodyCreate(world), } local id = level:addEntity(ball) ball.id = id table.insert(balls, id) if #balls > 200 then level:removeEntity(table.remove(balls, 1)) end local mass = ode.MassCreate() ode.MassSetSphere(mass, 1, 0.5) ode.BodySetMass(ball.physicsBody, mass) local function tiny() return (6 * math.random()) - 3 end local x, y, z = tiny(), tiny(), tiny() ode.GeomSetBody(ball.collisionShape, ball.physicsBody) ode.BodySetPosition(ball.physicsBody, x, y, z) ode.BodySetLinearVel(ball.physicsBody, x, y, z) ode.GeomSetData(ball.collisionShape, ball) end createNewBall() ode.CreatePlane(space, 0, 1, 0, -2) -- close window on ESCAPE key window:setKeyCallback(function(_, key, scancode, action) if action == glfw.PRESS then if key == glfw.KEY_ESCAPE then window:setShouldClose(true) elseif key == glfw.KEY_SPACE then paused = not paused elseif key == glfw.KEY_Z or key == glfw.KEY_X or key == glfw.KEY_C or key == glfw.KEY_V or key == glfw.KEY_B or key == glfw.KEY_N or key == glfw.KEY_M then createNewBall() end end end) -- resize window correctly window:setFramebufferSizeCallback(function(_, width, height) gl.Viewport(0, 0, width, height) camera.projection:perspectiveResize(width/height) vw, vh = width, height end) -- averager (for fps) function averager(memory) local buf = {} local avg = 0 for i=1,memory do table.insert(buf, 0) end return function(value) table.insert(buf, value) local val = table.remove(buf, 1) avg = avg + value - val return avg / memory end end local fpsAverage = averager(200) -- main loop honey.loop(window, function(dt) gl.ClearColor(0.2, 0.4, 1.0, 1.0) gl.Clear(gl.COLOR_BUFFER_BIT + gl.DEPTH_BUFFER_BIT + gl.STENCIL_BUFFER_BIT) gl.UseProgram(0) gl.Enable(gl.DEPTH_TEST) level:update(dt, paused) nvg.BeginFrame(vg, vw, vh, 1.0) nvg.StrokeColor(vg, nvg.RGBf(1, 1, 1)) nvg.FontFace(vg, "November") nvg.Text(vg, 50, 50, "fps: "..tostring(math.floor(fpsAverage(1/dt)))) nvg.BeginPath(vg) nvg.MoveTo(vg, 50, 50) nvg.LineTo(vg, 50, 100) nvg.LineTo(vg, 100, 50) nvg.Stroke(vg) nvg.EndFrame(vg) end) -- clean up honey.terminate()