<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Noor E Alam</title>
    <description>The latest articles on DEV Community by Noor E Alam (@noorealam).</description>
    <link>https://dev.to/noorealam</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4026341%2F04a56b40-c0e6-4c61-9a8d-c46e6ce1fae3.png</url>
      <title>DEV Community: Noor E Alam</title>
      <link>https://dev.to/noorealam</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/noorealam"/>
    <language>en</language>
    <item>
      <title>Building a Three.js 3D Product Configurator for WooCommerce: 4 Things I Didn't Expect</title>
      <dc:creator>Noor E Alam</dc:creator>
      <pubDate>Sun, 12 Jul 2026 18:47:35 +0000</pubDate>
      <link>https://dev.to/noorealam/building-a-threejs-3d-product-configurator-for-woocommerce-4-things-i-didnt-expect-il2</link>
      <guid>https://dev.to/noorealam/building-a-threejs-3d-product-configurator-for-woocommerce-4-things-i-didnt-expect-il2</guid>
      <description>&lt;p&gt;Most WooCommerce product pages still show the same thing stores have shown for 20 years: a handful of flat photos. I spent the last few months building Noorifa, a plugin that replaces that with an interactive Three.js viewer — customers rotate the model, zoom in, and switch colors/materials on specific meshes in real time, synced to the store's actual WooCommerce variations.&lt;/p&gt;

&lt;p&gt;The 3D rendering part was the easy 20%. The other 80% was a series of small, specific problems that don't show up in a Three.js tutorial. Here are four of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A directional light rig can't light a face it can't see
&lt;/h2&gt;

&lt;p&gt;Early on, customers rotating a table model would find the underside of the tabletop rendering near-black — no matter how far I pushed the light intensity.&lt;/p&gt;

&lt;p&gt;The rig at the time was a single key light plus a hemisphere ambient:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HemisphereLight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mh"&gt;0xffffff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x444444&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keyLight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DirectionalLight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mh"&gt;0xffffff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;keyLight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;keyLight&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bug was geometric, not a brightness problem: keyLight sits above the model, so its light direction only reaches surfaces whose normal faces back toward it. A downward-facing surface — the underside of an overhanging tabletop — can't receive any direct contribution from a light positioned above it, at any intensity. Cranking the brightness slider was scaling a number that was multiplying against zero.&lt;/p&gt;

&lt;p&gt;The fix was closer to actual three-point studio lighting: key, fill, and rim from above for shape and separation, plus a dedicated light from below, and a brighter hemisphere ground color to approximate bounced light:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;scene.add( new THREE.HemisphereLight( 0xffffff, 0x888888, 1.1 * brightness ) );&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;const keyLight = new THREE.DirectionalLight( 0xffffff, 1.1 * brightness );&lt;br&gt;
keyLight.position.set( 3, 5, 4 );&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;const fillLight = new THREE.DirectionalLight( 0xffffff, 0.5 * brightness );&lt;br&gt;
fillLight.position.set( -4, 2, 3 );&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;const rimLight = new THREE.DirectionalLight( 0xffffff, 0.45 * brightness );&lt;br&gt;
rimLight.position.set( -1, 3, -5 );&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;const underLight = new THREE.DirectionalLight( 0xffffff, 0.55 * brightness );&lt;/code&gt;&lt;br&gt;
&lt;code&gt;underLight.position.set( 0, -5, 2 );&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;None of these cast shadows, so the GPU cost is negligible — it's just four more cheap terms in the lighting sum, not a new render pass.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Unbounded OrbitControls zoom looks like a bug, not a feature
By default, OrbitControls' zoom range is [0, Infinity]. Customers were scrolling the model either through the near clipping plane or out until it was a speck — both read as "this is broken," not "I zoomed."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fix is bounding minDistance/maxDistance, but the useful bit is what to scale them against. A fixed number breaks the moment two products have wildly different physical sizes — a ring and a wardrobe can't share a zoom range. Scaling off the model's own auto-computed framing radius fixes that:&lt;/p&gt;

&lt;p&gt;`function applyFraming( framing ) {&lt;br&gt;
    camera.near = framing.radius / 100;&lt;br&gt;
    camera.far = framing.radius * 100;&lt;br&gt;
    camera.updateProjectionMatrix();&lt;br&gt;
    controls.target.copy( framing.center );&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Initial camera distance is framing.radius * sqrt(3) ≈ 1.73,
// so [1, 4] always brackets it comfortably regardless of the
// product's actual size.
controls.minDistance = framing.radius;
controls.maxDistance = framing.radius * 4;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}`&lt;/p&gt;

&lt;p&gt;Every product gets a zoom range proportional to itself, computed at load time from its bounding box, instead of a number that only happens to work for whatever model I was testing with.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Draco, and the WASM question (corrected from an earlier post of mine)
I originally claimed here — and in a Reddit post — that "WordPress.org disallows bundling .wasm binaries." A commenter fairly asked me to point to that rule, and I couldn't find one. Correcting it properly:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There's no line item that names WebAssembly specifically. What actually exists is broader: WordPress.org's plugin guidelines require all distributed code to be non-obfuscated and available as human-readable source. A compiled .wasm binary doesn't qualify as source in any form, so in practice it runs into that same rule — but that's an inference on my part, not a cited policy. Worth being precise about the difference.&lt;/p&gt;

&lt;p&gt;Practical effect is the same either way: Draco's decoder ships in both a WASM build (faster) and a pure-JS build (slower, fully inspectable as source). For WP.org distribution, I force the JS one:&lt;/p&gt;

&lt;p&gt;dracoLoader.setDecoderConfig( { type: 'js' } );&lt;br&gt;
Noticeably slower to decode on large meshes than the WASM path would be — the real cost of going through a plugin directory with source-transparency requirements, not a rule I can point to by name.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Don't reinvent variation pricing — hand off to WooCommerce's own form
The tempting-but-wrong approach when a customer clicks a color swatch: fetch the variation's price yourself and update the DOM. Don't — WooCommerce already has a variation form with its own price/stock lookup, AJAX-cached, that themes and other plugins already hook into. Reimplementing it means every theme customization and every other plugin's price-affecting hook silently stops working for your swatches.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instead, the swatch click just drives WooCommerce's own native :

button.addEventListener( 'click', () =&amp;gt; {
    select.value = swatch.selectValue;
    window.jQuery( select ).trigger( 'change' );
    currentSelections[ group.attributeKey ] = swatch.value;
    recomputeAndApply(); // repaints the 3D model's material
    syncActive();
} );

recomputeAndApply() handles the visual side (swapping the mesh's material). Everything price/stock-related comes from WooCommerce itself reacting to that change event, the same as if the customer had used the dropdown directly. One nice side effect: if a store's variation data has an inconsistency (a value left on "Any" instead of a specific option, which silently breaks WooCommerce's own price matching), it breaks the same way it would with the native dropdown — which made it easy to build a diagnostic warning for, instead of a mystery bug report.

Curious if anyone here has hit the WASM-via-obfuscation-rule issue on other plugin directories, or has a cleaner pattern than event-triggering a native  for handing control back to a host platform's own logic.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>showdev</category>
      <category>webdev</category>
      <category>wordpress</category>
    </item>
  </channel>
</rss>
