File size: 52,259 Bytes
ae05fc2
6f466be
ae05fc2
 
 
6f466be
 
ae05fc2
 
6f466be
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ae05fc2
 
6f466be
ae05fc2
 
6f466be
ae05fc2
 
 
 
 
6f466be
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ae05fc2
 
 
 
 
6f466be
ae05fc2
6f466be
ae05fc2
 
6f466be
ae05fc2
6f466be
 
 
ae05fc2
 
 
 
6f466be
 
ae05fc2
 
 
 
 
 
6f466be
ae05fc2
 
 
 
 
 
6f466be
ae05fc2
6f466be
ae05fc2
 
 
 
 
 
 
 
6f466be
ae05fc2
 
 
 
 
 
 
 
 
 
 
6f466be
ae05fc2
 
 
6f466be
ae05fc2
6f466be
ae05fc2
6f466be
ae05fc2
 
 
6f466be
ae05fc2
 
 
 
 
 
 
 
 
 
 
 
 
 
6f466be
ae05fc2
 
 
6f466be
ae05fc2
 
 
 
 
 
6f466be
ae05fc2
 
6f466be
ae05fc2
 
 
 
 
6f466be
 
ae05fc2
 
6f466be
ae05fc2
 
 
 
 
 
6f466be
ae05fc2
 
 
 
 
 
 
6f466be
ae05fc2
6f466be
ae05fc2
 
6f466be
ae05fc2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f466be
 
ae05fc2
 
 
6f466be
 
 
 
 
 
 
 
 
 
 
 
ae05fc2
6f466be
 
ae05fc2
 
 
 
 
6f466be
ae05fc2
6f466be
ae05fc2
 
6f466be
ae05fc2
6f466be
ae05fc2
6f466be
ae05fc2
 
 
 
 
 
6f466be
 
ae05fc2
 
 
6f466be
ae05fc2
 
6f466be
 
ae05fc2
 
 
6f466be
ae05fc2
 
 
 
 
 
 
 
6f466be
ae05fc2
 
 
6f466be
ae05fc2
6f466be
ae05fc2
6f466be
ae05fc2
 
6f466be
ae05fc2
6f466be
 
ae05fc2
 
 
 
6f466be
 
ae05fc2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f466be
ae05fc2
 
 
 
 
6f466be
ae05fc2
6f466be
ae05fc2
 
 
 
6f466be
 
 
 
 
 
ae05fc2
 
 
 
 
 
 
 
 
 
6f466be
ae05fc2
6f466be
 
ae05fc2
 
6f466be
 
 
 
 
 
 
 
 
 
ae05fc2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f466be
 
 
 
 
 
 
 
 
 
ae05fc2
6f466be
 
 
 
 
 
 
 
 
ae05fc2
6f466be
 
 
ae05fc2
6f466be
 
 
 
ae05fc2
 
 
 
 
6f466be
ae05fc2
 
6f466be
ae05fc2
 
 
 
 
 
 
 
 
 
6f466be
ae05fc2
6f466be
 
 
 
 
ae05fc2
6f466be
 
 
ae05fc2
 
 
6f466be
 
ae05fc2
6f466be
ae05fc2
6f466be
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ae05fc2
 
 
6f466be
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ae05fc2
 
 
 
 
 
6f466be
ae05fc2
 
 
 
 
 
 
 
 
 
 
 
 
 
6f466be
 
 
 
 
 
ae05fc2
 
6f466be
 
 
 
 
ae05fc2
6f466be
 
 
ae05fc2
 
6f466be
ae05fc2
6f466be
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ae05fc2
 
 
 
 
 
6f466be
ae05fc2
 
 
 
 
6f466be
ae05fc2
 
 
 
 
6f466be
 
ae05fc2
6f466be
 
ae05fc2
 
 
6f466be
 
 
 
ae05fc2
 
6f466be
 
 
 
 
 
 
 
 
 
 
 
 
 
ae05fc2
6f466be
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ae05fc2
 
 
 
 
 
 
 
6f466be
ae05fc2
 
 
 
 
 
 
 
 
 
 
 
6f466be
 
 
 
 
 
 
 
 
 
ae05fc2
6f466be
 
 
 
ae05fc2
 
 
 
 
 
 
 
 
 
 
 
 
6f466be
ae05fc2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f466be
ae05fc2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f466be
 
 
 
 
ae05fc2
6f466be
 
 
ae05fc2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f466be
 
ae05fc2
6f466be
ae05fc2
6f466be
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ae05fc2
 
6f466be
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ae05fc2
 
6f466be
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ae05fc2
6f466be
 
 
 
 
 
 
 
ae05fc2
 
6f466be
 
 
 
 
 
 
 
 
 
ae05fc2
 
6f466be
 
 
 
ae05fc2
 
be4284e
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
<!DOCTYPE html>
<html lang="en" class="bg-gray-100">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D Web Development Guide</title>
    <!-- Use a simple color palette for a friendly, warm tone -->
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap');
        
        body {
            font-family: 'Inter', sans-serif;
        }

        .container {
            max-width: 1200px;
        }

        .tab-inactive {
            color: #78716c; /* Warm gray */
            border-color: transparent;
        }

        .tab-active {
            color: #654321; /* Dark brown */
            border-color: #654321;
        }

        .interactive-canvas-container {
            position: relative;
            width: 100%;
            padding-top: 56.25%; /* 16:9 Aspect Ratio */
            border-radius: 0.5rem;
            overflow: hidden;
            background-color: #e7e5e4;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        }

        .interactive-canvas-container canvas {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
        }

        /* Basic Accordion Styling */
        .accordion-item {
            border: 1px solid #d1d5db;
            border-radius: 0.5rem;
        }

        .accordion-button {
            background-color: #f9fafb;
            color: #1f2937;
        }
        
        .accordion-button:hover {
            background-color: #f3f4f6;
        }

        .accordion-arrow {
            transition: transform 0.3s ease;
        }

        .accordion-button.open .accordion-arrow {
            transform: rotate(180deg);
        }

        .accordion-content {
            max-height: 0;
            overflow: hidden;
            transition: max-height 0.3s ease;
            background-color: #fff;
            padding: 0 1rem;
        }

        .accordion-content p {
            padding: 0.5rem 0;
        }

        .accordion-button.open + .accordion-content {
            max-height: 500px; /* Adjust as needed for content */
        }

        /* Debug Console Styling */
        .debug-console {
            background-color: #1e1e1e;
            color: #d4d4d4;
            padding: 1rem;
            border-radius: 0.5rem;
            font-family: 'Consolas', 'Courier New', monospace;
            white-space: pre-wrap;
            line-height: 1.5;
            box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
        }
        .debug-info { color: #569cd6; }
        .debug-warn { color: #dcdcaa; }
        .debug-error { color: #f44747; }
        .debug-line { padding: 0.2rem 0; cursor: pointer; }
        .debug-line:hover { background-color: rgba(255, 255, 255, 0.05); }

    </style>
</head>
<body class="bg-gray-100 text-gray-800">

    <div class="container mx-auto p-4 sm:p-6 lg:p-8">

        <header class="text-center mb-8">
            <h1 class="text-4xl md:text-5xl font-bold text-gray-800" style="color: #654321;">Varun Mulay's Guide to 3D Web Development</h1>
            <p class="mt-2 text-lg text-gray-600" style="color: #78716c;">An interactive guide to creating 3D games and simulations with Three.js.</p>
        </header>

        <nav class="mb-8 border-b border-gray-200 overflow-x-auto">
            <ul class="flex flex-nowrap text-sm font-medium text-center" id="tab-nav">
                <li class="flex-shrink-0">
                    <button class="inline-block p-4 border-b-2 rounded-t-lg tab-active" id="fundamentals-tab">1. Fundamentals</button>
                </li>
                <li class="flex-shrink-0">
                    <button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" id="controls-tab">2. Orbital Controls</button>
                </li>
                <li class="flex-shrink-0">
                    <button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" id="models-tab">3. Importing Models</button>
                </li>
                <li class="flex-shrink-0">
                    <button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" id="physics-tab">4. Physics with Cannon.js</button>
                </li>
                <li class="flex-shrink-0">
                    <button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" id="particles-tab">5. Particle & Fluid Simulation</button>
                </li>
                <li class="flex-shrink-0">
                    <button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" id="debugging-tab">6. Debugging</button>
                </li>
                <li class="flex-shrink-0">
                    <button class="inline-block p-4 border-b-2 rounded-t-lg tab-inactive" id="model-viewer-tab">7. Model Viewer</button>
                </li>
            </ul>
        </nav>

        <main id="tab-content">
            <!-- 1. Fundamentals -->
            <section id="fundamentals" class="space-y-6 hidden">
                <h2 class="text-3xl font-bold" style="color: #a58d6f;">Three.js Fundamentals: The Core Components</h2>
                <p>Every Three.js application, from a simple cube to a complex game, is built upon three core components: the <code>Scene</code>, the <code>Camera</code>, and the <code>Renderer</code>. This section provides an interactive demonstration of these foundational elements.</p>
                <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
                    <div>
                        <div class="interactive-canvas-container" id="fundamentals-canvas"></div>
                        <div class="mt-4 flex flex-wrap gap-2 justify-center">
                            <button id="fundamentals-move-x" class="px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition">Move X</button>
                            <button id="fundamentals-move-y" class="px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition">Move Y</button>
                            <button id="fundamentals-color" class="px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition">Change Color</button>
                        </div>
                    </div>
                    <div>
                        <h3 class="text-xl font-semibold mb-2">Core Logic Explained</h3>
                        <p class="mb-4">Below is the essential JavaScript code. The interactive buttons on the left directly call functions that modify the cube's properties.</p>
                        <pre><code class="language-javascript">{`
// 1. Scene: The container for all objects
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0);

// 2. Camera: Defines the viewpoint
const camera = new THREE.PerspectiveCamera(
    75, width / height, 0.1, 1000
);
camera.position.z = 5;

// 3. Renderer: Renders the scene
const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
container.appendChild(renderer.domElement);

// Add lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 5, 5);
scene.add(directionalLight);

// Create an object (Mesh = Geometry + Material)
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x846c5b });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

// Animation loop
function animate() {
    requestAnimationFrame(animate);
    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;
    renderer.render(scene, camera);
}
animate();
                        `}</code></pre>
                    </div>
                </div>
            </section>

            <!-- 2. Orbital Controls -->
            <section id="controls" class="space-y-6 hidden">
                <h2 class="text-3xl font-bold" style="color: #a58d6f;">Mastering Orbital Controls</h2>
                <p><code>OrbitControls</code> is a powerful tool that gives the user freedom to pan, zoom, and rotate the camera. This section explains the correct setup and provides a troubleshooting guide for common errors.</p>
                <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
                    <div>
                        <h3 class="text-xl font-semibold mb-2">Correct Implementation</h3>
                        <pre><code class="language-javascript">{`
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// ... After setting up scene, camera, renderer

// Initialize controls
const controls = new OrbitControls(camera, renderer.domElement);

// CRITICAL: Update controls in the animation loop
function animate() {
    requestAnimationFrame(animate);
    controls.update(); 
    renderer.render(scene, camera);
}
animate();
                        `}</code></pre>
                    </div>
                    <div class="space-y-4">
                        <h3 class="text-xl font-semibold mb-2">Troubleshooting Common Errors</h3>
                        <div class="accordion-item">
                            <button class="accordion-button flex justify-between items-center w-full p-4 text-left font-medium">
                                <span>My controls feel laggy or don't stop immediately.</span>
                                <span class="accordion-arrow transform transition-transform"></span>
                            </button>
                            <div class="accordion-content px-4 pb-4">
                                <p><strong>Cause:</strong> You have set <code>controls.enableDamping = true</code> but forgotten to call <code>controls.update()</code> inside your animation loop.</p>
                                <p><strong>Fix:</strong> Add <code>controls.update()</code> to your <code>animate</code> function.</p>
                            </div>
                        </div>
                        <div class="accordion-item">
                            <button class="accordion-button flex justify-between items-center w-full p-4 text-left font-medium">
                                <span>The camera zooms in/out but doesn't rotate.</span>
                                <span class="accordion-arrow transform transition-transform"></span>
                            </button>
                            <div class="accordion-content px-4 pb-4">
                                <p><strong>Cause:</strong> The OrbitControls script has not loaded correctly.</p>
                                <p><strong>Fix:</strong> Check your browser's developer console (F12) for errors. Ensure the path to <code>OrbitControls.js</code> in your import is correct.</p>
                            </div>
                        </div>
                        <div class="accordion-item">
                            <button class="accordion-button flex justify-between items-center w-full p-4 text-left font-medium">
                                <span>My camera can go below the "ground" plane.</span>
                                <span class="accordion-arrow transform transition-transform"></span>
                            </button>
                            <div class="accordion-content px-4 pb-4">
                                <p><strong>Cause:</strong> The default polar angle allows for 360-degree vertical rotation.</p>
                                <p><strong>Fix:</strong> Limit the vertical rotation by setting <code>controls.maxPolarAngle = Math.PI / 2;</code></p>
                            </div>
                        </div>
                    </div>
                </div>
            </section>

            <!-- 3. Importing Models -->
            <section id="models" class="space-y-6 hidden">
                <h2 class="text-3xl font-bold" style="color: #a58d6f;">Importing 3D Models (.glb/.gltf)</h2>
                <p>Use the button below to upload a model from your computer. You can use the sliders to interactively adjust its position, rotation, and scale, which are the fundamental transformations you'll apply to any object in your scene.</p>
                <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
                    <div>
                        <div class="interactive-canvas-container" id="models-canvas"></div>
                        <div class="mt-4 grid grid-cols-1 sm:grid-cols-3 gap-4 p-4 bg-white border rounded-md">
                            <div>
                                <label for="model-pos-x" class="block text-sm font-medium">Position X</label>
                                <input type="range" id="model-pos-x" min="-5" max="5" step="0.1" value="0" class="w-full">
                            </div>
                            <div>
                                <label for="model-rot-y" class="block text-sm font-medium">Rotation Y</label>
                                <input type="range" id="model-rot-y" min="-3.14" max="3.14" step="0.1" value="0" class="w-full">
                            </div>
                            <div>
                                <label for="model-scale" class="block text-sm font-medium">Scale</label>
                                <input type="range" id="model-scale" min="0.1" max="3" step="0.1" value="1.5" class="w-full">
                            </div>
                        </div>
                        <div class="mt-4 flex flex-wrap gap-2 justify-center">
                            <label class="px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition cursor-pointer">
                                Upload Model (.glb/.gltf)
                                <input type="file" id="model-upload" class="hidden" accept=".glb, .gltf">
                            </label>
                        </div>
                    </div>
                    <div>
                        <h3 class="text-xl font-semibold mb-2">Model Loader & Transformation Code</h3>
                        <p class="mb-4">Loading models is an asynchronous operation. You use a specific loader and define a callback function that executes once the model has loaded successfully.</p>
                        <pre><code class="language-javascript">{`
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

const loader = new GLTFLoader();
loader.load(
    'path/to/model.glb',
    function (gltf) {
        const model = gltf.scene;
        // Transformations applied here
        model.position.set(0, -1, 0);
        model.scale.set(1.5, 1.5, 1.5);
        scene.add(model);
    },
    undefined,
    function (error) {
        console.error('An error occurred while loading the model:', error);
    }
);
                        `}</code></pre>
                    </div>
                </div>
            </section>

            <!-- 4. Physics with Cannon.js -->
            <section id="physics" class="space-y-6 hidden">
                <h2 class="text-3xl font-bold" style="color: #a58d6f;">The Physics Engine: Cannon.js</h2>
                <p>Cannon.js handles the backend calculations for gravity, collisions, and forces, while you use its results to update the positions and rotations of your visible Three.js objects. Click "Drop Ball" to see the collision event.</p>
                <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
                    <div>
                        <div class="interactive-canvas-container" id="physics-canvas"></div>
                        <div class="mt-4 flex flex-wrap gap-2 justify-center">
                            <button id="drop-ball" class="px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition">Drop Ball</button>
                            <label class="flex items-center space-x-2 px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm">
                                <input type="checkbox" id="physics-debug" class="form-checkbox h-4 w-4 text-gray-600 transition-colors">
                                <span class="text-sm font-medium text-gray-700">Debug Physics</span>
                            </label>
                        </div>
                    </div>
                    <div>
                        <h3 class="text-xl font-semibold mb-2">Connecting Physics to Rendering</h3>
                        <p class="mb-4">The core loop of a physics-based game involves two key steps: first, advance the physics world, then update the visual objects based on the new physics positions.</p>
                        <pre><code class="language-javascript">{`
import * as CANNON from 'cannon-es';
import * as CannonDebugRenderer from 'cannon-es-debugger';

const world = new CANNON.World({ gravity: new CANNON.Vec3(0, -9.82, 0) });
const cannonDebugRenderer = new CannonDebugRenderer.default(scene, world);

// Create the physical ground plane
const groundBody = new CANNON.Body({ mass: 0 });
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
world.addBody(groundBody);

// Update both in the animation loop
function animate() {
    requestAnimationFrame(animate);
    world.fixedStep(); 
    cannonDebugRenderer.update();
    // Sync each Three.js mesh with its Cannon.js body
    renderer.render(scene, camera);
}
animate();
                        `}</code></pre>
                    </div>
                </div>
            </section>

            <!-- 5. Particle & Fluid Simulation -->
            <section id="particles" class="space-y-6 hidden">
                <h2 class="text-3xl font-bold" style="color: #a58d6f;">Buoyancy and Density Simulation</h2>
                <p>In this simple demonstration, we've created a custom particle system to show the difference between wood and metal. Wood, being less dense, floats on the surface, while metal, being denser, continues to sink.</p>
                <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
                    <div>
                        <div class="interactive-canvas-container" id="particles-canvas"></div>
                        <div class="mt-4 flex flex-wrap gap-2 justify-center">
                            <button id="drop-wood" class="px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition">Drop Wood</button>
                            <button id="drop-metal" class="px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition">Drop Metal</button>
                        </div>
                    </div>
                    <div>
                        <h3 class="text-xl font-semibold mb-2">Simulation Logic</h3>
                        <p class="mb-4">Here's a breakdown of the code that creates the simulation. The animation loop applies a different "physics" rule to each particle once it hits the water plane.</p>
                        <pre><code class="language-javascript">{`
// The main animation loop
function animate() {
    requestAnimationFrame(animate);
    
    // Update particle positions and apply physics
    for (let i = 0; i < particleCount * 3; i += 3) {
        const particleIndex = i / 3;
        const currentY = positions[i + 1];
        
        // If the particle is below the water surface
        if (currentY < water.position.y) {
            if (particleTypes[particleIndex] === 'wood') {
                // Wood floats: Stop it from sinking further
                positions[i + 1] = water.position.y;
                velocities[i + 1] = 0;
            } else if (particleTypes[particleIndex] === 'metal') {
                // Metal sinks: Continue downward motion with some resistance
                velocities[i + 1] *= 0.98;
            }
        } else {
            // In freefall, apply simple gravity
            velocities[i + 1] -= 0.005;
        }

        positions[i] += velocities[i];
        positions[i + 1] += velocities[i + 1];
        positions[i + 2] += velocities[i + 2];
    }
    
    particleGeometry.attributes.position.needsUpdate = true;
    renderer.render(scene, camera);
}
animate();
                        `}</code></pre>
                    </div>
                </div>
            </section>

            <!-- 6. Debugging -->
            <section id="debugging" class="space-y-6 hidden">
                <h2 class="text-3xl font-bold" style="color: #a58d6f;">A Game Developer's Debugging Toolkit</h2>
                <p>When things go wrong in 3D, the errors can be cryptic. A blank screen is a common symptom for many different problems. Click on each error line to understand its likely cause and how to approach fixing it, turning you into a more effective problem solver.</p>
                <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
                     <div class="space-y-4">
                        <h3 class="text-xl font-semibold mb-2">Interactive Debug Console</h3>
                        <div class="debug-console" id="debug-console">
                            <div class="debug-line debug-info" data-fix="fix1">▷ INFO: THREE.WebGLRenderer 164</div>
                            <div class="debug-line debug-warn" data-fix="fix2">▷ WARN: Scene has no lights. Objects may appear black.</div>
                            <div class="debug-line debug-error" data-fix="fix3">▷ ERROR: TypeError: Cannot read properties of undefined (reading 'scene')</div>
                            <div class="debug-line debug-error" data-fix="fix4">▷ ERROR: Failed to load resource: net::ERR_FILE_NOT_FOUND Horse.glb</div>
                            <div class="debug-line debug-info" data-fix="fix5">▷ INFO: Animation loop started.</div>
                            <div class="debug-line debug-error" data-fix="fix6">▷ ERROR: Uncaught TypeError: Cannot set properties of null (setting 'position')</div>
                        </div>
                     </div>
                     <div class="p-4 bg-white border rounded-md" id="debug-fix-display">
                        <h3 class="text-xl font-semibold mb-2">Analysis & Fix</h3>
                        <p class="text-gray-500">Click on an error in the console to see a detailed explanation and solution here.</p>
                     </div>
                </div>
            </section>
            
            <!-- 7. Model Viewer -->
            <section id="model-viewer" class="space-y-6 hidden">
                <h2 class="text-3xl font-bold" style="color: #a58d6f;">The Easy Way: Google's &lt;model-viewer&gt;</h2>
                <p>For some use cases, like displaying a single product or a piece of art, a full Three.js scene is overkill. Google's <code>&lt;model-viewer&gt;</code> is a web component that lets you declaratively add a 3D model to a webpage with minimal code. It's incredibly powerful for simple showcases, offering features like AR placement, animations, and camera controls right out of the box.</p>
                <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
                    <div>
                        <h3 class="text-xl font-semibold mb-2">Live Example</h3>
                        <div id="model-viewer-container">
                            <model-viewer
                                src="https://modelviewer.dev/shared-assets/models/Astronaut.glb"
                                alt="A 3D model of an astronaut"
                                ar
                                auto-rotate
                                camera-controls
                                style="width: 100%; height: 400px; background-color: #f0f0f0; border-radius: 8px;"
                            ></model-viewer>
                        </div>
                    </div>
                    <div>
                        <h3 class="text-xl font-semibold mb-2">When to Use Which?</h3>
                        <div class="space-y-4">
                            <div>
                                <h4 class="font-semibold text-lg" style="color: #846c5b;">Use <code>&lt;model-viewer&gt;</code> when:</h4>
                                <ul class="list-disc list-inside text-gray-700">
                                    <li>You need to display a single, pre-made 3D model.</li>
                                    <li>The main goal is showcasing the model (e.g., e-commerce, portfolio).</li>
                                    <li>You want easy integration with Augmented Reality (AR).</li>
                                    <li>You prefer a declarative HTML approach over writing JavaScript.</li>
                                </ul>
                            </div>
                            <div>
                                <h4 class="font-semibold text-lg" style="color: #846c5b;">Use <code>Three.js</code> when:</h4>
                                <ul class="list-disc list-inside text-gray-700">
                                    <li>You are building a game or a highly interactive simulation.</li>
                                    <li>You need to manage multiple objects, physics, and complex logic.</li>
                                    <li>You require custom shaders, post-processing effects, or particle systems.</li>
                                    <li>You need full programmatic control over every aspect of the 3D scene.</li>
                                </ul>
                            </div>
                        </div>
                    </div>
                </div>
            </section>
        </main>
    </div>

    <script type="module">
        import * as THREE from 'https://cdn.skypack.dev/three@0.132.2';
        import { OrbitControls } from 'https://cdn.skypack.dev/three@0.132.2/examples/jsm/controls/OrbitControls.js';
        import { GLTFLoader } from 'https://cdn.skypack.dev/three@0.132.2/examples/jsm/loaders/GLTFLoader.js';
        import * as CANNON from 'https://cdn.skypack.dev/cannon-es@0.20.0';
        import CannonDebugger from 'https://cdn.skypack.dev/cannon-es-debugger@1.0.0';
        import { Water } from 'https://cdn.skypack.dev/three@0.132.2/examples/jsm/objects/Water.js';

        const tabs = document.querySelectorAll('#tab-nav button');
        const sections = document.querySelectorAll('#tab-content section');
        const disposers = {};
        
        // Function to clean up a scene
        const cleanupScene = (canvasId) => {
            const canvasContainer = document.getElementById(canvasId);
            if (!canvasContainer) return;
            const canvas = canvasContainer.querySelector('canvas');
            if (canvas) {
                if (disposers[canvasId]) {
                    disposers[canvasId]();
                    delete disposers[canvasId];
                }
                canvas.parentNode.removeChild(canvas);
            }
        };

        // Function to set up the fundamentals scene
        const setupFundamentals = () => {
            const container = document.getElementById('fundamentals-canvas');
            if (!container) return;

            const scene = new THREE.Scene();
            scene.background = new THREE.Color(0xe7e5e4);
            const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
            const renderer = new THREE.WebGLRenderer({ antialias: true });

            renderer.setSize(container.clientWidth, container.clientHeight);
            container.appendChild(renderer.domElement);

            const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
            scene.add(ambientLight);
            const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
            directionalLight.position.set(5, 5, 5);
            scene.add(directionalLight);

            const geometry = new THREE.BoxGeometry(1, 1, 1);
            const material = new THREE.MeshStandardMaterial({ color: 0x846c5b, metalness: 0.3, roughness: 0.6 });
            const cube = new THREE.Mesh(geometry, material);
            scene.add(cube);
            camera.position.z = 3;

            const handleResize = () => {
                camera.aspect = container.clientWidth / container.clientHeight;
                camera.updateProjectionMatrix();
                renderer.setSize(container.clientWidth, container.clientHeight);
            };

            let animationFrameId;
            const animate = () => {
                animationFrameId = requestAnimationFrame(animate);
                cube.rotation.x += 0.005;
                cube.rotation.y += 0.005;
                renderer.render(scene, camera);
            };
            
            animate();
            window.addEventListener('resize', handleResize);
            
            document.getElementById('fundamentals-move-x').onclick = () => {
                cube.position.x += 0.5;
            };
            document.getElementById('fundamentals-move-y').onclick = () => {
                cube.position.y += 0.5;
            };
            document.getElementById('fundamentals-color').onclick = () => {
                cube.material.color.setHex(Math.random() * 0xffffff);
            };

            disposers['fundamentals-canvas'] = () => {
                window.removeEventListener('resize', handleResize);
                cancelAnimationFrame(animationFrameId);
                scene.children.forEach(child => scene.remove(child));
                renderer.dispose();
            };
        };
        
        // Function to set up the controls scene
        const setupControls = () => {
             const container = document.getElementById('controls-canvas');
            if (!container) return;

            const scene = new THREE.Scene();
            scene.background = new THREE.Color(0xe7e5e4);
            const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
            camera.position.z = 5;
            const renderer = new THREE.WebGLRenderer({ antialias: true });

            renderer.setSize(container.clientWidth, container.clientHeight);
            container.appendChild(renderer.domElement);

            const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
            scene.add(ambientLight);
            const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
            directionalLight.position.set(5, 5, 5);
            scene.add(directionalLight);

            const geometry = new THREE.BoxGeometry(1, 1, 1);
            const material = new THREE.MeshStandardMaterial({ color: 0x846c5b, metalness: 0.3, roughness: 0.6 });
            const cube = new THREE.Mesh(geometry, material);
            scene.add(cube);

            const controls = new OrbitControls(camera, renderer.domElement);
            controls.enableDamping = true; // an animation loop is required when damping is enabled
            controls.dampingFactor = 0.05;

            const handleResize = () => {
                camera.aspect = container.clientWidth / container.clientHeight;
                camera.updateProjectionMatrix();
                renderer.setSize(container.clientWidth, container.clientHeight);
            };
            
            let animationFrameId;
            const animate = () => {
                animationFrameId = requestAnimationFrame(animate);
                controls.update();
                renderer.render(scene, camera);
            };

            animate();
            window.addEventListener('resize', handleResize);

            disposers['controls-canvas'] = () => {
                window.removeEventListener('resize', handleResize);
                cancelAnimationFrame(animationFrameId);
                scene.children.forEach(child => scene.remove(child));
                renderer.dispose();
            };
        };

        // Function to set up the models scene
        const setupModels = () => {
            const container = document.getElementById('models-canvas');
            if (!container) return;

            const scene = new THREE.Scene();
            scene.background = new THREE.Color(0xe7e5e4);
            const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
            camera.position.set(0, 1.5, 4);
            const renderer = new THREE.WebGLRenderer({ antialias: true });

            renderer.setSize(container.clientWidth, container.clientHeight);
            container.appendChild(renderer.domElement);

            const controls = new OrbitControls(camera, renderer.domElement);
            controls.enableDamping = true;
            controls.target.set(0, 0.5, 0);

            const ambientLight = new THREE.AmbientLight(0xffffff, 2);
            scene.add(ambientLight);
            const dirLight = new THREE.DirectionalLight(0xffffff, 3);
            dirLight.position.set(5, 10, 7.5);
            scene.add(dirLight);

            const loader = new GLTFLoader();
            let model;
            loader.load('https://cdn.jsdelivr.net/npm/three@0.164.1/examples/models/gltf/Horse.glb', (gltf) => {
                model = gltf.scene;
                model.position.y = -1;
                model.scale.set(1.5, 1.5, 1.5);
                scene.add(model);
            });

            const handleResize = () => {
                camera.aspect = container.clientWidth / container.clientHeight;
                camera.updateProjectionMatrix();
                renderer.setSize(container.clientWidth, container.clientHeight);
            };
            
            let animationFrameId;
            const animate = () => {
                animationFrameId = requestAnimationFrame(animate);
                controls.update();
                renderer.render(scene, camera);
            };
            
            animate();
            window.addEventListener('resize', handleResize);

            document.getElementById('model-pos-x').oninput = (e) => {
                if (model) model.position.x = parseFloat(e.target.value);
            };
            document.getElementById('model-rot-y').oninput = (e) => {
                if (model) model.rotation.y = parseFloat(e.target.value);
            };
            document.getElementById('model-scale').oninput = (e) => {
                if (model) model.scale.set(parseFloat(e.target.value), parseFloat(e.target.value), parseFloat(e.target.value));
            };
            document.getElementById('model-upload').onchange = (e) => {
                const file = e.target.files[0];
                if (!file) return;

                const url = URL.createObjectURL(file);
                loader.load(url, (gltf) => {
                    if (model) scene.remove(model);
                    model = gltf.scene;
                    model.position.y = -1;
                    scene.add(model);
                });
            };

            disposers['models-canvas'] = () => {
                window.removeEventListener('resize', handleResize);
                cancelAnimationFrame(animationFrameId);
                scene.children.forEach(child => scene.remove(child));
                renderer.dispose();
            };
        };

        // Function to set up the physics scene
        const setupPhysics = () => {
            const container = document.getElementById('physics-canvas');
            if (!container) return;

            const scene = new THREE.Scene();
            scene.background = new THREE.Color(0xe7e5e4);
            const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
            camera.position.set(0, 5, 10);
            const renderer = new THREE.WebGLRenderer({ antialias: true });

            renderer.setSize(container.clientWidth, container.clientHeight);
            container.appendChild(renderer.domElement);

            const controls = new OrbitControls(camera, renderer.domElement);
            controls.enableDamping = true;

            const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
            scene.add(ambientLight);
            const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
            directionalLight.position.set(5, 10, 7.5);
            scene.add(directionalLight);
            
            const world = new CANNON.World({ gravity: new CANNON.Vec3(0, -9.82, 0) });
            const groundShape = new CANNON.Plane();
            const groundBody = new CANNON.Body({ mass: 0 });
            groundBody.addShape(groundShape);
            groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
            world.addBody(groundBody);

            const groundMesh = new THREE.Mesh(new THREE.PlaneGeometry(20, 20), new THREE.MeshStandardMaterial({ color: 0xcccccc }));
            groundMesh.rotation.x = -Math.PI / 2;
            scene.add(groundMesh);

            const meshes = [];
            const bodies = [];
            let debuggerInstance;

            const handleResize = () => {
                camera.aspect = container.clientWidth / container.clientHeight;
                camera.updateProjectionMatrix();
                renderer.setSize(container.clientWidth, container.clientHeight);
            };

            let animationFrameId;
            const timeStep = 1 / 60;
            const animate = () => {
                animationFrameId = requestAnimationFrame(animate);
                world.step(timeStep);
                controls.update();

                if (document.getElementById('physics-debug').checked) {
                    if (!debuggerInstance) {
                        debuggerInstance = new CannonDebugger(scene, world);
                    }
                    debuggerInstance.update();
                } else {
                    if (debuggerInstance) {
                        debuggerInstance.destroy();
                        debuggerInstance = null;
                    }
                }
                
                for (let i = 0; i < meshes.length; i++) {
                    meshes[i].position.copy(bodies[i].position);
                    meshes[i].quaternion.copy(bodies[i].quaternion);
                }

                renderer.render(scene, camera);
            };
            
            animate();
            window.addEventListener('resize', handleResize);

            document.getElementById('drop-ball').onclick = () => {
                const radius = 0.5;
                const sphereGeometry = new THREE.SphereGeometry(radius, 32, 32);
                const sphereMaterial = new THREE.MeshStandardMaterial({ color: 0x846c5b });
                const sphereMesh = new THREE.Mesh(sphereGeometry, sphereMaterial);
                sphereMesh.position.y = 10;
                scene.add(sphereMesh);
                meshes.push(sphereMesh);

                const sphereBody = new CANNON.Body({ mass: 1, shape: new CANNON.Sphere(radius) });
                sphereBody.position.y = 10;
                world.addBody(sphereBody);
                bodies.push(sphereBody);
                
                sphereBody.addEventListener('collide', (event) => {
                    sphereMesh.material.color.setHex(0xff0000);
                    setTimeout(() => {
                        sphereMesh.material.color.setHex(0x846c5b);
                    }, 200);
                });
            };

            disposers['physics-canvas'] = () => {
                window.removeEventListener('resize', handleResize);
                cancelAnimationFrame(animationFrameId);
                if (debuggerInstance) { debuggerInstance.destroy(); }
                scene.children.forEach(child => scene.remove(child));
                renderer.dispose();
                meshes.length = 0;
                bodies.length = 0;
            };
        };

        // Function to set up the particle simulation
        const setupParticles = () => {
            const container = document.getElementById('particles-canvas');
            if (!container) return;
            
            const scene = new THREE.Scene();
            scene.background = new THREE.Color(0x000000);
            const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000);
            camera.position.set(0, 10, 20);
            
            const renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setSize(container.clientWidth, container.clientHeight);
            container.appendChild(renderer.domElement);
            
            const controls = new OrbitControls(camera, renderer.domElement);
            controls.enableDamping = true;
            controls.target.set(0, 0, 0);

            const waterGeometry = new THREE.PlaneGeometry(2000, 2000);
            const water = new Water(
                waterGeometry,
                {
                    textureWidth: 512,
                    textureHeight: 512,
                    waterNormals: new THREE.TextureLoader().load('https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/waternormals.jpg', function (texture) {
                        texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
                    }),
                    sunDirection: new THREE.Vector3(0, 1, 0),
                    sunColor: 0x48494b,
                    waterColor: 0x005577,
                    distortionScale: 3.7,
                    fog: scene.fog !== undefined
                }
            );
            water.rotation.x = -Math.PI / 2;
            scene.add(water);

            const particleCount = 500;
            const positions = new Float32Array(particleCount * 3);
            const velocities = new Float32Array(particleCount * 3);
            const particleTypes = new Array(particleCount).fill(null);
            const colors = new Float32Array(particleCount * 3);

            const particleGeometry = new THREE.BufferGeometry();
            particleGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
            particleGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));

            const particleMaterial = new THREE.PointsMaterial({
                size: 0.5,
                vertexColors: true,
                sizeAttenuation: true,
                transparent: true,
                opacity: 0.9,
                blending: THREE.AdditiveBlending
            });
            const particleSystem = new THREE.Points(particleGeometry, particleMaterial);
            scene.add(particleSystem);
            
            const woodColor = new THREE.Color(0x8B4513);
            const metalColor = new THREE.Color(0x708090);

            let particleCursor = 0;

            const spawnParticles = (type) => {
                const count = 50;
                const spread = 20;
                const startY = 15;
                
                for (let i = 0; i < count; i++) {
                    const idx = particleCursor * 3;
                    positions[idx] = (Math.random() - 0.5) * spread;
                    positions[idx + 1] = startY + Math.random() * 5;
                    positions[idx + 2] = (Math.random() - 0.5) * spread;
                    
                    velocities[idx] = 0;
                    velocities[idx + 1] = -0.3 - Math.random() * 0.3;
                    velocities[idx + 2] = 0;
                    
                    particleTypes[particleCursor] = type;
                    
                    if (type === 'wood') {
                        woodColor.toArray(colors, idx);
                    } else {
                        metalColor.toArray(colors, idx);
                    }

                    particleCursor = (particleCursor + 1) % particleCount;
                }
                particleGeometry.attributes.position.needsUpdate = true;
                particleGeometry.attributes.color.needsUpdate = true;
            };
            
            const handleResize = () => {
                camera.aspect = container.clientWidth / container.clientHeight;
                camera.updateProjectionMatrix();
                renderer.setSize(container.clientWidth, container.clientHeight);
            };

            let animationFrameId;
            const animate = () => {
                animationFrameId = requestAnimationFrame(animate);
                
                controls.update();
                water.material.uniforms['time'].value += 1.0 / 60.0;
                
                for (let i = 0; i < particleCount * 3; i += 3) {
                    const particleIndex = i / 3;
                    const currentY = positions[i + 1];
                    
                    if (currentY < water.position.y) {
                        if (particleTypes[particleIndex] === 'wood') {
                            positions[i + 1] = water.position.y;
                            velocities[i + 1] = 0;
                        } else if (particleTypes[particleIndex] === 'metal') {
                            velocities[i + 1] *= 0.98;
                        }
                    } else {
                        velocities[i + 1] -= 0.005;
                    }

                    positions[i] += velocities[i];
                    positions[i + 1] += velocities[i + 1];
                    positions[i + 2] += velocities[i + 2];
                }
                
                particleGeometry.attributes.position.needsUpdate = true;
                renderer.render(scene, camera);
            };
            
            animate();
            window.addEventListener('resize', handleResize);

            document.getElementById('drop-wood').onclick = () => spawnParticles('wood');
            document.getElementById('drop-metal').onclick = () => spawnParticles('metal');

            disposers['particles-canvas'] = () => {
                window.removeEventListener('resize', handleResize);
                cancelAnimationFrame(animationFrameId);
                scene.children.forEach(child => scene.remove(child));
                renderer.dispose();
            };
        };

        const setupDebugging = () => {
            const fixes = {
                fix1: "This is a standard startup message confirming that the Three.js renderer has been successfully initialized. It's not an error. If you don't see this, it's likely Three.js itself failed to load.",
                fix2: "Cause: Most materials, like MeshStandardMaterial or MeshPhongMaterial, require light to be visible. Without any lights in the scene, objects using these materials will render as black. Fix: Add at least one light to your scene. An AmbientLight provides basic, flat illumination, while a DirectionalLight simulates a distant light source like the sun.",
                fix3: "Cause: This typically happens when your model loader's callback function returns, but the model data is not what you expect. Your code tries to access gltf.scene, but gltf is undefined. Fix: This is often a symptom of a failed model load. Check the console for an earlier error (like a 404 Not Found) that indicates the model file itself couldn't be fetched. The loader failed, so the callback received nothing.",
                fix4: "Cause: The path provided to the model loader is incorrect. The browser cannot find the file at that location. Fix: Double-check the file path. Is it relative or absolute? Is there a typo? Open your browser's 'Network' tab in the developer tools to see the exact URL it tried to fetch and the 404 error response.",
                fix5: "This is a useful debugging message you can add yourself with console.log() to confirm that your animate function is being called. If your scene is static when it should be moving, and you don't see this message, you've likely forgotten to call animate() to start the loop.",
                fix6: "Cause: You are trying to use a variable before the object it represents has been fully created and added to the scene. Fix: Make sure your object is initialized before you try to access its properties. A common solution is to wrap your code that interacts with the object in a conditional statement like if (cube) { ... }.",
            };
            const display = document.getElementById('debug-fix-display');
            const displayTitle = display.querySelector('h3');
            const displayContent = display.querySelector('p');
            
            document.querySelectorAll('.debug-line').forEach(line => {
                line.onclick = (e) => {
                    const fixKey = e.currentTarget.getAttribute('data-fix');
                    const titleText = e.currentTarget.textContent.replace('▷ ', '');
                    displayTitle.textContent = "Analysis & Fix";
                    displayContent.innerHTML = `<strong>${titleText}</strong><br>${fixes[fixKey]}`;
                };
            });

            disposers['debugging'] = () => {
                document.querySelectorAll('.debug-line').forEach(line => line.onclick = null);
                displayContent.innerHTML = "Click on an error in the console to see a detailed explanation and solution here.";
            };
        };

        // Function to set up the model viewer tab
        const setupModelViewer = () => {
            const script = document.createElement('script');
            script.type = 'module';
            script.src = 'https://ajax.googleapis.com/ajax/libs/model-viewer/3.5.0/model-viewer.min.js';
            document.head.appendChild(script);

            disposers['model-viewer'] = () => {
                // No cleanup for this simple setup, as the component manages itself
            };
        };

        const tabSetupFunctions = {
            'fundamentals': setupFundamentals,
            'controls': setupControls,
            'models': setupModels,
            'physics': setupPhysics,
            'particles': setupParticles,
            'debugging': setupDebugging,
            'model-viewer': setupModelViewer,
        };

        const switchTab = (tabId) => {
            // Cleanup the current scene if one exists
            sections.forEach(section => {
                if (!section.classList.contains('hidden')) {
                    const currentTabId = section.id;
                    if (disposers[currentTabId]) {
                        disposers[currentTabId]();
                    }
                }
            });

            // Hide all sections and deactivate all tabs
            sections.forEach(section => section.classList.add('hidden'));
            tabs.forEach(tab => tab.classList.remove('tab-active'));
            tabs.forEach(tab => tab.classList.add('tab-inactive'));

            // Show the selected section and activate its tab
            document.getElementById(tabId).classList.remove('hidden');
            document.getElementById(tabId + '-tab').classList.remove('tab-inactive');
            document.getElementById(tabId + '-tab').classList.add('tab-active');
            
            // Run the setup function for the new tab
            if (tabSetupFunctions[tabId]) {
                tabSetupFunctions[tabId]();
            }
        };

        // Add event listeners for tab clicks
        tabs.forEach(tab => {
            tab.addEventListener('click', (e) => {
                const tabId = e.target.id.replace('-tab', '');
                switchTab(tabId);
            });
        });

        document.querySelectorAll('.accordion-button').forEach(button => {
            button.addEventListener('click', (e) => {
                const content = e.currentTarget.nextElementSibling;
                e.currentTarget.classList.toggle('open');
                if (content.style.maxHeight) {
                    content.style.maxHeight = null;
                } else {
                    content.style.maxHeight = content.scrollHeight + "px";
                }
            });
        });

        // Initial setup for the first tab
        window.onload = () => {
            switchTab('fundamentals');
        };
    </script>
</body>
</html>