home
Rendering a bezier surface
Previously
I
wrote
a
blog
post
about
how
to
interpolate
a
bezier
curve.
A
bezier
curve
helps
us
to
create
smooth
lines,
surfaces,
type
faces.
A
bezier
surface
is
set
of
bezier
curves
that
is
used
for
imaging
a
part
of
something.
Before
rendering
a
bezier
surface,
I
will
mention
about
how
to
render
something
with
Python
programming
language.
I
will
demonstrate
it
by
rendering
a
set
of
cubes,
then
I
will
create
marbles
that
made
of
cubes.
A
marble
will
stay
at
position
of
an
interpolated
point
of
a
bezier
curve.
I
will
create
an
optimized
cube
for
making
the
drawing
process
easier.
-
code:python
-
def
create_cube(x,
y,
z,
rotate_horizontal,
rotate_vertical):
#
only
up
to
horizontally
90
degrees
implemented
in
this
example
if
rotate_vertical
>
0
and
rotate_vertical
<
90
and
rotate_horizontal
<
90
and
rotate_horizontal
>
0:
return
[
[
20,
#
this
is
in
percent
[x+0.5,
y+0.5,
z-0.5],
[x+0.5,
y+0.5,
z+0.5],
[x+0.5,
y-0.5,
z+0.5],
[x+0.5,
y-0.5,
z-0.5]
],
[
5,
[x-0.5,
y+0.5,
z-0.5],
[x+0.5,
y+0.5,
z-0.5],
[x+0.5,
y+0.5,
z+0.5],
[x-0.5,
y+0.5,
z+0.5]
],
[
30,
[x-0.5,
y+0.5,
z+0.5],
[x+0.5,
y+0.5,
z+0.5],
[x+0.5,
y-0.5,
z+0.5],
[x-0.5,
y-0.5,
z+0.5]
]
]
The
function
returns
diamond
shapes.
Set
of
diamond
shapes,
ordered
in
correct
way,
will
be
painted
to
an
image
file
in
Python
or
to
a
canvas
on
a
browser.
Vertically
minus
degrees
are
facing
upwards.
The
visible
faces
must
be
set
in
correct
order.
It
is
possible
to
do
that
by
ordering
faces
by
their
average
z-index,
but
I
prefer
not
to
complicate
that
process
further.
Z-sort
will
be
used
for
sorting
the
cubes.
-
code:python
-
import
math
def
rotate(x,
y,
deg):
cos
=
math.cos(math.radians(deg))
sin
=
math.sin(math.radians(deg))
return
[cos
*
x
-
sin
*
y,
cos
*
y
+
sin
*
x]
def
rotate_xyz(x,
y,
z,
rh,
rv):
xn,
zn
=
rotate(x,
z,
rh)
yn,
zn
=
rotate(y,
zn,
rv)
return
[xn,
yn,
zn]
A
point
in
three-d
should
be
rotated
first.
The
values
x-y
will
be
the
position
on
canvas.
On
canvas,
the
when
the
y
is
getting
higher,
it
means
it
is
being
drawn
to
the
end
of
canvas
in
vertical.
So
the
height
should
be
inreversed.
I
also
scale
them
with
a
multipier
in
order
to
see
the
cube's
borders.
At
the
end,
the
scale
may
be
minimized
as
less
as
possible.
So
the
cubes
won't
be
visible
in
result.
-
code:python
-
def
darken(r,
g,
b,
percent):
rblacks
=
int(r
*
(percent/100))
gblacks
=
int(g
*
(percent/100))
bblacks
=
int(b
*
(percent/100))
return
(r
-
rblacks,
g
-
gblacks,
b
-
bblacks)
A
darken
function
like
above
will
be
needed
in
order
to
make
a
cube's faces visually differentiable.
- code:python -
from render import render
render(
  "render-1",
  [
    [0, 0, 0, 255, 255, 255]
  ],
  4,
  4,
  1024,
  40,
  20,
)
I
am
using
Python's
PIL
library
for
drawing
in
two-d.
The
render
will
be
looking
like
below
when
we
run
it.
-
code:python
-
from
create_cube
import
create_cube
from
darken
import
darken
from
PIL
import
Image,
ImageDraw
def
render(name,
cubes,
width,
height,
unit_scale,
rotate_horizontal,
rotate_vertical,
save=True):
width
*=
unit_scale
height
*=
unit_scale
image
=
Image.new("RGB",
(width,
height),
(255,
255,
255))
draw
=
ImageDraw.Draw(image)
for
x,
y,
z,
r,
g,
b
in
cubes:
cube
=
create_cube(x,
y,
z,
rotate_horizontal,
rotate_vertical)
for
darken_percent,
*diamond
in
cube:
points
=
[]
for
xx,
yy,
zz
in
diamond:
xn,
yn,
zn
=
rotate_xyz(xn
*
unit_scale,
yn
*
unit_scale,
zn
*
unit_scale,
rotate_horizontal,
rotate_vertical)
x
=
w/2
+
xn
y
=
h/2
+
(yn
*
-1)
points.append((x,
y))
draw.polygon(points,
darken(r,
g,
b,
darken_percent))
if
save:
image.save(open("{}.png".format(name),
"wb"),
"PNG")
return
image
/examples/render-1.png
The
unit
of
width
and
height
will
be
cubes.
Unit
scale
is
too
much
for
demonstration
purpuse.
It
will
be
very
less
when
using
the
cubes
to
create
something
that
looks
like
not
made
of
cubes.
A
bezier
surface
will
be
rendered
with
balls
that
mades
of
cubes.
-
code:python
-
def
create_ball(r,
origin_x=0,
origin_y=0,
origin_z=0):
ball
=
[]
for
x
in
range(-r,
r):
for
y
in
range(-r,
r):
for
z
in
range(-r,
r):
if
x**2
+
y**2
+
z**2
<
r**2:
ball.append([origin_x
+
x,
origin_y
+
y,
origin_z
+
z])
return
ball
It
is
easy
to
create
a
ball
when
we
think
a
ball's
radius
is
it's
distance
to
the
origin
in
a
three-d
space.
The
ball
will
be
created
in
that
way,
starting
at
0,
0,
0
origin.
-
code:python
-
from
render
import
render
from
ball
import
create_ball
cubes
=
[]
for
x,
y,
z
in
create_ball(128):
cubes.append([x,
y,
z,
255,
255,
255])
render(
"render-2-1",
cubes,
1024,
1024,
4,
40,
20,
)
/examples/render-2.png
The
result
will
be
this.
A
bezier
surface
is
made
of
four
bezier
curves.
Lets
try
to
render
a
single
bezier
curve
and
complete
the
render
process
first.
-
code:python
-
from
render
import
render
from
ball
import
create_ball
from
bezier
import
bezier
cubes
=
[]
curve
=
[
[-60,
0,
-80],
[0,
0,
-80],
[0,
0,
60],
[60,
0,
60],
]
balls
=
200
for
i
in
range(0,
balls):
px,
py,
pz
=
bezier(i/balls,
curve[0],
curve[1],
curve[2],
curve[3])
for
x,
y,
z
in
create_ball(10):
cubes.append([px
+
x,
py
+
y,
pz
+
z,
255,
255,
255])
def
zsort(cube):
x,
y,
z,
r,
g,
b
=
cube
return
(y,
z,
x)
cubes.sort(key=zsort)
render(
"render-3",
cubes,
512,
512,
4,
40,
20,
)
Cubes
are
sorted
in
their
rendering
order
after
the
bezier
interpolation.
/examples/render-3.png
Each
interpolated
point
in
four
bezier
curves,
will
be
used
for
creating
another
curve.
So
in
that
way
it
will
be
possible
shape
a
solid
surface.
A
surface
in
this
example
has
four
curves
and
two
properties,
wide
and
fall.
Wide
is
the
one
is
longing,
and
fall
is
a
curve
that
created
by
longing
interpolated
points.
-
code:python
-
from
render
import
render
from
ball
import
create_ball
from
bezier
import
bezier
cubes
=
[]
surface
=
[
[[-40,0,-40],[4,0,-40],[40,0,-4],[40,0,40]],
[[-40,0,-16],[-6,0,-16],[16,0,6],[16,0,40]],
[[-40,-16,0],[-16,-16,0],[0,-16,16],[0,-16,40]],
[[-40,-40,0],[-16,-40,0],[0,-40,16],[0,-40,40]]
]
scale_surface
=
4
for
curve
in
surface:
for
p
in
curve:
p[0]
*=
scale_surface
p[1]
*=
scale_surface
p[2]
*=
scale_surface
wide
=
40
fall
=
40
ball_size
=
4
for
w
in
range(0,
wide):
wa
=
bezier(w/wide,
surface[0][0],
surface[0][1],
surface[0][2],
surface[0][3])
wb
=
bezier(w/wide,
surface[1][0],
surface[1][1],
surface[1][2],
surface[1][3])
wc
=
bezier(w/wide,
surface[2][0],
surface[2][1],
surface[2][2],
surface[2][3])
wd
=
bezier(w/wide,
surface[3][0],
surface[3][1],
surface[3][2],
surface[3][3])
for
f
in
range(0,
fall):
px,
py,
pz
=
bezier(f/fall,
wa,
wb,
wc,
wd)
for
x,
y,
z
in
create_ball(ball_size):
cubes.append([px
+
x,
py
+
y,
pz
+
z,
255,
255,
255])
def
zsort(cube):
x,
y,
z,
r,
g,
b
=
cube
return
(y,
z,
x)
cubes.sort(key=zsort)
render(
"render-4-3",
cubes,
1024,
1024,
4,
40,
20,
)
/examples/render-4-3.png
The
result
is
looking
like
this.
Keeping
the
wide
and
tall
parameters
less,
drawing
lines
between
sparse
points
with
their
segments
calculated
by
their
distance
may
give
better
results.
Lets
try
that.
-
code:python
-
from
render
import
render
from
ball
import
create_ball
from
bezier
import
bezier,
lerp
def
distance(a,
b):
ax,
ay,
az
=
a
bx,
by,
bz
=
b
dx
=
max(ax,
bx)
-
min(ax,
bx)
dy
=
max(ay,
by)
-
min(ay,
by)
dz
=
max(az,
bz)
-
min(az,
bz)
# the longest distance of axises will be used for interpolating the line
return
max(dx,
dy,
dz)
cubes
=
[]
surface
=
[
[[-40,0,-40],[4,0,-40],[40,0,-4],[40,0,40]],
[[-40,0,-16],[-6,0,-16],[16,0,6],[16,0,40]],
[[-40,-16,0],[-16,-16,0],[0,-16,16],[0,-16,40]],
[[-40,-40,0],[-16,-40,0],[0,-40,16],[0,-40,40]]
]
scale_surface
=
4
for
curve
in
surface:
for
p
in
curve:
p[0]
*=
scale_surface
p[1]
*=
scale_surface
p[2]
*=
scale_surface
wide
=
4
fall
=
4
ball_size
=
4
for
w
in
range(0,
wide):
wa
=
bezier(w/wide,
surface[0][0],
surface[0][1],
surface[0][2],
surface[0][3])
wb
=
bezier(w/wide,
surface[1][0],
surface[1][1],
surface[1][2],
surface[1][3])
wc
=
bezier(w/wide,
surface[2][0],
surface[2][1],
surface[2][2],
surface[2][3])
wd
=
bezier(w/wide,
surface[3][0],
surface[3][1],
surface[3][2],
surface[3][3])
wa_next
=
bezier((w+1)/wide,
surface[0][0],
surface[0][1],
surface[0][2],
surface[0][3])
wb_next
=
bezier((w+1)/wide,
surface[1][0],
surface[1][1],
surface[1][2],
surface[1][3])
wc_next
=
bezier((w+1)/wide,
surface[2][0],
surface[2][1],
surface[2][2],
surface[2][3])
wd_next
=
bezier((w+1)/wide,
surface[3][0],
surface[3][1],
surface[3][2],
surface[3][3])
for
f
in
range(0,
fall):
px,
py,
pz
=
bezier(f/fall,
wa,
wb,
wc,
wd)
for
x,
y,
z
in
create_ball(ball_size):
cubes.append([px
+
x,
py
+
y,
pz
+
z,
255,
255,
255])
a
=
bezier(f/fall,
wa,
wb,
wc,
wd)
b
=
bezier(f/fall,
wa_next,
wb_next,
wc_next,
wd_next)
c
=
bezier((f+1)/fall,
wa_next,
wb_next,
wc_next,
wd_next)
d
=
bezier((f+1)/fall,
wa,
wb,
wc,
wd)
segments_wide
=
max(distance(a,b),
distance(d,
c))
for
i
in
range(0,
int(segments_wide)):
linea
=
lerp(i/segments_wide,
a,
b)
lineb
=
lerp(i/segments_wide,
d,
c)
segments_line
=
distance(linea,
lineb)
for
j
in
range(0,
int(segments_line)):
px,
py,
pz
=
lerp(j/segments_line,
linea,
lineb)
for
x,
y,
z
in
create_ball(ball_size):
cubes.append([px
+
x,
py
+
y,
pz
+
z,
255,
255,
255])
def
zsort(cube):
x,
y,
z,
r,
g,
b
=
cube
return
(y,
z,
x)
cubes.sort(key=zsort)
render(
"render-5",
cubes,
1024,
1024,
4,
40,
20,
)
Next
step,
instead
of
showing
the
faces
with
balls
made
of
cubes,
finding
the
shaded
color
of
a
face
by
calculating
the
average
of
rendered
cubes
that
stays
in
the
area
of
it.
It
works
well,
plus
it
is
possible
to
scale
or
see
them
in
different
angles
without
a
need
to
build
and
render
again.
It
may
be
possible
to
find
shading
of
a
face,
without
need
to
render
them
with
cubes
first,
but
I
am
afraid
of
doing
that
math.
This
is
a
painter's
way
to
draw
something
on
canvas,
I
prefer
this
way
of
rendering.
Math
stuff
is
not
easy
as
it
is
written
on
black-white
blackboard,
and
most
likely
the
result
is
going
to
be
disappointment.
/examples/render-6.png
/examples/render-6-1.png
-
code:python
-
from
render
import
render
from
ball
import
create_ball
from
bezier
import
bezier,
lerp
from
PIL
import
Image,
ImageDraw
def
distance(a,
b):
ax,
ay,
az
=
a
bx,
by,
bz
=
b
dx
=
max(ax,
bx)
-
min(ax,
bx)
dy
=
max(ay,
by)
-
min(ay,
by)
dz
=
max(az,
bz)
-
min(az,
bz)
return
max(dx,
dy,
dz)
WIDTH
=
1024
HEIGHT
=
1024
UNIT_SCALE
=
2
ROTATE_H
=
40
ROTATE_V
=
20
surface
=
[
[[-40,0,-40],[4,0,-40],[40,0,-4],[40,0,40]],
[[-40,0,-16],[-6,0,-16],[16,0,6],[16,0,40]],
[[-40,-16,0],[-16,-16,0],[0,-16,16],[0,-16,40]],
[[-40,-40,0],[-16,-40,0],[0,-40,16],[0,-40,40]]
]
scale_surface
=
4
for
curve
in
surface:
for
p
in
curve:
p[0]
*=
scale_surface
p[1]
*=
scale_surface
p[2]
*=
scale_surface
wide
=
10
fall
=
10
ball_size
=
2
faces
=
[]
for
w
in
range(0,
wide):
wa
=
bezier(w/wide,
surface[0][0],
surface[0][1],
surface[0][2],
surface[0][3])
wb
=
bezier(w/wide,
surface[1][0],
surface[1][1],
surface[1][2],
surface[1][3])
wc
=
bezier(w/wide,
surface[2][0],
surface[2][1],
surface[2][2],
surface[2][3])
wd
=
bezier(w/wide,
surface[3][0],
surface[3][1],
surface[3][2],
surface[3][3])
wa_next
=
bezier((w+1)/wide,
surface[0][0],
surface[0][1],
surface[0][2],
surface[0][3])
wb_next
=
bezier((w+1)/wide,
surface[1][0],
surface[1][1],
surface[1][2],
surface[1][3])
wc_next
=
bezier((w+1)/wide,
surface[2][0],
surface[2][1],
surface[2][2],
surface[2][3])
wd_next
=
bezier((w+1)/wide,
surface[3][0],
surface[3][1],
surface[3][2],
surface[3][3])
for
f
in
range(0,
fall):
a
=
bezier(f/fall,
wa,
wb,
wc,
wd)
b
=
bezier(f/fall,
wa_next,
wb_next,
wc_next,
wd_next)
c
=
bezier((f+1)/fall,
wa_next,
wb_next,
wc_next,
wd_next)
d
=
bezier((f+1)/fall,
wa,
wb,
wc,
wd)
faces.append([a,
b,
c,
d,
255,
255,
255])
def
zsort(cube):
x,
y,
z,
r,
g,
b
=
cube
return
(y,
z,
x)
faces_rendered
=
[]
for
a,
b,
c,
d,
*rgb
in
faces:
cubes
=
[]
segments_wide
=
max(distance(a,b),
distance(d,
c))
for
i
in
range(0,
int(segments_wide)):
linea
=
lerp(i/segments_wide,
a,
b)
lineb
=
lerp(i/segments_wide,
d,
c)
segments_line
=
distance(linea,
lineb)
for
j
in
range(0,
int(segments_line)):
px,
py,
pz
=
lerp(j/segments_line,
linea,
lineb)
for
x,
y,
z
in
create_ball(ball_size):
cubes.append([px
+
x,
py
+
y,
pz
+
z,
*rgb])
cubes.sort(key=zsort)
img
=
render(
"face",
cubes,
WIDTH,
HEIGHT,
UNIT_SCALE,
ROTATE_H,
ROTATE_V,
False
)
widthimg,
heightimg
=
img.size
pixels
=
img.load()
shade_len
=
0
sum_r
=
0
sum_g
=
0
sum_b
=
0
for
x
in
range(widthimg):
for
y
in
range(heightimg):
rgb
=
pixels[x,
y]
if
not
(rgb[0]
==
255
and
rgb[1]
==
255
and
rgb[2]
==
255):
#
white
is
the
background
color
sum_r
+=
rgb[0]
sum_g
+=
rgb[1]
sum_b
+=
rgb[2]
shade_len
+=
1
avg_r
=
sum_r
/
shade_len
avg_g
=
sum_g
/
shade_len
avg_b
=
sum_b
/
shade_len
faces_rendered.append([a,
b,
c,
d,
[int(avg_r),
int(avg_g),
int(avg_b)]])
name
=
"render-6-1"
width
=
WIDTH
*
UNIT_SCALE
height
=
HEIGHT
*
UNIT_SCALE
image
=
Image.new("RGB",
(width,
height),
(255,
255,
255))
draw
=
ImageDraw.Draw(image)
faces_on_canvas
=
[]
for
a,
b,
c,
d,
rgb
in
faces_rendered:
points
=
[]
for
x,
y,
z
in
[a,
b,
c,
d]:
xn,
yn,
zn
=
rotate_xyz(x
*
unit_scale,
y
*
unit_scale,
z
*
unit_scale,
rh,
rv)
points.append((xn,
yn,
zn))
faces_on_canvas.append([points,
rgb])
def
centerz_sort(face):
points,
rgb
=
face
sumz
=
0
for
x,
y,
z
in
points:
z
+=
sumz
return
sumz
/
len(points)
faces_on_canvas.sort(key=centerz_sort)
for
face
in
faces_on_canvas:
points,
rgb
=
face
points_wo_z
=
[]
for
x,
y,
z
in
points:
points_wo_z.append((
width/2
+
x,
height/2
+
y*-1))
draw.polygon(points_wo_z,
tuple(rgb))
image.save(open("{}.png".format(name),
"wb"),
"PNG")
/examples/balconykarmelicka.png
/examples/sink.png
/examples/potato.png
/examples/coffee.png
/examples/pizza.png