Lulucat

מדוע החלפנו שלושה מנועי גאומטריה של מרקר בחותמות

Gaoge ZhangGaoge Zhang

נתוני Apple Pencil אמיתיים חשפו פגמים שבדיקות סינתטיות פספסו, והובילו את Lulucat Notes משלושה מנועי מתאר למודל חותמות של MaLiang.

עודכן

Lulucat Notes התחיל במרנדר מרקר קונבנציונלי: בניית מתאר גאומטרי סביב הנתיב שנדגם, ואז מילוי של אותו מתאר פעם אחת בצבע שקוף למחצה. מילוי יחיד מונע את הרצועות הכהות שמופיעות כאשר מקטעים שקופים למחצה חופפים. הוא גם מכניס לאותה בעיה גאומטרית כל חיבור, חיתוך עצמי, קצה ומשיכה קצרה.

שלושה דורות של גאומטריה מותאמת הסירו כל אחד קטגוריה אחת של פגמים וחשפו קטגוריה אחרת. המרנדר שאנו מפיצים כעת משתמש בחותמות מעגליות רכות המבוססות על MaLiang, מסגרת קוד פתוח לציור ברישיון MIT מאת Harley-xk. בתמורה להסרת קטגוריה שלמה של כשלים במתאר, קיבלנו חיתוכים עצמיים כהים יותר וקצוות שהולכים ומתחדדים לפי הלחץ.

כתב יד כחול במרקר שנלכד ב-iPad, עם ווים דקים הנראים בתחילתן ובסופן של כמה משיכות.

צילום המסך המקורי מהמכשיר בהגדלה של 228%. ווים קטנים מופיעים בשני הקצוות של כמה משיכות.

כתב יד אמיתי יצר ווים שסצנות סינתטיות לא יצרו

הסצנות הסינתטיות שלנו כיסו קווים ישרים, קשתות, נגיעות קצרות, חזרות לאחור, שינויי לחץ ופניות חדות, וכולן נרנדרו בצורה נקייה. כתב יד סיני אמיתי עדיין יצר ווים באורך של כ-1–3 נקודות בתחילתן ובסופן של משיכות רבות.

לכן הוספנו לחצן שמירה לאפליקציה והקלטנו 29 משיכות מלאות. כל דגימה שמרה את המיקום, הרדיוס, הלחץ, האזימוט, זווית הגובה, חותמת הזמן ודגלי ההערכה של UIKit. תוכנית לא מקוונת פענחה את ה-JSON הזה והידרה את אותם קבצי Stroke.swift, StrokeRenderer.swift ו-HighlighterStrokeBuilder.swift שבהם משתמשת האפליקציה. הפלט שלה תאם לצילום המסך מהמכשיר משיכה אחר משיכה, כולל הווים.

מכיוון שהמרנדר הלא מקוון שיחזר את הפגם מקלט מוקלט בלבד, מקור הבעיה היה בדגימות ובגאומטריה ולא בהתנהגות התצוגה. כעת יכולנו לקרוא את הדגימות המדויקות שנכנסו למרנדר.

ההשערה הראשונה שלנו האשימה את הכיוון המשוער של ה-Pencil במגע הראשוני ובהרמה מהמשטח. הנתונים דחו אותה. בכל 29 המשיכות, האזימוט השתנה לכל היותר ב-0.09 רדיאן וזווית הגובה לכל היותר ב-0.02 רדיאן בתוך שמונה הדגימות הראשונות והאחרונות, וכל דגלי ההערכה היו false ב-iPad הזה. הכיוון נשאר יציב בזמן שהלחץ והכיוון השתנו בחדות.

סף לחץ נבע מפער נקי בנתונים

לדגימה האחרונה בכל משיכה מוקלטת היה לחץ 0.000. דגימות כתיבה היו ב-0.01 או מעליו, ואילו דגימות שהוקלטו לאחר שה-Pencil עזב את המשטח היו ב-0.002 או מתחתיו. דגימות האוויר האלה נמשכו לעיתים קרובות עוד 2–7 נקודות ועברו 0.8–9.0 נקודות, והכיוון שלהן יכול היה לפנות בכ-90 מעלות הרחק מהמשיכה שנכתבה.

משיכה 2 מראה את המנגנון. הגוף שלה הסתיים בין 143 ל-180 מעלות. זנב הלחץ הנמוך פנה אז ל-−90 עד −67 מעלות ועבר עוד 8.5 נקודות. מרנדר המתאר התייחס לנקודת האוויר האחרונה כאל נקודת הקצה הקובעת, ולכן מתח פיסת גאומטריה צרה לעברה.

הלחצים שנמדדו מצביעים על פער של כמעט סדר גודל בין תנועת הכתיבה לתנועה באוויר, ולכן הצבנו את הסף בתוך הפער הזה:

private static let pressureFloor: CGFloat = 0.005

רק דגימות בלחץ נמוך שבשני הקצוות מוסרות. דגימה בלחץ נמוך באמצע המשיכה נשארת חלק ממנה. הכלל עוקב אחר גבול האירוע שנמדד ומשאיר את הנתיב שנכתב ללא שינוי.

תקריב ממכשור לפני החיתוך: דגימות קלט ירוקות פונות הרחק מהמשיכה, בעוד שהמתאר האדום יוצר וו צר.

לפני החיתוך, המרנדר מעגן את הקצה לדגימות האוויר. הנקודות הירוקות הן דגימות גולמיות; הקו האדום הוא המתאר שנוצר.

תקריב ממכשור אחרי החיתוך: המילוי הכחול מסתיים בדגימות הכתובות האחרונות, והנקודות הירוקות בעלות הלחץ הנמוך נשארות מחוצה לו.

אחרי חיתוך הדגימות שמתחת ל-0.005, הקצה מסתיים בנקודה הכתובה האחרונה. סף הלחץ הזה נשאר במרנדר החותמות הנוכחי.

הסף הסיר את הווים הארוכים באוויר. גרירות קצרות במגע הראשוני וקשתות בהרמה, שהלחץ שלהן נשאר מעל 0.005, שרדו אותו, מפני שהן תנועה כתובה. הטיפול בתנועות האלה בתוך מודל מתאר הוביל לסבבים הבאים של גאומטריה.

שלושה מנועי גאומטריה הזיזו את הפגמים ממקום למקום

הדור הראשון: מתאר אחד, מילוי אחד

המנוע הראשון שהיה רגיש ללחץ פעל בצינור עיבוד מוכר. הוא החליק עמוד שדרה, חישב היסטים שמאלה וימינה, חיבר אותם למצולע אחד ומילא את המצולע פעם אחת. החרט היה אליפסה שהונעה על ידי האזימוט וזווית הגובה של Apple Pencil, ולכן חתך הרוחב שלה השתנה לפי כיוון הכתיבה. לנגיעות קצרות הייתה צורה נפרדת, ובקצוות שטוחים נעשה שימוש באסימטריות קטנות כדי למנוע כיסים בעלי winding אפס.

המילוי היחיד פתר הצטברות אטימות בתוך משיכה, והוא דרש מצולע תקף בכל פנייה. גרירות במגע הראשוני יצרו טריזים בין הקצה לגוף. חזרות לאחור יכלו ליצור כיסים לבנים בצורת סהר. פניות חדות יכלו לגרום למתאר הפנימי להיחתך בעצמו ולהשאיר חריץ ללא מילוי.

הדור השני: תיקון המתאר ליד הקצוות

המנוע השני שמר על המתאר והוסיף תיקונים מקומיים: חיתוך חזרות לאחור, ריסון של מאפיינים משוערים מוקדמים, יישור נקודות קצה, חיתוך כיוון זמני והארכה קטנה של הקצה. כל תיקון טיפל בכשל ששוחזר. יחד הם גרמו להתנהגות הקצוות להיות תלויה בכמה ספים וחלונות.

כתיבה אמיתית יצרה לאחר מכן מקרים שמחוץ לחלונות האלה. חלון יישור קבוע של 24 נקודות מחק ווים מכוונים במשיכות קצרות. חלון אדפטיבי שמר על הווים האלה, וקשתות בהרמה עדיין יכלו להפוך לפינות נראות לעין. חיתוך שמתחשב בכיוון הסיר יותר תנועה זמנית, במחיר של השלכת כ-2.5 נקודות מהתחלה מעוקלת באמת במשיכה מוקלטת אחת.

כל תיקון תיקן את המקרה שלשמו נועד. המודל שבבסיסם עדיין דרש מתאר קשיח שיישאר תקף תחת קלט רועש ומעוקל.

הדור השלישי: איחוד כיסוי עם קצוות מרובעים חתוכים

המנוע השלישי הפסיק לבנות מתאר יחיד. הוא צייר טרפז לכל מקטע ודיסקה בכל דגימת פנים לתוך מסכת גווני אפור, חישב את איחוד הכיסוי שלהם, ואז החיל את צבע המרקר פעם אחת. סהרוני חזרה לאחור, התכהות בחיתוכים עצמיים וחורים של winding אפס נעלמו, מפני שהמרנדר כבר לא היה תלוי בכיוון הסיבוב של המצולע.

האיחוד עיגל את הקצוות, ולכן החזרנו קצוות מרובעים באמצעות חיתוך הכיסוי מול שני חצאי-מישורים בקצוות. החיתוך הזה תיקן קווים ישרים בלחץ קבוע והכניס פגם חמור יותר: גוף של משיכה מעוקלת יכול לחצות באופן לגיטימי חצי-מישור של קצה, ואז חיתוך הקצה מסיר חלק מאמצע המשיכה. בזמן קלט חי, כיוון נקודת הקצה המשיך להשתנות, ולכן האזור החתוך זז והמשיכה הבהבה.

משיכה כחולה מעוקלת לפני תיקון חיתוך הקצה, כשגופה העליון חתוך לאורך קו אלכסוני; הנקודות הירוקות מציגות את הנתיב המלא שנדגם.

אילוץ הקצה הגלובלי חותך דרך גוף של משיכה מעוקלת.

אותה משיכה כחולה מעוקלת אחרי הוספת שער אורך-קשת מקומי, כששני הענפים משוחזרים סביב הנתיב הירוק שנדגם.

הגבלת חיתוך הקצה לפרימיטיבים שנמצאים בתוך חצי רוחב אחד מנקודת הקצה משחזרת את המשיכה הזו.

שער אורך-קשת הפך את חיתוך הקצה למקומי ותיקן כל מקרה מוקלט. וו סינתטי קיצוני עדיין הראה חריץ לבן קטן במקום שבו קצה בעל לחץ נמוך פגש את הקצה החתוך. המנוע השלישי שוב החליף כשל גאומטרי אחד באחר.

MaLiang מסיר את הגבול הקשיח

MaLiang בונה משיכה מחותמות מרקם חוזרות במקום ממתאר סגור אחד. מחולל הנתיב שלו משתמש במקטעי Bézier ריבועיים העוברים דרך נקודות האמצע של דגימות סמוכות, ומרנדר הקווים מציב חותמות בצעד קבוע לאורך הנתיב הזה. גודל החותמת, הסיבוב, הצבע והאטימות יכולים להשתנות לאורך המשיכה.

חותמת רכה אינה נושאת קצה, חיבור או כלל winding נפרדים. שינוי חד בכיוון נשאר רצף של סימנים חופפים. חזרות לאחור אינן יכולות ליצור כיס מצולע ריק, וחצי-מישור של נקודת קצה אינו יכול לחתוך את אמצע המשיכה, מפני שהמודל אינו מכיל חיתוך קצה.

אימצנו את המודל ולא את הקוד. HighlighterStrokeBuilder הנוכחי מבצע ארבע פעולות:

  1. הסרת דגימות קצה בלבד שהלחץ שלהן נמוך מ-0.005.
  2. החלקת המיקומים באמצעות מקטעי Bézier ריבועיים של נקודות אמצע.
  3. הצבת חותמת מעגלית אחת לכל נקודת אורך-קשת.
  4. קביעת קוטר החותמת לפי לחץ מנורמל והרכבת החותמות באלפא source-over.

הבונה הצטמצם מ-686 שורות ל-190. האזימוט, זווית הגובה, בניית המתאר, מסכות האיחוד, חלונות כיוון של נקודות הקצה, הארכת הקצה וחיתוך הקצה — כולם עזבו את נתיב המרקר. אנחנו עדיין מקליטים את כיוון ה-Pencil, מפני שמודל הקלט תומך בו, והמרקר עצמו כבר לא קורא את השדות האלה.

נוסחת הלחץ של MaLiang דרשה כיול של המכשיר

MaLiang מחשב את גודל החותמת באמצעות נוסחה השקולה ל-

שמתבססת על ההנחה ש-force משתמש בחלק שימושי של הטווח מ-0 עד 1. נתוני המכשיר שלנו לא עשו זאת. מתוך כ-3,400 דגימות Apple Pencil, לחץ החציון היה 0.047 והערך המרבי היה 0.178. הזנה ישירה של הערכים האלה לנוסחה הפכה כתיבה רגילה לדקה בהרבה מגודל המברשת הנומינלי.

לכן נירמלנו סביב החציון שנצפה, ואז החלנו את החזקה ותחמנו את התוצאה:

לחץ הכתיבה החציוני ממופה כעת לקוטר הנומינלי של 12 נקודות. מגע קל מאוד נשאר נראה בגודל של 35% מהגודל הנומינלי, בעוד שלחץ חזק מפסיק לגדול ב-120%.

הכיול הזה שייך למכשיר הקלט ולהתנהגות המברשת, ולא ל-MaLiang. העתקת הנוסחה המקורית בלי למדוד את התפלגות הלחץ שלנו הייתה משמרת את הארכיטקטורה ומייצרת מברשת שגויה.

אלפא לכל חותמת נבעה ממספר החפיפות הידוע

MaLiang מפצה על חותמות שקופות למחצה חופפות באמצעות הביטוי האמפירי alpha ÷ overlapping × 2.5. למברשת שלנו יש קוטר נומינלי קבוע של 12 נקודות וצעד חותמת של 1 נקודה, ולכן קו האמצע מקבל כ-12 שכבות. במקרה שלנו מספר החפיפות ידוע, ואפשר לפתור את הפיצוי במקום לאמוד אותו.

עבור אטימות משיכה יעד ו- שכבות source-over שוות, אטימות חותמת מפיקה

ולכן חותמת אחת דורשת

עם ו-, כל חותמת משתמשת באטימות של בערך . המרכז מגיע לצבע המיועד לאחר הרכבה חוזרת, בעוד שהקצה מקבל פחות חותמות ושומר על מעבר מדורג של נקודה אחת או שתיים.

דף של משיכות מרקר כחולות מגוונות שנרנדרו במודל חותמות רכות סופי, כולל לולאות, ווים, סימנים קצרים וחיתוכים עצמיים.

המרנדר הסופי מנגן מחדש את נתיבי המכשיר המוקלטים. ווים, חיתוכים אלכסוניים וכיסים לבנים אינם מופיעים.

שני המחירים שקיבלנו

חיתוכים עצמיים נעשים כהים יותר. הרכבת source-over סופרת כל חותמת, כולל חותמות שהונחו במעבר מוקדם יותר על אותו אזור, ומנוע איחוד הכיסוי נמנע מכך. קיבלנו את החפיפה הכהה יותר מפני שהיא צפויה ומשאירה את צורת המשיכה שלמה.

הקצוות נעשים צרים יותר לפי הלחץ. הרמה אמיתית מהמשטח מפחיתה את הלחץ, מה שמפחית את קוטר החותמת, ולכן המשיכה הולכת ומתחדדת במקום להסתיים בקצה מרובע ברוחב מלא. התייחסנו לקצה המרובע הזה כאל דרישה לאורך כל שלושת מנועי הגאומטריה. הוויתור עליו הסיר את מנגנוני החיתוך ונקודות הקצה שעמדו מאחורי כמה מהכשלים לעיל, ולכן קיבלנו את הקצה המתחדד.

המודל שאנו מפיצים עושה אפוא פחות מהמודלים שקדמו לו. הוא משמר את אירוע הכתיבה שנמדד, מגיב ללחץ לאחר כיול המכשיר, ומרנדר נתיבים מעוקלים עם חיתוכים עצמיים בלי לדרוש מתאר תקף גלובלית. חרט בצורת אזמל, קצה מרובע לחלוטין והפוליליין המדויק בין הדגימות הגולמיות — אלה התכונות שעליהן ויתרנו.

מכל תוצאות החקירה, סף הלחץ הוא זה שעובר באופן הישיר ביותר לפרויקטים אחרים. הערך 0.005 הגיע מהפער בין שני אשכולות בקלט אמיתי: כתיבה ב-0.01 ומעלה, ומעקב באוויר ב-0.002 ומטה. אותן משיכות מוקלטות הראו היכן כל תיקון גאומטרי נכשל. הכשלים האלה הובילו למודל החותמות ולשני המחירים שהוא נושא.