Monday, 25 September 2017

ABAP Ray Tracer – Part 2 – The Overloader

Objectives


In this blog you’re gonna get to know the infrastructure powering this project, the content of the chapters, which I have read so far, plus certain C++ language features explained, which are missing in ABAP.

Project Infrastructure


Development Environment

The SAP system is running on a virtual machine powered by VirtualBox.

◉ SAP AS ABAP NW 7.5 SP02 ASE Development Edition
◉ 6 GB RAM
◉ 2 CPUs
◉ 200 GB Harddisk
◉ openSuse 42.2

◉ The coding gets done with Eclipse Mars (4.5.2) with the ABAP Development Tools.
◉ abapGit is used for code versioning and sharing and is hosted here: https://github.com/pixelbaker/ABAP-RayTracer


Package Structure


SAP ABAP Tutorials and Materials, ABAP Certifications, SAP ABAP Guides
ABAP

SAP ABAP Tutorials and Materials, ABAP Certifications, SAP ABAP Guides
C++

The package structure mimics the folder structure of the minimal ray tracer provided by the author and will grow with each chapter.

I am especially fond of the project acronym: ART

Not only does it stand in good SAP tradition using three letters, it also reflects that computer generated images can often be art.

As a special gimmick, when the customer space Z is added to the mix, then we end up with the word “ZART“, which is German for tender, delicate, gentle. A particular nice German word if you ask me.

The Book

I try to structure my blogs in regard to the chapters of the book “Ray Tracing from the Ground Up”.

Chapter 1: Ray Tracer Design and Programming

It starts by explaining the choice of using an object oriented programming language and in particular C++, and certain coding styles the book follows. Also, it gives tips on how to efficiently debug your renderer later on.

Chapter 2: Some Essential Mathematics

It then gets you up to speed by familiarising you with some of the math needed for a ray tracer, like understanding the differences between vectors, points and normals.

Chapter 3: Bare-Bones Ray Tracing

And then the party starts. The chapter helps you to understand…

◉ how ray casting works
◉ how rays are defined
◉ how to intersect a ray with a plane and a sphere
◉ how the structure of a simple ray tracer works

Ultimately you end up implementing a ray tracer that can render orthographic views of an arbitrary number of planes and spheres.

And the implementation with ABAP is the challenge I will constantly phase and the remainder of this blog will concentrate on.

Language Differences


On my quest writing a ray tracer, I am confronted with C++ language features, which ABAP hasn’t.
I needed to find alternative solutions to not drag my ray tracer into chaos right from the start.

In my blogs I will regularly shed light on certain pearls of language differences by showing my findings and conclusions.

Overloading


In C++ you can declare the same method name multiple times as long as the number or types of its parameters differ. The appropriate method gets called, based on the supplied parameters.

The advantage is cleaner code down the road. Less making up of method names to describe a commonality and differentiation, like set_center_by_point( point ),
set_center_by_components( i_x = x i_y = y ), … when set_center would suffice.

As ABAP doesn’t have this language feature in stock, I needed to come up with a viable workaround.

In the following section I’m gonna guide you through my attempts to find solutions for method, constructor and operator overloading.

Method Overloading


I was facing the following situation. Two methods with the same name, both setting the center of the sphere via two differnt sets of input parameters.

class Sphere {
  public:
    ...
    void set_center(const Point3D& c);
    void set_center(const double x, const double y, const double z);

I am going to simplify now to make my case more clear, by reducing the problem to a 2D circle.

I could come up with three solutions, all with their set of advantages and disadvantages.

We have a class defining a circle, and we want to set the center coordinates of this circle.

CLASS cl_circle DEFINITION.
  ...
  PRIVATE SECTION.
    DATA:
      _center TYPE zcl_point2d.

Optional

One solution is to create a method which takes all the input-parameter-sets and uses OPTIONAL to allow the user to decide which set he wants to supply.

METHODS:
  set_center
    IMPORTING
      i_point TYPE zcl_point2d OPTIONAL
      i_x TYPE decfloat16 OPTIONAL
      i_y TYPE decfloat16 OPTIONAL.

This often involves to check which set of input parameters was supplied.

METHOD set_center.
  IF i_point IS SUPPLIED.
    "set center
    RETURN.
  ENDIF.

  IF i_x IS SUPPLIED AND i_y IS SUPPLIED.
    "set center
    RETURN.
  ENDIF.
ENDMETHOD.

The user can make these kind of calls.

circle->set_center( i_point = point ).
circle->set_center( i_x = x  i_y = y ).
circle->set_center( i_x = x  i_y = y  i_point = point ).
circle->set_center( i_x = x  i_point = point  ).
circle->set_center( ).
The compiler wouldn’t complain about the last three.
Also if the RETURN is omitted it might overwrite the center with the later input set.
A user has a hard time by just looking at the method signature to grasp the idea of choice via input sets.

Dynamic

Ahh, nice. Only one set of input parameters. But hey, what should I shove in here?

METHODS:
  set_center
    IMPORTING
      i_coordinates TYPE data.

Working with dynamic input parameters would involve some code like this.

METHOD set_center.
  ASSIGN i_coordinates TO FIELD-SYMBOL(<coordinates>).
  DATA(typedescr) = cl_abap_typedescr=>describe_by_data( <coordinates> ).
  IF typedescr->absolute_type = '\TYPE=XY_STRUCTURE'.
    "set center
  ELSEIF typedescr->typekind_oref.
    "further checks if cl_point2d
    "set center
  ENDIF.
ENDMETHOD.

You can put anything in there. The compiler wouldn’t complain. Only at runtime you will face justice.

circle->set_center( VALUE xy_structure( x = x  y = y ) ).
circle->set_center( point ).
circle->set_center( unsupported_type ).
If the method got some documentation and/or the user looks into the method, he might get an idea of which valid input sets are possible. Also set_center must make sure, that unsupported types are handled properly, like throwing an exception.

Multiple Methods

Ok, so why not do the obvious and create for each input set a method.
That’s when you start to get creative with method names, but so help you god when you come close to the holy 30 character limit.

METHODS:
  set_center_by_point
    IMPORTING
      i_point TYPE cl_point2d,

  set_center_by_components
    IMPORTING
      i_x type decfloat16
      i_y type decfloat16.

The implementation is actually pretty straight forward, as one would like.

METHOD set_center_by_point.
  "set center
ENDMETHOD.

METHOD set_center_by_components.
  "set center
ENDMETHOD.
So, is calling the methods for the user.

circle->set_center_by_point( point ).
circle->set_center_by_components( i_x = x  i_y = y ).

The compiler would cry out if you haven’t supplied the right parameters.
The only drawback I see is dealing with the various method names.
Also the definition looks a bit noisy for my taste.

That will be the technique I will rely on most of the time during my ray tracer endeavour.

Constructor Overloading


The following piece of code shows the starting part of the public section of the C++ class Vector3D.
It allows for six different ways of constructing a Vector3D instance.

class Vector3D {
  public:
    double x, y, z;

    Vector3D(void);                            // default constructor
    Vector3D(double a);                        // constructor for unified components
    Vector3D(double _x, double _y, double _z); // constructor for individual components
    Vector3D(const Vector3D& v);           // copy constructor
    Vector3D(const Normal& n);             // constructs a vector from a Normal
    Vector3D(const Point3D& p);            // constructs a vector from a point

Constructors are a bit special in comparison to a regular method, like set_center. Special in the sense, that they get invoked by NEW or CREATE OBJECT, unless you have a bold plan then you might
use super->constructor().

First Attempt: OPTIONAL

My first run to mimic the intended behaviour, was to use the OPTIONAL solution as described above under Method Overloading.

The IS SUPPLIED statements intend to find out which constructor is desired to be called.
And the ASSERT statements make sure all parameters needed for the construction are in tip top shape.

CLASS zcl_art_vector3d DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC.

  PUBLIC SECTION.
    DATA:
      x TYPE decfloat16 READ-ONLY,
      y TYPE decfloat16 READ-ONLY,
      z TYPE decfloat16 READ-ONLY.


    METHODS:
      constructor
        IMPORTING
          VALUE(i_value) TYPE decfloat16 OPTIONAL
          VALUE(i_x) TYPE decfloat16 OPTIONAL
          VALUE(i_y) TYPE decfloat16 OPTIONAL
          VALUE(i_z) TYPE decfloat16 OPTIONAL
          REFERENCE(i_vector) TYPE REF TO zcl_art_vector3d OPTIONAL
          REFERENCE(i_normal) TYPE REF TO zcl_art_normal OPTIONAL
          REFERENCE(i_point) TYPE REF TO zcl_art_point3d OPTIONAL,

...

CLASS ZCL_ART_VECTOR3D IMPLEMENTATION.
  METHOD constructor.
    "constructor for unified components
    IF i_value IS SUPPLIED.
      x = y = z = i_value.
      RETURN.
    ENDIF.

    "constructor for individual components
    IF i_x IS SUPPLIED OR
       i_y IS SUPPLIED OR
       i_z IS SUPPLIED.

      ASSERT i_x IS SUPPLIED AND i_y IS SUPPLIED AND i_z IS SUPPLIED.

      x = i_x.
      y = i_y.
      z = i_z.
      RETURN.
    ENDIF.


    "Constructs a vector from a point
    IF i_point IS SUPPLIED.
      ASSERT i_point IS BOUND.

      x = i_point->x.
      y = i_point->y.
      z = i_point->z.
      RETURN.
    ENDIF.


    "Copy Constructor
    IF i_vector IS SUPPLIED.
      ASSERT i_vector IS BOUND.

      x = i_vector->x.
      y = i_vector->y.
      z = i_vector->z.
      RETURN.
    ENDIF.


    "Constructs a vector from a Normal
    IF i_normal IS SUPPLIED.
      ASSERT i_normal IS BOUND.

      x = i_normal->x.
      y = i_normal->y.
      z = i_normal->z.
      RETURN.
    ENDIF.


    "Default constructor
    x = y = z = '0.0'.
  ENDMETHOD.

This allows me to use the keyword NEW or CREATE OBJECT but comes with a full bag of draw backs.

The method signature isn’t intuitive. It leaves the user with no choice, but to look into the implementation.
And certainly with all the checking going on, it is more performance intensive than its C++ counterpart.


Second Attempt: Static methods


An alternative approach: The constructor must be refactored into individual static methods, which in turn set what they have to set on the vector instance after the default constructor was used.

CLASS zcl_art_vector3d DEFINITION
  PUBLIC
  FINAL
  CREATE PRIVATE. "<- Not PUBLIC anymore

  PUBLIC SECTION.
    DATA:
      ...

    CLASS-METHODS:
      new_default
        RETURNING
          VALUE(r_vector) TYPE REF TO zcl_art_vector3d,

      new_unified
        IMPORTING
          VALUE(i_value) TYPE decfloat16
        RETURNING
          VALUE(r_vector) TYPE REF TO zcl_art_vector3d,

      new_individual
        IMPORTING
          VALUE(i_x) TYPE decfloat16
          VALUE(i_y) TYPE decfloat16
          VALUE(i_z) TYPE decfloat16
        RETURNING
          VALUE(r_vector) TYPE REF TO zcl_art_vector3d,

      new_copy
        IMPORTING
          REFERENCE(i_vector) TYPE REF TO zcl_art_vector3d
        RETURNING
          VALUE(r_vector) TYPE REF TO zcl_art_vector3d,

      new_by_normal
        IMPORTING
          REFERENCE(i_normal) TYPE REF TO zcl_art_normal
        RETURNING
          VALUE(r_vector) TYPE REF TO zcl_art_vector3d,

      new_by_point
        IMPORTING
          REFERENCE(i_point) TYPE REF TO zcl_art_point3d
        RETURNING
          VALUE(r_vector) TYPE REF TO zcl_art_vector3d.

Full implementation

I would gain static compile time checks for the parameters and no need for the RETURN and IS SUPPLIED statements inside the method implementation.
It is more explicit, in telling which constructor needs which input, making it less error prone.

‘Instantiating’ looks from my code aesthetic point-of-view a less appealing, but on the other hand the i_point = my_point is gone, which gave me an itch before.

"With constructor method using OPTIONAL
DATA(vector) = NEW zcl_art_vector3d( i_point = my_point ).
DATA(vector) = NEW zcl_art_vector3d( i_x = 1 i_y = 2 i_z = 3 ).

"With static helper methods
DATA(vector) = zcl_art_vector3d=>new_by_point( my_point ).
DATA(vector) = zcl_art_vector3d=>new_individual( i_x = 1 i_y = 2 i_z = 3 ).

Operator Overloading


By now you might feel a bit overloaded, But fasten your seat belts, the royal league of overloading is about to appear: Overloading of operators

When the C++ compiler encounters an operator, such as +, -, * or /, it looks for a function with the name operator+, operator-, operator* or operator/, respectively, that takes the appropriate parameters. That way it is possible for the programmer to define how certain classes can use arithmetic calculations with common notations one is used to when working with integers for example.

Look at this beautiful math statement below. It’s part of the hit function to determine if a ray hits the surface of a sphere.

// C++
double t;
Vector3D temp;

sr.normal = (temp + t * ray.direction) / radius;

You have to imagine, the +, * and / are operations done between classes and/or floats.
ABAP/C++ doesn’t know how to add, multiply, … to your classes, you have to define that.

To get the t * ray.direction calculated, the following code gets run.
This function doesn’t live inside a class, but in the global scope!

// inlined non-member function
// multiplication by a double on the left
Vector3D  operator* (const double a, const Vector3D& v);

inline Vector3D operator* (const double a, const Vector3D& v) {
  return (Vector3D(a * v.x, a * v.y, a * v.z));
}

And the next piece of code multiplies a float value on the right to vector (e.g. ray.direction * t), but must live inside a class.

You just have to love C++.

// inlined member function
// multiplication by a double on the right
Vector3D operator* (const double a) const;

inline Vector3D Vector3D::operator* (const double a) const {
  return (Vector3D(x * a, y * a, z * a));
}

And that ugly monster is what I made out of the previous math statement.

"ABAP
c_shade_rec->normal->assignment_by_vector(
  i_ray->direction->get_product_by_decfloat( t
    )->get_sum_by_vector( temp
    )->get_quotient_by_decfloat( _radius ) ).

There are two problems I had to solve with my method names.

1. I needed to differentiate the input set types (e.g. _by_vector, _by_decfloat).
2. And I needed to communicate that the result of the operation is not applied to the instance, but returned as a new instance.

It’s a nightmare, totally decreases the readability. One might consider helper methods, I didn’t test it, but I don’t picture it to add more clarity.

I probably should consider splitting up the equation.

DATA(vector) = i_ray->direction->get_product_by_decfloat( t ).
vector = vector->get_sum_by_vector( temp ).
vector = vector->get_quotient_by_decfloat( _radius ).
c_shade_rec->normal->assignment_by_vector( vector ).

I guess I’ll do that from now on.

No comments:

Post a Comment