Inspired by Custom 3D engine in Matplotlib
First things first, we need to load our model. We’ll use a simplified version of the Stanford bunny. The file uses the wavefront format which is one of the simplest format, so let’s make a very simple (but error-prone) loader that will just do the job for this post (and this model):
bunny <- readr::read_delim('bunny.obj', col_names=F, comment = '#', delim = ' ') %>% select( type=1, x=2, y=3, z=4 )
V <- bunny %>% filter( type == 'v' ) %>% mutate( vertex=seq(nrow(.)) ) %>% select(-type) %>%
mutate( x= x - (max(x) + min(x)) / 2, y= y - (max(y) + min(y)) / 2, z= z - (max(z) + min(z)) / 2,
x = x / (max(x) - min(x)), y = y / (max(y) - min(y)), z = z / (max(z) - min(z)))
V %>% ggplot( aes(x=x, y=y) ) + geom_point() + coord_fixed(ratio=1)
Triangles <- bunny %>% filter( type == 'f' ) %>% select(v1=x, v2=y, v3=z) %>%
mutate( t1 = seq(nrow(.))) %>%
tidyr::pivot_longer( -t1, values_to = 'vertex' ) %>%
mutate( vertex = as.integer(vertex) )
Triangles %>%
left_join( V, by='vertex' ) %>%
ggplot( aes( x=x,y=y, group=t1 )) + geom_polygon(fill='white', color='black', size=0.2) + coord_fixed(ratio=1)
frustum <- function(left, right, bottom, top, znear, zfar){
M <- matrix( data=0, nrow=4, ncol=4)
M[1, 1] <- +2.0 * znear / (right - left)
M[2, 2] <- +2.0 * znear / (top - bottom)
M[3, 3] <- -(zfar + znear) / (zfar - znear)
M[1, 3] <- (right + left) / (right - left)
M[3, 2] <- (top + bottom) / (top - bottom)
M[3, 4] <- -2.0 * znear * zfar / (zfar - znear)
M[4, 3] <- -1.0
M
}
perspective <- function(fovy, aspect, znear, zfar){
h <- tan(fovy * pi / 360 ) * znear
w <- h * aspect
frustum(-w, w, -h, h, znear, zfar)
}
V2 <- V %>% mutate( x = x, y=y, z=z-3.5 ) %>%
( function(vertices) as.matrix( cbind( vertices[, c(1,2,3)] , 1 ) )) %*% t( perspective(25,1,1,100)) %>%
as_tibble() %>%
select( x=V1, y=V2, z=V3, w=V4) %>%
mutate( vertex=seq(nrow(.)), x=x/w, y=y/w, z=z/w, w=1)
Triangles %>%
left_join( V2, by='vertex' ) %>%
ggplot( aes( x=x,y=y, group=t1 )) + geom_polygon(fill='white', color='black', size=0.2) + coord_fixed(ratio=1)
translate <- function(x, y, z){
matrix(c( 1, 0, 0, x,
0, 1, 0, y,
0, 0, 1, z,
0, 0, 0, 1),
nrow=4, ncol=4, byrow = T)
}
xrotate <- function(theta){
tt <- pi * theta / 180
cs <- cos(tt)
ss <- sin(tt)
matrix(c( 1, 0, 0, 0,
0, cs, -ss, 0,
0, ss, cs, 0,
0, 0, 0, 1),
nrow=4, ncol=4, byrow = T)
}
yrotate <- function(theta){
tt <- pi * theta / 180
cs <- cos(tt)
ss <- sin(tt)
matrix(c( cs, 0, ss, 0,
0, 1, 0, 0,
-ss, 0, cs, 0,
0, 0, 0, 1),
nrow=4, ncol=4, byrow = T)
}
model <- xrotate(20) %*% yrotate(45)
view <- translate(0,0,-3.5)
proj <- perspective(25, 1, 1, 100)
MVP <- proj %*% view %*% model
MVP
## [,1] [,2] [,3] [,4]
## [1,] 3.1895526 0.0000000 3.1895526 0.000000
## [2,] 1.0908912 4.2386795 -1.0908912 0.000000
## [3,] 0.6778865 -0.3489296 -0.6778865 1.550505
## [4,] 0.6644630 -0.3420201 -0.6644630 3.500000
V3 <- V %>% mutate( x = x, y=y, z=z-3.5 ) %>%
( function(vertices) as.matrix( cbind( vertices[, c(1,2,3)] , 1 ) )) %*% t( MVP ) %>%
as_tibble() %>%
select( x=V1, y=V2, z=V3, w=V4) %>%
mutate( vertex=seq(nrow(.)), x=x/w, y=y/w, z=z/w, w=1)
Triangles %>%
left_join( V3, by='vertex' ) %>%
ggplot( aes( x=x,y=y, group=t1 )) + geom_polygon(fill='white', color='black', size=0.2) + coord_fixed(ratio=1)
p25 <- Triangles %>%
left_join( V %>% mutate( x = x, y=y, z=z-3.5 ) %>%
( function(vertices) as.matrix( cbind( vertices[, c(1,2,3)] , 1 ) )) %*% t( perspective(25, 1, 1, 100) %*% view %*% model ) %>%
as_tibble() %>%
select( x=V1, y=V2, z=V3, w=V4) %>%
mutate( vertex=seq(nrow(.)), x=x/w, y=y/w, z=z/w, w=1), by='vertex' ) %>%
ggplot( aes( x=x,y=y, group=t1 )) + geom_polygon(fill='white', color='black', size=0.2) + coord_fixed(ratio=1) + labs(x=NULL, y=NULL)
p40 <- Triangles %>%
left_join( V %>% mutate( x = x, y=y, z=z-3.5 ) %>%
( function(vertices) as.matrix( cbind( vertices[, c(1,2,3)] , 1 ) )) %*% t( perspective(40, 1, 1, 100) %*% view %*% model ) %>%
as_tibble() %>%
select( x=V1, y=V2, z=V3, w=V4) %>%
mutate( vertex=seq(nrow(.)), x=x/w, y=y/w, z=z/w, w=1), by='vertex' ) %>%
ggplot( aes( x=x,y=y, group=t1 )) + geom_polygon(fill='white', color='black', size=0.2) + coord_fixed(ratio=1) + labs(x=NULL, y=NULL)
p65 <- Triangles %>%
left_join( V %>% mutate( x = x, y=y, z=z-3.5 ) %>%
( function(vertices) as.matrix( cbind( vertices[, c(1,2,3)] , 1 ) )) %*% t( perspective(65, 1, 1, 100) %*% view %*% model ) %>%
as_tibble() %>%
select( x=V1, y=V2, z=V3, w=V4) %>%
mutate( vertex=seq(nrow(.)), x=x/w, y=y/w, z=z/w, w=1), by='vertex' ) %>%
ggplot( aes( x=x,y=y, group=t1 )) + geom_polygon(fill='white', color='black', size=0.2) + coord_fixed(ratio=1) + labs(x=NULL, y=NULL)
p80 <- Triangles %>%
left_join( V %>% mutate( x = x, y=y, z=z-3.5 ) %>%
( function(vertices) as.matrix( cbind( vertices[, c(1,2,3)] , 1 ) )) %*% t( perspective(80, 1, 1, 100) %*% view %*% model ) %>%
as_tibble() %>%
select( x=V1, y=V2, z=V3, w=V4) %>%
mutate( vertex=seq(nrow(.)), x=x/w, y=y/w, z=z/w, w=1), by='vertex' ) %>%
ggplot( aes( x=x,y=y, group=t1 )) + geom_polygon(fill='white', color='black', size=0.2) + coord_fixed(ratio=1) + labs(x=NULL, y=NULL)
p25 + p40 + p65 + p80
model <- xrotate(-30) %*% yrotate(180)
view <- translate(0,0,-3.5)
proj <- perspective(45, 1, 1, 100)
MVP <- proj %*% view %*% model
V3s <- V %>% mutate( x = x, y=y, z=z+3.5 ) %>%
( function(vertices) as.matrix( cbind( vertices[, c(1,2,3)] , 1 ) )) %*% t( MVP ) %>%
as_tibble() %>%
select( x=V1, y=V2, z=V3, w=V4) %>%
mutate( vertex=seq(nrow(.)), x=x/w, y=y/w, z=z/w, w=1)
T3s <- Triangles %>% left_join( V3s, by='vertex' )
torder <- T3s %>% group_by(t1) %>% summarize(depth=mean(z))
T3s %>% left_join(torder, by='t1') %>% arrange(depth) %>% mutate( t1=factor(t1, levels=unique( t1 ) )) %>%
ggplot( aes( x=x,y=y, group=t1 )) +
geom_polygon(fill='lightgrey', color='darkgrey', size=0.2) + coord_fixed(ratio=1) + guides(fill='none') +
labs(x=NULL, y=NULL)
model <- xrotate(-30) %*% yrotate(180)
view <- translate(0,0,-3.5)
proj <- perspective(45, 1, 1, 100)
MVP <- proj %*% view %*% model
V3s <- V %>% mutate( x = x, y=y, z=z+3.5 ) %>%
( function(vertices) as.matrix( cbind( vertices[, c(1,2,3)] , 1 ) )) %*% t( MVP ) %>%
as_tibble() %>%
select( x=V1, y=V2, z=V3, w=V4) %>%
mutate( vertex=seq(nrow(.)), x=x/w, y=y/w, z=z/w, w=1)
T3s <- Triangles %>% left_join( V3s, by='vertex' )
torder <- T3s %>% group_by(t1) %>% summarize(depth=mean(z))
T3s %>% left_join(torder, by='t1') %>% arrange(depth) %>% mutate( t1=factor(t1, levels=unique( t1 ) )) %>%
ggplot( aes( x=x,y=y, group=t1 )) +
geom_polygon(aes(fill=depth), color='darkgrey', size=0.2) + coord_fixed(ratio=1) + guides(fill='none') +
scale_fill_viridis_c( option='magma' ) + labs(x=NULL, y=NULL)