{"id":92,"date":"2026-02-19T21:28:04","date_gmt":"2026-02-19T21:28:04","guid":{"rendered":"https:\/\/mattbick.dev\/?p=92"},"modified":"2026-02-19T21:30:58","modified_gmt":"2026-02-19T21:30:58","slug":"engineering-real-roll-marks-using-my-laser-engraver","status":"publish","type":"post","link":"https:\/\/mattbick.dev\/?p=92","title":{"rendered":"Engineering Real Roll-Marks Using My Laser Engraver"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Well, I survived Mardi Gras, and in lieu of writing another post about C data structures, I have decided to showcase some code I wrote to simulate roll-markings using my MOPA laser engraver. <\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Why Standard Grayscale Engraving Fails \u2014 and How Geometry Fixes It<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">When engraving roll-marks with a laser, the common workflow is simple:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>White = no cut<\/li>\n\n\n\n<li>Black = maximum cut<\/li>\n\n\n\n<li>Midtones = proportional depth<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">This works for artistic relief.<br>It does not produce authentic roll mark geometry.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The issue isn\u2019t power.<br>It\u2019s shape.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here is a photo of an authentic roll-mark:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"591\" height=\"960\" src=\"https:\/\/mattbick.dev\/wp-content\/uploads\/2026\/02\/image.png\" alt=\"\" class=\"wp-image-99\" style=\"width:716px;height:auto\" srcset=\"https:\/\/mattbick.dev\/wp-content\/uploads\/2026\/02\/image.png 591w, https:\/\/mattbick.dev\/wp-content\/uploads\/2026\/02\/image-185x300.png 185w\" sizes=\"auto, (max-width: 591px) 100vw, 591px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">And here are the ACTUAL dies used to produce said roll-mark:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"720\" height=\"540\" src=\"https:\/\/mattbick.dev\/wp-content\/uploads\/2026\/02\/image-1.png\" alt=\"\" class=\"wp-image-101\" srcset=\"https:\/\/mattbick.dev\/wp-content\/uploads\/2026\/02\/image-1.png 720w, https:\/\/mattbick.dev\/wp-content\/uploads\/2026\/02\/image-1-300x225.png 300w\" sizes=\"auto, (max-width: 720px) 100vw, 720px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">And finally, here is a photo of an MP5 build I engraved:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img decoding=\"async\" src=\"https:\/\/bickhamfirearms.com\/wp-content\/uploads\/2026\/01\/Kal9mm.png\" alt=\"\" style=\"width:720px;height:auto\"\/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">And here is where our problem begins&#8230; <\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Hidden Assumption Behind Grayscale Engraving<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Most grayscale workflows assume:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">Pixel intensity directly maps to depth.<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">But that mapping produces a depth field \u2014 not controlled wall geometry.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you engrave a solid black shape using standard grayscale or binary engraving, the cross-section looks like this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"578\" height=\"455\" src=\"https:\/\/mattbick.dev\/wp-content\/uploads\/2026\/02\/output.png\" alt=\"\" class=\"wp-image-93\" srcset=\"https:\/\/mattbick.dev\/wp-content\/uploads\/2026\/02\/output.png 578w, https:\/\/mattbick.dev\/wp-content\/uploads\/2026\/02\/output-300x236.png 300w\" sizes=\"auto, (max-width: 578px) 100vw, 578px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The walls are vertical.<br>The edge is a cliff.<br>The geometry is boxy.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That is not what a real stamped roll mark looks like.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">What a Real Roll-Mark Looks Like<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Real stamped or roll-marked engravings have controlled wall taper.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Typical cross-section:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"578\" height=\"455\" src=\"https:\/\/mattbick.dev\/wp-content\/uploads\/2026\/02\/output-1.png\" alt=\"\" class=\"wp-image-94\" srcset=\"https:\/\/mattbick.dev\/wp-content\/uploads\/2026\/02\/output-1.png 578w, https:\/\/mattbick.dev\/wp-content\/uploads\/2026\/02\/output-1-300x236.png 300w\" sizes=\"auto, (max-width: 578px) 100vw, 578px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Or more accurately, a blended geometry:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"578\" height=\"455\" src=\"https:\/\/mattbick.dev\/wp-content\/uploads\/2026\/02\/output-2.png\" alt=\"\" class=\"wp-image-95\" srcset=\"https:\/\/mattbick.dev\/wp-content\/uploads\/2026\/02\/output-2.png 578w, https:\/\/mattbick.dev\/wp-content\/uploads\/2026\/02\/output-2-300x236.png 300w\" sizes=\"auto, (max-width: 578px) 100vw, 578px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The wall is not purely vertical.<br>There is a transition from edge to interior.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That transition affects:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Edge sharpness<\/li>\n\n\n\n<li>Shadow behavior<\/li>\n\n\n\n<li>Light reflection<\/li>\n\n\n\n<li>Perceived depth<\/li>\n\n\n\n<li>Authenticity<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Laser engraving often fails here because as stated before, it gives us flat walls that don&#8217;t really give the effect of an authentic roll-mark. <\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Why Grayscale Alone Cannot Fix This<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">You can attempt to fake taper using gradients in the artwork itself.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">But that has problems:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Gradient width does not scale with stroke thickness.<\/li>\n\n\n\n<li>Depth becomes power-curve dependent.<\/li>\n\n\n\n<li>You lose geometric consistency in millimeters.<\/li>\n\n\n\n<li>Complex shapes produce unpredictable transitions.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The fundamental issue:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Grayscale encoding does not know where the edge of the shape is.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It only knows brightness.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To control wall shape, we need edge-aware depth generation.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Distance Transform as a Geometric Tool<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of mapping pixel intensity to depth, we can compute:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For every pixel inside the shape:<br>Distance to the nearest boundary.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That gives us a scalar field that describes how far each point is from the edge.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If we normalize that distance over a defined wall width, we can generate a linear inward ramp.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Mathematically:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Let <code>d(x,y)<\/code> be distance to nearest edge.<\/li>\n\n\n\n<li>Let <code>w<\/code> be wall width in pixels.<\/li>\n\n\n\n<li>Ramp factor <code>t = clamp(d \/ w, 0, 1)<\/code><\/li>\n\n\n\n<li>Depth fraction <code>= 1 - t<\/code><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Result:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Edge pixels stay shallow.<\/li>\n\n\n\n<li>Interior pixels reach full depth.<\/li>\n\n\n\n<li>Transition width is controlled in millimeters.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">This produces a physically meaningful wall taper.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Comparing Profiles<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">1. Binary Engraving<\/h3>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"578\" height=\"455\" src=\"https:\/\/mattbick.dev\/wp-content\/uploads\/2026\/02\/output-3.png\" alt=\"\" class=\"wp-image-96\" srcset=\"https:\/\/mattbick.dev\/wp-content\/uploads\/2026\/02\/output-3.png 578w, https:\/\/mattbick.dev\/wp-content\/uploads\/2026\/02\/output-3-300x236.png 300w\" sizes=\"auto, (max-width: 578px) 100vw, 578px\" \/><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">2. Distance-Based Ramp<\/h3>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"578\" height=\"455\" src=\"https:\/\/mattbick.dev\/wp-content\/uploads\/2026\/02\/output-1-1.png\" alt=\"\" class=\"wp-image-97\" srcset=\"https:\/\/mattbick.dev\/wp-content\/uploads\/2026\/02\/output-1-1.png 578w, https:\/\/mattbick.dev\/wp-content\/uploads\/2026\/02\/output-1-1-300x236.png 300w\" sizes=\"auto, (max-width: 578px) 100vw, 578px\" \/><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">3. Square + Ramp Blen<\/h3>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"578\" height=\"455\" src=\"https:\/\/mattbick.dev\/wp-content\/uploads\/2026\/02\/output-2-1.png\" alt=\"\" class=\"wp-image-98\" srcset=\"https:\/\/mattbick.dev\/wp-content\/uploads\/2026\/02\/output-2-1.png 578w, https:\/\/mattbick.dev\/wp-content\/uploads\/2026\/02\/output-2-1-300x236.png 300w\" sizes=\"auto, (max-width: 578px) 100vw, 578px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The third profile most closely resembles real roll mark geometry.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It preserves a defined vertical component while adding a controlled taper.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Controlling Geometry in Real Units<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">One of the key improvements is unit consistency.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of defining ramp width in pixels, we define:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Wall width in millimeters.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Using export DPI:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">mm = (pixels \/ DPI) * 25.4<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That means:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Ramp geometry scales correctly.<\/li>\n\n\n\n<li>Stroke thickness does not break taper width.<\/li>\n\n\n\n<li>The profile is material-aware.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Now geometry is independent of artwork resolution and this makes our code a lot easier to reason about. <\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Blending Straight and Sloped Depth<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Real markings often have a square drop before taper.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We can represent total depth as:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Total depth = straight_wall_depth + sloped_wall_depth<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We generate two images:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Full-depth mask (binary ink)<\/li>\n\n\n\n<li>Sloped ramp (distance-based)<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Then blend them proportionally:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">w_full = straight \/ (straight + sloped)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The result is a controllable square-on-trapezoid profile.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Why This Matters Visually<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">When you engrave metal:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Light grazes edges.<\/li>\n\n\n\n<li>Micro-shadowing enhances perception.<\/li>\n\n\n\n<li>Taper affects highlight width.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">A vertical wall reflects differently than a tapered wall.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Even small taper changes dramatically improve authenticity.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The difference is subtle in isolation, but obvious side-by-side.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Depth Calibration and Pass Control<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Laser depth is rarely linear.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of relying on grayscale power mapping, we treat the grayscale output as:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A heightmap target.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Then calculate required passes using:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">passes = ceil(total_depth_mm \/ mm_per_pass)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This decouples geometry from laser power tuning.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Now:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Geometry is computed.<\/li>\n\n\n\n<li>Laser simply executes depth.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">And from there, Lightburn (the software that most people use to engrave with Galvo lasers) handles the rest. <\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Github Repo:<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">To see the ACTUAL code that does all of this, click <a href=\"https:\/\/github.com\/mb03minecrafter\/RollMarkEngravingTool\/\">here<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Yes, it is in python. The good thing about python is that its image processing libraries are really nice to work with and for that reason and that reason alone I decided to implement all of this math and logic in python. It also made getting to an MVP incredibly quick. All of this took me about a day of reasoning (lots of whiteboard shuffling and schizo lecturing to my girlfriend) and then about 2 hours to write the actual code. Put any other problem in front of me that requires actual speed and complexity and I for sure wouldn&#8217;t get within 10 feet of python. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You will also notice the lack of results photos. This is not because the code does not work, but because if I post a copy of a Colt roll-mark I did for a customer, I will get a very prompt C&amp;D from Colt \ud83d\ude42 <\/p>\n","protected":false},"excerpt":{"rendered":"<p>Well, I survived Mardi Gras, and in lieu of writing another post about C data structures, I have decided to showcase some code I wrote to simulate roll-markings using my MOPA laser engraver. Why Standard Grayscale Engraving Fails \u2014 and How Geometry Fixes It When engraving roll-marks with a laser, the common workflow is simple: [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-92","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/mattbick.dev\/index.php?rest_route=\/wp\/v2\/posts\/92","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/mattbick.dev\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mattbick.dev\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mattbick.dev\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/mattbick.dev\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=92"}],"version-history":[{"count":4,"href":"https:\/\/mattbick.dev\/index.php?rest_route=\/wp\/v2\/posts\/92\/revisions"}],"predecessor-version":[{"id":104,"href":"https:\/\/mattbick.dev\/index.php?rest_route=\/wp\/v2\/posts\/92\/revisions\/104"}],"wp:attachment":[{"href":"https:\/\/mattbick.dev\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=92"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mattbick.dev\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=92"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mattbick.dev\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=92"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}