robin_be Geplaatst: 1 november 2017 Rapport Geplaatst: 1 november 2017 (bewerkt) 3D wereld positie projecteren op de HUD suggesties voor een duidelijkere titel zijn welkom Benodigdheden GTA:SA (eventueel met CLEO) en Sannybuilder basis SCM-kennis (Basis SCM-scripten: tutorial door @Dutchy3010 & @PatrickW) kennis mbt geheugen (Werken met geheugen (adressen) in CLEO: tutorial door @Crypteq) Omdat we zaken in het geheugen aanpassen, zullen mijn voorbeelden waarschijnlijk niet op alle versies van SA werken. --------------------------- Abstract In deze tutorial toon ik hoe je een 3D punt kan projecteren op het scherm. Dit is leuk om bijvoorbeeld de health van een actor te tonen met tekst op het scherm (zoals in de video hierboven). In SA wordt dit ook gebruikt om de prijs van een huis te tonen boven de koop pickup. Natuurlijk kan je dit ook gebruiken om textures op het scherm te plaatsen, bijvoorbeeld save icoontjes voor huizen: Inhoud 1. Basisvoorbeeldcode 2. Theorie 3. Methode met hulp van ingebouwde functies 4. Manuele methode 5. Ingekorte manuele methode dankzij arraynotatie 6. Manuele methode zonder CLEO 7. Afwerking 1. Basisvoorbeeldcode Hieronder staat een stuk code waarin een actor gespawnt wordt. Zijn health wordt door een textdraw getoond in het midden van het scherm. De coordinaten van de actor zitten in variabelen 0@ 1@ 2@ (xyz). De bedoeling is dan om dat 3D punt te projecteren en op te slaan in 0@ en 1@ (xy). Als placeholder worden 0@ en 1@ nu gewoon ingesteld zodat de textdraw in het midden van het scherm staat. Deze code kan je gebruiken als je graag actief de tutorial meevolgt en zelf de resultaten wilt zien. 0247: load_model #MALE01 038B: load_requested_models :MODEL_LOAD 00D6: if 8248: not model #MALE01 available 004D: jump_if_false @MODEL_SPAWN 0001: wait 0 ms 0002: jump @MODEL_LOAD :MODEL_SPAWN 009A: 30@ = create_actor_pedtype 4 model #MALE01 at 2460.1829 -1655.1241 13.3359 0249: release_model #MALE01 :MAIN_LOOP 0001: wait 0 056D: actor 30@ defined 004D: jump_if_false @MAIN_LOOP 00A0: store_actor 30@ position_to 0@ 1@ 2@ 000B: 2@ += 1.0 // l+=i (float) 0226: 31@ = actor 30@ health // ------ // hier moeten de coordinaten 0@ 1@ 2@ geprojecteerd worden // voorlopig wordt dit in het midden van het scherm gezet 0007: 0@ = 320.0 // l=i (float) 0007: 1@ = 224.0 // l=i (float) // ------ 03F0: enable_text_draw 0 0340: set_text_draw_RGBA 255 33 255 255 033F: set_text_draw_letter_size 0.15 0.95 0342: set_text_draw_centered 1 081C: draw_text_outline 0 RGBA 0 0 0 255 060D: draw_text_shadow 1 rgba 0 0 0 255 0348: enable_text_draw_proportional 0 045A: draw_text_1number 0@ 1@ GXT 'NUMBER' number 31@ // ~$~1~ 0002: jump @MAIN_LOOP 2. De theorie Om een punt uit de wereld te projecteren op het scherm, moet dat punt 'simpelweg' vermenigvuldigd worden met de cameraviewmatrix. De cameraviewmatrix staat in het geheugen op adres 0xB6FA2C, maar deze moet eerst nog getransponeerd worden (= rijen en kolommen omwisselen). De cameraviewmatrix is een 4x4 matrix met daarin 16 floating point getallen (= 4 bytes per getal). cameraviewmatrix 0xB6FA2C | _11 _12 _13 _14 | | _21 _22 _23 _24 | | _31 _32 _33 _34 | | _41 _42 _43 _44 | 0xB6FA2C = _11 0xB6FA30 = _12 0xB6FA34 = _13 0xB6FA38 = _14 .. 0xB6FA68 = _44 Vermenigvuldiging (eerst transponeren (daarvoor staat het T-symbool)): | _11 _12 _13 _14 |T | x | | _11 _21 _31 _41 | | x | | _11 * x + _21 * y + _31 * z + _41 * 1 | | _21 _22 _23 _23 | | y | = | _12 _22 _32 _42 | | y | = | _12 * x + _22 * y + _32 * z + _42 * 1 | | _31 _32 _33 _34 | | z | | _13 _23 _33 _43 | | z | | _13 * x + _23 * y + _33 * z + _43 * 1 | | _41 _42 _43 _44 | | 1 | | _14 _24 _34 _44 | | 1 | | _14 * x + _24 * y + _34 * z + _44 * 1 | Dus: x = _11 * _x + _21 * _y + _31 * _z + _41 y = _12 * _x + _22 * _y + _32 * _z + _42 z = _13 * _x + _23 * _y + _33 * _z + _43 _x _y _z = de 3D coordinaten x y z = de geprojecteerde coordinaten Na deze bewerking hebben we de geprojecteerde xy coordinaten, maar deze zijn geprojecteerd op een vlak dat niet overeenkomt met het scherm. Het vlak is een vierkant, en z bevat de zijde van dat vierkant. Een uitkomst kan bv zijn x = 15, y = 15, z = 30. Hieruit kun je afleiden dat de positie dus in het midden van het scherm moet staan. Om correcte coordinaten te hebben, moeten de x en y waarden nog gedeeld worden door z. Daarna moeten ze nog vermenigvuldigd worden met de grootte van het scherm (textdraws werken altijd op een canvas van 640x448, dus x * 640 en y * 448). x = x * 640 / z y = y * 448 / z 3. Methode met hulp van ingebouwe functies In SA zit er al een functie die de matrixvermenigvuldiging kan doen voor ons. Die kunnen we dan aanroepen met CLEO. De functie zit op adres 0x59C890 en verwacht 3 parameters: het adres van het originele punt, het adres van de matrix, en het adres waar het resulterende punt in opgeslaan moet worden. Het adres van de matrix hebben we al: 0xB6FA2C. Om een adres te hebben van de wereldcoordinaten, kunnen we een aantal bytes zetten in ons script en het geheugenadres van die positie opvragen via een label: 0AC6: 6@ = label @PCOORDS offset // ergens in het script, op een plaats die nooit uitgevoerd wordt :PCOORDS hex 00 00 00 00 00 00 00 00 00 00 00 00 end Het label met die 12 bytes moeten op een plaats staan die nooit uitgevoerd kan worden. Dat stukje data wordt gebruikt om waardes op te slaan. Dus als het script daarover loopt, zal het hoogstwaarschijnlijk niet lukken om dit te interpreteren als correcte opcodes. Als gevolg zou het spel crashen. Natuurlijk moeten de correcte coordinaten gebruikt worden, dus we kunnen dit wegschrijven naar die positie: 0A8C: write_memory 6@ size 4 value 0@ vp 0 000A: 6@ += 4 // l+=i (int) 0A8C: write_memory 6@ size 4 value 1@ vp 0 000A: 6@ += 4 // l+=i (int) 0A8C: write_memory 6@ size 4 value 2@ vp 0 000E: 6@ -= 8 // l-=i (int) De x, y en z waarden moeten weggeschreven worden, dus 3 keer write_memory en iedere keer 4 bytes opschuiven. Op het einde wordt er 8 afgetrokken van het adres om terug het juiste adres te hebben. Voor het resulterende punt moet er ook een label gemaakt worden met daarin 12 bytes plaats, maar die moet niet opgevuld worden. Achteraf moet uit die positie het resultaat uitgelezen worden. Na de vermenigvuldiging moeten de xy coordinaten wel nog aangepast zodat ze de juiste scherm positie hebben, en niet zomaar op een willekeurig vlak (zie theorie). De code wordt: 0AC6: 6@ = label @PCOORDS offset 0A8C: write_memory 6@ size 4 value 0@ vp 0 // source x 000A: 6@ += 4 // l+=i (int) 0A8C: write_memory 6@ size 4 value 1@ vp 0 // source y 000A: 6@ += 4 // l+=i (int) 0A8C: write_memory 6@ size 4 value 2@ vp 0 // source z 000E: 6@ -= 8 // l-=i (int) 0AC6: 5@ = label @PSCREEN offset 0AA5: call 0x59C890 num_params 3 pop 3 in 6@ matrix 0xB6FA2C out 5@ 0A8D: 0@ = read_memory 5@ size 4 vp 0 // result x (float) 000A: 5@ += 4 // l+=i (int) 0A8D: 1@ = read_memory 5@ size 4 vp 0 // result y (float) 000A: 5@ += 4 // l+=i (int) 0A8D: 2@ = read_memory 5@ size 4 vp 0 // result z (float) 0013: 0@ *= 640.0 // l*=i (float) 0013: 1@ *= 448.0 // l*=i (float) 0073: 0@ /= 2@ // l/=l (float) 0073: 1@ /= 2@ // l/=l (float) // ergens in het script, op een plaats die nooit uitgevoerd wordt :PSCREEN hex 00 00 00 00 00 00 00 00 00 00 00 00 end :PCOORDS hex 00 00 00 00 00 00 00 00 00 00 00 00 end Let op, als je naar de andere kant kijkt, kan het zijn dat je daar opeens de textdraw ziet 'zweven'. Dit kan je zien in de video bovenaan. De oplossing hiervoor is om te kijken als de resulterende z coordinaat (2@) positief is. Als het niet positief is, dan kijkt de speler naar de andere kant en moet je de textdraw niet tonen! Voor deze methode zijn er 2 variabelen nodig (buiten 0@ 1@ 2@) 4. Manuele methode Natuurlijk kunnen we de matrixvermenigvuldiging ook manueel uitrekenen. Hoe dit moet staat beschreven in het theoriegedeelte. Ik denk niet dat er veel uitleg nodig is, dit vergt enkel wat meer opcodes en concentratie. // 10@ 11@ 12@ worden gebruikt als tijdelijke plaats voor x y z // x = _x * _11 + _y * _21 + _z * _31 + 1 * _41 0007: 10@ = 0.0 // l=i (float) 0087: 3@ = 0@ // l=l (float) 0A8D: 4@ = read_memory 0xB6FA2C size 4 vp 0 // _11 006B: 3@ *= 4@ // l*=l (float) 005B: 10@ += 3@ // l+=l (float) 0087: 3@ = 1@ // l=l (float) 0A8D: 4@ = read_memory 0xB6FA3C size 4 vp 0 // _21 006B: 3@ *= 4@ // l*=l (float) 005B: 10@ += 3@ // l+=l (float) 0087: 3@ = 2@ // l=l (float) 0A8D: 4@ = read_memory 0xB6FA4C size 4 vp 0 // _31 006B: 3@ *= 4@ // l*=l (float) 005B: 10@ += 3@ // l+=l (float) 0A8D: 4@ = read_memory 0xB6FA5C size 4 vp 0 // _41 005B: 10@ += 4@ // l+=l (float) // y = _x * _12 + _y * _22 + _z * _32 + 1 * _42 0007: 11@ = 0.0 // l=i (float) 0087: 3@ = 0@ // l=l (float) 0A8D: 4@ = read_memory 0xB6FA30 size 4 vp 0 // _12 006B: 3@ *= 4@ // l*=l (float) 005B: 11@ += 3@ // l+=l (float) 0087: 3@ = 1@ // l=l (float) 0A8D: 4@ = read_memory 0xB6FA40 size 4 vp 0 // _22 006B: 3@ *= 4@ // l*=l (float) 005B: 11@ += 3@ // l+=l (float) 0087: 3@ = 2@ // l=l (float) 0A8D: 4@ = read_memory 0xB6FA50 size 4 vp 0 // _32 006B: 3@ *= 4@ // l*=l (float) 005B: 11@ += 3@ // l+=l (float) 0A8D: 4@ = read_memory 0xB6FA60 size 4 vp 0 // _42 005B: 11@ += 4@ // l+=l (float) // z = _x * _13 + _y * _23 + _z * _33 + 1 * _43 0007: 12@ = 0.0 // l=i (float) 0087: 3@ = 0@ // l=l (float) 0A8D: 4@ = read_memory 0xB6FA34 size 4 vp 0 // _13 006B: 3@ *= 4@ // l*=l (float) 005B: 12@ += 3@ // l+=l (float) 0087: 3@ = 1@ // l=l (float) 0A8D: 4@ = read_memory 0xB6FA44 size 4 vp 0 // _23 006B: 3@ *= 4@ // l*=l (float) 005B: 12@ += 3@ // l+=l (float) 0087: 3@ = 2@ // l=l (float) 0A8D: 4@ = read_memory 0xB6FA54 size 4 vp 0 // _33 006B: 3@ *= 4@ // l*=l (float) 005B: 12@ += 3@ // l+=l (float) 0A8D: 4@ = read_memory 0xB6FA64 size 4 vp 0 // _43 005B: 12@ += 4@ // l+=l (float) // x *= 640.0 / z // y *= 448.0 / z 0007: 0@ = 640.0 // l=i (float) 0007: 1@ = 448.0 // l=i (float) 006B: 0@ *= 10@ // l*=l (float) 006B: 1@ *= 11@ // l*=l (float) 0073: 0@ /= 12@ // l/=l (float) 0073: 1@ /= 12@ // l/=l (float) Zelfde opmerking als hierboven: als 2@ negatief is, moet je de textdraw niet tonen. Voor deze methode zijn er 5 variabelen nodig (buiten 0@ 1@ 2@) 5. Ingekorte manuele methode dankzij arraynotatie In de manuele methode kun je waarschijnlijk zien dat er enorm veel herhaling is. Dit kan behoorlijk simpel weggewerkt worden door gebruik te maken van de arraynotatie. ([SA|SCM] Arrays) Het enige wat veranderd is de tijdelijke xyz variabele en het geheugenadres. De tijdelijke variabele kan vervangen worden door een arraynotatie. Voor het geheugenadres kan er simpelweg iedere keer een optelling gemaakt worden: in de vorige code kun je zien dat er iedere keer +4 wordt gedaan, en dan -0x2C voor het volgende stuk. 0006: 21@ = 0xB6FA2C // l=i (int) for 20@ = 0 to 2 0007: 10@(20@,1i) = 0.0 // l=i (float) 0087: 3@ = 0@ // l=l (float) 0A8D: 4@ = read_memory 21@ size 4 vp 0 // _11/_12/_13 006B: 3@ *= 4@ // l*=l (float) 000A: 21@ += 0x10 // l+=i (int) 005B: 10@(20@,1i) += 3@ // l+=l (float) 0087: 3@ = 1@ // l=l (float) 0A8D: 4@ = read_memory 21@ size 4 vp 0 // _21/_22/_23 006B: 3@ *= 4@ // l*=l (float) 000A: 21@ += 0x10 // l+=i (int) 005B: 10@(20@,1i) += 3@ // l+=l (float) 0087: 3@ = 2@ // l=l (float) 0A8D: 4@ = read_memory 21@ size 4 vp 0 // _31/_32/_33 006B: 3@ *= 4@ // l*=l (float) 000A: 21@ += 0x10 // l+=i (int) 005B: 10@(20@,1i) += 3@ // l+=l (float) 0A8D: 4@ = read_memory 21@ size 4 vp 0 // _41/_42/_43 000E: 21@ -= 0x2C // l-=i (int) 005B: 10@(20@,1i) += 4@ // l+=l (float) end 0087: 2@ = 12@ // l=l (float) // x *= 640.0 / z // y *= 448.0 / z 0007: 0@ = 640.0 // l=i (float) 0007: 1@ = 448.0 // l=i (float) 006B: 0@ *= 10@ // l*=l (float) 006B: 1@ *= 11@ // l*=l (float) 0073: 0@ /= 2@ // l/=l (float) 0073: 1@ /= 2@ // l/=l (float) Zelfs dit kan natuurlijk nog korter. De enige verandering is de 0@ 1@ 2@ variabelen, dus dit kan simpelweg vervangen worden door nog een arraynotatie. 0006: 21@ = 0xB6FA2C // l=i (int) for 20@ = 0 to 2 0007: 10@(20@,1i) = 0.0 // l=i (float) for 22@ = 0 to 2 0087: 3@ = 0@(22@,1i) // l=l (float) 0A8D: 4@ = read_memory 21@ size 4 vp 0 // _11/_12/_13 _21/_22/_23 _31/_32/_33 006B: 3@ *= 4@ // l*=l (float) 000A: 21@ += 0x10 // l+=i (int) 005B: 10@(20@,1i) += 3@ // l+=l (float) end 0A8D: 4@ = read_memory 21@ size 4 vp 0 // _41/_42/_43 000E: 21@ -= 0x2C // l-=i (int) 005B: 10@(20@,1i) += 4@ // l+=l (float) end 0087: 2@ = 12@ // l=l (float) // x *= 640.0 / z // y *= 448.0 / z 0007: 0@ = 640.0 // l=i (float) 0007: 1@ = 448.0 // l=i (float) 006B: 0@ *= 10@ // l*=l (float) 006B: 1@ *= 11@ // l*=l (float) 0073: 0@ /= 2@ // l/=l (float) 0073: 1@ /= 2@ // l/=l (float) Dit is een stuk korter dan de gewone manuele code, maar er zijn ook meer variabelen nodig: 8 (= 3 meer dan de lange manuele methode) (buiten 0@ 1@ 2@) Zelfde opmerking als hierboven: als 2@ negatief is, moet je de textdraw niet tonen. 6. Manuele methode zonder CLEO Ten slotte toon ik nog hoe je dit allemaal kan doen zonder CLEO (de read_memory opcode is enkel beschikbaar als je CLEO hebt). Hiervoor wordt er ADMA gebruikt, hoe dat allemaal werkt kan je in een andere tutorial van mij lezen: [SA|SCM] ADMA en geheugenmanipulatie zonder CLEO. Hiervoor neem ik de vorige code (met arrays), en vervang ik alle read_memory opcodes door adma geheugenmanipulatie. 0006: 21@ = 0xB6FA2C // l=i (int) for 20@ = 0 to 2 0007: 10@(20@,1i) = 0.0 // l=i (float) for 22@ = 0 to 2 0087: 3@ = 0@(22@,1i) // l=l (float) 0085: 4@ = 21@ // l=l (int) 000E: 4@ -= 0xA49960 // l-=i (int) 0016: 4@ /= 4 // l/=i (int) 006B: 3@ *= &0(4@,1i) // l*=g (float) 000A: 21@ += 0x10 // l+=i (int) 005B: 10@(20@,1i) += 3@ // l+=l (float) end 0085: 4@ = 21@ // l=l (int) 000E: 4@ -= 0xA49960 // l-=i (int) 0016: 4@ /= 4 // l/=i (int) 000E: 21@ -= 0x2C // l-=i (int) 005B: 10@(20@,1i) += &0(4@,1i) // l+=l (float) end 0087: 2@ = 12@ // l=l (float) // x *= 640.0 / z // y *= 448.0 / z 0007: 0@ = 640.0 // l=i (float) 0007: 1@ = 448.0 // l=i (float) 006B: 0@ *= 10@ // l*=l (float) 006B: 1@ *= 11@ // l*=l (float) 0073: 0@ /= 2@ // l/=l (float) 0073: 1@ /= 2@ // l/=l (float) Zelfde opmerking als hierboven: als 2@ negatief is, moet je de textdraw niet tonen. 7. Afwerking Zoals eerder vermeld, kan het zijn dat je de textdraw ziet 'zweven' als je naar de tegengestelde richting kijkt (wordt gedemonstreerd in de video bovenaan). Dit kan opgelost worden door te kijken naar de resulterende z coordinaat (2@). Als die niet positief is, dan kijkt de speler naar de andere kant en moet je de textdraw niet tonen! Een andere manier is om te checken als het te projecteren punt wel te zien is op het scherm. Dit kan je doen met opcode 002C: 00C2: sphere_onscreen 0@ 1@ 2@ radius 0.5 Die checkt als het punt zichtbaar is op het scherm. Let op: als je ver weg bent, is het punt ook niet zichtbaar volgens deze opcode, omdat je niet oneindig ver kan kijken! Als je dit gebruikt, kan je de berekening vermijden als het uiteindelijk toch niet op het scherm staat, dus kun je de code ietswat performanter maken. Dit kan echter niet altijd gebruikt worden: als je bijvoorbeeld een lijn tekent, kan het zijn dat één punt niet op het scherm staat. Bij deze opcode zou je dan de lijn helemaal niet tekenen, terwijl het misschien geen probleem is als je gebruikmaakt van 2@. -------- En dat is 'em! Applausje voor jezelf als je deze volledig doorlopen hebt , want het is niet bepaald de simpelste code. Plaats een reactie als je hiermee leuke dingen maakt, altijd leuk om te weten Met schermcoordinaten kun je nog andere leuke dingen doen, zoals lijnen tekenen. Dat kan met textures, of door echt lijnen te laten tekenen door de render engine (maar dit is nog een heel stuk moeilijker). Spoiler (dit is een mod dat de landingsbanen aangeeft van custom maps in een SA-MP server) Bewerkt: 1 november 2017 door robin_be Reageren
Recommended Posts
Een reactie plaatsen
Je kan nu een reactie plaatsen en pas achteraf registreren. Als je al lid bent, log eerst in om met je eigen account een reactie te plaatsen.