This document explains the project from zero C# and .NET knowledge. Read it from top to bottom once, then use the links and exercises while changing the code.
This is a small furniture-fitting application:
.glb model and places it against that wall.The application is deliberately small. A commercial application would add users, projects, saved layouts, authentication, permissions, more furniture, doors, windows, measurements, collision detection, and a model storage service.
BabylonLearningLab/
├── BabylonLearningLab.csproj .NET project and NuGet packages
├── Program.cs C# web server, API, models, database access
├── appsettings.json Application configuration
├── docker-compose.yml Local PostgreSQL container
├── database/001-furniture.sql Database table and seed records
├── wwwroot/index.html Browser page structure
├── wwwroot/styles.css Browser appearance and layout
├── wwwroot/app.js Room editor and Babylon.js logic
└── wwwroot/models/*.glb 3D model files served by the app
wwwroot is a special ASP.NET Core folder. Files inside it are public static files. For example:
wwwroot/models/bookshelf-60.glb
is available in a browser as:
/models/bookshelf-60.glb
.NET is the platform that runs the C# application. C# is the programming language; .NET provides the runtime, libraries, web server integration, and command-line tools.
The project file, BabylonLearningLab.csproj, says:
<TargetFramework>net10.0</TargetFramework>
This project targets .NET 10. It also references the Npgsql NuGet package:
<PackageReference Include="Npgsql" Version="10.0.3" />
Npgsql is the C# library that opens connections to PostgreSQL and executes SQL.
Useful commands:
dotnet restore # download NuGet packages
dotnet build # compile the C# project
dotnet run # build and start the web server
The dotnet command is similar to a combination of a compiler, package manager, and application runner.
Program.cs DoesProgram.cs is the startup file. It creates the web application, configures middleware, defines API endpoints, and starts the server.
using Npgsql;
using Microsoft.AspNetCore.StaticFiles;
using makes types from a library available without writing their full namespace every time.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
builder prepares the application. app is the web application that will receive HTTP requests.
var means the compiler infers the type from the value. It is still strongly typed; C# is not turning the value into an untyped JavaScript-style variable.
app.UseDefaultFiles();
var contentTypeProvider = new FileExtensionContentTypeProvider();
contentTypeProvider.Mappings[".glb"] = "model/gltf-binary";
app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = contentTypeProvider });
This lets the browser request index.html, CSS, JavaScript, and model files. The .glb mapping is important because ASP.NET Core does not automatically know the MIME type of every file extension.
Without this mapping, the files can exist on disk but the browser receives a 404 response.
app.MapGet("/api/furniture", async (...) =>
{
var catalog = CreateCatalog(configuration, fallbackCatalog);
return Results.Ok(await catalog.GetAllAsync(cancellationToken));
});
This defines an HTTP GET endpoint:
GET http://localhost:5090/api/furniture
The endpoint returns JSON. The browser does not call C# functions directly. It sends an HTTP request, and the server sends an HTTP response.
The other endpoints are:
| Endpoint | Purpose |
|---|---|
GET /api/scene |
Returns the page title, message, and background color |
GET /api/furniture |
Returns every catalog item |
GET /api/furniture/1 |
Returns one catalog item by ID |
Results.Ok(...) creates a successful HTTP response. If the requested furniture ID does not exist, the code returns Results.NotFound(), which becomes HTTP 404.
record FurnitureItem(
int Id,
string Name,
string Category,
string ModelUrl,
decimal Width,
decimal Height,
decimal Depth,
string Color);
A record is a compact way to define a data object. This record describes the JSON contract shared by the API and frontend.
The same object is represented in the browser as JSON:
{
"id": 1,
"name": "Bookshelf 60",
"modelUrl": "/models/bookshelf-60.glb",
"width": 0.6,
"height": 2.0,
"depth": 0.35
}
Important C# types here:
int: whole numbers such as an IDstring: textdecimal: precise numeric values, useful for measurements and pricesbool: true or falseTask<T>: a value that will be available after asynchronous work completesCancellationToken: allows work to stop if the request is cancelledinterface IFurnitureCatalog
{
Task<IReadOnlyList<FurnitureItem>> GetAllAsync(CancellationToken cancellationToken);
}
An interface describes what an object can do without specifying how it does it.
This project has two implementations:
InMemoryFurnitureCatalog: hard-coded data, useful when PostgreSQL is not runningPostgresFurnitureCatalog: executes SQL against PostgreSQLThe API uses IFurnitureCatalog, so the endpoint does not need to know which implementation supplied the data. This is an important pattern in company applications: the API depends on a capability, not on one concrete database implementation.
var connectionString = configuration.GetConnectionString("FurnitureDb")
?? configuration["DATABASE_URL"];
return string.IsNullOrWhiteSpace(connectionString)
? fallbackCatalog
: new PostgresFurnitureCatalog(connectionString);
The application checks configuration. If no connection string exists, it uses the in-memory catalog. If a connection string exists, it creates the PostgreSQL catalog.
The ?? operator means “use the value on the left unless it is null; otherwise use the value on the right.”
The PostgreSQL implementation follows this sequence:
NpgsqlConnection.NpgsqlCommand containing SQL.FurnitureItem.This syntax is important:
await using var connection = new NpgsqlConnection(connectionString);
await using ensures the connection is cleaned up even if an error occurs. Database connections must not be left open.
The single-item query uses a parameter:
command.Parameters.AddWithValue("id", id);
Parameters prevent user input from being concatenated into SQL and are an essential protection against SQL injection.
database/001-furniture.sql creates this table:
furniture_items
├── id
├── name
├── category
├── model_url
├── width
├── height
├── depth
└── color
The database stores metadata, not necessarily the binary model itself. In a production system, model_url could point to:
For this lab the files are local static files, so /models/bookshelf-60.glb is enough.
docker-compose.yml starts PostgreSQL locally. The API and the database are separate processes:
ASP.NET Core process --SQL/TCP--> PostgreSQL container
Browser --HTTP------> ASP.NET Core process
Browser --HTTP------> GLB static file
index.html Doesindex.html defines the controls and canvas:
These scripts are loaded in order:
<script src="https://cdn.babylonjs.com/babylon.js"></script>
<script src="https://cdn.babylonjs.com/loaders/babylonjs.loaders.min.js"></script>
<script src="/app.js"></script>
The first script provides Babylon.js. The second provides model-loading support. The third contains this application’s code.
app.js Doesconst state = {
room: { width: 5, length: 4, height: 2.6 },
scene: null,
furniture: [],
placedModels: []
};
This is the current client-side state. It is not the database. It is the browser’s working copy while the user edits the room.
const scene = new BABYLON.Scene(engine);
A Babylon scene contains 3D objects, cameras, lights, materials, and the render loop.
This project creates:
ArcRotateCamera: a camera the user can orbit with the mouseHemisphericLight: general ambient lightDirectionalLight: directional lightrebuildRoom() reads the current width, length, and height. It creates:
The room uses a simple coordinate system:
north (-Z)
west east
south (+Z)
The floor is at Y = 0. Wall height extends upward on the Y axis. Width uses X; length uses Z.
When the user changes dimensions, old room meshes are disposed and new meshes are created. This is a simple and understandable approach for the lab.
drawFloorPlan() uses the normal HTML canvas 2D API. It calculates a scale that fits the room inside the small canvas, then draws:
The 2D plan and the 3D room use the same state.room values. That shared data is what keeps them synchronized.
const response = await fetch('/api/furniture');
const furniture = await response.json();
fetch sends an HTTP request. await pauses this function until the response arrives. response.json() converts the JSON response into JavaScript objects.
The UI then creates one dropdown option for each returned item. Notice that the frontend does not contain the bookshelf URLs as its source of truth. It receives them from the API.
When the user clicks Place selected model, placeFurniture() does this:
/models/bookshelf-60.glb into a folder and filename.BABYLON.SceneLoader.ImportMeshAsync.The important boundary is:
API modelUrl: "/models/bookshelf-60.glb"
|
v
SceneLoader.ImportMeshAsync(...)
|
v
Babylon meshes in the scene
The C# API does not render the model. The browser downloads the model and Babylon.js renders it.
When the page loads:
1. Browser requests GET /
2. ASP.NET Core serves wwwroot/index.html
3. Browser loads Babylon.js, the loader, app.js, and styles.css
4. app.js requests GET /api/scene
5. app.js requests GET /api/furniture
6. C# selects the in-memory or PostgreSQL catalog
7. C# returns JSON
8. Browser fills the furniture dropdown
9. Babylon.js renders the empty room
When the user places a bookshelf:
1. User chooses a bookshelf and wall
2. Browser reads the selected FurnitureItem
3. Browser requests the modelUrl
4. ASP.NET Core serves the GLB bytes
5. Babylon.js parses the GLB
6. Browser scales and positions the imported meshes
7. Babylon.js renders the model every frame
There is no database request when the user clicks the placement button in this lab because the catalog data was already loaded. A production design might use GET /api/furniture/{id} at that point if the catalog response were intentionally small or if placement required fresh permission checks.
Without PostgreSQL, the fallback catalog is enough:
cd BabylonLearningLab
dotnet run
With PostgreSQL:
docker compose up -d
export ConnectionStrings__FurnitureDb='Host=localhost;Port=5432;Database=furniture_lab;Username=furniture_app;Password=furniture_dev'
dotnet run
Useful browser/API checks:
curl http://localhost:5090/api/scene
curl http://localhost:5090/api/furniture
curl -I http://localhost:5090/models/bookshelf-60.glb
A successful model response should have status 200 and content type model/gltf-binary.
Do these in order:
5 x 4 to 6 x 3 in app.js.2.6 to 3.0.price to the database and C# record.POST /api/projects endpoint to save a room layout.Program.cs.For this company-style architecture, the most useful next topics are: