Pull to refresh

Псевдо 3D эффект

Objective C
В последнее время обратил внимание на ролики программ, в которых реализован так называемый псевдо 3D эффект: когда картинка приложения изменяется в зависимости от положения пользователя относительно телефона. Или телефона относительно пользователя: смотря с какой стороны вы находитесь :). Для достижения этого эффекта можно использовать либо сенсоры либо отслеживать положение глаз пользователя (т.н. head tracking). Второй способ несколько сложнее, хотя даёт более правдоподобный результат.

В качестве эксперимента мы решили попробовать сделать такой 3Д фон в программе Deluxe Moon Pro (В версии на маркете пока этот эффект не реализован!).

Вот пример того, что у нас получилось:


Итак начнем.

Для того, чтобы достичь 3Д эффекта необходимо изображение разбить на «слои» и в зависимости от показаний акселерометра смещать каждый слой на определённую величину.

Казалось бы всё просто, но есть несколько проблем.

  1. Дрожание рук вызывает мерцание картинки
  2. Телефон держится под наклоном. Поэтому, при запуске программы лучше считать, что в этом положении взгляд пользователя был направлен по нормали к плоскости телефона, а все изменения сенсоров отсчитывать от этого начального значения.

Таким образом, общий подход к решению данной задачи такой:

  1. Разбить фон на несколько слоёв, которые будут двигаться друг относительно друга. Это может быть текст, фоновые изображения или же специальные элементы созданные для придания «глубины»
  2. Подписаться на события акселерометра.
  3. При изменении акселерометра:
  4. Подкоректировать показатели сенсоров относительно начальных значений.
  5. Сгладить колебания от дрожания рук
  6. Сдвигать каждый слой на свою величину.
  7. Сделать сенсоры опциональными: не всем пользователям может понравится такой сюрприз.

Итак, в нашем случае у нас есть изображение фона, + 2 слоя звезд, + слои различных элементов экрана.

Для нужд фильтрации устанавливаем некоторые константы:
#define kUpdateFrequency	240.0 //частота обновления фильтра
#define kCutoffFrequency	5.0 //частота фильтрации
#define kAccDataScale       2.2 //масштаб данных

При запуске программы мы инициализируем низкочастотный фильтр для того чтобы убрать дребезжание фона, вызванное дрожанием рук.
про него можно почитать вот здесь
и скачать пример реализации у Apple здесь
-(void)awakeFromNib {
    filter = [[LowpassFilter alloc] initWithSampleRate:kUpdateFrequency/10 cutoffFrequency:kCutoffFrequency]; //собственно фильтр низких частот
    
    if ([Options instance].useSensors) {
        [self performSelector:@selector(startAcc) withObject:nil afterDelay:4]; // стартуем с запаздыванием чтобы программа успела запуститься и заполнить нормальные начальные значения акселерометра 
    }
    
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didGoToBG:) name:UIApplicationDidEnterBackgroundNotification object:nil]; //программа ушла в фон
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didResume:) name:UIApplicationDidBecomeActiveNotification object:nil]; //Программа стала активна
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(useSensorsChanged) name:@"useSensors" object:nil]; //подписываемся на изменения опции
}

Реакцию на изменение опции по вкл/выкл 3д эффекта фона и на сворачивание программы в фон описывать думаю не стоит, там все банально

Теперь рассмотрим функцию которая запускает всю эту красоту
P.S. Используется ARC потому, никаких retain или release
-(void)startAcc {
    @try {
        if (!acc) {
            startY = 100; //это стартовое положение сенсоров по Y чтобы не приходилось постоянно держать устройство в горизонтальном положении
            acc = [UIAccelerometer sharedAccelerometer];
            acc.updateInterval = 1/kUpdateFrequency;
            acc.delegate = self;
        }
    }
    @catch (NSException *exception) {
        NSLog(@"startAcc %@",exception);
    }
}

Чтобы остановить все это дело
-(void)stopAcc {
    @try {
        if (acc) {
            acc.delegate = nil;
            acc = nil;
            [self resetState];
        }
    }
    @catch (NSException *exception) {
        NSLog(@"stopAcc %@",exception);
    }
}

Дальше нам остается только ловить событие обновления акселерометра
- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration {
    @try {
        [filter addAcceleration:acceleration]; //фильтруем данные чтобы избежать «тряски» фона
        double x = filter.x*kAccDataScale; //масштабируем данные 
        double y = filter.y*kAccDataScale;
        if (startY == 100) {
            startY = acceleration.y*kAccDataScale; // запоминаем начальные данные по Y
        }
		
	    // убираем лишнее по X
        if (x > 1) {
            x = 1; 
        }
        else if (x < -1) {
            x = -1;
        }

	    // убираем лишнее по Y
        if (y > 1) {
            y = 1;
        }
        else if (y < -1) {
            y = -1;
        }
	    // учитываем начальное значение
        y = (y - startY);

	    //Сдвигаем необходимые элементы
        [self setOffsetElementForX:x Y:y];
    
    }
    @catch (NSException *exception) {
        NSLog(@"accelerometer %@",exception);
    }
}
@end

И, наконец, в функции сдвига мы передвигаем разные слои на разную фиксированную величину. Поэтому, верхние элементы двигаются с меньшей скоростью чем фон что и создает эффект 3Д. При этом, все элементы должны сдвигаться в сторону поворота устройства.
-(void)setOffsetElementForX:(double)x Y:(double)y {
    @try {
        allEll.transform = CGAffineTransformMakeTranslation(-8.5*x, 11.5*y);
        movingButtonsView.transform = CGAffineTransformMakeTranslation(-8.5*x, 11.5*y);
        arcView.transform = CGAffineTransformMakeTranslation(-8.5*x, 11.5*y);
        bgView.transform = CGAffineTransformMakeTranslation(-17*x, 23*y);
        bgStar1View.transform = CGAffineTransformMakeTranslation(-13*x, 18*y);
        bgStar2View.transform = CGAffineTransformMakeTranslation(-10*x, 13*y);
        allEll2.transform = CGAffineTransformMakeTranslation(-17*x, 21*y);
    }
    @catch (NSException *exception) {
        NSLog(@"offsetElementForX %@",exception);
    }
}

Эти 4 простых шага позволят сделать программы привлекательнее, интереснее и инновационее. 21-й век всё-таки. Помните, что эффектный и стильный дизайн может положительно повлиять на оценку вашего приложения ревьюверами, а также выделить их из сотен тысяч конкурентов. Мы в Lifeware Solutions (http://www.lifewaresolutions.com/) активно занимаемся исследованиями и будем рады, если наш опыт будет вам полезен. Буду очень благодарен за ваши отзывы.
Tags:objective-cui3diphone development
Hubs: Objective C
Total votes 79: ↑72 and ↓7+65
Views46K

Popular right now