惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Vercel News
Vercel News
F
Fortinet All Blogs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
GbyAI
GbyAI
MongoDB | Blog
MongoDB | Blog
Jina AI
Jina AI
aimingoo的专栏
aimingoo的专栏
I
InfoQ
IT之家
IT之家
罗磊的独立博客
Blog — PlanetScale
Blog — PlanetScale
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The Cloudflare Blog
爱范儿
爱范儿
Microsoft Azure Blog
Microsoft Azure Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
美团技术团队
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
B
Blog RSS Feed
G
Google Developers Blog
大猫的无限游戏
大猫的无限游戏
博客园_首页
Engineering at Meta
Engineering at Meta
Martin Fowler
Martin Fowler

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Software Rendering Pipeline with Backface Culling
yubin yang · 2026-05-31 · via DEV Community

yubin yang

1. Overview

  • In this project, I implemented a simple software renderer using Python and Pygame.
  • The renderer follows the fundamental stages of a 3D rendering pipeline.
Local Space
↓
World Space
↓
View Space
↓
Clip Space
↓
Screen Space

  • Model Transformation (Local Space → World Space)
  • View Transformation (World Space → View Space)
  • Perspective Projection (View Space → Clip Space)
  • Viewport Transformation (Clip Space → Screen Space)
  • Backface Culling
  • Painter's Algorithm for depth sorting

2. Rendering Pipeline Implementation

- The original code is too long, so I only posted the important parts.
-Please check GitHub.

Transforming World Space into View Space using the View Matrix

def GetViewMatrix(CamPos, TargetPos, Up):
    ViewZ = TargetPos - CamPos
    ViewZ = ViewZ / np.linalg.norm(ViewZ)

    ViewX = np.cross(Up, ViewZ)
    ViewX = ViewX / np.linalg.norm(ViewX)

    ViewY = np.cross(ViewZ, ViewX)

    CamInv = np.array([
        [ViewX[0], ViewX[1], ViewX[2], -np.dot(ViewX, CamPos)],
        [ViewY[0], ViewY[1], ViewY[2], -np.dot(ViewY, CamPos)],
        [ViewZ[0], ViewZ[1], ViewZ[2], -np.dot(ViewZ, CamPos)],
        [0, 0, 0, 1]
    ])

    FlipY = np.array([
        [-1, 0, 0, 0],
        [0, 1, 0, 0],
        [0, 0, -1, 0],
        [0, 0, 0, 1]
    ])

    return np.matmul(FlipY, CamInv)

  • GetViewMatrix() creates a View Matrix that transforms objects from World Space into View Space.
  • The camera position, target position, and up vector are used to calculate the camera's local coordinate system. Once the View Matrix is applied, all objects are transformed into coordinates relative to the camera.

Transforming View Space into Clip Space using the Projection Matrix

def GetProjectionMatrix(FovDeg, Width, Height, Near=0.1, Far=1000):
    Fov = math.radians(FovDeg)
    Aspect = Width / Height
    Distance = 1 / math.tan(Fov / 2)

    return np.array([
        [Distance / Aspect, 0, 0, 0],
        [0, Distance, 0, 0],
        [0, 0, (Near + Far) / (Near - Far), (2 * Near * Far) / (Near - Far)],
        [0, 0, -1, 0]
    ])

  • GetProjectionMatrix() creates a Perspective Projection Matrix.
  • This matrix applies perspective to the scene, making distant objects appear smaller and nearby objects appear larger. It transforms coordinates from View Space into Clip Space.

Transforming Normalized Coordinates into Screen Coordinates using the Viewport Matrix

def GetViewportMatrix(Width, Height):
    return np.array([
        [Width / 2, 0, 0, Width / 2],
        [0, -Height / 2, 0, Height / 2],
        [0, 0, 0.5, 0.5],
        [0, 0, 0, 1]
    ])

  • GetViewportMatrix() converts normalized device coordinates (NDC) into actual screen coordinates.
  • After projection, coordinates exist in a normalized range of -1 to 1. This matrix maps those coordinates into the screen resolution so that they can be rendered.

Determining Visible Faces using Backface Culling

def IsFrontFace(V0, V1, V2):
    Edge1 = V1[:3] - V0[:3]
    Edge2 = V2[:3] - V0[:3]

    Normal = np.cross(Edge1, Edge2)

    Center = (V0[:3] + V1[:3] + V2[:3]) / 3

    # Camera is placed at the origin in view space
    ViewDirection = -Center

    # Visible when the face normal points toward the camera
    return np.dot(Normal, ViewDirection) > 0

  • IsFrontFace() determines whether a triangle is facing the camera.
  • The function calculates the face normal using a cross product and compares it with the camera direction using a dot product. If the triangle is facing away from the camera, it is discarded before rendering.
  • This process is known as Backface Culling and helps reduce unnecessary rendering work.

Transforming Cube Vertices and Rendering Visible Triangles

    ViewVertices = []
    ProjectedVertices = []

    for Vertex in Vertices:
        # Local space -> World space
        WorldVertex = np.matmul(Translate, Vertex)

        # World space -> View space
        ViewVertex = np.matmul(ViewMatrix, WorldVertex)
        ViewVertices.append(ViewVertex)

        # View space -> Clip space
        ClipVertex = np.matmul(ProjectionMatrix, ViewVertex)

        # Perspective divide
        if ClipVertex[3] != 0:
            NdcVertex = ClipVertex / ClipVertex[3]
        else:
            NdcVertex = ClipVertex

        # NDC -> Screen space
        ScreenVertex = np.matmul(ViewportMatrix, NdcVertex)
        ProjectedVertices.append((int(ScreenVertex[0]), int(ScreenVertex[1]), ScreenVertex[2]))

    FaceDepths = []

    for Face in Faces:
        V0 = ViewVertices[Face[0]]
        V1 = ViewVertices[Face[1]]
        V2 = ViewVertices[Face[2]]

        # Skip triangles facing away from the camera
        if not IsFrontFace(V0, V1, V2):
            continue

        AvgDepth = sum(ProjectedVertices[Index][2] for Index in Face) / 3
        FaceDepths.append((AvgDepth, Face))

        FaceDepths.sort(key=lambda Item: Item[0], reverse=True)

        for _, Face in FaceDepths:
            Points = [
                (ProjectedVertices[Index][0],
                ProjectedVertices[Index][1])
                for Index in Face
            ]

            pygame.draw.polygon(Screen, Color, Points)
            pygame.draw.polygon(Screen, (0, 0, 0), Points, 2)

            # Update screen immediately
            pygame.display.flip()

  • DrawCube() contains the main software rendering pipeline.
  • The cube vertices are first created in Local Space and then transformed through multiple coordinate spaces.
Local Space
→ World Space
→ View Space
→ Clip Space
→ NDC
→ Screen Space

  • After the transformations, Backface Culling is applied to remove invisible faces.
  • The remaining triangles are depth-sorted using Painter's Algorithm and finally rendered.

Results

The final renderer displays multiple cubes in the 3D scene by applying perspective projection and back culling.

Although simple compared to modern pipelines, manually implementing these steps allowed me to gain a much deeper understanding of how the rendering system works internally.

Through this project, I was able to learn various knowledge necessary for rendering, such as computer graphics mathematics, matrix transformations, coordinate space, and visibility.


Preview gif

Full Video

YouTube Video

Github Repository

Github - Graphics