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.
def
create_cube(x,
y,
z,
rotate_horizontal,
rotate_vertical):
#
the
cube
will
be
drawn
in
a
square
form
if
the
scenery
is
in
two-dimension
if
rotate_vertical
==
0
and
rotate_horizontal
==
0:
return
[
[
0,
#
this
value
is
the
darkening
amount
of
shape.
#
the
color
will
stay
as
it
is
in
two-d.
[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],
]
]
#
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,
[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.
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
rotate
function
that
rotates
x-z
horizontally,
then
y-z
in
vertical
axis
will
be
needed.
from
rotate
import
rotate_xyz
def
xyz_on_canvas(x,
y,
z,
unit_scale,
w,
h,
rh,
rv,
include_z=False):
xn,
yn,
zn
=
rotate_xyz(x
*
unit_scale,
y
*
unit_scale,
z
*
unit_scale,
rh,
rv)
x_on_canvas
=
w/2
+
xn
y_on_canvas
=
h/2
+
(yn*-1)
if
include_z:
return
(x_on_canvas,
y_on_canvas,
z)
return
(x_on_canvas,
y_on_canvas)
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.
def
darken(r,
g,
b,
percent):
rblacks
=
int(r
*
(percent/100))
gblacks
=
int(g
*
(percent/100))
bblacks
=
int(b
*
(percent/100))
return
(max(r
-
rblacks,
0),
max(g
-
gblacks,
0),
max(b
-
bblacks,
0))
A
darken
function
like
above
will
be
needed
in
order
to
make
a
cube's
faces
visually
differentiable.
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.
from
create_cube
import
create_cube
from
xyz_on_canvas
import
xyz_on_canvas
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:
x,
y
=
xyz_on_canvas(xx,
yy,
zz,
unit_scale,
width,
height,
rotate_horizontal,
rotate_vertical)
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
The
unit
of
width
and
height
will
be
cubes.
examples/render-1.png
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.
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
very
easy
to
create
a
ball
when
we
think
a
ball's
radius
is
the
distance
to
the
origin
in
a
three-d
space.
The
ball
will
be
created
in
that
way,
starting
at
0,
0,
0
origin.
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,
)
The
result
will
be
this.
examples/render-2.png
A
bezier
surface
is
made
of
four
bezier
curves.
Lets
try
to
render
a
single
bezier
curve
and
complete
the
render
process
first.
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
evaluation.
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
interpolated
points.
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
=
200
fall
=
200
ball_size
=
2
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,
2,
40,
20,
)
The
result
is
looking
like
this
when
the
wide
way
more
than
fall.
examples/render-4-1.png
Here
is
when
the
fall
is
higher
than
the
wide.
examples/render-4-2.png
When
they
are
both
higher,
the
surface
will
be
visible.
examples/render-4-3.png
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.
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
#
between
a
and
b.
no
square
root
calculation
needed
here.
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
=
2
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,
2,
40,
20,
)
In
that
way
we
will
get
rid
of
sparse
dots
when
rendering
something.
examples/render-5.png
Next
step,
instead
of
showing
the
faces
with
balls
made
of
cubes,
finding
the
shading
of
a
face
by
getting
the
average
color
of
rendered
cubes.
I
tried
that
it
works
well,
however
I
like
cube
rendered
faces
more
than
this
one.
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
painters
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
from
render
import
render
from
ball
import
create_ball
from
bezier
import
bezier,
lerp
from
xyz_on_canvas
import
xyz_on_canvas
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
=
4
fall
=
4
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"
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]:
points.append(xyz_on_canvas(x,
y,
z,
UNIT_SCALE,
width,
height,
ROTATE_H,
ROTATE_V,
True))
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((x,
y))
draw.polygon(points_wo_z,
tuple(rgb))
image.save(open("{}.png".format(name),
"wb"),
"PNG")
The
render
part
is
written
in
Python
language.
I
edit
the
surfaces
by
an
application
that
runs
on
a
browser
written
with
javascript
when
I
am
creating
something.
Here
are
some
examples
that
I
rendered
in
this
way,
with
cubes.
examples/balconykarmelicka.png
examples/sink.png
examples/pan-w-eggs-done.png
examples/sandwich.png
examples/sandwich1x.png
Good
luck
playing
with
that.