Created
April 28, 2021 06:04
-
-
Save simsaens/3356c5e5786155595bbfac114d8fa22f to your computer and use it in GitHub Desktop.
Codea project which uses mesh instancing to draw snow
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--# Main | |
-- 3D Snow | |
function setup() | |
viewer.mode = FULLSCREEN | |
parameter.watch("FPS") | |
parameter.boolean("camOrtho", false) | |
parameter.number("timeSpeed", 0, 8, 1) | |
parameter.number("hspread", 0, 50, 50) | |
parameter.number("vspread", 0, 50, 0) | |
parameter.number("stretch", 0, 16, 0) | |
parameter.boolean("vortex", false) | |
parameter.boolean("night", false) | |
instances = 300 | |
snowMesh = mesh() -- Generate a mesh with 100 triangles randomly positioned and rotated in it, with texture coordinates | |
local v = {} | |
local tc = {} | |
local size = 0.5 -- Lower this to make each individual triangle smaller | |
local hspread = 25 -- The horizontal spread of triangles within the mesh | |
local vspread = 5 -- The vertical spread of triangles within the mesh | |
for i = 1, 100 do | |
local rel = {vec3(-0.5, -0.5, 0), vec3(0.5, -0.5, 0.0), vec3(0.5, 0.5, 0.0)} | |
for k, vec in ipairs(rel) do | |
local seed = math.tointeger(math.floor(i * math.pi * 10000 * 123456789)) | |
math.randomseed(seed) | |
vec = matrix() | |
:translate((math.random() * 2 - 1) * hspread, (math.random() * 2 - 1) * vspread, (math.random() * 2 - 1) * hspread) | |
:rotate(math.random(0, 359), 1, 0, 0):rotate(math.random(0, 359), 0, 1, 0):rotate(math.random(0, 359), 0, 0, 1) | |
*vec3(vec.x * size, vec.y * size, vec.z * size) | |
table.insert(v, vec) | |
end | |
table.insert(tc, vec2(0, 0)) | |
table.insert(tc, vec2(1, 0)) | |
table.insert(tc, vec2(1, 1)) | |
end | |
snowMesh.vertices = v | |
snowMesh.texCoords = tc | |
snowMesh.shader = shader("Snow") -- The shader transforms and textures the vertices | |
snowMesh.shader.stretch = 0 | |
transform = snowMesh:buffer("transform") | |
transform.instanced = true | |
transform:resize(instances) | |
instTint = snowMesh:buffer("tint") | |
instTint.instanced = true | |
instTint:resize(instances) | |
instAlpha = snowMesh:buffer("alpha") | |
instAlpha.instanced = true | |
instAlpha:resize(instances) | |
for i = 1, instances do | |
math.randomseed(i) | |
transform[i] = matrix() | |
instTint[i] = color(255) | |
end | |
time = 0 | |
ts = timeSpeed | |
pos = vec3(0, -50, 0) -- Variables to handle camera movement and direction | |
ang = vec2(1, 1):normalize() | |
ang2 = vec2(1, 1):normalize() | |
tpos = vec3(pos.x, pos.y, pos.z) | |
tang = vec2(ang.x, ang.y) | |
tang2 = vec2(ang2.x, ang2.y) | |
mId = 0 | |
lId = 0 | |
zoom = 20 | |
tzoom = zoom | |
end | |
function draw() | |
if night then | |
background(0) | |
else | |
background(191, 220, 255) | |
end | |
local pmix = 0.0375 -- Smoothes the camera movement | |
pos.x = pos.x * (1 - pmix) + tpos.x * pmix | |
pos.y = pos.y * (1 - pmix) + tpos.y * pmix | |
pos.z = pos.z * (1 - pmix) + tpos.z * pmix | |
local amix = 0.1 | |
ang.x = ang.x * (1 - amix) + tang.x * amix | |
ang.y = ang.y * (1 - amix) + tang.y * amix | |
ang2.x = ang2.x * (1 - amix) + tang2.x * amix | |
ang2.y = ang2.y * (1 - amix) + tang2.y * amix | |
local zmix = 0.1 | |
zoom = zoom * (1 - zmix) + tzoom * zmix | |
local smix = 0.05 | |
ts = ts * (1 - smix) + timeSpeed * smix | |
time = time + DeltaTime * ts | |
if camOrtho then | |
camera(ang.x * ang2.x * -100, ang2.y * -100, ang.y * ang2.x * -100, ang.x * ang2.x, ang2.y, ang.y * ang2.x, 0, 1, 0) | |
local aspect = WIDTH / HEIGHT | |
ortho(-zoom * aspect, zoom * aspect, -zoom, zoom, 0.001, 65536) | |
else | |
camera(pos.x, pos.y, pos.z, pos.x + ang.x * ang2.x, pos.y + ang2.y, pos.z + ang.y * ang2.x, 0, 1, 0) | |
perspective(70, WIDTH / HEIGHT, 0.001, 65536) | |
end | |
--local hspread = 0 | |
--local vspread = 0 | |
local step = 1 | |
local height = 20 | |
local pOffset = 1 | |
local lOffset = 0 | |
for i = 1, instances do | |
math.randomseed(math.floor(i * math.pi * 10000)) | |
local max = (instances * height * step * lOffset + height * step * (1 - lOffset)) | |
local offset = (time + i * step * pOffset) % (instances * height * step * lOffset + height * step * (1 - lOffset)) | |
transform[i] = matrix() | |
if vortex then | |
transform[i] = transform[i]:rotate(time * 45, 0, 1, 0) | |
end | |
transform[i] = transform[i]:translate(0.0, -offset * 3.0, 0.0) | |
:translate((math.random() * 2 - 1) * hspread, (math.random() * 2 - 1) * vspread, (math.random() * 2 - 1) * hspread) | |
:rotate(math.random(0, 359), 0, 1, 0) | |
instAlpha[i] = math.min(1, (offset / max) * height) * math.min(1, (1 - offset / max) * height) | |
end | |
snowMesh.shader.stretch = stretch | |
snowMesh:draw(instances) | |
collectgarbage() | |
collectgarbage() | |
collectgarbage() | |
end | |
function touched(touch) -- Handles camera movement and direction | |
if touch.state == BEGAN then | |
if touch.x < WIDTH / 2 and mId == 0 then | |
mId = touch.id | |
elseif touch.x >= WIDTH / 2 and lId == 0 then | |
lId = touch.id | |
end | |
end | |
if touch.id == mId then | |
if camOrtho then | |
tzoom = tzoom - touch.deltaY / 20 * (tzoom / 40 + 0.5) | |
else | |
local speed = 1 / 32 | |
tpos.x = tpos.x + tang:rotate(math.pi / 2).x * touch.deltaX * speed | |
tpos.z = tpos.z + tang:rotate(math.pi / 2).y * touch.deltaX * speed | |
tpos.x = tpos.x + tang.x * tang2.x * touch.deltaY * speed | |
tpos.z = tpos.z + tang.y * tang2.x * touch.deltaY * speed | |
tpos.y = tpos.y + tang2.y * touch.deltaY * speed | |
end | |
elseif touch.id == lId then | |
local speed = zoom / 40 + 0.5 | |
tang = tang:rotate(math.rad(touch.deltaX / 2 * speed)) | |
tang2 = tang2:rotate(math.rad(touch.deltaY / 2 * speed)) | |
if tang2.x < 0.01 then | |
if tang2.y < 0 then | |
tang2 = vec2(0.01, -1):normalize() | |
else | |
tang2 = vec2(0.01, 1):normalize() | |
end | |
end | |
end | |
if touch.state == ENDED or touch.state == CANCELLED then | |
if touch.id == mId then | |
mId = 0 | |
elseif touch.id == lId then | |
lId = 0 | |
end | |
end | |
end | |
vert=[[ | |
uniform mat4 modelViewProjection; | |
attribute mat4 transform; | |
attribute vec4 position; | |
attribute vec4 color; | |
attribute mediump vec2 texCoord; | |
varying lowp vec4 vColor; | |
varying mediump vec2 vTexCoord; | |
void main() | |
{ | |
vColor = color; | |
vTexCoord = texCoord; | |
gl_Position = modelViewProjection * (transform * position); | |
} | |
]] | |
frag=[[ | |
uniform sampler2D texture; | |
varying lowp vec4 vColor; | |
varying mediump vec2 vTexCoord; | |
void main() | |
{ | |
gl_FragColor = vec4(1.0);//texture2D(texture, vTexCoord) * vColor; | |
} | |
]] | |
--# Primitive | |
Primitive = class() | |
-- Primitive class originally by @spacemonkey | |
-- Edited by @SkyTheCoder to add options for width, height, length, and position, also to generate texture coordinates for the cube | |
--primitves gives basic mesh building for cubes and isospheres | |
--triangles are wound consistently to avoid gl_facing issues | |
function Primitive:Cube(w, h, l, x, y, z) | |
local s = 1 | |
w = w or 1 | |
h = h or w | |
l = l or h | |
x = x or 0 | |
y = y or 0 | |
z = z or 0 | |
local vertices = { | |
vec3(-0.5*s, -0.5*s, 0.5*s), -- Left bottom front | |
vec3( 0.5*s, -0.5*s, 0.5*s), -- Right bottom front | |
vec3( 0.5*s, 0.5*s, 0.5*s), -- Right top front | |
vec3(-0.5*s, 0.5*s, 0.5*s), -- Left top front | |
vec3(-0.5*s, -0.5*s, -0.5*s), -- Left bottom back | |
vec3( 0.5*s, -0.5*s, -0.5*s), -- Right bottom back | |
vec3( 0.5*s, 0.5*s, -0.5*s), -- Right top back | |
vec3(-0.5*s, 0.5*s, -0.5*s), -- Left top back | |
} | |
-- now construct a cube out of the vertices above | |
v = { | |
-- Front | |
vertices[1], vertices[2], vertices[3], | |
vertices[1], vertices[3], vertices[4], | |
-- Right | |
vertices[2], vertices[6], vertices[7], | |
vertices[2], vertices[7], vertices[3], | |
-- Back | |
vertices[6], vertices[5], vertices[8], | |
vertices[6], vertices[8], vertices[7], | |
-- Left | |
vertices[5], vertices[1], vertices[4], | |
vertices[5], vertices[4], vertices[8], | |
-- Top | |
vertices[4], vertices[3], vertices[7], | |
vertices[4], vertices[7], vertices[8], | |
-- Bottom | |
vertices[5], vertices[6], vertices[2], | |
vertices[5], vertices[2], vertices[1], | |
} | |
for i, j in ipairs(v) do | |
v[i] = vec3(j.x * w + x, j.y * h + y, j.z * l + z) | |
end | |
return v | |
end | |
function Primitive:CubeTexCoords(w, h, x) | |
w = w or 1 | |
h = h or w | |
x = x or Primitive:Cube(1) | |
local ret = {} | |
for i = 1, #x / 3 do | |
table.insert(ret, vec2(0.0, 0.0)) | |
table.insert(ret, vec2(w, 0.0)) | |
table.insert(ret, vec2(w, h)) | |
table.insert(ret, vec2(0.0, 0.0)) | |
table.insert(ret, vec2(w, h)) | |
table.insert(ret, vec2(0.0, h)) | |
end | |
return ret | |
end | |
function Primitive:Sphere(w, h, l, x, y, z, depth) | |
local s = 1 | |
w = w or 1 | |
h = h or w | |
l = l or h | |
x = x or 0 | |
y = y or 0 | |
z = z or 0 | |
depth = depth or 1 | |
local t = (1 + math.sqrt(5)) / 2 | |
--all the vertices of an icosohedron | |
local vertices = { | |
vec3(-1 , t, 0):normalize(), | |
vec3(1 , t, 0):normalize(), | |
vec3(-1 , -t, 0):normalize(), | |
vec3(1 , -t, 0):normalize(), | |
vec3(0 , -1, t):normalize(), | |
vec3(0 , 1, t):normalize(), | |
vec3(0 , -1, -t):normalize(), | |
vec3(0 , 1, -t):normalize(), | |
vec3(t , 0, -1):normalize(), | |
vec3(t , 0, 1):normalize(), | |
vec3(-t , 0, -1):normalize(), | |
vec3(-t , 0, 1):normalize() | |
} | |
--20 faces | |
icovertices = { | |
-- 5 faces around point 0 | |
vertices[1], vertices[12], vertices[6], | |
vertices[1], vertices[6], vertices[2], | |
vertices[1], vertices[2], vertices[8], | |
vertices[1], vertices[8], vertices[11], | |
vertices[1], vertices[11], vertices[12], | |
-- 5 adjacent faces | |
vertices[2], vertices[6], vertices[10], | |
vertices[6], vertices[12], vertices[5], | |
vertices[12], vertices[11], vertices[3], | |
vertices[11], vertices[8], vertices[7], | |
vertices[8], vertices[2], vertices[9], | |
-- 5 faces around point 3 | |
vertices[4], vertices[10], vertices[5], | |
vertices[4], vertices[5], vertices[3], | |
vertices[4], vertices[3], vertices[7], | |
vertices[4], vertices[7], vertices[9], | |
vertices[4], vertices[9], vertices[10], | |
--5 adjacent faces | |
vertices[5], vertices[10], vertices[6], | |
vertices[3], vertices[5], vertices[12], | |
vertices[7], vertices[3], vertices[11], | |
vertices[9], vertices[7], vertices[8], | |
vertices[10], vertices[9], vertices[2] | |
} | |
local finalVertices = {} | |
--divide each triangle into 4 sub triangles to make an isosphere | |
--this can be repeated (based on depth) for higher res spheres | |
for j=1,depth do | |
for i=1,#icovertices/3 do | |
midpoint1 = ((icovertices[i*3-2] + icovertices[i*3-1])/2):normalize() | |
midpoint2 = ((icovertices[i*3-1] + icovertices[i*3])/2):normalize() | |
midpoint3 = ((icovertices[i*3] + icovertices[i*3-2])/2):normalize() | |
--triangle 1 | |
table.insert(finalVertices,icovertices[i*3-2]) | |
table.insert(finalVertices,midpoint1) | |
table.insert(finalVertices,midpoint3) | |
--triangle 2 | |
table.insert(finalVertices,midpoint1) | |
table.insert(finalVertices,icovertices[i*3-1]) | |
table.insert(finalVertices,midpoint2) | |
--triangle 3 | |
table.insert(finalVertices,midpoint2) | |
table.insert(finalVertices,icovertices[i*3]) | |
table.insert(finalVertices,midpoint3) | |
--triangle 4 | |
table.insert(finalVertices,midpoint1) | |
table.insert(finalVertices,midpoint2) | |
table.insert(finalVertices,midpoint3) | |
end | |
icovertices = finalVertices | |
finalVertices = {} | |
end | |
for i, j in ipairs(icovertices) do | |
icovertices[i] = vec3(j.x * w + x, j.y * h + y, j.z * l + z) | |
end | |
return icovertices | |
end | |
--# FPS | |
FPS = 0 -- Code to calculate accurate frames per secoond | |
local frames = 0 | |
local time = 0 | |
tween.delay(0.001, function() | |
local d = draw | |
draw = function() | |
frames = frames + 1 | |
if math.floor(ElapsedTime) ~= math.floor(time) then | |
FPS = frames - 1 | |
frames = 1 | |
end | |
time = ElapsedTime | |
d() | |
end | |
end) | |
--# Shaders | |
Shaders = { | |
--shaderstart:Snow | |
Snow = { | |
vS = [[ | |
// | |
// A basic vertex shader | |
// | |
//This is the current model * view * projection matrix | |
// Codea sets it automatically | |
uniform mat4 modelViewProjection; | |
uniform highp float stretch; | |
//This is the current mesh vertex position, color and tex coord | |
// Set automatically | |
attribute vec4 position; | |
attribute vec4 color; | |
attribute vec2 texCoord; | |
attribute mat4 transform; | |
attribute vec4 tint; | |
attribute lowp float alpha; | |
//This is an output variable that will be passed to the fragment shader | |
varying lowp vec4 vColor; | |
varying highp vec2 vTexCoord; | |
varying lowp vec4 vTint; | |
varying lowp float vAlpha; | |
void main() | |
{ | |
//Pass the mesh color to the fragment shader | |
vColor = color; | |
vTexCoord = texCoord; | |
vTint = tint; | |
vAlpha = alpha; | |
vec4 offset = vec4(0.0); | |
if (vTexCoord.xy == vec2(1.0, 1.0)) offset = vec4(0.0, stretch, 0.0, 0.0); | |
//Multiply the vertex position by our combined transform | |
gl_Position = modelViewProjection * (transform * (position + offset)); | |
} | |
]], | |
fS = [[ | |
// | |
// A basic fragment shader | |
// | |
//Default precision qualifier | |
precision highp float; | |
//This represents the current texture on the mesh | |
uniform lowp sampler2D texture; | |
//The interpolated vertex color for this fragment | |
varying lowp vec4 vColor; | |
//The interpolated texture coordinate for this fragment | |
varying highp vec2 vTexCoord; | |
varying lowp vec4 vTint; | |
varying lowp float vAlpha; | |
void main() | |
{ | |
//Sample the texture at the interpolated coordinate | |
//lowp vec4 col = texture2D( texture, vTexCoord ) * vColor; | |
float dist = (1.0 - pow(min(1.0, distance(vTexCoord, vec2(0.75, 0.25)) * 4.0), 3.0)); | |
if (dist <= 0.1) discard; | |
//Set the output color to the texture color | |
gl_FragColor = vec4(vTint.rgb, vAlpha) * dist;//col; | |
} | |
]], | |
}, | |
--shaderend:Snow | |
} | |
--# Utility | |
function math.dec(x) | |
return x - math.floor(x) | |
end | |
function math.mix(a, b, x) | |
return a * x + b * (1 - x) | |
end | |
-- Function tweak to make shaders easier, so I can call shader("X") instead of shader(Shaders.X.vS, Shaders.X.fS) | |
local _shader = shader | |
function shader(...) | |
if select("#", ...) == 1 then | |
local data = Shaders[select(1, ...)] | |
if data ~= nil then | |
return _shader(data.vS, data.fS) | |
else | |
return _shader(...) | |
end | |
else | |
return _shader(...) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment