A real-time 3D visualization of the magnetic field distribution around a rectangular current-carrying loop, computed using the Biot-Savart law.
Original: Ph3DSys by Mars, September--October 2003, built with Borland C++ Builder 6.0, OpenGL, and the VCL framework on Windows.
Port: SDL2 + legacy OpenGL + Dear ImGui on Linux, April 2026.
- Background — CCBP
- Building and Running
- Dependencies
- Controls
- Project Structure
- How the Port Was Done
- Physics
- Architecture Comparison
- Original Project Files
- Known Differences from the Original
- Performance Notes
This project was originally developed as part of the CCBP (A Course in Calculus-Computer-Based Physics) teaching system at Huazhong University of Science and Technology (HUST).
CCBP was a physics education reform framework created by Professor Li Yuanjie (李元杰) of HUST, proposed in 1997 and published as a two-volume textbook (大学物理CCBP教程, Hubei Science and Technology Press, July 2000). The system integrated digital computation, three-dimensional visualization and simulation, and traditional calculus-based physics instruction — using computer programs to make abstract classical and quantum phenomena visible and intuitive for science and engineering students.
The CCBP system was adopted at over 40 universities across China. Professor Li
was invited to present it at institutions including Tsinghua University and
Peking University, and received the National First Hundred Outstanding Teaching
Masters Award for this work. The original simulations were written in C/C++ for
DOS/Windows using Borland's BGI graphics and Turbo C 2.0; the CCBP/ directory
in this project contains the TOpenGLAPPanel component — a custom VCL/OpenGL
panel from the AP.Soft library that was distributed as part of the CCBP toolkit.
After Professor Li left HUST in 2004, the CCBP program gradually faded as its underlying technology became obsolete. The textbooks are now out of print.
References:
- 李元杰 — Baidu Baike biography
- 大学物理CCBP教程 — Dushu.com book details
- 大学物理CCBP教程 — Douban book entry
- 大学物理CCBP教程 — Dangdang listing (sold out)
- 大学物理CCBP体系的研究与实践 — Doc88
- Li Yuanjie profile — EneTedu online education platform
- Li Yuanjie at SUSTech — Science Net news (2011)
- 用Python学普通物理 — Zhihu column adapting CCBP to Python
# Install dependencies (Ubuntu/Debian)
sudo apt install libsdl2-dev libglu1-mesa-dev cmake g++
# Clone Dear ImGui into the project root (if not already present)
git clone --depth 1 https://github.com/ocornut/imgui.git imgui
# Build
mkdir -p build && cd build
cmake ..
make -j$(nproc)
# Run
./ph3dsysThe executable is built in the build/ directory. No installation step is
needed; just run it from there.
| Library | Purpose | Install (apt) |
|---|---|---|
| SDL2 | Windowing, input, OpenGL context | libsdl2-dev |
| OpenGL | 3D rendering (legacy fixed-function pipeline) | Comes with GPU drivers |
| GLU | Utility functions: perspective projection, quadric objects | libglu1-mesa-dev |
| Dear ImGui | Immediate-mode GUI (sliders, buttons, menus) | Vendored in imgui/ |
| CMake >= 3.10 | Build system | cmake |
| C++11 compiler | g++ or clang++ | g++ |
| Action | Effect |
|---|---|
| Left-drag on the 3D view | Rotate the camera |
| Scroll wheel | Zoom in / out |
| Key | Effect |
|---|---|
1 |
Switch to Rectangular Grid mode |
2 |
Switch to Circular Grid mode |
Space |
Toggle auto-rotation on/off |
R |
Reset camera to default position and angle |
Esc |
Quit |
- Visualization Mode — Radio buttons to choose between the rectangular (Cartesian) grid and the circular (polar) grid.
- Animation — Static or auto-rotate.
- R (radius) — Slider controlling the current loop radius (1--50).
- A (scale) — Slider controlling the spatial scaling factor (1--30).
- I (current) — Slider controlling the current magnitude (10,000--500,000). The "Effective I" readout below it shows the derived value matching the original's display formula.
- X, Y, Z — Drag-integers to shift the mesh position in 3D space.
- Distance — Slider for camera distance (zoom).
- Reset Position — Button to reset all view and position parameters to defaults.
- Help > About — Shows project origin and description.
- Help > Quit — Exits the application.
| Directory | Origin | Description |
|---|---|---|
src/ |
Port | Linux port source code (main.cpp, ph3dfunc.cpp/h) |
build/ |
Port | CMake build output; contains the compiled ph3dsys executable |
imgui/ |
Port | Dear ImGui library (vendored, git-cloned from GitHub) |
CCBP/ |
Original | CCBP (Comprehensive Computer-Based Physics) component library — contains TOpenGLAPPanel source, the custom VCL/OpenGL panel component from AP.Soft |
OpenGL/ |
Original | Bundled OpenGL headers and Windows DLLs/LIBs (gl.h, glu.h, glut.h, opengl32.dll, etc.) |
doc/ |
Original | Chinese development manual (开发手册.doc, Word 5.1 format, code page 936) |
res/ |
Original | 15 JPEG screenshot/diagram images from the original documentation |
windows_build/ |
Original | Pre-built Windows executable (Ph3DSys.exe) with runtime dependencies (DLLs, BMP buttons) |
| File(s) | Origin | Description |
|---|---|---|
CMakeLists.txt |
Port | CMake build configuration for the Linux port |
README.md |
Port | This file |
MainFunc.cpp |
Original | Core simulation logic: physics (fz()), mesh generation, VCL UI event handlers (872 lines) |
MainFunc.h |
Original | TForm1 class definition, POINT3F and RGBcolor class definitions (173 lines) |
MainFunc.dfm |
Original | VCL form designer layout (binary text format defining the UI) |
Ph3DSys.cpp |
Original | WinMain entry point (33 lines) |
Ph3DSys.bpr |
Original | Borland C++ Builder 6 project file (XML) |
Ph3DSys.dsk |
Original | Borland IDE desktop/session state |
Ph3DFunc.h |
Original | Header for the graphics DLL — the only documentation of its API (58 lines) |
Ph3DFunc.dll |
Original | Closed-source Windows DLL providing 3D drawing functions |
Ph3DFunc.lib |
Original | Import library for the DLL |
Ph3DSys.exe |
Original | Compiled Windows executable (630 KB) |
*.obj |
Original | Borland compiler object files |
*.res, *.tds, *.ddp |
Original | Borland resource, debug symbol, and IDE state files |
*.BMP |
Original | 10 button icon bitmaps (32x32, 16-color) used by the VCL UI |
glut32.dll |
Original | GLUT runtime DLL (Windows) |
USE.cpp/h/dfm |
Original | Stub helper form (minimal, unused in the port) |
imgui.ini |
Port | Dear ImGui layout state (auto-generated at runtime) |
src/
|-- main.cpp 573 lines Main application: SDL2 init, event loop,
| physics (fz + mesh generation), ImGui UI,
| OpenGL rendering, mesh caching
|-- ph3dfunc.cpp 483 lines Reimplementation of the original Ph3DFunc.dll
| (30+ OpenGL wrapper functions)
|-- ph3dfunc.h 77 lines Public API matching the original DLL header
The original application depended on three non-portable layers:
-
Borland VCL (Visual Component Library) — The entire UI: the window, panels, scrollbars, buttons, radio buttons, menus, combo boxes, timers, mouse events, and image-based navigation buttons. VCL is a Delphi/C++ Builder-specific framework with no Linux equivalent.
-
Ph3DFunc.dll — A closed-source Windows DLL providing ~30 high-level 3D drawing functions (
phSurface,phSphere,phCone,phCoordinate,phSetLight, etc.). Only the header file (Ph3DFunc.h) and the compiled.dll/.libwere available; no source code. -
TOpenGLAPPanel — A custom VCL component (from a third-party package called "AP.Soft") that managed the OpenGL rendering context within a Borland form, including WGL context creation, pixel format setup, double buffering, and font rendering. Source was available in
CCBP/but tightly coupled to VCL.
The physics and math code — the fz() function implementing the Biot-Savart
law and the two mesh-generation loops — are pure C floating-point arithmetic
with no platform dependencies. The OpenGL rendering calls in render_scene()
(matrix transforms, polygon modes, lighting setup) use standard OpenGL that
works identically on Linux.
| Original Layer | Replacement | Rationale |
|---|---|---|
| VCL windowing + events | SDL2 | Cross-platform, lightweight, handles OpenGL context natively |
| VCL UI widgets | Dear ImGui | Immediate-mode GUI; trivial SDL2+OpenGL integration; sliders, radio buttons, menus out of the box |
| TOpenGLAPPanel (WGL context) | SDL2 SDL_GL_CreateContext() |
SDL2 abstracts platform-specific GL context creation |
| Ph3DFunc.dll (30 functions) | Reimplemented in ph3dfunc.cpp |
Using legacy OpenGL (glBegin/glEnd) and GLU quadrics to match the era |
| VCL TTimer | SDL_GetTicks() + frame delta |
Standard SDL2 time-based animation |
| BMP button images | ImGui buttons/sliders | Modern UI; no bitmap resources needed |
Ported verbatim from MainFunc.cpp lines 42--431. Variable names,
constants, loop bounds, boundary conditions, and the trapezoidal integration
are identical. The only changes are:
float→ explicitcosf/sinf/powf/tanf(C99 float variants instead of implicit double-to-float conversions in old Borland C++)- Physics parameters (
R,A,I) renamed toR_param,A_param,I_paramto avoid conflicts with standard library symbols
The DLL exported ~30 functions. The simulation only uses about 10 of them. All 30 were reimplemented for completeness:
-
phSetLight(number, position, ambient, diffuse)— WrapsglLightfvto configure OpenGL light sources. The ambient/diffuse parameters are scaled from the original's 0--10 range to 0.0--1.0 floats. -
phSetColor(r, g, b)— Sets bothglColor3fandglMaterialfv(GL_AMBIENT_AND_DIFFUSE)so the color works with or without lighting enabled. -
phSetMaterial(ambient, diffuse)— MapsRGBcolor(0--255) to OpenGL material properties (0.0--1.0). -
phTranslatef(position)— Thin wrapper aroundglTranslatef. -
phRotate(O, P, angle)— Rotates around an arbitrary axis defined by points O and P. Translates to origin, rotates, translates back. -
phCoordinate(O, X, Y, length)— Draws a colored 3D coordinate system: red X axis, green Y axis, blue Z axis (computed as X cross Y). Temporarily disables lighting for clean line rendering. -
phSurface(p, maxi, maxj, color)— The critical function. Renders anmaxi x maxjgrid ofPOINT3Fvertices asGL_QUAD_STRIProws with per-face computed normals. This is what draws the magnetic field surface. -
phSphere,phCone,phCylinder,phDisk— Use GLU quadric objects (gluSphere,gluCylinder,gluDisk) with automatic orientation alignment viaglRotatefto match the two-point API of the original. -
phArrow,phSpring,phMissile— Composed from the above primitives. -
phPoint,phLine,phTriangle,phQuads,phPolygon— DirectglBegin/glEndwrappers with automatic normal computation. -
Math helpers (
normalize,normcrossprod,calcNormal,ReduceToUnit) — Standard vector math, used internally by the surface and polygon renderers.
The original's OpenGLAPPanelInit and OpenGLAPPanelResize mapped directly:
Original (VCL/WGL) Port (SDL2)
───────────────── ──────────
TForm1::OpenGLAPPanelInit() → gl_init()
glShadeModel(GL_SMOOTH) (identical)
glClearColor(0,0,0,1) (identical)
glEnable(GL_DEPTH_TEST) (identical)
glEnable(GL_BLEND) (identical)
TForm1::OpenGLAPPanelResize() → gl_resize(w, h)
glViewport(...) (identical)
gluPerspective(10, ...) (identical)
TForm1::OpenGLAPPanelPaint() → render_scene()
Camera setup at z=-800 (identical)
Rotate -90 around X and Z (identical)
Translate by 'trans' (identical)
Light setup via phSetLight (identical)
Mesh generation + phSurface (moved to cached functions)
Every VCL widget was replaced with an ImGui equivalent:
Original VCL Widget ImGui Replacement
──────────────────── ─────────────────
TRadioButton (RadioButton1/2) → ImGui::RadioButton()
TScrollBar (ScrollBar_R/A/I) → ImGui::SliderFloat()
TEdit (Edit_R/A/I, Edit_X/Y/Z) → ImGui::Text() / ImGui::DragInt()
TComboBox (ChoiseCartoon) → ImGui::RadioButton() (simpler)
TImage buttons (up/down/left/right) → ImGui::DragInt() for X/Y/Z
TImage buttons (turnMAX/turnMIN) → ImGui::SliderFloat("Distance")
TButton (RePosition) → ImGui::Button("Reset Position")
TMainMenu (EXIT/HELP/USE/ABOUT) → ImGui::BeginMainMenuBar()
TTimer (Timer1, animation) → SDL_GetTicks() delta-time loop
Mouse drag rotation → SDL_MOUSEMOTION events
Application::MessageBox (About) → ImGui::Begin("About") window
The original handled mouse drag via OpenGLAPPanelMouseDown/Move/Up events
on the VCL panel. The port uses SDL_MOUSEBUTTONDOWN, SDL_MOUSEMOTION, and
SDL_MOUSEBUTTONUP events with io.WantCaptureMouse gating so that dragging
over the ImGui panel doesn't rotate the scene.
Scroll-wheel zoom was added as an improvement over the original's button-based zoom (turnMAX/turnMIN image buttons).
The simulation computes the magnetic field strength at each point on a 2D grid that is then displayed as a 3D surface where height (Z) represents field intensity.
The magnetic field B at a point due to a current-carrying wire element is:
dB = (mu_0 / 4*pi) * (I * dl x r_hat) / r^2
where mu_0 is the permeability of free space (1.26e-6 H/m), I is the
current, dl is the wire element vector, and r is the distance from the
wire element to the field point.
Used by visualization mode 1 (Rectangular Grid). Integrates the Biot-Savart contribution from all four sides of a unit square current loop:
- Side 1 (angles -45 to +45): Bottom edge, parameterized by angle
- Side 2 (angles +45 to +135): Right edge
- Side 3 (angles +135 to +225): Top edge
- Side 4 (angles +225 to +315): Left edge
Each side uses trapezoidal-rule integration with 10-degree steps (15-degree
for side 4). The endpoints are weighted by 1/2 (standard trapezoidal rule).
The result is scaled by mu_0 * 10 * I / (40 * pi).
Uses a 43x43 polar coordinate mesh. For each grid point, iterates around a circular current loop in 12-degree steps (30 iterations), computing the Biot-Savart contribution at each step using a direct discrete summation (trapezoidal rule with Simpson-like weighting). Field values are clamped: +65 inside the loop, -55 outside.
Uses a 69x69 Cartesian mesh spanning [-34, +34] units. Coordinates are
normalized by the scaling factor A. The fz() function is called for each
point, with special boundary handling near the wire location (0.85 < |x| < 1.2
or 0.85 < |y| < 1.22) where the field would diverge, clamping to fz(0.8, _)
to avoid singularities.
| Constant | Symbol | Value | Description |
|---|---|---|---|
| Permeability of free space | mu_0 | 1.26e-6 H/m | Magnetic constant |
| Current | I | 250,000 A (default) | Adjustable via slider |
| Loop radius | R | 17 units (default) | Adjustable via slider |
| Scale factor | A | 10 units (default) | Adjustable via slider |
| Angular divisions | de | 27 | Fixed; used in polar mode |
WinMain (Ph3DSys.cpp)
|
v
TForm1 (VCL) ─── MainFunc.cpp
| |
|-- TOpenGLAPPanel (WGL context, CCBP/)
|-- TTimer ─────── Timer1Timer() → Refresh + animation
|-- TScrollBar ─── ScrollBar_R/A/IChange()
|-- TRadioButton ─ RadioButton1/2Click()
|-- TImage ─────── up/down/left/rightMouseDown/Up()
|-- TComboBox ──── ComboBox_ChoiseCartoonChange()
|-- TMainMenu ──── EXIT/USE/ABOUTClick()
|
v
OpenGLAPPanelPaint()
|-- fz() ──────── Biot-Savart calculation (every frame!)
|-- phSurface() ─ Ph3DFunc.dll (closed-source)
|-- phSetLight()
|-- phCoordinate()
main() ─── src/main.cpp
|
|-- SDL2 window + OpenGL 2.1 context
|-- Dear ImGui (SDL2 + OpenGL2 backends)
|
v
Main loop:
|-- SDL_PollEvent() ─── mouse drag, scroll, keyboard
|-- ImGui frame ─────── control panel, menu bar
|-- check_mesh_dirty()
| |-- compute_rect_mesh() ── fz() called only when params change
| |-- compute_polar_mesh()
|-- render_scene() ──── OpenGL draw from cached mesh
| |-- phSurface() ─ src/ph3dfunc.cpp (open-source reimplementation)
| |-- phSetLight()
| |-- phCoordinate()
|-- SDL_GL_SwapWindow()
The original Windows source files are preserved unmodified in the project root. They are not used by the build but kept for reference.
| File | Size | Description |
|---|---|---|
MainFunc.cpp |
872 lines | Core simulation logic, UI event handlers, physics |
MainFunc.h |
173 lines | TForm1 class definition, POINT3F and RGBcolor classes |
MainFunc.dfm |
~200 lines | VCL form designer layout (binary text format) |
Ph3DSys.cpp |
33 lines | WinMain entry point |
Ph3DSys.bpr |
~300 lines | Borland C++ Builder 6 project file (XML) |
Ph3DFunc.h |
58 lines | DLL header — the only documentation of the graphics API |
Ph3DFunc.dll |
175 KB | Closed-source graphics library (Windows PE) |
Ph3DFunc.lib |
4 KB | Import library for the DLL |
CCBP/ |
~55 KB | TOpenGLAPPanel source (VCL component, AP.Soft) |
OpenGL/ |
~130 KB | Bundled OpenGL headers and Windows DLLs/LIBs |
doc/ |
52 KB | Development manual in Chinese (Word 5.1, code page 936) |
res/ |
~50 KB | 15 JPEG screenshot/diagram images |
*.BMP |
630 bytes each | 10 button icon bitmaps (32x32, 16-color) |
windows_build/ |
~810 KB | Original build output (Ph3DSys.exe + runtime DLLs + BMP buttons) |
The original UI labels, menu items, and About/Help message boxes were in
Chinese (Simplified, GB2312/code page 936). The source files contain Chinese
characters in comments that display as mojibake on Linux terminals due to
encoding mismatch (the files are raw GB2312 bytes, not UTF-8). The DFM form
file stores Chinese text as Unicode escape sequences (#19977#32500...).
The port uses English for all UI text. The original Chinese strings can be
found in MainFunc.cpp lines 863 and 869 (ABOUTClick and USEClick).
| Aspect | Original | Port |
|---|---|---|
| Platform | Windows (9x/XP) | Linux |
| UI framework | Borland VCL (native Windows widgets) | Dear ImGui (rendered overlay) |
| Window size | 1025x735 fixed | 1280x800 resizable |
| Zoom control | Two image buttons (magnify/shrink) | Scroll wheel + slider |
| Navigation | Image buttons for up/down/left/right | ImGui DragInt widgets for X/Y/Z |
| Physics recomputation | Every frame (~60 FPS) | Only when R, A, I, or mode changes |
| OpenGL context | WGL via TOpenGLAPPanel | SDL2 SDL_GL_CreateContext() |
| Graphics library | Ph3DFunc.dll (closed-source) | ph3dfunc.cpp (open-source reimplementation) |
| Language | Chinese UI labels | English UI labels |
| BMP button images | 10 bitmap files loaded at runtime | Not used (ImGui widgets instead) |
| Menu bar | VCL TMainMenu (native Windows) | ImGui main menu bar |
| About dialog | Application->MessageBox() (modal) |
ImGui window (non-modal) |
| Auto-rotate speed | Timer-driven (~fixed interval) | Delta-time-driven (frame-rate independent) |
This port uses OpenGL 2.1 with the Compatibility Profile — sometimes
called "legacy" or "fixed-function" OpenGL. This means calls like
glBegin/glEnd, glVertex3f, glRotatef, and glLightfv rather than
the modern shader-based pipeline (OpenGL 3.2+ Core Profile).
These functions were deprecated in the OpenGL 3.0 spec (2008) and removed from the Core Profile in 3.2, but they remain fully supported through the Compatibility Profile, which every major Linux GPU driver ships:
- NVIDIA proprietary: Full compatibility profile support (OpenGL 4.6 Compat)
- AMD (Mesa RADV/radeonsi): Full compatibility profile support
- Intel (Mesa iris/i965): Full compatibility profile support
No GPU vendor has shipped a Linux driver with only Core Profile support in the 17 years since deprecation. Mesa (the open-source driver stack) explicitly maintains compatibility profile as a project goal.
SDL2 requests a Compatibility Profile context by default, which is what this
project uses. The relevant lines in main.cpp:
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);If modernization were ever needed, the change would be localized to
ph3dfunc.cpp: replace glBegin/glEnd calls with vertex buffer objects
(VBOs) and write a simple vertex + fragment shader pair (~50 lines of GLSL).
The physics code and application structure would not change.
The most expensive operation is the physics mesh computation:
- Rectangular grid: 69x69 = 4,761 calls to
fz(), each running 4 integration loops with ~10 iterations each. Roughly 190,000 trig function calls. - Circular grid: 43x43 = 1,849 grid points, each with up to 30 inner iterations of 2 Biot-Savart evaluations. Roughly 220,000 trig function calls.
The original recomputed this every frame, causing sustained high CPU load (85--95 C on a modern system running at 60 FPS).
The port caches the computed mesh and only recomputes when a physics parameter (R, A, I) changes or the visualization mode is switched. This drops idle CPU temperature to ~55 C and only spikes briefly to ~65 C during parameter adjustment.
View-only operations (rotation, zoom, position shift) just re-render the cached mesh vertices through the OpenGL pipeline, which is essentially free.
