Friday, September 25, 2026

MS 3d Viewer'ı modlamaktan bıkıp sıfırdan STL Viewer yazmak.



Windows üzerinde STL dosyalarını hızlı şekilde kontrol etmek için kullandığım Microsoft 3D Viewer'ın desteği sonlandırıldı. Uygulama çalışmaya devam ediyordu, ancak her açılışta örnek Bee modeli yükleniyor ve arayüzün üst bölümünde 3D Viewer'ın artık desteklenmediğini bildiren bir banner gösteriliyordu.

İlk amaç yeni bir program yazmak değildi. Bee modelini ve banner'ı mevcut uygulamadan kaldırmak yeterli olacaktı.

1. Bee.glb nereden geliyor?

İlk olarak uygulamanın kullanıcı profili altındaki geçici dosyaları incelendi. Başlangıç modeli burada görünüyordu:

C:\Users\...\AppData\Local\Packages\
Microsoft.Microsoft3DViewer_8wekyb3d8bbwe\
TempState\Assets\Bee.glb

Dosya yaklaşık 20 MB büyüklüğündeydi. Bee.glb silinip uygulama yeniden başlatıldığında dosya tekrar oluşturuldu. Aynı davranış ağ bağlantısı olmadan da devam etti.

Bu nedenle modelin internetten indirilmediği, uygulama paketinin içerisinde başka bir kopyasının bulunduğu açıktı.

Paket içerisindeki Assets\Archive.zip incelendiğinde kaynak bulundu:

Assets\Archive.zip
└── ModelShelf
    └── Bee.glb

Archive içerisindeki Bee.glb ile TempState altında oluşturulan dosyanın büyüklüğü aynıydı:

Bee.glb
Length           : 20177864 bytes
CompressedLength : 16013449 bytes

2. WindowsApps paketini doğrudan değiştirmemek

Kurulu uygulama şu dizindeydi:

C:\Program Files\WindowsApps\
Microsoft.Microsoft3DViewer_7.2602.8012.0_x64__8wekyb3d8bbwe

WindowsApps altındaki paket üzerinde doğrudan değişiklik yapmak yerine uygulamanın tamamı ayrı bir çalışma dizinine kopyalandı:

C:\Users\...\Desktop\3DViewerMod

Store paketine ait imza ve metadata dosyaları çalışma kopyasından çıkarıldı:

AppxSignature.p7x
AppxBlockMap.xml
AppxMetadata\
microsoft.system.package.metadata

Daha sonra AppxManifest.xml içerisindeki package identity değiştirildi:

Microsoft.Microsoft3DViewer
        ↓
Kadir.3DViewerMod

Developer Mode altında paket yeniden register edildi:

Add-AppxPackage -Register ".\AppxManifest.xml"

Sonuçta Microsoft Store tarafından kurulan orijinal uygulama yerinde kalırken, dosyaları değiştirilebilen bağımsız bir development package elde edildi.

3. Bee'yi boş bir GLB ile değiştirmek

İlk denemede Archive.zip içerisindeki Bee.glb yerine sözdizimsel olarak geçerli 112 byte'lık boş bir GLB yerleştirildi.

Bee kayboldu ancak uygulama bu kez model yükleme hatası verdi:

Couldn't load 3D model
Try again later.


Daha sonra tek üçgenden oluşan geçerli bir GLB oluşturuldu. Sonuç değişmedi. Bu test uygulamanın yalnızca GLB parser'ından geçen herhangi bir dosyayı başlangıç modeli olarak kabul etmediğini gösteriyordu.

Bu noktada yeni bir GLB üretmek yerine orijinal Bee.glb'nin yapısını koruyup yalnızca geometrisini etkisiz hale getirmek daha güvenliydi.

4. GLB dosyasının iç yapısı

GLB 2.0 dosyası temel olarak bir header, JSON chunk ve binary BIN chunk'tan oluşuyor:

+-----------------------+
| GLB Header            |
| magic / version / len |
+-----------------------+
| JSON Chunk Header     |
+-----------------------+
| JSON                  |
+-----------------------+
| BIN Chunk Header      |
+-----------------------+
| Binary vertex data    |
| indices               |
| animation data        |
| ...                   |
+-----------------------+

Orijinal Bee modeli analiz edildiğinde aşağıdaki yapı elde edildi:

Magic       : 0x46546C67
Version     : 2
TotalLength : 20177864

Meshes      : 1
Nodes       : 112
Accessors   : 1934
BufferViews : 1938
Materials   : 1
Animations  : 3

Mesh'in POSITION verisini kullanan accessor bulundu:

POSITION accessor : 1
ComponentType     : 5126 (FLOAT)
Type              : VEC3
Count             : 29614

BufferView        : 1
ViewByteOffset    : 310388
ViewByteLength    : 355368

Boyut da beklenen değerle tam olarak eşleşiyordu:

29614 vertices
× 3 coordinates
× 4 bytes / float
-----------------
355368 bytes

5. Bee geometrisini tek noktaya çökertmek

GLB içerisindeki BIN chunk'ın başlangıç adresi hesaplandı:

jsonLength      = *(uint32_t *)(GLB + 12)
binHeaderOffset = 20 + jsonLength
binDataOffset   = binHeaderOffset + 8

POSITION verisinin gerçek dosya offset'i ise:

positionStart =
    binDataOffset
    + bufferView.byteOffset
    + accessor.byteOffset

Bu dosyada hesaplanan değerler:

BIN data starts : 397920
POSITION starts : 708308
POSITION bytes  : 355368

JSON, node yapısı, material, animation, index buffer ve diğer bütün veriler korundu. Yalnızca POSITION accessor'a ait 355368 byte sıfırlandı.

[Array]::Clear(
    $bytes,
    $positionStart,
    $positionBytes
)

Böylece 29614 vertex'in tamamı:

(0.0, 0.0, 0.0)

koordinatına taşındı. Dosya hâlâ orijinal Bee modelinin bütün yapısına sahipti, ancak mesh geometrisi tek bir noktaya çökmüştü.

Bee geometrisi kaldırılmış Microsoft 3D Viewer

Değiştirilmiş GLB tekrar Archive.zip içerisine yerleştirildiğinde başlangıç modeli görünmedi ve uygulama herhangi bir model yükleme hatası da üretmedi.

6. Deprecation banner

Bee problemi çözüldükten sonra ikinci hedef destek sonlandırma banner'ıydı.



resources.pri içerisinde banner'a ait localization resource'ları bulundu:
InfoBannerPromptAfterText
InfoBannerPromptBeforeText
InfoBannerDialogTitleAfter
InfoBannerDialogTitleBefore
InfoBannerDialogBodyAfter
InfoBannerDialogBodyBefore
InfoBannerDialogCloseButtonText

Mevcut banner metni InfoBannerPromptAfterText tarafından sağlanıyordu. Ancak resource string'i silmek yalnızca metni ortadan kaldıracaktı. Banner'ın kapladığı alan ve container ekranda kalacaktı.

7. 3DViewer.dll içerisinde banner kodu

Resource isimlerinin UTF-16 referansları 3DViewer.dll içerisinde bulundu. Banner metnini hazırlayan fonksiyonlardan biri 0x180EB59E0 adresindeydi.

180eb59e0  push   ...
...
180eb5a10  lea    0x1807cecb8,%rcx
           ; InfoBannerPromptBeforeText

           call   0x1809f4880
           mov    %rax,%rsi

180eb5a25  lea    0x1807ced08,%rcx
           ; InfoBannerPromptAfterText

           call   0x1809f4880
           mov    %rax,%rdi

...

180eb5a5d  test   %al,%al
180eb5a5f  cmove  %rdi,%rsi

180eb5a63  mov    $0x1,%r8b
180eb5a66  mov    %rsi,%rdx
180eb5a69  call   0x180dce7b0

180eb5a71  mov    0x3a0(%r14),%rdx
...
180eb5a9d  call   0x180ad3d40
           ret

Kod hem Before hem de After metnini yüklüyor, çalışma zamanındaki duruma göre birini seçiyor ve this + 0x3A0 alanındaki XAML nesnesine uyguluyordu.

Üst seviye initializer içerisindeki çağrı da bulundu:

180edd9e9  call  0x180e97aa0
180edd9f1  call  0x180eb5be0
180edd9f9  call  0x180eb5970
180edda01  call  0x180eb59e0

Son çağrı geçici olarak NOP ile değiştirildi:

VA          : 0x180EDDA01
File offset : 0xEDAA01

Original:
E8 DA 7F FD FF

Patched:
90 90 90 90 90

Test sonucu fonksiyonun görevi konusunda doğrudan doğrulama sağladı: banner metni kayboldu ancak kırmızı container ekranda kaldı.

8. Generated XAML

Bir sonraki adım compiled XAML tarafına indi. Uygulamada fiziksel XAML dosyaları bulunmuyordu. UI bağlantıları 3DViewer.dll içerisinde generated code olarak bulunuyordu.

İlgili Connect fonksiyonu 0x180EB4C40 adresindeydi:

180eb4c40  push   ...
180eb4c4b  mov    %rcx,%rdi
180eb4c4e  lea    -0x1(%rdx),%eax

           ; connectionId - 1

180eb4c51  cmp    $0x2b,%eax

           ; 43 connection IDs

180eb4c54  jae    0x180eb54aa
180eb4c5a  movslq %eax,%rdx

180eb4c5d  lea    0x18032e220,%rax

           ; jump table

180eb4c64  movslq (%rax,%rdx,4),%rdx
180eb4c68  add    %rax,%rdx
180eb4c6b  jmp    *%rdx

Bu klasik generated-XAML Connect(connectionId, target) dispatch yapısıydı.

Banner metninde kullanılan this + 0x3A0 member'ı connection ID 41 tarafından atanıyordu:

180eb5349  mov    %r8,%rcx
180eb534c  lea    0x18001f968,%rdx
180eb5353  call   *0x180749b20
180eb5359  mov    %rax,%rdx

180eb535c  lea    0x3a0(%rdi),%rcx
180eb5363  call   *0x180749af0

180eb5369  jmp    0x180eb54aa

Komşu XAML member'ları da aynı dispatch içerisinde görünüyordu:

Connection ID 39  → this + 0x390
Connection ID 40  → this + 0x398
Connection ID 41  → this + 0x3A0
Connection ID 42  → event-connected control
Connection ID 43  → event-connected control

Örneğin ID 40:

180eb5324  mov    %r8,%rcx
180eb5327  lea    0x18005e630,%rdx
180eb532e  call   *0x180749b20
180eb5334  mov    %rax,%rdx

180eb5337  lea    0x398(%rdi),%rcx
180eb533e  call   *0x180749af0

Bu aşamada banner'ın text element'i ile surrounding XAML controls arasındaki bağlantılar incelenebiliyordu. Ancak yapılmakta olan iş artık STL görüntülemek için gereken işin çok ötesine geçmişti.

9. Burada reverse engineering'i bırakmak

Başlangıç modeli için GLB vertex buffer değiştirilmiş, Store paketinin development copy'si oluşturulmuş, resources.pri incelenmiş, PE section adresleri hesaplanmış, 3DViewer.dll disassemble edilmiş ve generated XAML connection table'a kadar inilmiştir.

Ama ihtiyaç değişmemişti:

Open STL.
Rotate model.
Inspect geometry.

Bu nedenle 3D Viewer üzerinde çalışmaya devam etmek yerine aynı işi yapan küçük bir native Windows programı yazmak daha uygun hale geldi.

10. Native STL Viewer

Yeni uygulama C++17, Win32 API, Windows Common Controls ve OpenGL kullanılarak yazıldı.

STLViewer.exe
│
├── Win32
│   ├── HWND
│   ├── HMENU
│   └── message loop
│
├── comctl32.dll
│   ├── ToolbarWindow32
│   ├── SysTreeView32
│   ├── SysListView32
│   └── msctls_statusbar32
│
├── uxtheme.dll
│
├── WGL
│   └── OpenGL context
│
└── STL loader
    ├── Binary STL
    └── ASCII STL

Qt, Electron, CEF, WinUI, HTML/CSS veya .NET UI katmanı kullanılmadı. Viewport da ayrı bir native child HWND üzerinde çalışan WGL context'tir.

11. Binary STL parser

Binary STL formatı yeterince basit olduğu için ayrı bir model import framework'ü kullanılmadı.

80 bytes  header
 4 bytes  uint32 triangle count

for each triangle:

12 bytes  normal       (3 × float32)
12 bytes  vertex 1     (3 × float32)
12 bytes  vertex 2     (3 × float32)
12 bytes  vertex 3     (3 × float32)
 2 bytes  attribute

---------------------------------
50 bytes per triangle

Binary STL kontrolünde beklenen dosya büyüklüğü:

expected_size =
    84 + triangle_count * 50

ile gerçek dosya boyutu karşılaştırılıyor. Eşleşme yoksa parser ASCII STL okumayı deniyor.

12. İlk build

İlk build'de iki küçük Win32/CMake problemi çıktı.

Mouse koordinatlarında kullanılan:

GET_X_LPARAM
GET_Y_LPARAM

makroları için windowsx.h eklenmesi gerekiyordu:

#include <windows.h>
#include <windowsx.h>
#include <commctrl.h>

İkinci problem manifest'in hem resource script hem de Visual Studio tarafından eklenmesiydi:

CVTRES : fatal error CVT1100:
duplicate resource. type:MANIFEST, name:1

LINK : fatal error LNK1123:
failure during conversion to COFF

Tekrarlanan manifest resource kaldırıldıktan sonra executable oluşturuldu.

STL Viewer ilk çalışan build

13. İlk STL testi

İlk gerçek test dosyası:

sadboys_mirror_battery_cover_mod.STL

Dosya binary STL olarak algılandı ve 8712 triangle sorunsuz yüklendi. Hesaplanan bounding dimensions:

X : 43.501 mm
Y : 25.244 mm
Z : 49.200 mm

Triangles : 8712
Format    : Binary STL
File size : 425 KB
STL Viewer v0.1 gerçek STL modeli

Version 0.1 bu aşamada binary ve ASCII STL açabiliyor, modeli OpenGL ile çizebiliyor, orbit ve zoom yapabiliyor, shaded/wireframe görünüm sunuyor, grid ve XYZ axes gösterebiliyor ve model boyutlarıyla triangle sayısını hesaplayabiliyor.

14. Arayüz

Arayüz için yeni bir UI framework kullanılmadı. Program geleneksel Windows desktop uygulaması olarak tutuldu.

Ana pencere ve bütün yardımcı kontroller Win32/Common Controls. Bu nedenle menü, TreeView, ListView, status bar ve pencere davranışı Windows tarafından çiziliyor.

Windows desktop UI tasarım referansı

Geliştirme sırasında hedeflenen arayüz yoğunluğu Windows 7 / Office 2010 dönemi desktop utility uygulamalarına yakın tutuldu: küçük toolbar ikonları, standart menüler, sol model/property paneli, büyük viewport ve status bar.

15. Mevcut durum

Proje şu anda ilk çalışan prototip aşamasında. Renderer bilinçli olarak legacy OpenGL immediate mode kullanıyor. Bu, ilk aşamada STL parser ve Win32 shell'i renderer optimizasyonundan bağımsız geliştirmeyi kolaylaştırıyor.

Sonraki adımlar arasında gerçek fit-to-view hesabı, pan, daha kontrollü orbit, splitter, toolbar image list ve daha sonra gerekirse VBO/VAO tabanlı renderer bulunuyor.

Programın kapsamı değişmeyecek:

No cloud.
No account.
No AI.
No telemetry.
No bees.

Opens STL files.