Case Study

Fragscentric

1 shared GLB model ~$0 infrastructure/month

I’m a perfume collector. In 2022 I decided to share my passion with others and started selling decants. The motto was simple: make the luxury affordable for others, as well as for me, so I can collect more.

I shared reviews on social channels and was receiving orders through Telegram DMs. So I became irregular at managing this. Because I’ve been their choice for personalized selling. My recommendations and so on.

I was getting plenty of questions, often the same ones. Seasonal recommendations, stock availability, pricing queries, restock requests.

So I decided to build a site to cover all these. First I tried to create a WooCommerce site. But the experience I wanted to give my customers, WordPress was not the best option in terms of price and experience. So I made a native application. I picked Astro + Hono and Cloudflare because it’s affordable. That much which feels almost free.

Firstly, this was not traditional e-commerce. So I wanted to ensure a hybrid experience. My journey and my business. Both on the same place.


The Bottle Problem

As I’m always picky about the details, so I was suffering with the product showcase. My labeling is two-sided. When I show the front side, it looks cropped. When I show the left side, it feels like logo only. When I bring it half-half, the bottle looks triangular. Which confused my customers. So finally I decided to render 3D bottles.

When it comes to 3D bottles, speed and experience was the crucial thing. Because it’s not a site where people land to experiment. They come with real purpose.

I prepared the 3D model of my bottle with Blender. Then embedded and managed lighting and shading on the web with Three.js.


One Model, All Products

But then the challenge arose: if I prepare a separate 3D model for each product, it makes the site almost unusable. Dozens of different GLB files? That’s not happening. So I tried to manage the textures and labels dynamically. Site-wide, it’s actually a single model. But it looks like a separate 3D for each product page.

Here’s how I solved it.

Each product has its own label image, a flat PNG. And the bottle model has a second UV channel specifically mapped for the label area. So when a user lands on a product page, Three.js loads that product’s label texture and swaps it onto the material at runtime. One .glb file. Every label different. The bottle shape stays the same because it IS the same. Only the skin changes.

The trick was getting the UV mapping right in Blender. The primary UV channel handles the bottle geometry, glass, cap, all that. But the label mesh gets a second UV channel with perfect 0-to-1 bounds. So the texture always maps cleanly. No stretching. No tiling. No weird artifacts.


Glass, Light, and Studio Feel

Then came the glass problem.

Real glass is tricky on the web. You need light to pass through it. You need refraction. You need reflections. But you also need it to run at 60fps on someone’s phone. So I used MeshPhysicalMaterial, Three.js’s most realistic material, and dialed in the properties carefully. Transmission at 0.92 for that see-through quality. Roughness at 0.015 so it’s nearly mirror-smooth. A slight warm tint on the edges. And a clearcoat layer on top for that glossy, just-polished look.

For lighting, I didn’t go with the typical HDR environment map approach. Instead I built a five-light setup that mimics actual studio product photography. A key light from the left-front. A fill from the right. A top light for the cap. A warm bounce from below. And ambient everywhere to prevent harsh shadows. All warm tones, golden, not clinical white. Because this is a luxury product, not a product listing.


The Liquid Inside

The liquid inside the bottle is not part of the 3D model. It’s generated procedurally at runtime. I create a rounded box geometry that fills most of the bottle volume, then use a clipping plane to cut it off at the fill level. And on top of that, a curved surface, a meniscus, to simulate that concave dip you see in real liquid. The liquid even bobs up and down with the bottle. Subtle, but it makes the whole thing feel alive.

The bottle gently floats. Sinusoidal motion, just 0.018 units up and down. It auto-rotates slowly, and pauses when the user grabs it to inspect. The shadow underneath pulses in sync. It’s these small things that make it feel real instead of rendered.


Performance as a Non-Negotiable

Now, performance. This was the non-negotiable. I’m selling perfume, not demoing WebGL. People come to buy, not to wait. So:

  • Draco compression on the GLB file. Keeps it small.
  • Pixel ratio capped at 2. No need to render 4x on a Retina display.
  • Fake shadow instead of real shadow maps. A canvas-drawn gradient on a plane. Looks 90% as good, costs 10% of the render.
  • The entire 3D viewer is a React island inside an otherwise static Astro page. So the rest of the page loads instantly. The 3D loads only where it’s needed.

Serverless Backend

For the backend, I needed something equally lean. Orders, stock management, admin panel, but without paying for a server. Cloudflare D1 gave me SQLite at the edge. Hono handles the API routes as Cloudflare Pages Functions. The whole backend is serverless. No always-on server, no monthly VPS bill. When someone places an order, it hits /api/orders, writes to D1, done. The admin panel is just static pages that talk to the API with JWT auth.

The total infrastructure cost is effectively zero. Cloudflare Pages free tier. D1 free tier. Static assets on CDN. Serverless functions only run when called. For a business that started as Telegram DMs, that matters.


The Site Today

Each fragrance has full notes, top, middle, base, seasonal tags, time-of-day recommendations. Customers can filter by season, by occasion, and price range. The stock updates in real-time from the database. Restock requests are built in. The questions I used to answer manually now the site handles them.

I built it because I was trying to share a personalized perfume experience, not a marketplace. A story, not a storefront.

Constraints are always a feature. I couldn’t afford a server, so I went serverless. I couldn’t model dozens of bottles, so I made one model work for all. I couldn’t use heavy shadow maps, so I faked them. Every limitation pushed me toward a simpler, faster solution. And the result is something that loads fast, looks premium, and costs almost nothing to run.

That’s Fragscentric. Not built to impress developers. Built to share my passion beautifully. And if you want to fuel up my journey you can.