Initial
This commit is contained in:
commit
b8d1a8022c
649 changed files with 171549 additions and 0 deletions
174
g3d/camera.lua
Normal file
174
g3d/camera.lua
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
-- written by groverbuger for g3d
|
||||
-- september 2021
|
||||
-- MIT license
|
||||
|
||||
local newMatrix = require(g3d.path .. "/matrices")
|
||||
local g3d = g3d -- save a reference to g3d in case the user makes it non-global
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
-- define the camera singleton
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
local camera = {
|
||||
fov = math.pi/2,
|
||||
nearClip = 0.01,
|
||||
farClip = 1000,
|
||||
aspectRatio = love.graphics.getWidth()/love.graphics.getHeight(),
|
||||
position = {0,0,0},
|
||||
target = {1,0,0},
|
||||
up = {0,0,1},
|
||||
|
||||
viewMatrix = newMatrix(),
|
||||
projectionMatrix = newMatrix(),
|
||||
}
|
||||
|
||||
-- private variables used only for the first person camera functions
|
||||
local fpsController = {
|
||||
direction = 0,
|
||||
pitch = 0,
|
||||
}
|
||||
|
||||
-- read-only variables, can't be set by the end user
|
||||
function camera.getDirectionPitch()
|
||||
return fpsController.direction, fpsController.pitch
|
||||
end
|
||||
|
||||
-- convenient function to return the camera's normalized look vector
|
||||
function camera.getLookVector()
|
||||
local vx = camera.target[1] - camera.position[1]
|
||||
local vy = camera.target[2] - camera.position[2]
|
||||
local vz = camera.target[3] - camera.position[3]
|
||||
local length = math.sqrt(vx^2 + vy^2 + vz^2)
|
||||
|
||||
-- make sure not to divide by 0
|
||||
if length > 0 then
|
||||
return vx/length, vy/length, vz/length
|
||||
end
|
||||
return vx,vy,vz
|
||||
end
|
||||
|
||||
-- give the camera a point to look from and a point to look towards
|
||||
function camera.lookAt(x,y,z, xAt,yAt,zAt)
|
||||
camera.position[1] = x
|
||||
camera.position[2] = y
|
||||
camera.position[3] = z
|
||||
camera.target[1] = xAt
|
||||
camera.target[2] = yAt
|
||||
camera.target[3] = zAt
|
||||
|
||||
-- update the fpsController's direction and pitch based on lookAt
|
||||
local dx,dy,dz = camera.getLookVector()
|
||||
fpsController.direction = math.pi/2 - math.atan2(dz, dx)
|
||||
fpsController.pitch = math.atan2(dy, math.sqrt(dx^2 + dz^2))
|
||||
|
||||
-- update the camera in the shader
|
||||
camera.updateViewMatrix()
|
||||
end
|
||||
|
||||
-- move and rotate the camera, given a point and a direction and a pitch (vertical direction)
|
||||
function camera.lookInDirection(x,y,z, directionTowards,pitchTowards)
|
||||
camera.position[1] = x or camera.position[1]
|
||||
camera.position[2] = y or camera.position[2]
|
||||
camera.position[3] = z or camera.position[3]
|
||||
|
||||
fpsController.direction = directionTowards or fpsController.direction
|
||||
fpsController.pitch = pitchTowards or fpsController.pitch
|
||||
|
||||
-- turn the cos of the pitch into a sign value, either 1, -1, or 0
|
||||
local sign = math.cos(fpsController.pitch)
|
||||
sign = (sign > 0 and 1) or (sign < 0 and -1) or 0
|
||||
|
||||
-- don't let cosPitch ever hit 0, because weird camera glitches will happen
|
||||
local cosPitch = sign*math.max(math.abs(math.cos(fpsController.pitch)), 0.00001)
|
||||
|
||||
-- convert the direction and pitch into a target point
|
||||
camera.target[1] = camera.position[1]+math.sin(fpsController.direction)*cosPitch
|
||||
camera.target[2] = camera.position[2]-math.sin(fpsController.pitch)
|
||||
camera.target[3] = camera.position[3]+math.cos(fpsController.direction)*cosPitch
|
||||
|
||||
-- update the camera in the shader
|
||||
camera.updateViewMatrix()
|
||||
end
|
||||
|
||||
-- recreate the camera's view matrix from its current values
|
||||
-- and send the matrix to the shader specified, or the default shader
|
||||
function camera.updateViewMatrix(shaderGiven)
|
||||
local shader = shaderGiven or g3d.shader
|
||||
camera.viewMatrix:setViewMatrix(camera.position, camera.target, camera.up)
|
||||
shader:send("viewMatrix", camera.viewMatrix)
|
||||
end
|
||||
|
||||
-- recreate the camera's projection matrix from its current values
|
||||
-- and send the matrix to the shader specified, or the default shader
|
||||
function camera.updateProjectionMatrix(shaderGiven)
|
||||
local shader = shaderGiven or g3d.shader
|
||||
camera.projectionMatrix:setProjectionMatrix(camera.fov, camera.nearClip, camera.farClip, camera.aspectRatio)
|
||||
shader:send("projectionMatrix", camera.projectionMatrix)
|
||||
end
|
||||
|
||||
-- recreate the camera's orthographic projection matrix from its current values
|
||||
-- and send the matrix to the shader specified, or the default shader
|
||||
function camera.updateOrthographicMatrix(shaderGiven, size)
|
||||
local shader = shaderGiven or g3d.shader
|
||||
camera.projectionMatrix:setOrthographicMatrix(camera.fov, size or 5, camera.nearClip, camera.farClip, camera.aspectRatio)
|
||||
shader:send("projectionMatrix", camera.projectionMatrix)
|
||||
end
|
||||
|
||||
-- sends the camera's view and projection matrices to the given shader
|
||||
function camera.sendMatricesToShader(shader)
|
||||
shader:send("viewMatrix", camera.viewMatrix)
|
||||
shader:send("projectionMatrix", camera.projectionMatrix)
|
||||
end
|
||||
|
||||
-- simple first person camera movement with WASD
|
||||
-- put this local function in your love.update to use, passing in dt
|
||||
function camera.firstPersonMovement(dt)
|
||||
-- collect inputs
|
||||
local moveX, moveY = 0, 0
|
||||
local cameraMoved = false
|
||||
local speed = 9
|
||||
if love.keyboard.isDown "w" then moveX = moveX + 1 end
|
||||
if love.keyboard.isDown "a" then moveY = moveY + 1 end
|
||||
if love.keyboard.isDown "s" then moveX = moveX - 1 end
|
||||
if love.keyboard.isDown "d" then moveY = moveY - 1 end
|
||||
if love.keyboard.isDown "space" then
|
||||
camera.position[2] = camera.position[2] + speed*dt
|
||||
cameraMoved = true
|
||||
end
|
||||
if love.keyboard.isDown "lshift" then
|
||||
camera.position[2] = camera.position[2] - speed*dt
|
||||
cameraMoved = true
|
||||
end
|
||||
|
||||
-- do some trigonometry on the inputs to make movement relative to camera's direction
|
||||
-- also to make the player not move faster in diagonal directions
|
||||
if moveX ~= 0 or moveY ~= 0 then
|
||||
local angle = math.atan2(moveY, moveX)
|
||||
local sign = math.cos(fpsController.pitch)
|
||||
sign = (sign > 0 and 1) or (sign < 0 and -1) or 0
|
||||
local cosPitch = sign*math.max(math.abs(math.cos(fpsController.pitch)), 0.00001)
|
||||
camera.position[1] = camera.position[1] + math.sin(fpsController.direction + angle) * speed * dt*cosPitch
|
||||
camera.position[3] = camera.position[3] + math.cos(fpsController.direction + angle) * speed * dt*cosPitch
|
||||
cameraMoved = true
|
||||
end
|
||||
|
||||
-- update the camera's in the shader
|
||||
-- only if the camera moved, for a slight performance benefit
|
||||
if cameraMoved then
|
||||
camera.lookInDirection()
|
||||
end
|
||||
end
|
||||
|
||||
-- use this in your love.mousemoved function, passing in the movements
|
||||
function camera.firstPersonLook(dx,dy)
|
||||
-- capture the mouse
|
||||
love.mouse.setRelativeMode(true)
|
||||
|
||||
local sensitivity = 1/300
|
||||
fpsController.direction = fpsController.direction - dx*sensitivity
|
||||
fpsController.pitch = math.max(math.min(fpsController.pitch - dy*sensitivity, math.pi*0.5), math.pi*-0.5)
|
||||
|
||||
camera.lookInDirection(camera.position[1],camera.position[2],camera.position[3], fpsController.direction,fpsController.pitch)
|
||||
end
|
||||
|
||||
return camera
|
||||
486
g3d/collisions.lua
Normal file
486
g3d/collisions.lua
Normal file
|
|
@ -0,0 +1,486 @@
|
|||
-- written by groverbuger for g3d
|
||||
-- september 2021
|
||||
-- MIT license
|
||||
|
||||
local vectors = require(g3d.path .. "/vectors")
|
||||
local fastSubtract = vectors.subtract
|
||||
local vectorAdd = vectors.add
|
||||
local vectorCrossProduct = vectors.crossProduct
|
||||
local vectorDotProduct = vectors.dotProduct
|
||||
local vectorNormalize = vectors.normalize
|
||||
local vectorMagnitude = vectors.magnitude
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
-- collision detection functions
|
||||
----------------------------------------------------------------------------------------------------
|
||||
--
|
||||
-- none of these functions are required for developing 3D games
|
||||
-- however these collision functions are very frequently used in 3D games
|
||||
--
|
||||
-- be warned! a lot of this code is butt-ugly
|
||||
-- using a table per vector would create a bazillion tables and lots of used memory
|
||||
-- so instead all vectors are all represented using three number variables each
|
||||
-- this approach ends up making the code look terrible, but collision functions need to be efficient
|
||||
|
||||
local collisions = {}
|
||||
|
||||
-- finds the closest point to the source point on the given line segment
|
||||
local function closestPointOnLineSegment(
|
||||
a_x,a_y,a_z, -- point one of line segment
|
||||
b_x,b_y,b_z, -- point two of line segment
|
||||
x,y,z -- source point
|
||||
)
|
||||
local ab_x, ab_y, ab_z = b_x - a_x, b_y - a_y, b_z - a_z
|
||||
local t = vectorDotProduct(x - a_x, y - a_y, z - a_z, ab_x, ab_y, ab_z) / (ab_x^2 + ab_y^2 + ab_z^2)
|
||||
t = math.min(1, math.max(0, t))
|
||||
return a_x + t*ab_x, a_y + t*ab_y, a_z + t*ab_z
|
||||
end
|
||||
|
||||
-- model - ray intersection
|
||||
-- based off of triangle - ray collision from excessive's CPML library
|
||||
-- does a triangle - ray collision for every face in the model to find the shortest collision
|
||||
--
|
||||
-- sources:
|
||||
-- https://github.com/excessive/cpml/blob/master/modules/intersect.lua
|
||||
-- http://www.lighthouse3d.com/tutorials/maths/ray-triangle-intersection/
|
||||
local tiny = 2.2204460492503131e-16 -- the smallest possible value for a double, "double epsilon"
|
||||
local function triangleRay(
|
||||
tri_0_x, tri_0_y, tri_0_z,
|
||||
tri_1_x, tri_1_y, tri_1_z,
|
||||
tri_2_x, tri_2_y, tri_2_z,
|
||||
n_x, n_y, n_z,
|
||||
src_x, src_y, src_z,
|
||||
dir_x, dir_y, dir_z
|
||||
)
|
||||
|
||||
-- cache these variables for efficiency
|
||||
local e11,e12,e13 = fastSubtract(tri_1_x,tri_1_y,tri_1_z, tri_0_x,tri_0_y,tri_0_z)
|
||||
local e21,e22,e23 = fastSubtract(tri_2_x,tri_2_y,tri_2_z, tri_0_x,tri_0_y,tri_0_z)
|
||||
local h1,h2,h3 = vectorCrossProduct(dir_x,dir_y,dir_z, e21,e22,e23)
|
||||
local a = vectorDotProduct(h1,h2,h3, e11,e12,e13)
|
||||
|
||||
-- if a is too close to 0, ray does not intersect triangle
|
||||
if math.abs(a) <= tiny then
|
||||
return
|
||||
end
|
||||
|
||||
local s1,s2,s3 = fastSubtract(src_x,src_y,src_z, tri_0_x,tri_0_y,tri_0_z)
|
||||
local u = vectorDotProduct(s1,s2,s3, h1,h2,h3) / a
|
||||
|
||||
-- ray does not intersect triangle
|
||||
if u < 0 or u > 1 then
|
||||
return
|
||||
end
|
||||
|
||||
local q1,q2,q3 = vectorCrossProduct(s1,s2,s3, e11,e12,e13)
|
||||
local v = vectorDotProduct(dir_x,dir_y,dir_z, q1,q2,q3) / a
|
||||
|
||||
-- ray does not intersect triangle
|
||||
if v < 0 or u + v > 1 then
|
||||
return
|
||||
end
|
||||
|
||||
-- at this stage we can compute t to find out where
|
||||
-- the intersection point is on the line
|
||||
local thisLength = vectorDotProduct(q1,q2,q3, e21,e22,e23) / a
|
||||
|
||||
-- if hit this triangle and it's closer than any other hit triangle
|
||||
if thisLength >= tiny and (not finalLength or thisLength < finalLength) then
|
||||
--local norm_x, norm_y, norm_z = vectorCrossProduct(e11,e12,e13, e21,e22,e23)
|
||||
|
||||
return thisLength, src_x + dir_x*thisLength, src_y + dir_y*thisLength, src_z + dir_z*thisLength, n_x,n_y,n_z
|
||||
end
|
||||
end
|
||||
|
||||
-- detects a collision between a triangle and a sphere
|
||||
--
|
||||
-- sources:
|
||||
-- https://wickedengine.net/2020/04/26/capsule-collision-detection/
|
||||
local function triangleSphere(
|
||||
tri_0_x, tri_0_y, tri_0_z,
|
||||
tri_1_x, tri_1_y, tri_1_z,
|
||||
tri_2_x, tri_2_y, tri_2_z,
|
||||
tri_n_x, tri_n_y, tri_n_z,
|
||||
src_x, src_y, src_z, radius
|
||||
)
|
||||
|
||||
-- recalculate surface normal of this triangle
|
||||
local side1_x, side1_y, side1_z = tri_1_x - tri_0_x, tri_1_y - tri_0_y, tri_1_z - tri_0_z
|
||||
local side2_x, side2_y, side2_z = tri_2_x - tri_0_x, tri_2_y - tri_0_y, tri_2_z - tri_0_z
|
||||
local n_x, n_y, n_z = vectorNormalize(vectorCrossProduct(side1_x, side1_y, side1_z, side2_x, side2_y, side2_z))
|
||||
|
||||
-- distance from src to a vertex on the triangle
|
||||
local dist = vectorDotProduct(src_x - tri_0_x, src_y - tri_0_y, src_z - tri_0_z, n_x, n_y, n_z)
|
||||
|
||||
-- collision not possible, just return
|
||||
if dist < -radius or dist > radius then
|
||||
return
|
||||
end
|
||||
|
||||
-- itx stands for intersection
|
||||
local itx_x, itx_y, itx_z = src_x - n_x * dist, src_y - n_y * dist, src_z - n_z * dist
|
||||
|
||||
-- determine whether itx is inside the triangle
|
||||
-- project it onto the triangle and return if this is the case
|
||||
local c0_x, c0_y, c0_z = vectorCrossProduct(itx_x - tri_0_x, itx_y - tri_0_y, itx_z - tri_0_z, tri_1_x - tri_0_x, tri_1_y - tri_0_y, tri_1_z - tri_0_z)
|
||||
local c1_x, c1_y, c1_z = vectorCrossProduct(itx_x - tri_1_x, itx_y - tri_1_y, itx_z - tri_1_z, tri_2_x - tri_1_x, tri_2_y - tri_1_y, tri_2_z - tri_1_z)
|
||||
local c2_x, c2_y, c2_z = vectorCrossProduct(itx_x - tri_2_x, itx_y - tri_2_y, itx_z - tri_2_z, tri_0_x - tri_2_x, tri_0_y - tri_2_y, tri_0_z - tri_2_z)
|
||||
if vectorDotProduct(c0_x, c0_y, c0_z, n_x, n_y, n_z) <= 0
|
||||
and vectorDotProduct(c1_x, c1_y, c1_z, n_x, n_y, n_z) <= 0
|
||||
and vectorDotProduct(c2_x, c2_y, c2_z, n_x, n_y, n_z) <= 0 then
|
||||
n_x, n_y, n_z = src_x - itx_x, src_y - itx_y, src_z - itx_z
|
||||
|
||||
-- the sphere is inside the triangle, so the normal is zero
|
||||
-- instead, just return the triangle's normal
|
||||
if n_x == 0 and n_y == 0 and n_z == 0 then
|
||||
return vectorMagnitude(n_x, n_y, n_z), itx_x, itx_y, itx_z, tri_n_x, tri_n_y, tri_n_z
|
||||
end
|
||||
|
||||
return vectorMagnitude(n_x, n_y, n_z), itx_x, itx_y, itx_z, n_x, n_y, n_z
|
||||
end
|
||||
|
||||
-- itx is outside triangle
|
||||
-- find points on all three line segments that are closest to itx
|
||||
-- if distance between itx and one of these three closest points is in range, there is an intersection
|
||||
local radiussq = radius * radius
|
||||
local smallestDist
|
||||
|
||||
local line1_x, line1_y, line1_z = closestPointOnLineSegment(tri_0_x, tri_0_y, tri_0_z, tri_1_x, tri_1_y, tri_1_z, src_x, src_y, src_z)
|
||||
local dist = (src_x - line1_x)^2 + (src_y - line1_y)^2 + (src_z - line1_z)^2
|
||||
if dist <= radiussq then
|
||||
smallestDist = dist
|
||||
itx_x, itx_y, itx_z = line1_x, line1_y, line1_z
|
||||
end
|
||||
|
||||
local line2_x, line2_y, line2_z = closestPointOnLineSegment(tri_1_x, tri_1_y, tri_1_z, tri_2_x, tri_2_y, tri_2_z, src_x, src_y, src_z)
|
||||
local dist = (src_x - line2_x)^2 + (src_y - line2_y)^2 + (src_z - line2_z)^2
|
||||
if (smallestDist and dist < smallestDist or not smallestDist) and dist <= radiussq then
|
||||
smallestDist = dist
|
||||
itx_x, itx_y, itx_z = line2_x, line2_y, line2_z
|
||||
end
|
||||
|
||||
local line3_x, line3_y, line3_z = closestPointOnLineSegment(tri_2_x, tri_2_y, tri_2_z, tri_0_x, tri_0_y, tri_0_z, src_x, src_y, src_z)
|
||||
local dist = (src_x - line3_x)^2 + (src_y - line3_y)^2 + (src_z - line3_z)^2
|
||||
if (smallestDist and dist < smallestDist or not smallestDist) and dist <= radiussq then
|
||||
smallestDist = dist
|
||||
itx_x, itx_y, itx_z = line3_x, line3_y, line3_z
|
||||
end
|
||||
|
||||
if smallestDist then
|
||||
n_x, n_y, n_z = src_x - itx_x, src_y - itx_y, src_z - itx_z
|
||||
|
||||
-- the sphere is inside the triangle, so the normal is zero
|
||||
-- instead, just return the triangle's normal
|
||||
if n_x == 0 and n_y == 0 and n_z == 0 then
|
||||
return vectorMagnitude(n_x, n_y, n_z), itx_x, itx_y, itx_z, tri_n_x, tri_n_y, tri_n_z
|
||||
end
|
||||
|
||||
return vectorMagnitude(n_x, n_y, n_z), itx_x, itx_y, itx_z, n_x, n_y, n_z
|
||||
end
|
||||
end
|
||||
|
||||
-- finds the closest point on the triangle from the source point given
|
||||
--
|
||||
-- sources:
|
||||
-- https://wickedengine.net/2020/04/26/capsule-collision-detection/
|
||||
local function trianglePoint(
|
||||
tri_0_x, tri_0_y, tri_0_z,
|
||||
tri_1_x, tri_1_y, tri_1_z,
|
||||
tri_2_x, tri_2_y, tri_2_z,
|
||||
tri_n_x, tri_n_y, tri_n_z,
|
||||
src_x, src_y, src_z
|
||||
)
|
||||
|
||||
-- recalculate surface normal of this triangle
|
||||
local side1_x, side1_y, side1_z = tri_1_x - tri_0_x, tri_1_y - tri_0_y, tri_1_z - tri_0_z
|
||||
local side2_x, side2_y, side2_z = tri_2_x - tri_0_x, tri_2_y - tri_0_y, tri_2_z - tri_0_z
|
||||
local n_x, n_y, n_z = vectorNormalize(vectorCrossProduct(side1_x, side1_y, side1_z, side2_x, side2_y, side2_z))
|
||||
|
||||
-- distance from src to a vertex on the triangle
|
||||
local dist = vectorDotProduct(src_x - tri_0_x, src_y - tri_0_y, src_z - tri_0_z, n_x, n_y, n_z)
|
||||
|
||||
-- itx stands for intersection
|
||||
local itx_x, itx_y, itx_z = src_x - n_x * dist, src_y - n_y * dist, src_z - n_z * dist
|
||||
|
||||
-- determine whether itx is inside the triangle
|
||||
-- project it onto the triangle and return if this is the case
|
||||
local c0_x, c0_y, c0_z = vectorCrossProduct(itx_x - tri_0_x, itx_y - tri_0_y, itx_z - tri_0_z, tri_1_x - tri_0_x, tri_1_y - tri_0_y, tri_1_z - tri_0_z)
|
||||
local c1_x, c1_y, c1_z = vectorCrossProduct(itx_x - tri_1_x, itx_y - tri_1_y, itx_z - tri_1_z, tri_2_x - tri_1_x, tri_2_y - tri_1_y, tri_2_z - tri_1_z)
|
||||
local c2_x, c2_y, c2_z = vectorCrossProduct(itx_x - tri_2_x, itx_y - tri_2_y, itx_z - tri_2_z, tri_0_x - tri_2_x, tri_0_y - tri_2_y, tri_0_z - tri_2_z)
|
||||
if vectorDotProduct(c0_x, c0_y, c0_z, n_x, n_y, n_z) <= 0
|
||||
and vectorDotProduct(c1_x, c1_y, c1_z, n_x, n_y, n_z) <= 0
|
||||
and vectorDotProduct(c2_x, c2_y, c2_z, n_x, n_y, n_z) <= 0 then
|
||||
n_x, n_y, n_z = src_x - itx_x, src_y - itx_y, src_z - itx_z
|
||||
|
||||
-- the sphere is inside the triangle, so the normal is zero
|
||||
-- instead, just return the triangle's normal
|
||||
if n_x == 0 and n_y == 0 and n_z == 0 then
|
||||
return vectorMagnitude(n_x, n_y, n_z), itx_x, itx_y, itx_z, tri_n_x, tri_n_y, tri_n_z
|
||||
end
|
||||
|
||||
return vectorMagnitude(n_x, n_y, n_z), itx_x, itx_y, itx_z, n_x, n_y, n_z
|
||||
end
|
||||
|
||||
-- itx is outside triangle
|
||||
-- find points on all three line segments that are closest to itx
|
||||
-- if distance between itx and one of these three closest points is in range, there is an intersection
|
||||
local line1_x, line1_y, line1_z = closestPointOnLineSegment(tri_0_x, tri_0_y, tri_0_z, tri_1_x, tri_1_y, tri_1_z, src_x, src_y, src_z)
|
||||
local dist = (src_x - line1_x)^2 + (src_y - line1_y)^2 + (src_z - line1_z)^2
|
||||
local smallestDist = dist
|
||||
itx_x, itx_y, itx_z = line1_x, line1_y, line1_z
|
||||
|
||||
local line2_x, line2_y, line2_z = closestPointOnLineSegment(tri_1_x, tri_1_y, tri_1_z, tri_2_x, tri_2_y, tri_2_z, src_x, src_y, src_z)
|
||||
local dist = (src_x - line2_x)^2 + (src_y - line2_y)^2 + (src_z - line2_z)^2
|
||||
if smallestDist and dist < smallestDist then
|
||||
smallestDist = dist
|
||||
itx_x, itx_y, itx_z = line2_x, line2_y, line2_z
|
||||
end
|
||||
|
||||
local line3_x, line3_y, line3_z = closestPointOnLineSegment(tri_2_x, tri_2_y, tri_2_z, tri_0_x, tri_0_y, tri_0_z, src_x, src_y, src_z)
|
||||
local dist = (src_x - line3_x)^2 + (src_y - line3_y)^2 + (src_z - line3_z)^2
|
||||
if smallestDist and dist < smallestDist then
|
||||
smallestDist = dist
|
||||
itx_x, itx_y, itx_z = line3_x, line3_y, line3_z
|
||||
end
|
||||
|
||||
if smallestDist then
|
||||
n_x, n_y, n_z = src_x - itx_x, src_y - itx_y, src_z - itx_z
|
||||
|
||||
-- the sphere is inside the triangle, so the normal is zero
|
||||
-- instead, just return the triangle's normal
|
||||
if n_x == 0 and n_y == 0 and n_z == 0 then
|
||||
return vectorMagnitude(n_x, n_y, n_z), itx_x, itx_y, itx_z, tri_n_x, tri_n_y, tri_n_z
|
||||
end
|
||||
|
||||
return vectorMagnitude(n_x, n_y, n_z), itx_x, itx_y, itx_z, n_x, n_y, n_z
|
||||
end
|
||||
end
|
||||
|
||||
-- finds the collision point between a triangle and a capsule
|
||||
-- capsules are defined with two points and a radius
|
||||
--
|
||||
-- sources:
|
||||
-- https://wickedengine.net/2020/04/26/capsule-collision-detection/
|
||||
local function triangleCapsule(
|
||||
tri_0_x, tri_0_y, tri_0_z,
|
||||
tri_1_x, tri_1_y, tri_1_z,
|
||||
tri_2_x, tri_2_y, tri_2_z,
|
||||
n_x, n_y, n_z,
|
||||
tip_x, tip_y, tip_z,
|
||||
base_x, base_y, base_z,
|
||||
a_x, a_y, a_z,
|
||||
b_x, b_y, b_z,
|
||||
capn_x, capn_y, capn_z,
|
||||
radius
|
||||
)
|
||||
|
||||
-- find the normal of this triangle
|
||||
-- tbd if necessary, this sometimes fixes weird edgecases
|
||||
local side1_x, side1_y, side1_z = tri_1_x - tri_0_x, tri_1_y - tri_0_y, tri_1_z - tri_0_z
|
||||
local side2_x, side2_y, side2_z = tri_2_x - tri_0_x, tri_2_y - tri_0_y, tri_2_z - tri_0_z
|
||||
local n_x, n_y, n_z = vectorNormalize(vectorCrossProduct(side1_x, side1_y, side1_z, side2_x, side2_y, side2_z))
|
||||
|
||||
local dotOfNormals = math.abs(vectorDotProduct(n_x, n_y, n_z, capn_x, capn_y, capn_z))
|
||||
|
||||
-- default reference point to an arbitrary point on the triangle
|
||||
-- for when dotOfNormals is 0, because then the capsule is parallel to the triangle
|
||||
local ref_x, ref_y, ref_z = tri_0_x, tri_0_y, tri_0_z
|
||||
|
||||
if dotOfNormals > 0 then
|
||||
-- capsule is not parallel to the triangle's plane
|
||||
-- find where the capsule's normal vector intersects the triangle's plane
|
||||
local t = vectorDotProduct(n_x, n_y, n_z, (tri_0_x - base_x) / dotOfNormals, (tri_0_y - base_y) / dotOfNormals, (tri_0_z - base_z) / dotOfNormals)
|
||||
local plane_itx_x, plane_itx_y, plane_itx_z = base_x + capn_x*t, base_y + capn_y*t, base_z + capn_z*t
|
||||
local _
|
||||
|
||||
-- then clamp that plane intersect point onto the triangle itself
|
||||
-- this is the new reference point
|
||||
_, ref_x, ref_y, ref_z = trianglePoint(
|
||||
tri_0_x, tri_0_y, tri_0_z,
|
||||
tri_1_x, tri_1_y, tri_1_z,
|
||||
tri_2_x, tri_2_y, tri_2_z,
|
||||
n_x, n_y, n_z,
|
||||
plane_itx_x, plane_itx_y, plane_itx_z
|
||||
)
|
||||
end
|
||||
|
||||
-- find the closest point on the capsule line to the reference point
|
||||
local c_x, c_y, c_z = closestPointOnLineSegment(a_x, a_y, a_z, b_x, b_y, b_z, ref_x, ref_y, ref_z)
|
||||
|
||||
-- do a sphere cast from that closest point to the triangle and return the result
|
||||
return triangleSphere(
|
||||
tri_0_x, tri_0_y, tri_0_z,
|
||||
tri_1_x, tri_1_y, tri_1_z,
|
||||
tri_2_x, tri_2_y, tri_2_z,
|
||||
n_x, n_y, n_z,
|
||||
c_x, c_y, c_z, radius
|
||||
)
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
-- function appliers
|
||||
----------------------------------------------------------------------------------------------------
|
||||
-- these functions apply the collision test functions on the given list of triangles
|
||||
|
||||
-- runs a given intersection function on all of the triangles made up of a given vert table
|
||||
local function findClosest(self, verts, func, ...)
|
||||
-- declare the variables that will be returned by the function
|
||||
local finalLength, where_x, where_y, where_z, norm_x, norm_y, norm_z
|
||||
|
||||
-- cache references to this model's properties for efficiency
|
||||
local translation_x, translation_y, translation_z, scale_x, scale_y, scale_z = 0, 0, 0, 1, 1, 1
|
||||
if self then
|
||||
if self.translation then
|
||||
translation_x = self.translation[1]
|
||||
translation_y = self.translation[2]
|
||||
translation_z = self.translation[3]
|
||||
end
|
||||
if self.scale then
|
||||
scale_x = self.scale[1]
|
||||
scale_y = self.scale[2]
|
||||
scale_z = self.scale[3]
|
||||
end
|
||||
end
|
||||
|
||||
for v=1, #verts, 3 do
|
||||
-- apply the function given with the arguments given
|
||||
-- also supply the points of the current triangle
|
||||
local n_x, n_y, n_z = vectorNormalize(
|
||||
verts[v][6]*scale_x,
|
||||
verts[v][7]*scale_x,
|
||||
verts[v][8]*scale_x
|
||||
)
|
||||
|
||||
local length, wx,wy,wz, nx,ny,nz = func(
|
||||
verts[v][1]*scale_x + translation_x,
|
||||
verts[v][2]*scale_y + translation_y,
|
||||
verts[v][3]*scale_z + translation_z,
|
||||
verts[v+1][1]*scale_x + translation_x,
|
||||
verts[v+1][2]*scale_y + translation_y,
|
||||
verts[v+1][3]*scale_z + translation_z,
|
||||
verts[v+2][1]*scale_x + translation_x,
|
||||
verts[v+2][2]*scale_y + translation_y,
|
||||
verts[v+2][3]*scale_z + translation_z,
|
||||
n_x,
|
||||
n_y,
|
||||
n_z,
|
||||
...
|
||||
)
|
||||
|
||||
-- if something was hit
|
||||
-- and either the finalLength is not yet defined or the new length is closer
|
||||
-- then update the collision information
|
||||
if length and (not finalLength or length < finalLength) then
|
||||
finalLength = length
|
||||
where_x = wx
|
||||
where_y = wy
|
||||
where_z = wz
|
||||
norm_x = nx
|
||||
norm_y = ny
|
||||
norm_z = nz
|
||||
end
|
||||
end
|
||||
|
||||
-- normalize the normal vector before it is returned
|
||||
if finalLength then
|
||||
norm_x, norm_y, norm_z = vectorNormalize(norm_x, norm_y, norm_z)
|
||||
end
|
||||
|
||||
-- return all the information in a standardized way
|
||||
return finalLength, where_x, where_y, where_z, norm_x, norm_y, norm_z
|
||||
end
|
||||
|
||||
-- runs a given intersection function on all of the triangles made up of a given vert table
|
||||
local function findAny(self, verts, func, ...)
|
||||
-- cache references to this model's properties for efficiency
|
||||
local translation_x, translation_y, translation_z, scale_x, scale_y, scale_z = 0, 0, 0, 1, 1, 1
|
||||
if self then
|
||||
if self.translation then
|
||||
translation_x = self.translation[1]
|
||||
translation_y = self.translation[2]
|
||||
translation_z = self.translation[3]
|
||||
end
|
||||
if self.scale then
|
||||
scale_x = self.scale[1]
|
||||
scale_y = self.scale[2]
|
||||
scale_z = self.scale[3]
|
||||
end
|
||||
end
|
||||
|
||||
for v=1, #verts, 3 do
|
||||
-- apply the function given with the arguments given
|
||||
-- also supply the points of the current triangle
|
||||
local n_x, n_y, n_z = vectorNormalize(
|
||||
verts[v][6]*scale_x,
|
||||
verts[v][7]*scale_x,
|
||||
verts[v][8]*scale_x
|
||||
)
|
||||
|
||||
local length = func(
|
||||
verts[v][1]*scale_x + translation_x,
|
||||
verts[v][2]*scale_y + translation_y,
|
||||
verts[v][3]*scale_z + translation_z,
|
||||
verts[v+1][1]*scale_x + translation_x,
|
||||
verts[v+1][2]*scale_y + translation_y,
|
||||
verts[v+1][3]*scale_z + translation_z,
|
||||
verts[v+2][1]*scale_x + translation_x,
|
||||
verts[v+2][2]*scale_y + translation_y,
|
||||
verts[v+2][3]*scale_z + translation_z,
|
||||
n_x,
|
||||
n_y,
|
||||
n_z,
|
||||
...
|
||||
)
|
||||
|
||||
-- if something was hit
|
||||
-- and either the finalLength is not yet defined or the new length is closer
|
||||
-- then update the collision information
|
||||
if length then return true end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
-- collision functions that apply on lists of vertices
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
function collisions.rayIntersection(verts, transform, src_x, src_y, src_z, dir_x, dir_y, dir_z)
|
||||
return findClosest(transform, verts, triangleRay, src_x, src_y, src_z, dir_x, dir_y, dir_z)
|
||||
end
|
||||
|
||||
function collisions.isPointInside(verts, transform, x, y, z)
|
||||
return findAny(transform, verts, triangleRay, x, y, z, 0, 0, 1)
|
||||
end
|
||||
|
||||
function collisions.sphereIntersection(verts, transform, src_x, src_y, src_z, radius)
|
||||
return findClosest(transform, verts, triangleSphere, src_x, src_y, src_z, radius)
|
||||
end
|
||||
|
||||
function collisions.closestPoint(verts, transform, src_x, src_y, src_z)
|
||||
return findClosest(transform, verts, trianglePoint, src_x, src_y, src_z)
|
||||
end
|
||||
|
||||
function collisions.capsuleIntersection(verts, transform, tip_x, tip_y, tip_z, base_x, base_y, base_z, radius)
|
||||
-- the normal vector coming out the tip of the capsule
|
||||
local norm_x, norm_y, norm_z = vectorNormalize(tip_x - base_x, tip_y - base_y, tip_z - base_z)
|
||||
|
||||
-- the base and tip, inset by the radius
|
||||
-- these two coordinates are the actual extent of the capsule sphere line
|
||||
local a_x, a_y, a_z = base_x + norm_x*radius, base_y + norm_y*radius, base_z + norm_z*radius
|
||||
local b_x, b_y, b_z = tip_x - norm_x*radius, tip_y - norm_y*radius, tip_z - norm_z*radius
|
||||
|
||||
return findClosest(
|
||||
transform,
|
||||
verts,
|
||||
triangleCapsule,
|
||||
tip_x, tip_y, tip_z,
|
||||
base_x, base_y, base_z,
|
||||
a_x, a_y, a_z,
|
||||
b_x, b_y, b_z,
|
||||
norm_x, norm_y, norm_z,
|
||||
radius
|
||||
)
|
||||
end
|
||||
|
||||
return collisions
|
||||
40
g3d/g3d.vert
Normal file
40
g3d/g3d.vert
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// written by groverbuger for g3d
|
||||
// september 2021
|
||||
// MIT license
|
||||
|
||||
// this vertex shader is what projects 3d vertices in models onto your 2d screen
|
||||
|
||||
uniform mat4 projectionMatrix; // handled by the camera
|
||||
uniform mat4 viewMatrix; // handled by the camera
|
||||
uniform mat4 modelMatrix; // models send their own model matrices when drawn
|
||||
uniform bool isCanvasEnabled; // detect when this model is being rendered to a canvas
|
||||
|
||||
// the vertex normal attribute must be defined, as it is custom unlike the other attributes
|
||||
attribute vec3 VertexNormal;
|
||||
|
||||
// define some varying vectors that are useful for writing custom fragment shaders
|
||||
varying vec4 worldPosition;
|
||||
varying vec4 viewPosition;
|
||||
varying vec4 screenPosition;
|
||||
varying vec3 vertexNormal;
|
||||
varying vec4 vertexColor;
|
||||
|
||||
vec4 position(mat4 transformProjection, vec4 vertexPosition) {
|
||||
// calculate the positions of the transformed coordinates on the screen
|
||||
// save each step of the process, as these are often useful when writing custom fragment shaders
|
||||
worldPosition = modelMatrix * vertexPosition;
|
||||
viewPosition = viewMatrix * worldPosition;
|
||||
screenPosition = projectionMatrix * viewPosition;
|
||||
|
||||
// save some data from this vertex for use in fragment shaders
|
||||
vertexNormal = VertexNormal;
|
||||
vertexColor = VertexColor;
|
||||
|
||||
// for some reason models are flipped vertically when rendering to a canvas
|
||||
// so we need to detect when this is being rendered to a canvas, and flip it back
|
||||
if (isCanvasEnabled) {
|
||||
screenPosition.y *= -1.0;
|
||||
}
|
||||
|
||||
return screenPosition;
|
||||
}
|
||||
62
g3d/init.lua
Normal file
62
g3d/init.lua
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
-- written by groverbuger for g3d
|
||||
-- september 2021
|
||||
-- MIT license
|
||||
|
||||
--[[
|
||||
__ __
|
||||
/'__`\ /\ \
|
||||
__ /\_\L\ \ \_\ \
|
||||
/'_ `\/_/_\_<_ /'_` \
|
||||
/\ \L\ \/\ \L\ \/\ \L\ \
|
||||
\ \____ \ \____/\ \___,_\
|
||||
\/___L\ \/___/ \/__,_ /
|
||||
/\____/
|
||||
\_/__/
|
||||
--]]
|
||||
|
||||
g3d = {
|
||||
_VERSION = "g3d 1.5",
|
||||
_DESCRIPTION = "Simple and easy 3D engine for LÖVE.",
|
||||
_URL = "https://github.com/groverburger/g3d",
|
||||
_LICENSE = [[
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 groverburger
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
]],
|
||||
path = ...,
|
||||
shaderpath = ... .. "/g3d.vert",
|
||||
}
|
||||
|
||||
-- the shader is what does the heavy lifting, displaying 3D meshes on your 2D monitor
|
||||
g3d.shader = love.graphics.newShader(g3d.shaderpath)
|
||||
g3d.newModel = require(g3d.path .. "/model")
|
||||
g3d.camera = require(g3d.path .. "/camera")
|
||||
g3d.collisions = require(g3d.path .. "/collisions")
|
||||
g3d.camera.updateProjectionMatrix()
|
||||
g3d.camera.updateViewMatrix()
|
||||
|
||||
-- so that far polygons don't overlap near polygons
|
||||
love.graphics.setDepthMode("lequal", true)
|
||||
|
||||
-- get rid of g3d from the global namespace and return it instead
|
||||
local g3d = g3d
|
||||
_G.g3d = nil
|
||||
return g3d
|
||||
130
g3d/matrices.lua
Normal file
130
g3d/matrices.lua
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
-- written by groverbuger for g3d
|
||||
-- september 2021
|
||||
-- MIT license
|
||||
|
||||
local vectors = require(g3d.path .. "/vectors")
|
||||
local vectorCrossProduct = vectors.crossProduct
|
||||
local vectorDotProduct = vectors.dotProduct
|
||||
local vectorNormalize = vectors.normalize
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
-- matrix class
|
||||
----------------------------------------------------------------------------------------------------
|
||||
-- matrices are 16 numbers in table, representing a 4x4 matrix like so:
|
||||
--
|
||||
-- | 1 2 3 4 |
|
||||
-- | |
|
||||
-- | 5 6 7 8 |
|
||||
-- | |
|
||||
-- | 9 10 11 12 |
|
||||
-- | |
|
||||
-- | 13 14 15 16 |
|
||||
|
||||
local matrix = {}
|
||||
matrix.__index = matrix
|
||||
|
||||
local function newMatrix()
|
||||
local self = setmetatable({}, matrix)
|
||||
|
||||
-- initialize a matrix as the identity matrix
|
||||
self[1], self[2], self[3], self[4] = 1, 0, 0, 0
|
||||
self[5], self[6], self[7], self[8] = 0, 1, 0, 0
|
||||
self[9], self[10], self[11], self[12] = 0, 0, 1, 0
|
||||
self[13], self[14], self[15], self[16] = 0, 0, 0, 1
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
-- automatically converts a matrix to a string
|
||||
-- for printing to console and debugging
|
||||
function matrix:__tostring()
|
||||
return ("%f\t%f\t%f\t%f\n%f\t%f\t%f\t%f\n%f\t%f\t%f\t%f\n%f\t%f\t%f\t%f"):format(unpack(self))
|
||||
end
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
-- transformation, projection, and rotation matrices
|
||||
----------------------------------------------------------------------------------------------------
|
||||
-- the three most important matrices for 3d graphics
|
||||
-- these three matrices are all you need to write a simple 3d shader
|
||||
|
||||
-- returns a transformation matrix
|
||||
-- translation, rotation, and scale are all 3d vectors
|
||||
function matrix:setTransformationMatrix(translation, rotation, scale)
|
||||
-- translations
|
||||
self[4] = translation[1]
|
||||
self[8] = translation[2]
|
||||
self[12] = translation[3]
|
||||
|
||||
-- rotations
|
||||
if #rotation == 3 then
|
||||
-- use 3D rotation vector as euler angles
|
||||
-- source: https://en.wikipedia.org/wiki/Rotation_matrix
|
||||
local ca, cb, cc = math.cos(rotation[3]), math.cos(rotation[2]), math.cos(rotation[1])
|
||||
local sa, sb, sc = math.sin(rotation[3]), math.sin(rotation[2]), math.sin(rotation[1])
|
||||
self[1], self[2], self[3] = ca*cb, ca*sb*sc - sa*cc, ca*sb*cc + sa*sc
|
||||
self[5], self[6], self[7] = sa*cb, sa*sb*sc + ca*cc, sa*sb*cc - ca*sc
|
||||
self[9], self[10], self[11] = -sb, cb*sc, cb*cc
|
||||
else
|
||||
-- use 4D rotation vector as a quaternion
|
||||
local qx, qy, qz, qw = rotation[1], rotation[2], rotation[3], rotation[4]
|
||||
self[1], self[2], self[3] = 1 - 2*qy^2 - 2*qz^2, 2*qx*qy - 2*qz*qw, 2*qx*qz + 2*qy*qw
|
||||
self[5], self[6], self[7] = 2*qx*qy + 2*qz*qw, 1 - 2*qx^2 - 2*qz^2, 2*qy*qz - 2*qx*qw
|
||||
self[9], self[10], self[11] = 2*qx*qz - 2*qy*qw, 2*qy*qz + 2*qx*qw, 1 - 2*qx^2 - 2*qy^2
|
||||
end
|
||||
|
||||
-- scale
|
||||
local sx, sy, sz = scale[1], scale[2], scale[3]
|
||||
self[1], self[2], self[3] = self[1] * sx, self[2] * sy, self[3] * sz
|
||||
self[5], self[6], self[7] = self[5] * sx, self[6] * sy, self[7] * sz
|
||||
self[9], self[10], self[11] = self[9] * sx, self[10] * sy, self[11] * sz
|
||||
|
||||
-- fourth row is not used, just set it to the fourth row of the identity matrix
|
||||
self[13], self[14], self[15], self[16] = 0, 0, 0, 1
|
||||
end
|
||||
|
||||
-- returns a perspective projection matrix
|
||||
-- (things farther away appear smaller)
|
||||
-- all arguments are scalars aka normal numbers
|
||||
-- aspectRatio is defined as window width divided by window height
|
||||
function matrix:setProjectionMatrix(fov, near, far, aspectRatio)
|
||||
local top = near * math.tan(fov/2)
|
||||
local bottom = -1*top
|
||||
local right = top * aspectRatio
|
||||
local left = -1*right
|
||||
|
||||
self[1], self[2], self[3], self[4] = 2*near/(right-left), 0, (right+left)/(right-left), 0
|
||||
self[5], self[6], self[7], self[8] = 0, 2*near/(top-bottom), (top+bottom)/(top-bottom), 0
|
||||
self[9], self[10], self[11], self[12] = 0, 0, -1*(far+near)/(far-near), -2*far*near/(far-near)
|
||||
self[13], self[14], self[15], self[16] = 0, 0, -1, 0
|
||||
end
|
||||
|
||||
-- returns an orthographic projection matrix
|
||||
-- (things farther away are the same size as things closer)
|
||||
-- all arguments are scalars aka normal numbers
|
||||
-- aspectRatio is defined as window width divided by window height
|
||||
function matrix:setOrthographicMatrix(fov, size, near, far, aspectRatio)
|
||||
local top = size * math.tan(fov/2)
|
||||
local bottom = -1*top
|
||||
local right = top * aspectRatio
|
||||
local left = -1*right
|
||||
|
||||
self[1], self[2], self[3], self[4] = 2/(right-left), 0, 0, -1*(right+left)/(right-left)
|
||||
self[5], self[6], self[7], self[8] = 0, 2/(top-bottom), 0, -1*(top+bottom)/(top-bottom)
|
||||
self[9], self[10], self[11], self[12] = 0, 0, -2/(far-near), -(far+near)/(far-near)
|
||||
self[13], self[14], self[15], self[16] = 0, 0, 0, 1
|
||||
end
|
||||
|
||||
-- returns a view matrix
|
||||
-- eye, target, and up are all 3d vectors
|
||||
function matrix:setViewMatrix(eye, target, up)
|
||||
local z1, z2, z3 = vectorNormalize(eye[1] - target[1], eye[2] - target[2], eye[3] - target[3])
|
||||
local x1, x2, x3 = vectorNormalize(vectorCrossProduct(up[1], up[2], up[3], z1, z2, z3))
|
||||
local y1, y2, y3 = vectorCrossProduct(z1, z2, z3, x1, x2, x3)
|
||||
|
||||
self[1], self[2], self[3], self[4] = x1, x2, x3, -1*vectorDotProduct(x1, x2, x3, eye[1], eye[2], eye[3])
|
||||
self[5], self[6], self[7], self[8] = y1, y2, y3, -1*vectorDotProduct(y1, y2, y3, eye[1], eye[2], eye[3])
|
||||
self[9], self[10], self[11], self[12] = z1, z2, z3, -1*vectorDotProduct(z1, z2, z3, eye[1], eye[2], eye[3])
|
||||
self[13], self[14], self[15], self[16] = 0, 0, 0, 1
|
||||
end
|
||||
|
||||
return newMatrix
|
||||
232
g3d/model.lua
Normal file
232
g3d/model.lua
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
-- written by groverbuger for g3d
|
||||
-- september 2021
|
||||
-- MIT license
|
||||
|
||||
local newMatrix = require(g3d.path .. "/matrices")
|
||||
local loadObjFile = require(g3d.path .. "/objloader")
|
||||
local collisions = require(g3d.path .. "/collisions")
|
||||
local vectors = require(g3d.path .. "/vectors")
|
||||
local vectorCrossProduct = vectors.crossProduct
|
||||
local vectorNormalize = vectors.normalize
|
||||
|
||||
local inspect = require "../inspect"
|
||||
----------------------------------------------------------------------------------------------------
|
||||
-- define a model class
|
||||
----------------------------------------------------------------------------------------------------
|
||||
|
||||
local model = {}
|
||||
model.__index = model
|
||||
|
||||
-- define some default properties that every model should inherit
|
||||
-- that being the standard vertexFormat and basic 3D shader
|
||||
model.vertexFormat = {
|
||||
{"VertexPosition", "float", 3},
|
||||
{"VertexTexCoord", "float", 2},
|
||||
{"VertexNormal", "float", 3},
|
||||
{"VertexColor", "byte", 4},
|
||||
}
|
||||
model.shader = g3d.shader
|
||||
|
||||
-- this returns a new instance of the model class
|
||||
-- a model must be given a .obj file or equivalent lua table, and a texture
|
||||
-- translation, rotation, and scale are all 3d vectors and are all optional
|
||||
local function newModel(verts, texture, translation, rotation, scale, flipU, flipV)
|
||||
local self = setmetatable({}, model)
|
||||
|
||||
-- if verts is a string, use it as a path to a .obj file
|
||||
-- otherwise verts is a table, use it as a model defintion
|
||||
if type(verts) == "string" then
|
||||
verts = loadObjFile(verts, flipU, flipV)
|
||||
end
|
||||
assert(verts and type(verts) == "table", "Invalid vertices given to newModel")
|
||||
|
||||
-- if texture is a string, use it as a path to an image file
|
||||
-- otherwise texture is already an image, so don't bother
|
||||
if type(texture) == "string" then
|
||||
texture = love.graphics.newImage(texture)
|
||||
end
|
||||
|
||||
-- initialize my variables
|
||||
self.verts = verts
|
||||
self.texture = texture
|
||||
self.mesh = love.graphics.newMesh(self.vertexFormat, self.verts, "triangles")
|
||||
self.mesh:setTexture(self.texture)
|
||||
self.matrix = newMatrix()
|
||||
if type(scale) == "number" then scale = {scale, scale, scale} end
|
||||
self:setTransform(translation or {0,0,0}, rotation or {0,0,0}, scale or {1,1,1})
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function model:merge(other_one)
|
||||
|
||||
for k,v in pairs(other_one.verts) do self.verts[#self.verts+1] = v end
|
||||
self.mesh = love.graphics.newMesh(self.vertexFormat, self.verts, "triangles")
|
||||
self.mesh:setTexture(self.texture)
|
||||
self.matrix = newMatrix()
|
||||
end
|
||||
|
||||
-- populate model's normals in model's mesh automatically
|
||||
-- if true is passed in, then the normals are all flipped
|
||||
function model:makeNormals(isFlipped)
|
||||
for i=1, #self.verts, 3 do
|
||||
if isFlipped then
|
||||
self.verts[i+1], self.verts[i+2] = self.verts[i+2], self.verts[i+1]
|
||||
end
|
||||
|
||||
local vp = self.verts[i]
|
||||
local v = self.verts[i+1]
|
||||
local vn = self.verts[i+2]
|
||||
|
||||
local n_1, n_2, n_3 = vectorNormalize(vectorCrossProduct(v[1]-vp[1], v[2]-vp[2], v[3]-vp[3], vn[1]-v[1], vn[2]-v[2], vn[3]-v[3]))
|
||||
vp[6], v[6], vn[6] = n_1, n_1, n_1
|
||||
vp[7], v[7], vn[7] = n_2, n_2, n_2
|
||||
vp[8], v[8], vn[8] = n_3, n_3, n_3
|
||||
end
|
||||
|
||||
self.mesh = love.graphics.newMesh(self.vertexFormat, self.verts, "triangles")
|
||||
self.mesh:setTexture(self.texture)
|
||||
end
|
||||
|
||||
-- move and rotate given two 3d vectors
|
||||
function model:setTransform(translation, rotation, scale)
|
||||
self.translation = translation or self.translation
|
||||
self.rotation = rotation or self.rotation
|
||||
self.scale = scale or self.scale
|
||||
self:updateMatrix()
|
||||
end
|
||||
|
||||
-- move given one 3d vector
|
||||
function model:setTranslation(tx,ty,tz)
|
||||
self.translation[1] = tx
|
||||
self.translation[2] = ty
|
||||
self.translation[3] = tz
|
||||
self:updateMatrix()
|
||||
end
|
||||
|
||||
-- rotate given one 3d vector
|
||||
-- using euler angles
|
||||
function model:setRotation(rx,ry,rz)
|
||||
self.rotation[1] = rx
|
||||
self.rotation[2] = ry
|
||||
self.rotation[3] = rz
|
||||
self.rotation[4] = nil
|
||||
self:updateMatrix()
|
||||
end
|
||||
|
||||
-- create a quaternion from an axis and an angle
|
||||
function model:setAxisAngleRotation(x,y,z,angle)
|
||||
x,y,z = vectorNormalize(x,y,z)
|
||||
angle = angle / 2
|
||||
|
||||
self.rotation[1] = x * math.sin(angle)
|
||||
self.rotation[2] = y * math.sin(angle)
|
||||
self.rotation[3] = z * math.sin(angle)
|
||||
self.rotation[4] = math.cos(angle)
|
||||
|
||||
self:updateMatrix()
|
||||
end
|
||||
|
||||
-- rotate given one quaternion
|
||||
function model:setQuaternionRotation(x,y,z,w)
|
||||
self.rotation[1] = x
|
||||
self.rotation[2] = y
|
||||
self.rotation[3] = z
|
||||
self.rotation[4] = w
|
||||
self:updateMatrix()
|
||||
end
|
||||
|
||||
-- resize model's matrix based on a given 3d vector
|
||||
function model:setScale(sx,sy,sz)
|
||||
self.scale[1] = sx
|
||||
self.scale[2] = sy or sx
|
||||
self.scale[3] = sz or sx
|
||||
self:updateMatrix()
|
||||
end
|
||||
|
||||
-- update the model's transformation matrix
|
||||
function model:updateMatrix()
|
||||
self.matrix:setTransformationMatrix(self.translation, self.rotation, self.scale)
|
||||
end
|
||||
|
||||
-- draw the model
|
||||
function model:draw(shader)
|
||||
local shader = shader or self.shader
|
||||
love.graphics.setShader(shader)
|
||||
shader:send("modelMatrix", self.matrix)
|
||||
if shader:hasUniform "isCanvasEnabled" then
|
||||
shader:send("isCanvasEnabled", love.graphics.getCanvas() ~= nil)
|
||||
end
|
||||
love.graphics.draw(self.mesh)
|
||||
love.graphics.setShader()
|
||||
end
|
||||
|
||||
-- the fallback function if ffi was not loaded
|
||||
function model:compress()
|
||||
print("[g3d warning] Compression requires FFI!\n" .. debug.traceback())
|
||||
end
|
||||
|
||||
-- makes models use less memory when loaded in ram
|
||||
-- by storing the vertex data in an array of vertix structs instead of lua tables
|
||||
-- requires ffi
|
||||
-- note: throws away the model's verts table
|
||||
local success, ffi = pcall(require, "ffi")
|
||||
if success then
|
||||
ffi.cdef([[
|
||||
struct vertex {
|
||||
float x, y, z;
|
||||
float u, v;
|
||||
float nx, ny, nz;
|
||||
uint8_t r, g, b, a;
|
||||
}
|
||||
]])
|
||||
|
||||
function model:compress()
|
||||
local data = love.data.newByteData(ffi.sizeof("struct vertex") * #self.verts)
|
||||
local datapointer = ffi.cast("struct vertex *", data:getFFIPointer())
|
||||
|
||||
for i, vert in ipairs(self.verts) do
|
||||
local dataindex = i - 1
|
||||
datapointer[dataindex].x = vert[1]
|
||||
datapointer[dataindex].y = vert[2]
|
||||
datapointer[dataindex].z = vert[3]
|
||||
datapointer[dataindex].u = vert[4] or 0
|
||||
datapointer[dataindex].v = vert[5] or 0
|
||||
datapointer[dataindex].nx = vert[6] or 0
|
||||
datapointer[dataindex].ny = vert[7] or 0
|
||||
datapointer[dataindex].nz = vert[8] or 0
|
||||
datapointer[dataindex].r = (vert[9] or 1)*255
|
||||
datapointer[dataindex].g = (vert[10] or 1)*255
|
||||
datapointer[dataindex].b = (vert[11] or 1)*255
|
||||
datapointer[dataindex].a = (vert[12] or 1)*255
|
||||
end
|
||||
|
||||
self.mesh:release()
|
||||
self.mesh = love.graphics.newMesh(self.vertexFormat, #self.verts, "triangles")
|
||||
self.mesh:setVertices(data)
|
||||
self.mesh:setTexture(self.texture)
|
||||
self.verts = nil
|
||||
end
|
||||
end
|
||||
|
||||
function model:rayIntersection(...)
|
||||
return collisions.rayIntersection(self.verts, self, ...)
|
||||
end
|
||||
|
||||
function model:isPointInside(...)
|
||||
return collisions.isPointInside(self.verts, self, ...)
|
||||
end
|
||||
|
||||
function model:sphereIntersection(...)
|
||||
return collisions.sphereIntersection(self.verts, self, ...)
|
||||
end
|
||||
|
||||
function model:closestPoint(...)
|
||||
return collisions.closestPoint(self.verts, self, ...)
|
||||
end
|
||||
|
||||
function model:capsuleIntersection(...)
|
||||
return collisions.capsuleIntersection(self.verts, self, ...)
|
||||
end
|
||||
|
||||
return newModel
|
||||
84
g3d/objloader.lua
Normal file
84
g3d/objloader.lua
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
-- written by groverbuger for g3d
|
||||
-- september 2021
|
||||
-- MIT license
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
-- simple obj loader
|
||||
----------------------------------------------------------------------------------------------------
|
||||
local inspect = require "inspect"
|
||||
|
||||
-- give path of file
|
||||
-- returns a lua table representation
|
||||
return function (path, flipU, flipV)
|
||||
local positions, faces, uvs, normals, model = {}, {}, {}, {}, {}
|
||||
|
||||
-- go line by line through the file
|
||||
for line in love.filesystem.lines(path) do
|
||||
local words = {}
|
||||
|
||||
-- split the line into words
|
||||
for word in line:gmatch "([^%s]+)" do
|
||||
table.insert(words, word)
|
||||
end
|
||||
|
||||
local firstWord = words[1]
|
||||
|
||||
if firstWord == "v" then
|
||||
-- if the first word in this line is a "v", then this defines a vertex's position
|
||||
|
||||
table.insert(positions, {tonumber(words[2]), tonumber(words[3]), tonumber(words[4])})
|
||||
--print(table.concat(positions[#positions],";"))
|
||||
elseif firstWord == "vt" then
|
||||
-- if the first word in this line is a "vt", then this defines a texture coordinate
|
||||
|
||||
-- Blender 2.9+ V axis points UP, whereas LÖVE's Y axis points DOWN
|
||||
-- Not sure if any other software has U axis inverted, but why not add this just in case?
|
||||
local U, V = tonumber(words[2]), tonumber(words[3])
|
||||
if flipU then U = 1 - U end
|
||||
if flipV then V = 1 - V end
|
||||
|
||||
table.insert(uvs, {U,V})
|
||||
elseif firstWord == "vn" then
|
||||
-- if the first word in this line is a "vn", then this defines a vertex normal
|
||||
|
||||
table.insert(normals, {tonumber(words[2]), tonumber(words[3]), tonumber(words[4])})
|
||||
elseif firstWord == "f" then
|
||||
-- if the first word in this line is a "f", then this is a face
|
||||
-- a face takes three point definitions
|
||||
-- the arguments a point definition takes are vertex, vertex texture, vertex normal in that order
|
||||
|
||||
assert(#words == 4, ("Faces in %s must be triangulated before they can be used in g3d!"):format(path))
|
||||
local face = {v = {}, vt = {}, n = {}}
|
||||
|
||||
|
||||
for i=2, #words do
|
||||
local v, vt, n = words[i]:match "(%d+)/(%d+)/(%d+)"
|
||||
--print(v,vt,n)
|
||||
if not (v and vt and n) then
|
||||
v, vt = words[i]:match "(%d+)/(%d+)"
|
||||
n = 1
|
||||
end
|
||||
table.insert(face.v, tonumber(v))
|
||||
table.insert(face.vt, tonumber(vt))
|
||||
table.insert(face.n, tonumber(n))
|
||||
|
||||
end
|
||||
table.insert(faces, face)
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
-- put it all together in the right order
|
||||
for _, face in pairs(faces) do
|
||||
for i=1, 3 do
|
||||
local vert = {unpack(positions[face.v[i]])}
|
||||
for _, uv in ipairs(uvs[face.vt[i]]) do table.insert(vert, uv) end
|
||||
if (normals[face.n[i]]) then
|
||||
for _, normal in ipairs(normals[face.n[i]]) do table.insert(vert, normal) end
|
||||
end
|
||||
table.insert(model, vert)
|
||||
end
|
||||
end
|
||||
|
||||
return model
|
||||
end
|
||||
42
g3d/vectors.lua
Normal file
42
g3d/vectors.lua
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
-- written by groverbuger for g3d
|
||||
-- september 2021
|
||||
-- MIT license
|
||||
|
||||
----------------------------------------------------------------------------------------------------
|
||||
-- vector functions
|
||||
----------------------------------------------------------------------------------------------------
|
||||
-- some basic vector functions that don't use tables
|
||||
-- because these functions will happen often, this is done to avoid frequent memory allocation
|
||||
|
||||
local vectors = {}
|
||||
|
||||
function vectors.subtract(v1,v2,v3, v4,v5,v6)
|
||||
return v1-v4, v2-v5, v3-v6
|
||||
end
|
||||
|
||||
function vectors.add(v1,v2,v3, v4,v5,v6)
|
||||
return v1+v4, v2+v5, v3+v6
|
||||
end
|
||||
|
||||
function vectors.scalarMultiply(scalar, v1,v2,v3)
|
||||
return v1*scalar, v2*scalar, v3*scalar
|
||||
end
|
||||
|
||||
function vectors.crossProduct(a1,a2,a3, b1,b2,b3)
|
||||
return a2*b3 - a3*b2, a3*b1 - a1*b3, a1*b2 - a2*b1
|
||||
end
|
||||
|
||||
function vectors.dotProduct(a1,a2,a3, b1,b2,b3)
|
||||
return a1*b1 + a2*b2 + a3*b3
|
||||
end
|
||||
|
||||
function vectors.normalize(x,y,z)
|
||||
local mag = math.sqrt(x^2 + y^2 + z^2)
|
||||
return x/mag, y/mag, z/mag
|
||||
end
|
||||
|
||||
function vectors.magnitude(x,y,z)
|
||||
return math.sqrt(x^2 + y^2 + z^2)
|
||||
end
|
||||
|
||||
return vectors
|
||||
Loading…
Add table
Add a link
Reference in a new issue