Structs, Static Members, and the Anatomy of a Ray Tracer

7 August 2026

Structs did not struct me!!!

#include <iostream> 
#include <string>
using namespace std;
struct YoutubeChannel{
    string Name;
    int Subscribers_Count;
    YoutubeChannel(string name, int subscriberscount){
        Name = name;
        Subscribers_Count = subscriberscount;
    }
};
int main(){
    YoutubeChannel YT1= YoutubeChannel("CodeBeauty",75000);
    cout << YT1;
}

The above code chunk looks like something the compiler would run. But SIKE!!!! It won’t. Why?

  • compiler has no idea how to print a YoutubeChannel datatype since it is not a built in datatype

  • you neeed to do something known as Operator Overloading in order to make this work

in python if a user defined data type doesn’t have a function, we build one for it

#python
def print_channel(stream, channel):
    stream.write("Name:" + channel.name)

print_channel(sys.stdout,channel)                      

similarly in cpp we write

//cpp
ostream& operator<<(ostream& COUT, YoutubeChannel& ytchannel){
    COUT << "Name: " << ytchannel.Name << endl;
    COUT << "Subscribers Count: " << ytchannel.Subscribers_Count << endl;
    return COUT;
}

⚠️ What does it mean to return cout

I initially thought return COUT and return cout were the same thing, both are returning cout right? But they’re not.

COUT is just the parameter name for whatever stream was passed in. cout is specifically the global standard output stream. And ostream& can accept more than just cout. It accepts anything that inherits from ostream:

  • cout — standard console output
  • cerr — standard error output
  • ofstream — file output
  • ostringstream — string output

So COUT is basically a placeholder for whichever stream gets passed in. If someone passes a file stream

ofstream file("output.txt");
file << YT1;

return COUT passes the file stream forward chaining works [ calling << multiple times]. return cout will expect only cout to be returned. This will break.

So the rule is simple — return whatever came in, not a hardcoded stream.

⚠️ What is ostream& — why not just ostream?

ostream& means we’re taking the stream by reference, not making a copy of it. Think of COUT like a google doc. ostream& passes the actual doc around — everyone writes to the same place. ostream makes a xerox copy each time — you end up writing to a throwaway copy and the output gets lost.

⚠️ difference between structs and class

data members in structs are public by default while in class they are private.

Static & Non-Static

A function defined within a class, acting on the data members defined inside the class is called as a non-static member function

non static data member is a data member of a class/struct that is unique for all it’s instances.

#include <iostream>
#include <string>

// Define the Car structure
struct Car {
    // Non-static data members (each Car object gets its own copy)
    std::string model;
    int speed;

    // Non-static member function
    // It operates on the specific object that calls it
    void accelerate(int increment) {
        speed += increment;
        std::cout << model << " accelerated by " << increment 
                  << " mph. Current speed: " << speed << " mph.\n";
    }
};

int main(){
    Car car1;
    car1.model="Sedan";
    car1.speed=20;
    car1.accelerate(15);
    return 0;
}

In the above example, speed is unique for each object of class Car.

Static Function work only on static data members i.e a data member of a class whose value is same for all instances of a class.

struct YoutubeChannel {
    string Name;
    static int Count;

    YoutubeChannel(string name, int subscriberscount){
        Name = name;
        Count++;
    }

    void printAll() {
        cout << this->Name;
        cout << Count;
    }

    static void printCount() {
        cout << Count;
        cout << Name;  // error ❌
    }
};

int YoutubeChannel::Count = 0;  // initialize outside

YoutubeChannel YT1("CodeBeauty", 75000);  // Count = 1
YoutubeChannel YT2("Fireship", 200000);   // Count = 2

YT1.printAll();
YT2.printAll();
YoutubeChannel::printCount();

Why non static function works with both non static and static data member but this is not the same case for static function?

It is because of something called as this.

When we run YT1.printall() and YT2.printall()consecutively the compiler sees it the following way

printAll(&YT1);
printAll(&YT2);

So inside printAll

cout << this -> Name;
cout << Count;

this = &YT1 at the first instance of the function and &YT2 in the second instance.Count is the same reference for both instances, so that is not an issue.

But in the case of YouTubeChannel::printCount() the compiler reads it differently

No object is involved

YoutubeChannel::printCount();

inside it

cout << Count;
cout << Name; 

Count doesn’t depend on the instance, it shares the same address for all instances of this struct. But for Name it is different. We declare a Name for each instances.

Does this-> have a technical jargon?

Yeppers, this-> is called an implicit parameter. The parameter in the above example is &YT1 and &YT2 which was discreetly passed into this-> and it just infers which instance it is refering to. It is passed implicitly, so yeah a pretty dumb name if the definition is known. Otherwise it sounds intimidating.

Header classes, how are rendered images stored , etc..

Header files are files where you declare things - functions, classes, structs - that you want to use across multiple .cpp files.

So basically user defined libraries in python, where you import functions, classes written in another file to your main file.

But there is a difference between how functions from other files are used in cpp and python.

In python, when you say import math you are making a reference to the library.

For instance: There is a file called

# main.py
import math
math.add(1,2)

And another file called

# utils.py
import math
math.add(3,4)

main.py and utils.py are pointing to the same math in memory. No duplication.

In CPP files are processed differently.

For example there are 3 files (one header file, 2 cpp files)

// main.cpp
#include "math.h"

int main(){
    add(10,5);
    return 0;
}

// utils.cpp
#include "math.h"

void summation(int x,int y){
    add(2,3);
}

Now if inside math.h if you declare the func add, along with its definition. Lets’s see how the cpp compiler processes it if main.cpp and utils.cpp uses add function.

#ifndef MATH_H
#define MATH_H

int add(int a, int b){
    return a+b;

#endif
}

STAGE 1 - Pre-processor

main.cpp and utils.cpp include math.h.

Note: Header guards (#ifndef MATH_H and #define MATH_H) only prevent the same file from including twice in the same file. So if I write #include "math.h" twice within main.cpp, header guards will only prevent it from printing it twice while compiling it.

STAGE 2 - Compiler

Compiler compiles each file independently into object files

main.cpp -> main.o (has add body)
utils.cpp -> utils.o (has add body)

No error yet - compiler is still processing at separate files

STAGE 3 - Linker

Combines main.o and utils.o while processing - sees add defined in both.

Error: multiple definition of add 

what is the solution?

  • we can write only the template in math.h and define the function in math.cpp. So what math.h tells is a promise that a function add exists. Now you have 4 files to run math.h, math.cpp, main.cpp and utils.cpp. So when the compiler compiles all files separtely, the template is printed multiple times but the actual function is compiled only once, which is at math.cpp. The linker figures out where the function is and runs it, as it has been promised by math.h that it exists.

  • we can use something called as an inline keyword. Instead of writing the function in a separate file called math.cpp, we write in math.h

    #ifndef MATH_H
    #define MATH_H
    
    inline int add(int a, int b){
        return a+b;
    }
    
    #endif

    when we run the files, the add function will be written twice (in main.cpp and utils.cpp) but inline tells the linker that the function definition will be written multiple times, choose one and ignore the rest.

Anatomy of a ray tracing engine

This is my rough notes for the book ray tracing engine in a weekend by Peter Shirley, Trevor D Black and Steve Hollasch. The initial drafts of these notes are going to be pretty chaotic and I will update it periodically.

In the book they start showing us how rendered(created by computer) images are being stored are seen by computer. Peter starts with talking about PPM format to store an image. Basically it looks like a list of 3 element arrays (RGB).

    int image_width = 256;
    int image_height = 256;

    // Render
    for (int j = 0; j < image_height; j++) {
        for (int i = 0; i < image_width; i++) {
            auto r = double(i) / (image_width-1);
            auto g = double(j) / (image_height-1);
            auto b = 0.0;

            int ir = int(255.999 * r);
            int ig = int(255.999 * g);
            int ib = int(255.999 * b);

            std::cout << ir << ' ' << ig << ' ' << ib << '\n';
        }
    }

Commands to run this

 >> g++ hello_world.cpp -o hello_world
 >> ./hello_world> image.ppm
    Done                           
 >> eog image.ppm

so what is basically happening ?

I am setting blue color to 0 and the red pixel intensity value will increase along the width and the green pixel intensity value will increase along the height of a pixel square (dimensions will be integers) of length 255 (256-1).

What am I getting?

green red square

in ppm format

251 255 0
252 255 0
253 255 0
254 255 0
255 255 0

as we know red light and green light combined gives yello

building a viewport with a blue gradient and a red sphere

After showing us how a ppm format works the author talks about something about building a viewport.

Viewport: a virtual rectangle in 3D space it defines what the camera sees not visible itself - just a mathematical boundary.

Header Files

vec3

Peter starts with creating a header file called as vec3. It is the basic data-structure we are going to use to render the pixel, and interact with it.

class vec3(){
    public:
    // declare data structure vec3 by declaring a 3 element array
    Constructor: 
    - vec3() -> default initializes all components to 0
    - vec3(double e0, double e1, double e2) -> builds 3 values in an array

    Accessors(getter functions):
    - x(), y(),z()- returns e[0],e[1],e[2] respectively

    Operators:    
    - operator-() -> unary negation, takes vec3 v returns vec3 -v
    - operator[](int i) const -> reads componenent (const version, read only, returns copy)
    - operator[](int i) -> can modify original array i.e references original value
    - operator+=(const vec3&v) -> adds another vector into this one, in place
    - operator*=(double t)-> scales vec3 by t
    - operator/=(double t) -> scales vec3 by 1/t
    
    Length:
    - length_squared() -> squared distance of length
    - length() -> absolute value of the return of the sum of the squared components
};

using point3=vec3;//we are declaring an alias for vec3 which will be used in other header files

// utility functions
- operator<< -> print a vector
- operator+ -> add two vectors
- operator- -> subtract two vectors
- operator* -> vector*vector -> component wise multiply
- operator* -> scalar*vector and vector*scalar -> scale
- operator/ -> division by scalar
- dot - gives a single number (dot product/scalar product) -> returns a double
- cross - cross product -> (returns a vec3)
- unit vector -> normalize to length 1
  • An instance of vec3 represents a vector in a 3D coordinate. In the class Peter has defined two types of constructors (a spl func that initialises values for an obj of a class)

  • There is a default constructor that sets a vec3 instance with zero value and a parameterized constructor that initializes an object of vec3 with the values you passed to it.

  • Within the class, Peter has also had defined a getter function. As the name suggests a getter function basically returns the value of a parameter that is private. Here we use const, to not modify the value and make it only read only value.

  • const is a keyword in cpp that you pass as a prefix to a function or a variable, to indicate that the value cannot be modified. There are some specific criterias to using these const functions/methods and variables.
    Let us have a look at few examples:

    • a const object can only call a method with const marked on it
    double length_squared() const:{e[0]*e[0]+e[1]*e[1]+e[2]*e[2]} {}
    const vec3 v_1(1.0,3.0,4.0);
    double a = v_1.length_squared(); // will work because length_squared has const marked in it
    v_1[0] = 5.0; // Error, as v is a const object and we can only apply methods with const marked on it
    • a non-const object can call non-const and const methods
    vec3 v_2(6.0,6.0,7.0); // non-const object
    double a = v_2.length_squared(); // const method
    v_2[0] = 3.8; //non-const method
    • NOTE : an interesting case of const in utility functions
    double operator[](int i) const{return e[i]};
    double& operator[](int i) {return e[i]};

    There are two picking an element based on index ’[ ]’ functions. A const object automatically picks the one in the top. If it picks that, the method allows not to modify the value. It is read only. If a non-const object is used, it picks the second ’[ ]’ function, it modifies the original arrray not the copy of the array as we are calling it by reference.

  • I have to understand the differences between vec3, vec3* and vec3& are different.

    • vec3 as a type declaration denotes just the object
    vec3 a(1.0,2.0,3.0);
    • vec3* ptr holds a memory address, can be null(null ptr), can be reassigned to point something else.
    vec3* ptr = &a
    • vec3& ref is an alias for an existing object, no new memory is created to store it and it has same object but different name.
    vec3& ref = a;
  • macros are shortcuts or placeholders that the pre-processor replaces before the code is compiled

// syntax
// #define MACRO_NAME macro_definition

#include <iostream>
using namespace std;
#define SQUARE(x) (x*x)
int main()
{
    int n = 7;
    int result = SQUARE(n);
    cout << result <<endl;
    return 0;
}
  • Why did I talk about MACRO? To understand NULL. NULL is a macro defined as the integer 0. It is inherited from C and is used to represent an empty ptr. It it not type safe i.e. the compiler can confuse it with an integer 0.
int* ptr = NULL;

ptr holds address 0 => meaning it points to nothing but as we know NULL is #define NULL 0

// so it is same as
int* ptr = 0;
  • Is there a typesafe version of NULL? cpp11 introduced a keyword called nullptr. It actually represents a ptr, unlike NULL. It has it’s own type std::nullptr-t which is distinct from any integer type. This makes it type-safe. The compiler can always tell it is a pointer and never confuse it with an integer
int* ptr = nullptr;
// clearly a pointer pointing to nothing
  • Why does NULL==nullptr evaluate to true? NULL is just 0 and nullptr converts to 0 when compared - so their values are equal. But nullptr is type safe so it is better.
cout<<NULL==nullptr
// >> 1
  • when does it create a problem?
void foo(int x) {
    cout<< "integer version ";
    }
void foo(char* x) {
    cout<<"ptr version";
}


foo(0); //compiler picks foo(int x)
foo(NULL); // compiler might pick foo(int x) when we want it to pick foo(char* x) as NULL is intended to be a ptr
foo(nullptr); //compiler pickes foo(char*) as nullptr is type safe
  • how do you give value to a ptr?
vec3* ptr = &a;

ptr is of type vec3*, it stores address of a which is an instance of vec3

  • when Peter defined + operator for the class vec3, he chose vec3& as the type declaration instead of void.
vec3& operator+=(const vec3& v) {
    e[0] += v.e[0];
    e[1] += v.e[1];
    e[2] += v.e[2];
    return *this;
}

Why did he not write the code as follows?

void operator+=(const vec3& v) {
    e[0] += v.e[0];
    e[1] += v.e[1];
    e[2] += v.e[2];
}

There is a concept called operator chaining/method chaining . As the name suggests, it is where you perform multiple operators or functions sequentially in a single statement. When we declare the type to be void instead of vec3 lets see what happens

void operator+=(const vec3& v) {
:
:
}
vec3 a(1.0,2.0,3.0);
vec3 b(2.0,2.0,5.0);
vec3 c(3.0,5.0,3.0);

a+=b+=c;

The compiler interprets it as a+=(b+=c) but since += for vec3 is defined to return nothing, after performing b+=c it will store nothing and further addition cannot happen.

Instead if it was

vec3& operator+=(const vec3& v){
:
:
}
vec3 a(1.0,2.0,3.0);
vec3 b(2.0,2.0,5.0);
vec3 c(3.0,5.0,3.0);

a+=b+=c;

This will work as b+=c returns a value.

  • what does return *this do? this is a pointer. *this dereferences the pointer and it produces the object/output.

  • What is the need for utility functions when it can be kept inside the class as a member function?

    • for a member function it should be of the form class.operator(external_paramter) that is the thing on the left to the operator should be the class itself. Lets take few of em as examples.
    vec3 v(1, 2, 3);
    v.x();          // v (a vec3) on the left of the dot

    -> here x() is an operator where v is to the left of it

    • now if we want to perform a scalar multiplication of vec3 v like double t* vec3 v, it cannot be incorporated into class vec3 as a member function as the left operand is of type double. That is why type it outside.

Color header file and Ray header file

Will be updated soon