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.
Skipping
the
not
visible
parts
may
affect
the
glass-like
surfaces,
but
I
am
willing
to
create
only
solid
things.
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
a
diamond
shape.
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):
xn,
yn,
zn
=
rotate_xyz(x
*
unit_scale,
y
*
unit_scale,
z
*
unit_scale,
rh,
rv)
return
(int(w/2+xn),
int(h/2+(yn*-1)))
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
the
cube
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):
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))
image.save(open("{}.png".format(name),
"wb"),
"PNG")
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):
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([x,
y,
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(16):
cubes.append([x,
y,
z,
255,
255,
255])
render(
"render-2",
cubes,
128,
128,
16,
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
import
math
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
the
curve
that
created
by
interpolated
points.
from
render
import
render
from
ball
import
create_ball
from
bezier
import
bezier
import
math
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
=
10
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(5):
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-1",
cubes,
1024,
1024,
2,
40,
20,
)
The
result
is
looking
like
this.
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
I
keep
the
rendering
part
in
Python.
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.
examples/pan-w-eggs-done.png
examples/hot-dog.png
examples/mushroom.png
Good
luck
playing
with
that.