diff --git a/BUILD_WIN.md b/BUILD_WIN.md index 3222e75c66..5836d5bfb5 100644 --- a/BUILD_WIN.md +++ b/BUILD_WIN.md @@ -9,15 +9,17 @@ Note: The prerequisites will require about 10 GB of space on your drive. You wil If you don’t have Community or Professional edition of Visual Studio 2017, download [Visual Studio Community 2017](https://www.visualstudio.com/downloads/). -When selecting components, check "Desktop development with C++." Also check "Windows 8.1 SDK and UCRT SDK" and "VC++ 2015.3 v140 toolset (x86,x64)" on the Summary toolbar on the right. +When selecting components, check "Desktop development with C++." Also on the right on the Summary toolbar, check "Windows 8.1 SDK and UCRT SDK" and "VC++ 2015.3 v140 toolset (x86,x64)". ### Step 2. Installing CMake -Download and install the latest version of CMake 3.9. Download the file named win64-x64 Installer from the [CMake Website](https://cmake.org/download/). Make sure to check "Add CMake to system PATH for all users" when prompted during installation. +Download and install the latest version of CMake 3.9. + +Download the file named win64-x64 Installer from the [CMake Website](https://cmake.org/download/). You can access the installer on this [3.9 Version page](https://cmake.org/files/v3.9/). During installation, make sure to check "Add CMake to system PATH for all users" when prompted. ### Step 3. Installing Qt -Download and install the [Qt Online Installer](https://www.qt.io/download-open-source/?hsCtaTracking=f977210e-de67-475f-a32b-65cec207fd03%7Cd62710cd-e1db-46aa-8d4d-2f1c1ffdacea). While installing, you only need to have the following components checked under Qt 5.10.1: "msvc2017 64-bit", "Qt WebEngine", and "Qt Script (Deprecated)". +Download and install the [Qt Open Source Online Installer](https://www.qt.io/download-open-source/?hsCtaTracking=f977210e-de67-475f-a32b-65cec207fd03%7Cd62710cd-e1db-46aa-8d4d-2f1c1ffdacea). While installing, you only need to have the following components checked under Qt 5.10.1: "msvc2017 64-bit", "Qt WebEngine", and "Qt Script (Deprecated)". Note: Installing the Sources is optional but recommended if you have room for them (~2GB). @@ -56,9 +58,9 @@ Where `%HIFI_DIR%` is the directory for the highfidelity repository. Open `%HIFI_DIR%\build\hifi.sln` using Visual Studio. -Change the Solution Configuration (next to the green play button) from "Debug" to "Release" for best performance. +Change the Solution Configuration (menu ribbon under the menu bar, next to the green play button) from "Debug" to "Release" for best performance. -Run `Build > Build Solution`. +Run from the menu bar `Build > Build Solution`. ### Step 9. Testing Interface @@ -66,7 +68,7 @@ Create another environment variable (see Step #4) * Set "Variable name": `_NO_DEBUG_HEAP` * Set "Variable value": `1` -In Visual Studio, right+click "interface" under the Apps folder in Solution Explorer and select "Set as Startup Project". Run `Debug > Start Debugging`. +In Visual Studio, right+click "interface" under the Apps folder in Solution Explorer and select "Set as Startup Project". Run from the menu bar `Debug > Start Debugging`. Now, you should have a full build of High Fidelity and be able to run the Interface using Visual Studio. Please check our [Docs](https://wiki.highfidelity.com/wiki/Main_Page) for more information regarding the programming workflow. diff --git a/android/app/build.gradle b/android/app/build.gradle index 70f7c622a0..699008092c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -22,11 +22,19 @@ android { '-DHIFI_ANDROID_PRECOMPILED=' + HIFI_ANDROID_PRECOMPILED, '-DRELEASE_NUMBER=' + RELEASE_NUMBER, '-DRELEASE_TYPE=' + RELEASE_TYPE, - '-DBUILD_BRANCH=' + BUILD_BRANCH, + '-DSTABLE_BUILD=' + STABLE_BUILD, '-DDISABLE_QML=OFF', '-DDISABLE_KTX_CACHE=OFF' } } + signingConfigs { + release { + storeFile project.hasProperty("HIFI_ANDROID_KEYSTORE") ? file(HIFI_ANDROID_KEYSTORE) : null + storePassword project.hasProperty("HIFI_ANDROID_KEYSTORE_PASSWORD") ? HIFI_ANDROID_KEYSTORE_PASSWORD : '' + keyAlias project.hasProperty("HIFI_ANDROID_KEY_ALIAS") ? HIFI_ANDROID_KEY_ALIAS : '' + keyPassword project.hasProperty("HIFI_ANDROID_KEY_PASSWORD") ? HIFI_ANDROID_KEY_PASSWORD : '' + } + } } compileOptions { @@ -38,6 +46,10 @@ android { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + signingConfig project.hasProperty("HIFI_ANDROID_KEYSTORE") && + project.hasProperty("HIFI_ANDROID_KEYSTORE_PASSWORD") && + project.hasProperty("HIFI_ANDROID_KEY_ALIAS") && + project.hasProperty("HIFI_ANDROID_KEY_PASSWORD")? signingConfigs.release : null } } @@ -116,4 +128,3 @@ dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') } - diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e105f5bccf..0b52046057 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -49,12 +49,6 @@ android:label="@string/app_name" android:launchMode="singleTop" > - - - - - - diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java b/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java index 2165339918..28acc77609 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java @@ -14,12 +14,16 @@ package io.highfidelity.hifiinterface; import android.content.Intent; import android.content.res.AssetManager; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Vibrator; import android.view.HapticFeedbackConstants; import android.view.WindowManager; import android.util.Log; + +import org.qtproject.qt5.android.QtLayout; +import org.qtproject.qt5.android.QtSurface; import org.qtproject.qt5.android.bindings.QtActivity; /*import com.google.vr.cardboard.DisplaySynchronizer; @@ -31,6 +35,9 @@ import android.content.pm.ActivityInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.view.View; +import android.widget.FrameLayout; + +import java.lang.reflect.Field; public class InterfaceActivity extends QtActivity { @@ -134,6 +141,7 @@ public class InterfaceActivity extends QtActivity { protected void onResume() { super.onResume(); nativeEnterForeground(); + surfacesWorkaround(); //gvrApi.resumeTracking(); } @@ -158,6 +166,41 @@ public class InterfaceActivity extends QtActivity { Log.w("[VR]", "Portrait detected but not in VR mode. Should not happen"); } } + surfacesWorkaround(); + } + + private void surfacesWorkaround() { + FrameLayout fl = findViewById(android.R.id.content); + if (fl.getChildCount() > 0) { + QtLayout qtLayout = (QtLayout) fl.getChildAt(0); + if (qtLayout.getChildCount() > 1) { + QtSurface s1 = (QtSurface) qtLayout.getChildAt(0); + QtSurface s2 = (QtSurface) qtLayout.getChildAt(1); + Integer subLayer1 = 0; + Integer subLayer2 = 0; + try { + String field; + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + field = "mSubLayer"; + } else { + field = "mWindowType"; + } + Field f = s1.getClass().getSuperclass().getDeclaredField(field); + f.setAccessible(true); + subLayer1 = (Integer) f.get(s1); + subLayer2 = (Integer) f.get(s2); + if (subLayer1 < subLayer2) { + s1.setVisibility(View.VISIBLE); + s2.setVisibility(View.INVISIBLE); + } else { + s1.setVisibility(View.INVISIBLE); + s2.setVisibility(View.VISIBLE); + } + } catch (ReflectiveOperationException e) { + Log.e(TAG, "Workaround failed"); + } + } + } } public void openUrlInAndroidWebView(String urlString) { diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java index e05b25f3c3..b98849d051 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/HomeFragment.java @@ -64,7 +64,11 @@ public class HomeFragment extends Fragment { mDomainsView.setLayoutManager(gridLayoutMgr); mDomainAdapter = new DomainAdapter(getContext(), HifiUtils.getInstance().protocolVersionSignature(), nativeGetLastLocation()); mDomainAdapter.setClickListener((view, position, domain) -> { - new Handler(getActivity().getMainLooper()).postDelayed(() -> mListener.onSelectedDomain(domain.url), 400); // a delay so the ripple effect can be seen + new Handler(getActivity().getMainLooper()).postDelayed(() -> { + if (mListener != null) { + mListener.onSelectedDomain(domain.url); + } + }, 400); // a delay so the ripple effect can be seen }); mDomainAdapter.setListener(new DomainAdapter.AdapterListener() { @Override @@ -116,7 +120,9 @@ public class HomeFragment extends Fragment { if (!urlString.trim().isEmpty()) { urlString = HifiUtils.getInstance().sanitizeHifiUrl(urlString); } - mListener.onSelectedDomain(urlString); + if (mListener != null) { + mListener.onSelectedDomain(urlString); + } return true; } return false; diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/provider/UserStoryDomainProvider.java b/android/app/src/main/java/io/highfidelity/hifiinterface/provider/UserStoryDomainProvider.java index ea03864695..ca5e0c17bd 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/provider/UserStoryDomainProvider.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/provider/UserStoryDomainProvider.java @@ -1,10 +1,11 @@ package io.highfidelity.hifiinterface.provider; import android.util.Log; -import android.util.MutableInt; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import io.highfidelity.hifiinterface.HifiUtils; import io.highfidelity.hifiinterface.view.DomainAdapter; @@ -47,24 +48,42 @@ public class UserStoryDomainProvider implements DomainProvider { suggestions = new ArrayList<>(); } + @Override + public synchronized void retrieve(String filterText, DomainCallback domainCallback) { + if (!startedToGetFromAPI) { + startedToGetFromAPI = true; + fillDestinations(filterText, domainCallback); + } else { + filterChoicesByText(filterText, domainCallback); + } + } + private void fillDestinations(String filterText, DomainCallback domainCallback) { StoriesFilter filter = new StoriesFilter(filterText); - final MutableInt counter = new MutableInt(0); - allStories.clear(); - getUserStoryPage(1, + + List taggedStories = new ArrayList<>(); + Set taggedStoriesIds = new HashSet<>(); + getUserStoryPage(1, taggedStories, TAGS_TO_SEARCH, e -> { - allStories.subList(counter.value, allStories.size()).forEach(userStory -> { - filter.filterOrAdd(userStory); - }); - if (domainCallback != null) { - domainCallback.retrieveOk(suggestions); //ended - } - }, - a -> { - allStories.forEach(userStory -> { - counter.value++; - filter.filterOrAdd(userStory); + taggedStories.forEach(userStory -> { + taggedStoriesIds.add(userStory.id); }); + + allStories.clear(); + getUserStoryPage(1, allStories, null, + ex -> { + allStories.forEach(userStory -> { + if (taggedStoriesIds.contains(userStory.id)) { + userStory.tagFound = true; + } + filter.filterOrAdd(userStory); + }); + if (domainCallback != null) { + domainCallback.retrieveOk(suggestions); //ended + } + } + ); + } ); } @@ -73,25 +92,22 @@ public class UserStoryDomainProvider implements DomainProvider { restOfPagesCallback.callback(new Exception("Error accessing url [" + url + "]", t)); } - private void getUserStoryPage(int pageNumber, Callback restOfPagesCallback, Callback firstPageCallback) { + private void getUserStoryPage(int pageNumber, List userStoriesList, String tagsFilter, Callback restOfPagesCallback) { Call userStories = mUserStoryDomainProviderService.getUserStories( INCLUDE_ACTIONS_FOR_PLACES, "open", true, mProtocol, - TAGS_TO_SEARCH, + tagsFilter, pageNumber); Log.d("API-USER-STORY-DOMAINS", "Protocol [" + mProtocol + "] include_actions [" + INCLUDE_ACTIONS_FOR_PLACES + "]"); userStories.enqueue(new retrofit2.Callback() { @Override public void onResponse(Call call, Response response) { UserStories data = response.body(); - allStories.addAll(data.user_stories); + userStoriesList.addAll(data.user_stories); if (data.current_page < data.total_pages && data.current_page <= MAX_PAGES_TO_GET) { - if (pageNumber == 1 && firstPageCallback != null) { - firstPageCallback.callback(null); - } - getUserStoryPage(pageNumber + 1, restOfPagesCallback, null); + getUserStoryPage(pageNumber + 1, userStoriesList, tagsFilter, restOfPagesCallback); return; } restOfPagesCallback.callback(null); @@ -107,12 +123,16 @@ public class UserStoryDomainProvider implements DomainProvider { private class StoriesFilter { String[] mWords = new String[]{}; public StoriesFilter(String filterText) { - mWords = filterText.toUpperCase().split("\\s+"); + mWords = filterText.trim().toUpperCase().split("\\s+"); + if (mWords.length == 1 && (mWords[0] == null || mWords[0].length() <= 0 ) ) { + mWords = null; + } } private boolean matches(UserStory story) { - if (mWords.length <= 0) { - return true; + if (mWords == null || mWords.length <= 0) { + // No text filter? So filter by tag + return story.tagFound; } for (String word : mWords) { @@ -128,6 +148,9 @@ public class UserStoryDomainProvider implements DomainProvider { suggestions.add(story.toDomain()); } + /** + * if the story matches this filter criteria it's added into suggestions + * */ public void filterOrAdd(UserStory story) { if (matches(story)) { addToSuggestions(story); @@ -144,16 +167,6 @@ public class UserStoryDomainProvider implements DomainProvider { domainCallback.retrieveOk(suggestions); } - @Override - public synchronized void retrieve(String filterText, DomainCallback domainCallback) { - if (!startedToGetFromAPI) { - startedToGetFromAPI = true; - fillDestinations(filterText, domainCallback); - } else { - filterChoicesByText(filterText, domainCallback); - } - } - public interface UserStoryDomainProviderService { @GET("api/v1/user_stories") Call getUserStories(@Query("include_actions") String includeActions, @@ -166,12 +179,14 @@ public class UserStoryDomainProvider implements DomainProvider { class UserStory { public UserStory() {} + String id; String place_name; String path; String thumbnail_url; String place_id; String domain_id; private String searchText; + private boolean tagFound; // Locally used // New fields? tags, description diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/view/DomainAdapter.java b/android/app/src/main/java/io/highfidelity/hifiinterface/view/DomainAdapter.java index 97558d2681..4f8b33b481 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/view/DomainAdapter.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/view/DomainAdapter.java @@ -54,27 +54,10 @@ public class DomainAdapter extends RecyclerView.Adapter domain) { if (filterText.length() == 0) { - Domain lastVisitedDomain = new Domain(mContext.getString(R.string.your_last_location), mLastLocation, DEFAULT_THUMBNAIL_PLACE); - if (!mLastLocation.isEmpty() && mLastLocation.contains("://")) { - int startIndex = mLastLocation.indexOf("://"); - int endIndex = mLastLocation.indexOf("/", startIndex + 3); - String toSearch = mLastLocation.substring(0, endIndex + 1).toLowerCase(); - for (Domain d : domain) { - if (d.url.toLowerCase().startsWith(toSearch)) { - lastVisitedDomain.thumbnail = d.thumbnail; - } - } - } - domain.add(0, lastVisitedDomain); + addLastLocation(domain); } - for (Domain d : domain) { - // we override the default picture added in the server by an android specific version - if (d.thumbnail != null && - d.thumbnail.endsWith("assets/places/thumbnail-default-place-e5a3f33e773ab699495774990a562f9f7693dc48ef90d8be6985c645a0280f75.png")) { - d.thumbnail = DEFAULT_THUMBNAIL_PLACE; - } - } + overrideDefaultThumbnails(domain); mDomains = new Domain[domain.size()]; mDomains = domain.toArray(mDomains); @@ -96,6 +79,31 @@ public class DomainAdapter extends RecyclerView.Adapter domain) { + for (Domain d : domain) { + // we override the default picture added in the server by an android specific version + if (d.thumbnail != null && + d.thumbnail.endsWith("assets/places/thumbnail-default-place-e5a3f33e773ab699495774990a562f9f7693dc48ef90d8be6985c645a0280f75.png")) { + d.thumbnail = DEFAULT_THUMBNAIL_PLACE; + } + } + } + + private void addLastLocation(List domain) { + Domain lastVisitedDomain = new Domain(mContext.getString(R.string.your_last_location), mLastLocation, DEFAULT_THUMBNAIL_PLACE); + if (!mLastLocation.isEmpty() && mLastLocation.contains("://")) { + int startIndex = mLastLocation.indexOf("://"); + int endIndex = mLastLocation.indexOf("/", startIndex + 3); + String toSearch = mLastLocation.substring(0, endIndex + 1).toLowerCase(); + for (Domain d : domain) { + if (d.url.toLowerCase().startsWith(toSearch)) { + lastVisitedDomain.thumbnail = d.thumbnail; + } + } + } + domain.add(0, lastVisitedDomain); + } + @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = mInflater.inflate(R.layout.domain_view, parent, false); diff --git a/android/build.gradle b/android/build.gradle index 1dfef97f91..f1fe4ffc7f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -37,7 +37,7 @@ task clean(type: Delete) { ext { RELEASE_NUMBER = project.hasProperty('RELEASE_NUMBER') ? project.getProperty('RELEASE_NUMBER') : '0' RELEASE_TYPE = project.hasProperty('RELEASE_TYPE') ? project.getProperty('RELEASE_TYPE') : 'DEV' - BUILD_BRANCH = project.hasProperty('BUILD_BRANCH') ? project.getProperty('BUILD_BRANCH') : '' + STABLE_BUILD = project.hasProperty('STABLE_BUILD') ? project.getProperty('STABLE_BUILD') : '0' EXEC_SUFFIX = Os.isFamily(Os.FAMILY_WINDOWS) ? '.exe' : '' QT5_DEPS = [ 'Qt5Concurrent', @@ -66,19 +66,19 @@ ext { def baseFolder = new File(HIFI_ANDROID_PRECOMPILED) def appDir = new File(projectDir, 'app') def jniFolder = new File(appDir, 'src/main/jniLibs/arm64-v8a') -def baseUrl = 'https://hifi-public.s3.amazonaws.com/austin/android/' +def baseUrl = 'https://hifi-public.s3.amazonaws.com/dependencies/android/' def qtFile='qt-5.9.3_linux_armv8-libcpp_openssl.tgz' def qtChecksum='04599670ccca84bd2b15f6915568eb2d' -def qtVersionId='PeoqzN31n.YvLfs9JE2SgHgZ4.IaKAlt' +def qtVersionId='8QbCma4ryEPgBYn_8kgYgB10IvNx9I1W' if (Os.isFamily(Os.FAMILY_MAC)) { qtFile = 'qt-5.9.3_osx_armv8-libcpp_openssl.tgz' qtChecksum='4b02de9d67d6bfb202355a808d2d9c59' - qtVersionId='HygCmtMLPYioyil0DfXckGVzhw2SXZA9' + qtVersionId='2gfgoYCggJGyXxKiazaPGsMs1Gn9j4og' } else if (Os.isFamily(Os.FAMILY_WINDOWS)) { qtFile = 'qt-5.9.3_win_armv8-libcpp_openssl.tgz' qtChecksum='c3e25db64002d0f43cf565e0ef708911' - qtVersionId='HeVObSVLCBoc7yY7He1oBMvPIH0VkClT' + qtVersionId='xKIteC6HO0xrmcWeMmhQcmKyPEsnUrcZ' } def packages = [ @@ -88,62 +88,67 @@ def packages = [ checksum: qtChecksum, ], bullet: [ - file: 'bullet-2.83_armv8-libcpp.tgz', - versionId: 'ljb7v.1IjVRqyopUKVDbVnLA4z88J8Eo', - checksum: '2c558d604fce337f5eba3eb7ec1252fd', + file: 'bullet-2.88_armv8-libcpp.tgz', + versionId: 'S8YaoED0Cl8sSb8fSV7Q2G1lQJSNDxqg', + checksum: '81642779ccb110f8c7338e8739ac38a0', ], draco: [ file: 'draco_armv8-libcpp.tgz', - versionId: 'cA3tVJSmkvb1naA3l6D_Jv2Noh.4yc4m', + versionId: '3.B.uBj31kWlgND3_R2xwQzT_TP6Dz_8', checksum: '617a80d213a5ec69fbfa21a1f2f738cd', ], glad: [ file: 'glad_armv8-libcpp.zip', - versionId: 'Q9szthzeye8fFyAA.cY26Lgn2B8kezEE', + versionId: 'r5Zran.JSCtvrrB6Q4KaqfIoALPw3lYY', checksum: 'a8ee8584cf1ccd34766c7ddd9d5e5449', ], glm: [ - file: 'glm-0.9.8.tgz', - versionId: 'BlkJNwaYV2Gfy5XwMeU7K0uzPDRKFMt2', - checksum: 'd2b42cee31d2bc17bab6ce69e6b3f30a', + file: 'glm-0.9.8.5-patched.tgz', + versionId: 'cskfMoJrFlAeqI3WPxemyO_Cxt7rT9EJ', + checksum: '067b5fe16b220b5b1a1039ba51b062ae', ], gvr: [ file: 'gvrsdk_v1.101.0.tgz', - versionId: 'UTberAIFraEfF9IVjoV66u1DTPTopgeY', + versionId: 'nqBV_j81Uc31rC7bKIrlya_Hah4v3y5r', checksum: '57fd02baa069176ba18597a29b6b4fc7', ], nvtt: [ file: 'nvtt_armv8-libcpp.zip', - versionId: 'vLqrqThvpq4gp75BHMAqO6HhfTXaa0An', + versionId: 'lmkBVR5t4UF1UUwMwEirnk9H_8Nt90IO', checksum: 'eb46d0b683e66987190ed124aabf8910', sharedLibFolder: 'lib', includeLibs: ['libnvtt.so', 'libnvmath.so', 'libnvimage.so', 'libnvcore.so'], ], openssl: [ file: 'openssl-1.1.0g_armv8.tgz', - versionId: 'DmahmSGFS4ltpHyTdyQvv35WOeUOiib9', + versionId: 'AiiPjmgUZTgNj7YV1EEx2lL47aDvvvAW', checksum: 'cabb681fbccd79594f65fcc266e02f32', ], polyvox: [ file: 'polyvox_armv8-libcpp.tgz', - versionId: 'LDJtzMTvdm4SAc2KYg8Cg6uwWk4Vq3e3', - checksum: '349ad5b72aaf2749ca95d847e60c5314', + versionId: 'A2kbKiNhpIenGq23bKRRzg7IMAI5BI92', + checksum: 'dba88b3a098747af4bb169e9eb9af57e', sharedLibFolder: 'lib', includeLibs: ['Release/libPolyVoxCore.so', 'libPolyVoxUtil.so'], ], tbb: [ file: 'tbb-2018_U1_armv8_libcpp.tgz', - versionId: 'YZliDD8.Menh1IVXKEuLPeO3xAjJ1UdF', + versionId: 'mrRbWnv4O4evcM1quRH43RJqimlRtaKB', checksum: '20768f298f53b195e71b414b0ae240c4', sharedLibFolder: 'lib/release', includeLibs: ['libtbb.so', 'libtbbmalloc.so'], ], hifiAC: [ file: 'libplugins_libhifiCodec.zip', - versionId: 'mzKhsRCgVmloqq5bvE.0IwYK1NjGQc_G', + versionId: 'i31pW.qNbvFOXRxbyiJUxg3sphaFNmZU', checksum: '9412a8e12c88a4096c1fc843bb9fe52d', sharedLibFolder: '', includeLibs: ['libplugins_libhifiCodec.so'] + ], + etc2comp: [ + file: 'etc2comp-patched-armv8-libcpp.tgz', + versionId: 'bHhGECRAQR1vkpshBcK6ByNc1BQIM8gU', + checksum: '14b02795d774457a33bbc60e00a786bc' ] ] @@ -152,15 +157,15 @@ def scribeLocalFile='scribe' + EXEC_SUFFIX def scribeFile='scribe_linux_x86_64' def scribeChecksum='ca4b904f52f4f993c29175ba96798fa6' -def scribeVersion='wgpf4dB2Ltzg4Lb2jJ4nPFsHoDkmK_OO' +def scribeVersion='u_iTrJDaE95i2abTPXOpPZckGBIim53G' if (Os.isFamily(Os.FAMILY_MAC)) { scribeFile = 'scribe_osx_x86_64' scribeChecksum='72db9d32d4e1e50add755570ac5eb749' - scribeVersion='o_NbPrktzEYtBkQf3Tn7zc1nZWzM52w6' + scribeVersion='DAW0DmnjCRib4MD8x93bgc2Z2MpPojZC' } else if (Os.isFamily(Os.FAMILY_WINDOWS)) { scribeFile = 'scribe_win32_x86_64.exe' scribeChecksum='678e43d290c90fda670c6fefe038a06d' - scribeVersion='GCCJxlmd2irvNOFWfZR0U1UCLHndHQrC' + scribeVersion='PuullrA_bPlO9kXZRt8rLe536X1UI.m7' } def options = [ @@ -361,6 +366,7 @@ task verifyOpenSSL(type: Verify) { def p = packages['openssl']; src new File(bas task verifyPolyvox(type: Verify) { def p = packages['polyvox']; src new File(baseFolder, p['file']); checksum p['checksum'] } task verifyTBB(type: Verify) { def p = packages['tbb']; src new File(baseFolder, p['file']); checksum p['checksum'] } task verifyHifiAC(type: Verify) { def p = packages['hifiAC']; src new File(baseFolder, p['file']); checksum p['checksum'] } +task verifyEtc2Comp(type: Verify) { def p = packages['etc2comp']; src new File(baseFolder, p['file']); checksum p['checksum'] } task verifyDependencyDownloads(dependsOn: downloadDependencies) { } verifyDependencyDownloads.dependsOn verifyQt @@ -371,6 +377,7 @@ verifyDependencyDownloads.dependsOn verifyOpenSSL verifyDependencyDownloads.dependsOn verifyPolyvox verifyDependencyDownloads.dependsOn verifyTBB verifyDependencyDownloads.dependsOn verifyHifiAC +verifyDependencyDownloads.dependsOn verifyEtc2Comp task extractDependencies(dependsOn: verifyDependencyDownloads) { doLast { @@ -535,7 +542,7 @@ task cleanDependencies(type: Delete) { -// FIXME this code is prototyping the desired functionality for doing build time binary dependency resolution. +// FIXME this code is prototyping the desired functionality for doing build time binary dependency resolution. // See the comment on the qtBundle task above /* // FIXME derive the path from the gradle environment diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index d4d4f847ee..bc08c6f24a 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -624,8 +624,8 @@ AudioMixerClientData::IgnoreZone& AudioMixerClientData::IgnoreZoneMemo::get(unsi scale = MIN_IGNORE_BOX_SCALE; } - // quadruple the scale (this is arbitrary number chosen for comfort) - const float IGNORE_BOX_SCALE_FACTOR = 4.0f; + // (this is arbitrary number determined empirically for comfort) + const float IGNORE_BOX_SCALE_FACTOR = 2.4f; scale *= IGNORE_BOX_SCALE_FACTOR; // create the box (we use a box for the zone for convenience) diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index 984884adb2..b6fe765287 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -271,8 +271,9 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) if (glm::any(glm::lessThan(otherNodeBoxScale, minBubbleSize))) { otherNodeBox.setScaleStayCentered(minBubbleSize); } - // Quadruple the scale of both bounding boxes - otherNodeBox.embiggen(4.0f); + // Change the scale of both bounding boxes + // (This is an arbitrary number determined empirically) + otherNodeBox.embiggen(2.4f); // Perform the collision check between the two bounding boxes if (nodeBox.touches(otherNodeBox)) { diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp index c108dad6cf..3ca8c1ecd1 100644 --- a/assignment-client/src/entities/EntityServer.cpp +++ b/assignment-client/src/entities/EntityServer.cpp @@ -44,6 +44,7 @@ EntityServer::EntityServer(ReceivedMessage& message) : auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); packetReceiver.registerListenerForTypes({ PacketType::EntityAdd, + PacketType::EntityClone, PacketType::EntityEdit, PacketType::EntityErase, PacketType::EntityPhysics, diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index fad2c1f026..acf0abeed1 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -233,7 +233,6 @@ OctreeServer::OctreeServer(ReceivedMessage& message) : _argc(0), _argv(NULL), _parsedArgV(NULL), - _httpManager(NULL), _statusPort(0), _packetsPerClientPerInterval(10), _packetsTotalPerInterval(DEFAULT_PACKETS_PER_INTERVAL), @@ -285,7 +284,7 @@ void OctreeServer::initHTTPManager(int port) { QString documentRoot = QString("%1/web").arg(PathUtils::getAppDataPath()); // setup an httpManager with us as the request handler and the parent - _httpManager = new HTTPManager(QHostAddress::AnyIPv4, port, documentRoot, this, this); + _httpManager.reset(new HTTPManager(QHostAddress::AnyIPv4, port, documentRoot, this)); } bool OctreeServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler) { diff --git a/assignment-client/src/octree/OctreeServer.h b/assignment-client/src/octree/OctreeServer.h index b25e537d70..87145dd46e 100644 --- a/assignment-client/src/octree/OctreeServer.h +++ b/assignment-client/src/octree/OctreeServer.h @@ -183,7 +183,7 @@ protected: bool _isShuttingDown = false; - HTTPManager* _httpManager; + std::unique_ptr _httpManager; int _statusPort; QString _statusHost; diff --git a/cmake/externals/LibOVR/CMakeLists.txt b/cmake/externals/LibOVR/CMakeLists.txt index 97508be0c5..ed76f181e7 100644 --- a/cmake/externals/LibOVR/CMakeLists.txt +++ b/cmake/externals/LibOVR/CMakeLists.txt @@ -17,8 +17,8 @@ if (WIN32) ExternalProject_Add( ${EXTERNAL_NAME} - URL https://static.oculus.com/sdk-downloads/1.11.0/Public/1486063832/ovr_sdk_win_1.11.0_public.zip - URL_MD5 ea484403757cbfdfa743b6577fb1f9d2 + URL http://hifi-public.s3.amazonaws.com/dependencies/ovr_sdk_win_1.26.0_public.zip + URL_MD5 06804ff9727b910dcd04a37c800053b5 CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH= PATCH_COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/LibOVRCMakeLists.txt" /CMakeLists.txt LOG_DOWNLOAD 1 diff --git a/cmake/externals/LibOVR/LibOVRCMakeLists.txt b/cmake/externals/LibOVR/LibOVRCMakeLists.txt index 556533f0c2..a52cff5463 100644 --- a/cmake/externals/LibOVR/LibOVRCMakeLists.txt +++ b/cmake/externals/LibOVR/LibOVRCMakeLists.txt @@ -4,7 +4,7 @@ project(LibOVR) include_directories(LibOVR/Include LibOVR/Src) file(GLOB HEADER_FILES LibOVR/Include/*.h) file(GLOB EXTRA_HEADER_FILES LibOVR/Include/Extras/*.h) -file(GLOB_RECURSE SOURCE_FILES LibOVR/Src/*.c LibOVR/Src/*.cpp) +file(GLOB_RECURSE SOURCE_FILES LibOVR/Shim/*.c LibOVR/Shim/*.cpp) set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DOVR_BUILD_DEBUG") add_library(LibOVR STATIC ${SOURCE_FILES} ${HEADER_FILES} ${EXTRA_HEADER_FILES}) set_target_properties(LibOVR PROPERTIES DEBUG_POSTFIX "d") diff --git a/cmake/externals/bullet/CMakeLists.txt b/cmake/externals/bullet/CMakeLists.txt index 317e3302d9..91860a7470 100644 --- a/cmake/externals/bullet/CMakeLists.txt +++ b/cmake/externals/bullet/CMakeLists.txt @@ -17,8 +17,8 @@ include(ExternalProject) if (WIN32) ExternalProject_Add( ${EXTERNAL_NAME} - URL http://hifi-public.s3.amazonaws.com/dependencies/bullet-2.83-ccd-and-cmake-fixes.tgz - URL_MD5 03051bf112dcc78ddd296f9cab38fd68 + URL http://hifi-public.s3.amazonaws.com/dependencies/bullet-2.88.tgz + URL_MD5 0a6876607ebe83e227427215f15946fd CMAKE_ARGS ${PLATFORM_CMAKE_ARGS} -DCMAKE_INSTALL_PREFIX:PATH= -DBUILD_EXTRAS=0 -DINSTALL_LIBS=1 -DBUILD_BULLET3=0 -DBUILD_OPENGL3_DEMOS=0 -DBUILD_BULLET2_DEMOS=0 -DBUILD_UNIT_TESTS=0 -DUSE_GLUT=0 -DUSE_DX11=0 LOG_DOWNLOAD 1 LOG_CONFIGURE 1 @@ -28,8 +28,8 @@ if (WIN32) else () ExternalProject_Add( ${EXTERNAL_NAME} - URL http://hifi-public.s3.amazonaws.com/dependencies/bullet-2.83-ccd-and-cmake-fixes.tgz - URL_MD5 03051bf112dcc78ddd296f9cab38fd68 + URL http://hifi-public.s3.amazonaws.com/dependencies/bullet-2.88.tgz + URL_MD5 0a6876607ebe83e227427215f15946fd CMAKE_ARGS ${PLATFORM_CMAKE_ARGS} -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_INSTALL_PREFIX:PATH= -DBUILD_EXTRAS=0 -DINSTALL_LIBS=1 -DBUILD_BULLET3=0 -DBUILD_OPENGL3_DEMOS=0 -DBUILD_BULLET2_DEMOS=0 -DBUILD_UNIT_TESTS=0 -DUSE_GLUT=0 LOG_DOWNLOAD 1 LOG_CONFIGURE 1 diff --git a/cmake/externals/etc2comp/CMakeLists.txt b/cmake/externals/etc2comp/CMakeLists.txt new file mode 100644 index 0000000000..d6d21d6703 --- /dev/null +++ b/cmake/externals/etc2comp/CMakeLists.txt @@ -0,0 +1,55 @@ +set(EXTERNAL_NAME etc2comp) + +if (ANDROID) + set(ANDROID_CMAKE_ARGS "-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}" "-DANDROID_NATIVE_API_LEVEL=19") +endif () + +if (APPLE) + set(EXTRA_CMAKE_FLAGS -DCMAKE_CXX_FLAGS=-stdlib=libc++ -DCMAKE_EXE_LINKER_FLAGS=-stdlib=libc++) +endif () + +include(ExternalProject) +# We use a patched version of etc2comp that properly generates all the necessary mips +# See https://github.com/google/etc2comp/pull/29 +# We also use part of https://github.com/google/etc2comp/pull/1, which fixes a bug +# that would override CMAKE_CXX_FLAGS +ExternalProject_Add( + ${EXTERNAL_NAME} + URL https://hifi-public.s3.amazonaws.com/dependencies/etc2comp-patched.zip + URL_MD5 4c96153eb179acbe619e3d99d3330595 + CMAKE_ARGS ${ANDROID_CMAKE_ARGS} ${EXTRA_CMAKE_FLAGS} + BINARY_DIR ${EXTERNAL_PROJECT_PREFIX}/build + INSTALL_COMMAND "" + LOG_DOWNLOAD 1 + LOG_CONFIGURE 1 + LOG_BUILD 1 +) + +# Hide this external target (for ide users) +set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals") + +ExternalProject_Get_Property(${EXTERNAL_NAME} SOURCE_DIR) +ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR) + +string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) + +if (WIN32) + set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG ${INSTALL_DIR}/build/EtcLib/Debug/EtcLib.lib CACHE FILEPATH "Path to Etc2Comp debug library") + + # use generator expression to ensure the correct library is found when building different configurations in VS + set(_LIB_FOLDER "$<$:build/EtcLib/RelWithDebInfo>") + set(_LIB_FOLDER "${_LIB_FOLDER}$<$:build/EtcLib/MinSizeRel>") + set(_LIB_FOLDER "${_LIB_FOLDER}$<$,$>:build/EtcLib/Release>") + + set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/${_LIB_FOLDER}/EtcLib.lib CACHE FILEPATH "Path to Etc2Comp release library") +elseif (APPLE) + set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG ${INSTALL_DIR}/build/EtcLib/Debug/libEtcLib.a CACHE FILEPATH "Path to EtcLib debug library") + set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/build/EtcLib/Release/libEtcLib.a CACHE FILEPATH "Path to EtcLib release library") +else () + set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG "" CACHE FILEPATH "Path to EtcLib debug library") + set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/build/EtcLib/libEtcLib.a CACHE FILEPATH "Path to EtcLib release library") +endif () + +set(ETC_INCLUDE_DIR ${SOURCE_DIR}/EtcLib/Etc CACHE FILEPATH "Path to Etc2Comp/Etc include directory") +set(ETCCODEC_INCLUDE_DIR ${SOURCE_DIR}/EtcLib/EtcCodec CACHE FILEPATH "Path to Etc2Comp/EtcCodec include directory") +# ETC2COMP_INCLUDE_DIRS will be set later by FindEtc2Comp \ No newline at end of file diff --git a/cmake/externals/glm/CMakeLists.txt b/cmake/externals/glm/CMakeLists.txt index bc8089074f..0a83004438 100644 --- a/cmake/externals/glm/CMakeLists.txt +++ b/cmake/externals/glm/CMakeLists.txt @@ -3,8 +3,8 @@ set(EXTERNAL_NAME glm) include(ExternalProject) ExternalProject_Add( ${EXTERNAL_NAME} - URL https://hifi-public.s3.amazonaws.com/dependencies/glm-0.9.8.zip - URL_MD5 579ac77a3110befa3244d68c0ceb7281 + URL https://hifi-public.s3.amazonaws.com/dependencies/glm-0.9.8.5-patched.zip + URL_MD5 7d39ecc1cea275427534c3cfd6dd63f0 BINARY_DIR ${EXTERNAL_PROJECT_PREFIX}/build CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH= ${EXTERNAL_ARGS} LOG_DOWNLOAD 1 @@ -18,4 +18,4 @@ set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals") ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR) string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER) -set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${INSTALL_DIR}/include CACHE PATH "List of glm include directories") \ No newline at end of file +set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${INSTALL_DIR}/include CACHE PATH "List of glm include directories") diff --git a/cmake/externals/serverless-content/CMakeLists.txt b/cmake/externals/serverless-content/CMakeLists.txt index cad6d40b49..81b82e8651 100644 --- a/cmake/externals/serverless-content/CMakeLists.txt +++ b/cmake/externals/serverless-content/CMakeLists.txt @@ -4,8 +4,8 @@ set(EXTERNAL_NAME serverless-content) ExternalProject_Add( ${EXTERNAL_NAME} - URL http://cdn.highfidelity.com/content-sets/serverless-tutorial-RC67-v4.zip - URL_MD5 ba32aed18bfeaac4ccaf5ebb8ea3e804 + URL http://cdn.highfidelity.com/content-sets/serverless-tutorial-RC68-v2.zip + URL_MD5 f7d290471baf7f5694c147217b8fc548 CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "" diff --git a/cmake/macros/SetPackagingParameters.cmake b/cmake/macros/SetPackagingParameters.cmake index 29947c793c..30a7311542 100644 --- a/cmake/macros/SetPackagingParameters.cmake +++ b/cmake/macros/SetPackagingParameters.cmake @@ -17,15 +17,13 @@ macro(SET_PACKAGING_PARAMETERS) set(DEV_BUILD 0) set(BUILD_GLOBAL_SERVICES "DEVELOPMENT") set(USE_STABLE_GLOBAL_SERVICES 0) + set(BUILD_NUMBER 0) set(APP_USER_MODEL_ID "com.highfidelity.sandbox-dev") set_from_env(RELEASE_TYPE RELEASE_TYPE "DEV") set_from_env(RELEASE_NUMBER RELEASE_NUMBER "") - set_from_env(BUILD_BRANCH BRANCH "") - string(TOLOWER "${BUILD_BRANCH}" BUILD_BRANCH) + set_from_env(STABLE_BUILD STABLE_BUILD 0) - message(STATUS "The BUILD_BRANCH variable is: ${BUILD_BRANCH}") - message(STATUS "The BRANCH environment variable is: $ENV{BRANCH}") message(STATUS "The RELEASE_TYPE variable is: ${RELEASE_TYPE}") # setup component categories for installer @@ -47,17 +45,17 @@ macro(SET_PACKAGING_PARAMETERS) # if the build is a PRODUCTION_BUILD from the "stable" branch # then use the STABLE gobal services - if (BUILD_BRANCH STREQUAL "stable") - message(STATUS "The RELEASE_TYPE is PRODUCTION and the BUILD_BRANCH is stable...") + if (STABLE_BUILD) + message(STATUS "The RELEASE_TYPE is PRODUCTION and STABLE_BUILD is 1") set(BUILD_GLOBAL_SERVICES "STABLE") set(USE_STABLE_GLOBAL_SERVICES 1) - endif() + endif () elseif (RELEASE_TYPE STREQUAL "PR") set(DEPLOY_PACKAGE TRUE) set(PR_BUILD 1) set(BUILD_VERSION "PR${RELEASE_NUMBER}") - set(BUILD_ORGANIZATION "High Fidelity - ${BUILD_VERSION}") + set(BUILD_ORGANIZATION "High Fidelity - PR${RELEASE_NUMBER}") set(INTERFACE_BUNDLE_NAME "Interface") set(INTERFACE_ICON_PREFIX "interface-beta") @@ -76,6 +74,54 @@ macro(SET_PACKAGING_PARAMETERS) string(TIMESTAMP BUILD_TIME "%d/%m/%Y") + # if STABLE_BUILD is 1, PRODUCTION_BUILD must be 1 and + # DEV_BUILD and PR_BUILD must be 0 + if (STABLE_BUILD) + if ((NOT PRODUCTION_BUILD) OR PR_BUILD OR DEV_BUILD) + message(FATAL_ERROR "Cannot produce STABLE_BUILD without PRODUCTION_BUILD") + endif () + endif () + + if ((PRODUCTION_BUILD OR PR_BUILD) AND NOT STABLE_BUILD) + # append the abbreviated commit SHA to the build version + # since this is a PR build or master/nightly builds + + # for PR_BUILDS, we need to grab the abbreviated SHA + # for the second parent of HEAD (not HEAD) since that is the + # SHA of the commit merged to master for the build + if (PR_BUILD) + set(_GIT_LOG_FORMAT "%p") + else () + set(_GIT_LOG_FORMAT "%h") + endif () + + execute_process( + COMMAND git log -1 --format=${_GIT_LOG_FORMAT} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE _GIT_LOG_OUTPUT + ERROR_VARIABLE _GIT_LOG_ERROR + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + if (PR_BUILD) + separate_arguments(_COMMIT_PARENTS UNIX_COMMAND ${_GIT_LOG_OUTPUT}) + list(GET _COMMIT_PARENTS 1 GIT_COMMIT_HASH) + else () + set(GIT_COMMIT_HASH ${_GIT_LOG_OUTPUT}) + endif () + + if (_GIT_LOG_ERROR OR NOT GIT_COMMIT_HASH) + message(FATAL_ERROR "Could not retreive abbreviated SHA for PR or production master build") + endif () + + set(BUILD_VERSION_NO_SHA ${BUILD_VERSION}) + set(BUILD_VERSION "${BUILD_VERSION}-${GIT_COMMIT_HASH}") + + # pass along a release number without the SHA in case somebody + # wants to compare master or PR builds as integers + set(BUILD_NUMBER ${RELEASE_NUMBER}) + endif () + if (DEPLOY_PACKAGE) # for deployed packages always grab the serverless content set(DOWNLOAD_SERVERLESS_CONTENT ON) @@ -129,8 +175,8 @@ macro(SET_PACKAGING_PARAMETERS) set(CONSOLE_SHORTCUT_NAME "Sandbox") set(APP_USER_MODEL_ID "com.highfidelity.sandbox") else () - set(INTERFACE_SHORTCUT_NAME "High Fidelity Interface - ${BUILD_VERSION}") - set(CONSOLE_SHORTCUT_NAME "Sandbox - ${BUILD_VERSION}") + set(INTERFACE_SHORTCUT_NAME "High Fidelity Interface - ${BUILD_VERSION_NO_SHA}") + set(CONSOLE_SHORTCUT_NAME "Sandbox - ${BUILD_VERSION_NO_SHA}") endif () set(INTERFACE_HF_SHORTCUT_NAME "${INTERFACE_SHORTCUT_NAME}") diff --git a/cmake/macros/SetupHifiTestCase.cmake b/cmake/macros/SetupHifiTestCase.cmake index b0edb41e36..017a0222f5 100644 --- a/cmake/macros/SetupHifiTestCase.cmake +++ b/cmake/macros/SetupHifiTestCase.cmake @@ -61,16 +61,21 @@ macro(SETUP_HIFI_TESTCASE) endif() endforeach() + # Find test classes to build into test executables. # Warn about any .cpp files that are *not* test classes (*Test[s].cpp), since those files will not be used. foreach (SRC_FILE ${TEST_PROJ_SRC_FILES}) string(REGEX MATCH ".+Tests?\\.cpp$" TEST_CPP_FILE ${SRC_FILE}) string(REGEX MATCH ".+\\.cpp$" NON_TEST_CPP_FILE ${SRC_FILE}) + string(REGEX MATCH ".+\\.qrc$" QRC_FILE ${SRC_FILE}) if (TEST_CPP_FILE) list(APPEND TEST_CASE_FILES ${TEST_CPP_FILE}) elseif (NON_TEST_CPP_FILE) message(WARNING "ignoring .cpp file (not a test class -- this will not be linked or compiled!): " ${NON_TEST_CPP_FILE}) endif () + if (QRC_FILE) + list(APPEND EXTRA_FILES ${QRC_FILE}) + endif() endforeach () if (TEST_CASE_FILES) @@ -88,7 +93,7 @@ macro(SETUP_HIFI_TESTCASE) # grab the implemenation and header files set(TARGET_SRCS ${TEST_FILE}) # only one source / .cpp file (the test class) - add_executable(${TARGET_NAME} ${TEST_FILE}) + add_executable(${TARGET_NAME} ${TEST_FILE} ${EXTRA_FILES}) add_test(${TARGET_NAME}-test ${TARGET_NAME}) set_target_properties(${TARGET_NAME} PROPERTIES EXCLUDE_FROM_DEFAULT_BUILD TRUE diff --git a/cmake/macros/TargetEtc2Comp.cmake b/cmake/macros/TargetEtc2Comp.cmake new file mode 100644 index 0000000000..44152a58d2 --- /dev/null +++ b/cmake/macros/TargetEtc2Comp.cmake @@ -0,0 +1,22 @@ +# +# Copyright 2018 High Fidelity, Inc. +# Created by Sam Gondelman on 5/2/2018 +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +# +macro(TARGET_ETC2COMP) + if (ANDROID) + set(INSTALL_DIR ${HIFI_ANDROID_PRECOMPILED}/etc2comp) + set(ETC2COMP_INCLUDE_DIRS "${INSTALL_DIR}/include/Etc" "${INSTALL_DIR}/include/EtcCodec") + set(ETC2COMP_LIBRARY_DEBUG ${INSTALL_DIR}/lib/libEtcLib.a) + set(ETC2COMP_LIBRARY_RELEASE ${INSTALL_DIR}/lib/libEtcLib.a) + select_library_configurations(ETC2COMP) + else() + add_dependency_external_projects(etc2comp) + find_package(Etc2Comp REQUIRED) + endif() + + target_include_directories(${TARGET_NAME} PRIVATE ${ETC2COMP_INCLUDE_DIRS}) + target_link_libraries(${TARGET_NAME} ${ETC2COMP_LIBRARIES}) +endmacro() diff --git a/cmake/modules/FindEtc2Comp.cmake b/cmake/modules/FindEtc2Comp.cmake new file mode 100644 index 0000000000..1b990368fd --- /dev/null +++ b/cmake/modules/FindEtc2Comp.cmake @@ -0,0 +1,37 @@ +# +# FindEtc2Comp.cmake +# +# Try to find the Etc2Comp compression library. +# +# Once done this will define +# +# ETC2COMP_FOUND - system found Etc2Comp +# ETC2COMP_INCLUDE_DIRS - the Etc2Comp include directory +# ETC2COMP_LIBRARIES - link to this to use Etc2Comp +# +# Created on 5/2/2018 by Sam Gondelman +# Copyright 2018 High Fidelity, Inc. +# +# Distributed under the Apache License, Version 2.0. +# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +# + +include("${MACRO_DIR}/HifiLibrarySearchHints.cmake") +hifi_library_search_hints("etc2comp") + +find_path(ETC_INCLUDE_DIR NAMES Etc.h HINTS ${ETC2COMP_SEARCH_DIRS}) +find_path(ETCCODEC_INCLUDE_DIR NAMES EtcBlock4x4.h HINTS ${ETC2COMP_SEARCH_DIRS}) +set(ETC2COMP_INCLUDE_DIRS "${ETC_INCLUDE_DIR}" "${ETCCODEC_INCLUDE_DIR}") + +find_library(ETC2COMP_LIBRARY_DEBUG NAMES ETC2COMP ETC2COMP_LIB PATH_SUFFIXES EtcLib/Debug HINTS ${ETC2COMP_SEARCH_DIRS}) +find_library(ETC2COMP_LIBRARY_RELEASE NAMES ETC2COMP ETC2COMP_LIB PATH_SUFFIXES EtcLib/Release EtcLib HINTS ${ETC2COMP_SEARCH_DIRS}) + +include(SelectLibraryConfigurations) +select_library_configurations(ETC2COMP) + +set(ETC2COMP_LIBRARIES ${ETC2COMP_LIBRARY}) + +find_package_handle_standard_args(ETC2COMP "Could NOT find ETC2COMP, try to set the path to ETC2COMP root folder in the system variable ETC2COMP_ROOT_DIR or create a directory etc2comp in HIFI_LIB_DIR and paste the necessary files there" + ETC2COMP_INCLUDE_DIRS ETC2COMP_LIBRARIES) + +mark_as_advanced(ETC2COMP_INCLUDE_DIRS ETC2COMP_LIBRARIES ETC2COMP_SEARCH_DIRS) diff --git a/cmake/templates/BuildInfo.h.in b/cmake/templates/BuildInfo.h.in index 904d17293b..9fc9d9be81 100644 --- a/cmake/templates/BuildInfo.h.in +++ b/cmake/templates/BuildInfo.h.in @@ -24,8 +24,26 @@ namespace BuildInfo { const QString MODIFIED_ORGANIZATION = "@BUILD_ORGANIZATION@"; const QString ORGANIZATION_DOMAIN = "highfidelity.io"; const QString VERSION = "@BUILD_VERSION@"; - const QString BUILD_BRANCH = "@BUILD_BRANCH@"; + const QString BUILD_NUMBER = "@BUILD_NUMBER@"; const QString BUILD_GLOBAL_SERVICES = "@BUILD_GLOBAL_SERVICES@"; const QString BUILD_TIME = "@BUILD_TIME@"; -} + enum BuildType { + Dev, + PR, + Master, + Stable + }; + +#if defined(PR_BUILD) + const BuildType BUILD_TYPE = PR; + const QString BUILD_TYPE_STRING = "pr"; +#elif defined(PRODUCTION_BUILD) + const BuildType BUILD_TYPE = @STABLE_BUILD@ ? Stable : Master; + const QString BUILD_TYPE_STRING = @STABLE_BUILD@ ? "stable" : "master"; +#else + const BuildType BUILD_TYPE = Dev; + const QString BUILD_TYPE_STRING = "dev"; +#endif + +} diff --git a/cmake/templates/FixupBundlePostBuild.cmake.in b/cmake/templates/FixupBundlePostBuild.cmake.in index 57379bb48b..bb96fe49f3 100644 --- a/cmake/templates/FixupBundlePostBuild.cmake.in +++ b/cmake/templates/FixupBundlePostBuild.cmake.in @@ -11,6 +11,35 @@ include(BundleUtilities) +# replace copy_resolved_item_into_bundle +# +# The official version of copy_resolved_item_into_bundle will print out a "warning:" when +# the resolved item matches the resolved embedded item. This not not really an issue that +# should rise to the level of a "warning" so we replace this message with a "status:" +# +# Source: https://github.com/jherico/OculusMinimalExample/blob/master/cmake/templates/FixupBundlePostBuild.cmake.in +# +function(copy_resolved_item_into_bundle resolved_item resolved_embedded_item) + if (WIN32) + # ignore case on Windows + string(TOLOWER "${resolved_item}" resolved_item_compare) + string(TOLOWER "${resolved_embedded_item}" resolved_embedded_item_compare) + else() + set(resolved_item_compare "${resolved_item}") + set(resolved_embedded_item_compare "${resolved_embedded_item}") + endif() + + if ("${resolved_item_compare}" STREQUAL "${resolved_embedded_item_compare}") + # this is our only change from the original version + message(STATUS "status: resolved_item == resolved_embedded_item - not copying...") + else() + execute_process(COMMAND ${CMAKE_COMMAND} -E copy "${resolved_item}" "${resolved_embedded_item}") + if (UNIX AND NOT APPLE) + file(RPATH_REMOVE FILE "${resolved_embedded_item}") + endif() + endif() +endfunction() + function(gp_resolved_file_type_override resolved_file type_var) if( file MATCHES ".*VCRUNTIME140.*" ) set(type "system" PARENT_SCOPE) diff --git a/cmake/templates/console-build-info.json.in b/cmake/templates/console-build-info.json.in index 49f2ea2fda..7e3d2b11b9 100644 --- a/cmake/templates/console-build-info.json.in +++ b/cmake/templates/console-build-info.json.in @@ -1,5 +1,6 @@ { "releaseType": "@RELEASE_TYPE@", "buildIdentifier": "@BUILD_VERSION@", + "organization": "@BUILD_ORGANIZATION@", "appUserModelId": "@APP_USER_MODEL_ID@" } diff --git a/domain-server/src/AssetsBackupHandler.cpp b/domain-server/src/AssetsBackupHandler.cpp index 2369b01690..6bcabc0bf1 100644 --- a/domain-server/src/AssetsBackupHandler.cpp +++ b/domain-server/src/AssetsBackupHandler.cpp @@ -34,8 +34,9 @@ static const chrono::minutes MAX_REFRESH_TIME { 5 }; Q_DECLARE_LOGGING_CATEGORY(asset_backup) Q_LOGGING_CATEGORY(asset_backup, "hifi.asset-backup"); -AssetsBackupHandler::AssetsBackupHandler(const QString& backupDirectory) : - _assetsDirectory(backupDirectory + ASSETS_DIR) +AssetsBackupHandler::AssetsBackupHandler(const QString& backupDirectory, bool assetServerEnabled) : + _assetsDirectory(backupDirectory + ASSETS_DIR), + _assetServerEnabled(assetServerEnabled) { // Make sure the asset directory exists. QDir(_assetsDirectory).mkpath("."); @@ -53,6 +54,7 @@ void AssetsBackupHandler::setupRefreshTimer() { auto nodeList = DependencyManager::get(); QObject::connect(nodeList.data(), &LimitedNodeList::nodeActivated, this, [this](SharedNodePointer node) { if (node->getType() == NodeType::AssetServer) { + assert(_assetServerEnabled); // run immediately for the first time. _mappingsRefreshTimer.start(0); } @@ -233,12 +235,12 @@ void AssetsBackupHandler::createBackup(const QString& backupName, QuaZip& zip) { return; } - if (_lastMappingsRefresh.time_since_epoch().count() == 0) { + if (_assetServerEnabled && _lastMappingsRefresh.time_since_epoch().count() == 0) { qCWarning(asset_backup) << "Current mappings not yet loaded."; return; } - if ((p_high_resolution_clock::now() - _lastMappingsRefresh) > MAX_REFRESH_TIME) { + if (_assetServerEnabled && (p_high_resolution_clock::now() - _lastMappingsRefresh) > MAX_REFRESH_TIME) { qCWarning(asset_backup) << "Backing up asset mappings that might be stale."; } diff --git a/domain-server/src/AssetsBackupHandler.h b/domain-server/src/AssetsBackupHandler.h index 82d684c2c3..427dc6831a 100644 --- a/domain-server/src/AssetsBackupHandler.h +++ b/domain-server/src/AssetsBackupHandler.h @@ -30,7 +30,7 @@ class AssetsBackupHandler : public QObject, public BackupHandlerInterface { Q_OBJECT public: - AssetsBackupHandler(const QString& backupDirectory); + AssetsBackupHandler(const QString& backupDirectory, bool assetServerEnabled); std::pair isAvailable(const QString& backupName) override; std::pair getRecoveryStatus() override; @@ -65,6 +65,7 @@ private: void updateMappings(); QString _assetsDirectory; + bool _assetServerEnabled { false }; QTimer _mappingsRefreshTimer; p_high_resolution_clock::time_point _lastMappingsRefresh; diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index d78f0aaeb3..b4685b907f 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -16,6 +16,8 @@ #include #include +#include + #include #include @@ -479,7 +481,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect limitedNodeList->killNodeWithUUID(existingNodeID); } - // add the connecting node (or re-use the matched one from eachNodeBreakable above) + // add the connecting node SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection); // set the edit rights for this user @@ -508,26 +510,22 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect return newNode; } -SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection, - QUuid nodeID) { +SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection) { HifiSockAddr discoveredSocket = nodeConnection.senderSockAddr; SharedNetworkPeer connectedPeer = _icePeers.value(nodeConnection.connectUUID); - if (connectedPeer) { - // this user negotiated a connection with us via ICE, so re-use their ICE client ID - nodeID = nodeConnection.connectUUID; - - if (connectedPeer->getActiveSocket()) { - // set their discovered socket to whatever the activated socket on the network peer object was - discoveredSocket = *connectedPeer->getActiveSocket(); - } - } else { - // we got a connectUUID we didn't recognize, either use the hinted node ID or randomly generate a new one - if (nodeID.isNull()) { - nodeID = QUuid::createUuid(); - } + if (connectedPeer && connectedPeer->getActiveSocket()) { + // set their discovered socket to whatever the activated socket on the network peer object was + discoveredSocket = *connectedPeer->getActiveSocket(); } + // create a new node ID for the verified connecting node + auto nodeID = QUuid::createUuid(); + + // add a mapping from connection node ID to ICE peer ID + // so that we can remove the ICE peer once we see this node connect + _nodeToICEPeerIDs.insert(nodeID, nodeConnection.connectUUID); + auto limitedNodeList = DependencyManager::get(); Node::LocalID newLocalID = findOrCreateLocalID(nodeID); @@ -541,6 +539,15 @@ SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const Node return newNode; } +void DomainGatekeeper::cleanupICEPeerForNode(const QUuid& nodeID) { + // remove this node ID from our node to ICE peer ID map + // and the associated ICE peer (if it still exists) + auto icePeerID = _nodeToICEPeerIDs.take(nodeID); + if (!icePeerID.isNull()) { + _icePeers.remove(icePeerID); + } +} + bool DomainGatekeeper::verifyUserSignature(const QString& username, const QByteArray& usernameSignature, const HifiSockAddr& senderSockAddr) { diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index 8402e58559..2cb9b4c8a9 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -39,8 +39,8 @@ public: void addPendingAssignedNode(const QUuid& nodeUUID, const QUuid& assignmentUUID, const QUuid& walletUUID, const QString& nodeVersion); QUuid assignmentUUIDForPendingAssignment(const QUuid& tempUUID); - - void removeICEPeer(const QUuid& peerUUID) { _icePeers.remove(peerUUID); } + + void cleanupICEPeerForNode(const QUuid& nodeID); Node::LocalID findOrCreateLocalID(const QUuid& uuid); @@ -77,8 +77,7 @@ private: SharedNodePointer processAgentConnectRequest(const NodeConnectionData& nodeConnection, const QString& username, const QByteArray& usernameSignature); - SharedNodePointer addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection, - QUuid nodeID = QUuid()); + SharedNodePointer addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection); bool verifyUserSignature(const QString& username, const QByteArray& usernameSignature, const HifiSockAddr& senderSockAddr); @@ -101,6 +100,10 @@ private: std::unordered_map _pendingAssignedNodes; QHash _icePeers; + + using ConnectingNodeID = QUuid; + using ICEPeerID = QUuid; + QHash _nodeToICEPeerIDs; QHash _connectionTokenHash; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index baeac043e4..a34deebc95 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -149,7 +149,6 @@ DomainServer::DomainServer(int argc, char* argv[]) : QCoreApplication(argc, argv), _gatekeeper(this), _httpManager(QHostAddress::AnyIPv4, DOMAIN_SERVER_HTTP_PORT, QString("%1/resources/web/").arg(QCoreApplication::applicationDirPath()), this), - _httpsManager(NULL), _allAssignments(), _unfulfilledAssignments(), _isUsingDTLS(false), @@ -177,7 +176,7 @@ DomainServer::DomainServer(int argc, char* argv[]) : qDebug() << "[VERSION] Build sequence:" << qPrintable(applicationVersion()); qDebug() << "[VERSION] MODIFIED_ORGANIZATION:" << BuildInfo::MODIFIED_ORGANIZATION; qDebug() << "[VERSION] VERSION:" << BuildInfo::VERSION; - qDebug() << "[VERSION] BUILD_BRANCH:" << BuildInfo::BUILD_BRANCH; + qDebug() << "[VERSION] BUILD_TYPE_STRING:" << BuildInfo::BUILD_TYPE_STRING; qDebug() << "[VERSION] BUILD_GLOBAL_SERVICES:" << BuildInfo::BUILD_GLOBAL_SERVICES; qDebug() << "[VERSION] We will be using this name to find ICE servers:" << _iceServerAddr; @@ -308,7 +307,7 @@ DomainServer::DomainServer(int argc, char* argv[]) : connect(_contentManager.get(), &DomainContentBackupManager::started, _contentManager.get(), [this](){ _contentManager->addBackupHandler(BackupHandlerPointer(new EntitiesBackupHandler(getEntitiesFilePath(), getEntitiesReplacementFilePath()))); - _contentManager->addBackupHandler(BackupHandlerPointer(new AssetsBackupHandler(getContentBackupDir()))); + _contentManager->addBackupHandler(BackupHandlerPointer(new AssetsBackupHandler(getContentBackupDir(), isAssetServerEnabled()))); _contentManager->addBackupHandler(BackupHandlerPointer(new ContentSettingsBackupHandler(_settingsManager))); }); @@ -385,6 +384,8 @@ DomainServer::~DomainServer() { _contentManager->terminate(); } + DependencyManager::destroy(); + // cleanup the AssetClient thread DependencyManager::destroy(); _assetClientThread.quit(); @@ -439,7 +440,7 @@ bool DomainServer::optionallyReadX509KeyAndCertificate() { QSslCertificate sslCertificate(&certFile); QSslKey privateKey(&keyFile, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, keyPassphraseString.toUtf8()); - _httpsManager = new HTTPSManager(QHostAddress::AnyIPv4, DOMAIN_SERVER_HTTPS_PORT, sslCertificate, privateKey, QString(), this, this); + _httpsManager.reset(new HTTPSManager(QHostAddress::AnyIPv4, DOMAIN_SERVER_HTTPS_PORT, sslCertificate, privateKey, QString(), this)); qDebug() << "TCP server listening for HTTPS connections on" << DOMAIN_SERVER_HTTPS_PORT; @@ -990,15 +991,11 @@ void DomainServer::populateDefaultStaticAssignmentsExcludingTypes(const QSet(static_cast(defaultedType) + 1)) { if (!excludedTypes.contains(defaultedType) && defaultedType != Assignment::AgentType) { - if (defaultedType == Assignment::AssetServerType) { - // Make sure the asset-server is enabled before adding it here. - // Initially we do not assign it by default so we can test it in HF domains first - static const QString ASSET_SERVER_ENABLED_KEYPATH = "asset_server.enabled"; - - if (!_settingsManager.valueOrDefaultValueForKeyPath(ASSET_SERVER_ENABLED_KEYPATH).toBool()) { - // skip to the next iteration if asset-server isn't enabled - continue; - } + // Make sure the asset-server is enabled before adding it here. + // Initially we do not assign it by default so we can test it in HF domains first + if (defaultedType == Assignment::AssetServerType && !isAssetServerEnabled()) { + // skip to the next iteraion if asset-server isn't enabled + continue; } // type has not been set from a command line or config file config, use the default @@ -1017,15 +1014,22 @@ void DomainServer::processListRequestPacket(QSharedPointer mess sendingNode->setPublicSocket(nodeRequestData.publicSockAddr); sendingNode->setLocalSocket(nodeRequestData.localSockAddr); - // update the NodeInterestSet in case there have been any changes DomainServerNodeData* nodeData = static_cast(sendingNode->getLinkedData()); + if (!nodeData->hasCheckedIn()) { + nodeData->setHasCheckedIn(true); + + // on first check in, make sure we've cleaned up any ICE peer for this node + _gatekeeper.cleanupICEPeerForNode(sendingNode->getUUID()); + } + // guard against patched agents asking to hear about other agents auto safeInterestSet = nodeRequestData.interestList.toSet(); if (sendingNode->getType() == NodeType::Agent) { safeInterestSet.remove(NodeType::Agent); } + // update the NodeInterestSet in case there have been any changes nodeData->setNodeInterestSet(safeInterestSet); // update the connecting hostname in case it has changed @@ -1114,7 +1118,7 @@ void DomainServer::handleConnectedNode(SharedNodePointer newNode) { } void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr &senderSockAddr) { - const int NUM_DOMAIN_LIST_EXTENDED_HEADER_BYTES = NUM_BYTES_RFC4122_UUID + NLPacket::NUM_BYTES_LOCALID + + const int NUM_DOMAIN_LIST_EXTENDED_HEADER_BYTES = NUM_BYTES_RFC4122_UUID + NLPacket::NUM_BYTES_LOCALID + NUM_BYTES_RFC4122_UUID + NLPacket::NUM_BYTES_LOCALID + 4; // setup the extended header for the domain list packets @@ -2676,7 +2680,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl QString settingsPassword = settingsPasswordVariant.isValid() ? settingsPasswordVariant.toString() : ""; QString hexHeaderPassword = headerPassword.isEmpty() ? "" : QCryptographicHash::hash(headerPassword.toUtf8(), QCryptographicHash::Sha256).toHex(); - + if (settingsUsername == headerUsername && hexHeaderPassword == settingsPassword) { return true; } @@ -2938,6 +2942,12 @@ bool DomainServer::shouldReplicateNode(const Node& node) { } }; + +bool DomainServer::isAssetServerEnabled() { + static const QString ASSET_SERVER_ENABLED_KEYPATH = "asset_server.enabled"; + return _settingsManager.valueOrDefaultValueForKeyPath(ASSET_SERVER_ENABLED_KEYPATH).toBool(); +} + void DomainServer::nodeAdded(SharedNodePointer node) { // we don't use updateNodeWithData, so add the DomainServerNodeData to the node here node->setLinkedData(std::unique_ptr { new DomainServerNodeData() }); @@ -2945,7 +2955,7 @@ void DomainServer::nodeAdded(SharedNodePointer node) { void DomainServer::nodeKilled(SharedNodePointer node) { // if this peer connected via ICE then remove them from our ICE peers hash - _gatekeeper.removeICEPeer(node->getUUID()); + _gatekeeper.cleanupICEPeerForNode(node->getUUID()); DomainServerNodeData* nodeData = static_cast(node->getLinkedData()); @@ -2978,6 +2988,8 @@ void DomainServer::nodeKilled(SharedNodePointer node) { } } } + + broadcastNodeDisconnect(node); } SharedAssignmentPointer DomainServer::dequeueMatchingAssignment(const QUuid& assignmentUUID, NodeType_t nodeType) { @@ -3163,18 +3175,23 @@ void DomainServer::handleKillNode(SharedNodePointer nodeToKill) { const QUuid& nodeUUID = nodeToKill->getUUID(); limitedNodeList->killNodeWithUUID(nodeUUID); +} - static auto removedNodePacket = NLPacket::create(PacketType::DomainServerRemovedNode, NUM_BYTES_RFC4122_UUID); +void DomainServer::broadcastNodeDisconnect(const SharedNodePointer& disconnectedNode) { + auto limitedNodeList = DependencyManager::get(); + + static auto removedNodePacket = NLPacket::create(PacketType::DomainServerRemovedNode, NUM_BYTES_RFC4122_UUID, true); removedNodePacket->reset(); - removedNodePacket->write(nodeUUID.toRfc4122()); + removedNodePacket->write(disconnectedNode->getUUID().toRfc4122()); // broadcast out the DomainServerRemovedNode message - limitedNodeList->eachMatchingNode([this, &nodeToKill](const SharedNodePointer& otherNode) -> bool { + limitedNodeList->eachMatchingNode([this, &disconnectedNode](const SharedNodePointer& otherNode) -> bool { // only send the removed node packet to nodes that care about the type of node this was - return isInInterestSet(otherNode, nodeToKill); + return isInInterestSet(otherNode, disconnectedNode); }, [&limitedNodeList](const SharedNodePointer& otherNode){ - limitedNodeList->sendUnreliablePacket(*removedNodePacket, *otherNode); + auto removedNodePacketCopy = NLPacket::createCopy(*removedNodePacket); + limitedNodeList->sendPacket(std::move(removedNodePacketCopy), *otherNode); }); } diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index b118008d3d..3703877fa1 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -72,6 +72,8 @@ public: static const QString REPLACEMENT_FILE_EXTENSION; + bool isAssetServerEnabled(); + public slots: /// Called by NodeList to inform us a node has been added void nodeAdded(SharedNodePointer node); @@ -165,6 +167,7 @@ private: unsigned int countConnectedUsers(); void handleKillNode(SharedNodePointer nodeToKill); + void broadcastNodeDisconnect(const SharedNodePointer& disconnnectedNode); void sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr& senderSockAddr); @@ -219,7 +222,7 @@ private: DomainGatekeeper _gatekeeper; HTTPManager _httpManager; - HTTPSManager* _httpsManager; + std::unique_ptr _httpsManager; QHash _allAssignments; QQueue _unfulfilledAssignments; diff --git a/domain-server/src/DomainServerNodeData.h b/domain-server/src/DomainServerNodeData.h index 6b8e9a1718..f465cceb96 100644 --- a/domain-server/src/DomainServerNodeData.h +++ b/domain-server/src/DomainServerNodeData.h @@ -67,8 +67,11 @@ public: const QString& getPlaceName() { return _placeName; } void setPlaceName(const QString& placeName) { _placeName = placeName; } - bool wasAssigned() const { return _wasAssigned; }; + bool wasAssigned() const { return _wasAssigned; } void setWasAssigned(bool wasAssigned) { _wasAssigned = wasAssigned; } + + bool hasCheckedIn() const { return _hasCheckedIn; } + void setHasCheckedIn(bool hasCheckedIn) { _hasCheckedIn = hasCheckedIn; } private: QJsonObject overrideValuesIfNeeded(const QJsonObject& newStats); @@ -94,6 +97,8 @@ private: QString _placeName; bool _wasAssigned { false }; + + bool _hasCheckedIn { false }; }; #endif // hifi_DomainServerNodeData_h diff --git a/ice-server/src/IceServer.cpp b/ice-server/src/IceServer.cpp index 3cf1e1450e..6896c7a9c9 100644 --- a/ice-server/src/IceServer.cpp +++ b/ice-server/src/IceServer.cpp @@ -13,6 +13,7 @@ #include +#include #include #include #include diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index ac9441319b..4204718976 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -133,13 +133,7 @@ if (APPLE) # set where in the bundle to put the resources file set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/icon/${INTERFACE_ICON_FILENAME} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) - set(DISCOVERED_RESOURCES "") - - # use the add_resources_to_os_x_bundle macro to recurse into resources - add_resources_to_os_x_bundle("${CMAKE_CURRENT_SOURCE_DIR}/resources") - # append the discovered resources to our list of interface sources - list(APPEND INTERFACE_SRCS ${DISCOVERED_RESOURCES}) list(APPEND INTERFACE_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/icon/${INTERFACE_ICON_FILENAME}) endif() @@ -316,18 +310,27 @@ if (APPLE) set(SCRIPTS_INSTALL_DIR "${INTERFACE_INSTALL_APP_PATH}/Contents/Resources") set(RESOURCES_DEV_DIR "$/../Resources") - # copy script files beside the executable add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + # copy script files beside the executable COMMAND "${CMAKE_COMMAND}" -E copy_directory - "${CMAKE_SOURCE_DIR}/scripts" - "${RESOURCES_DEV_DIR}/scripts" - ) - - # copy JSDoc files beside the executable - add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + "${CMAKE_SOURCE_DIR}/scripts" + "${RESOURCES_DEV_DIR}/scripts" + # copy JSDoc files beside the executable COMMAND "${CMAKE_COMMAND}" -E copy_directory - "${CMAKE_SOURCE_DIR}/tools/jsdoc/out" - "${RESOURCES_DEV_DIR}/jsdoc" + "${CMAKE_SOURCE_DIR}/tools/jsdoc/out" + "${RESOURCES_DEV_DIR}/jsdoc" + # copy the resources files beside the executable + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "${RESOURCES_RCC}" + "${RESOURCES_DEV_DIR}" + # FIXME, the edit script code loads HTML from the scripts folder + # which in turn relies on CSS that refers to the fonts. In theory + # we should be able to modify the CSS to reference the QRC path to + # the ttf files, but doing so generates a CORS policy violation, + # so we have to retain a copy of the fonts outside of the resources binary + COMMAND "${CMAKE_COMMAND}" -E copy_directory + "${PROJECT_SOURCE_DIR}/resources/fonts" + "${RESOURCES_DEV_DIR}/fonts" ) # call the fixup_interface macro to add required bundling commands for installation @@ -356,13 +359,10 @@ else() COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${PROJECT_SOURCE_DIR}/resources/serverless/tutorial.json" "${RESOURCES_DEV_DIR}/serverless/tutorial.json" - ) - - # copy JSDoc files beside the executable - add_custom_command(TARGET ${TARGET_NAME} POST_BUILD + # copy JSDoc files beside the executable COMMAND "${CMAKE_COMMAND}" -E copy_directory - "${CMAKE_SOURCE_DIR}/tools/jsdoc/out" - "${INTERFACE_EXEC_DIR}/jsdoc" + "${CMAKE_SOURCE_DIR}/tools/jsdoc/out" + "${INTERFACE_EXEC_DIR}/jsdoc" ) # link target to external libraries diff --git a/interface/resources/meshes/defaultAvatar_full.fst b/interface/resources/meshes/defaultAvatar_full.fst index aa1c17fc40..b47faeb8a8 100644 --- a/interface/resources/meshes/defaultAvatar_full.fst +++ b/interface/resources/meshes/defaultAvatar_full.fst @@ -2,88 +2,88 @@ name = mannequin type = body+head scale = 1 filename = mannequin/mannequin.baked.fbx -joint = jointRoot = Hips +joint = jointNeck = Neck joint = jointLean = Spine -joint = jointLeftHand = LeftHand -joint = jointHead = Head joint = jointEyeLeft = LeftEye joint = jointEyeRight = RightEye +joint = jointRoot = Hips +joint = jointLeftHand = LeftHand joint = jointRightHand = RightHand -joint = jointNeck = Neck +joint = jointHead = Head freeJoint = LeftArm freeJoint = LeftForeArm freeJoint = RightArm freeJoint = RightForeArm -bs = EyeBlink_L = blink = 1 bs = JawOpen = mouth_Open = 1 bs = LipsFunnel = Oo = 1 bs = BrowsU_L = brow_Up = 1 -jointIndex = RightHandPinky2 = 19 -jointIndex = LeftHandMiddle4 = 61 -jointIndex = LeftHand = 41 -jointIndex = LeftHandRing4 = 49 -jointIndex = RightHandMiddle3 = 36 -jointIndex = LeftHandThumb4 = 57 -jointIndex = RightToe_End = 10 -jointIndex = LeftHandRing1 = 46 -jointIndex = LeftForeArm = 40 -jointIndex = RightHandIndex4 = 29 -jointIndex = LeftShoulder = 38 -jointIndex = RightHandMiddle4 = 37 -jointIndex = RightShoulder = 14 -jointIndex = LeftLeg = 2 -jointIndex = LeftToe_End = 5 -jointIndex = Hips = 0 -jointIndex = RightFoot = 8 -jointIndex = RightHandThumb2 = 31 -jointIndex = LeftHandMiddle3 = 60 -jointIndex = RightHandThumb1 = 30 -jointIndex = Neck = 62 -jointIndex = Spine = 11 -jointIndex = RightHandThumb4 = 33 -jointIndex = RightHandMiddle1 = 34 -jointIndex = LeftHandIndex4 = 53 -jointIndex = face = 68 -jointIndex = RightHandRing3 = 24 -jointIndex = LeftHandPinky4 = 45 -jointIndex = LeftHandMiddle2 = 59 -jointIndex = RightHandThumb3 = 32 +bs = EyeBlink_L = blink = 1 jointIndex = LeftHandPinky3 = 44 -jointIndex = HeadTop_End = 66 -jointIndex = Spine1 = 12 -jointIndex = LeftHandRing3 = 48 -jointIndex = mannequin1 = 67 -jointIndex = RightEye = 65 -jointIndex = RightHandRing4 = 25 -jointIndex = RightHandPinky4 = 21 -jointIndex = LeftHandRing2 = 47 -jointIndex = RightHandIndex3 = 28 -jointIndex = RightUpLeg = 6 -jointIndex = LeftArm = 39 -jointIndex = LeftHandThumb3 = 56 -jointIndex = RightHandIndex2 = 27 -jointIndex = RightForeArm = 16 -jointIndex = RightArm = 15 -jointIndex = RightHandRing2 = 23 -jointIndex = LeftHandMiddle1 = 58 -jointIndex = Spine2 = 13 -jointIndex = LeftHandThumb2 = 55 -jointIndex = RightHandMiddle2 = 35 -jointIndex = RightHandPinky1 = 18 -jointIndex = LeftUpLeg = 1 -jointIndex = RightLeg = 7 -jointIndex = LeftHandIndex2 = 51 +jointIndex = LeftHand = 41 +jointIndex = RightHandMiddle1 = 34 +jointIndex = LeftHandPinky4 = 45 jointIndex = RightHand = 17 -jointIndex = LeftHandIndex3 = 52 -jointIndex = LeftFoot = 3 jointIndex = RightHandPinky3 = 20 -jointIndex = RightHandIndex1 = 26 -jointIndex = LeftHandPinky1 = 42 -jointIndex = RightToeBase = 9 -jointIndex = LeftHandIndex1 = 50 -jointIndex = LeftToeBase = 4 -jointIndex = LeftHandPinky2 = 43 -jointIndex = RightHandRing1 = 22 -jointIndex = LeftHandThumb1 = 54 -jointIndex = LeftEye = 64 +jointIndex = LeftFoot = 3 jointIndex = Head = 63 +jointIndex = Spine1 = 12 +jointIndex = RightHandRing4 = 25 +jointIndex = RightHandPinky1 = 18 +jointIndex = LeftHandIndex1 = 50 +jointIndex = RightHandIndex3 = 28 +jointIndex = LeftHandIndex3 = 52 +jointIndex = LeftToe_End = 5 +jointIndex = RightArm = 15 +jointIndex = RightHandRing3 = 24 +jointIndex = RightHandThumb2 = 31 +jointIndex = Spine2 = 13 +jointIndex = HeadTop_End = 66 +jointIndex = LeftToeBase = 4 +jointIndex = RightUpLeg = 6 +jointIndex = RightForeArm = 16 +jointIndex = LeftHandMiddle1 = 58 +jointIndex = LeftHandRing3 = 48 +jointIndex = RightHandPinky4 = 21 +jointIndex = RightHandIndex1 = 26 +jointIndex = Hips = 0 +jointIndex = RightEye = 65 +jointIndex = RightHandPinky2 = 19 +jointIndex = LeftHandMiddle2 = 59 +jointIndex = LeftHandPinky1 = 42 +jointIndex = LeftHandRing4 = 49 +jointIndex = RightFoot = 8 +jointIndex = RightHandIndex2 = 27 +jointIndex = RightToe_End = 10 +jointIndex = RightHandThumb3 = 32 +jointIndex = LeftHandMiddle3 = 60 +jointIndex = LeftHandThumb4 = 57 +jointIndex = LeftHandMiddle4 = 61 +jointIndex = LeftHandThumb1 = 54 +jointIndex = LeftHandThumb3 = 56 +jointIndex = body = 67 +jointIndex = LeftArm = 39 +jointIndex = RightToeBase = 9 +jointIndex = LeftEye = 64 +jointIndex = RightLeg = 7 +jointIndex = face = 68 +jointIndex = LeftForeArm = 40 +jointIndex = RightHandThumb4 = 33 +jointIndex = RightHandRing1 = 22 +jointIndex = LeftUpLeg = 1 +jointIndex = LeftHandPinky2 = 43 +jointIndex = LeftLeg = 2 +jointIndex = LeftHandIndex4 = 53 +jointIndex = RightHandThumb1 = 30 +jointIndex = LeftHandRing2 = 47 +jointIndex = RightHandMiddle2 = 35 +jointIndex = RightHandMiddle3 = 36 +jointIndex = Spine = 11 +jointIndex = RightHandMiddle4 = 37 +jointIndex = LeftHandIndex2 = 51 +jointIndex = RightHandRing2 = 23 +jointIndex = LeftHandThumb2 = 55 +jointIndex = LeftShoulder = 38 +jointIndex = Neck = 62 +jointIndex = RightHandIndex4 = 29 +jointIndex = LeftHandRing1 = 46 +jointIndex = RightShoulder = 14 diff --git a/interface/resources/meshes/mannequin/Eyes.png b/interface/resources/meshes/mannequin/Eyes.png new file mode 100644 index 0000000000..df35dc9fee Binary files /dev/null and b/interface/resources/meshes/mannequin/Eyes.png differ diff --git a/interface/resources/meshes/mannequin/Eyes.ktx b/interface/resources/meshes/mannequin/Eyes_bcn.ktx similarity index 99% rename from interface/resources/meshes/mannequin/Eyes.ktx rename to interface/resources/meshes/mannequin/Eyes_bcn.ktx index ada45776a0..934cba3180 100644 Binary files a/interface/resources/meshes/mannequin/Eyes.ktx and b/interface/resources/meshes/mannequin/Eyes_bcn.ktx differ diff --git a/interface/resources/meshes/mannequin/StingrayPBS10_Base_Color.png b/interface/resources/meshes/mannequin/StingrayPBS10_Base_Color.png new file mode 100644 index 0000000000..941955916a Binary files /dev/null and b/interface/resources/meshes/mannequin/StingrayPBS10_Base_Color.png differ diff --git a/interface/resources/meshes/mannequin/StingrayPBS10_Base_Color_bcn.ktx b/interface/resources/meshes/mannequin/StingrayPBS10_Base_Color_bcn.ktx new file mode 100644 index 0000000000..cbb6cfa992 Binary files /dev/null and b/interface/resources/meshes/mannequin/StingrayPBS10_Base_Color_bcn.ktx differ diff --git a/interface/resources/meshes/mannequin/StingrayPBS10_Normal_OpenGL.png b/interface/resources/meshes/mannequin/StingrayPBS10_Normal_OpenGL.png new file mode 100644 index 0000000000..fc86048656 Binary files /dev/null and b/interface/resources/meshes/mannequin/StingrayPBS10_Normal_OpenGL.png differ diff --git a/interface/resources/meshes/mannequin/StingrayPBS10_Normal_OpenGL_bcn.ktx b/interface/resources/meshes/mannequin/StingrayPBS10_Normal_OpenGL_bcn.ktx new file mode 100644 index 0000000000..aad0b108b3 Binary files /dev/null and b/interface/resources/meshes/mannequin/StingrayPBS10_Normal_OpenGL_bcn.ktx differ diff --git a/interface/resources/meshes/mannequin/lambert1_Base_Color.ktx b/interface/resources/meshes/mannequin/lambert1_Base_Color.ktx deleted file mode 100644 index f151352592..0000000000 Binary files a/interface/resources/meshes/mannequin/lambert1_Base_Color.ktx and /dev/null differ diff --git a/interface/resources/meshes/mannequin/lambert1_Normal_OpenGL.ktx b/interface/resources/meshes/mannequin/lambert1_Normal_OpenGL.ktx deleted file mode 100644 index fb738063ae..0000000000 Binary files a/interface/resources/meshes/mannequin/lambert1_Normal_OpenGL.ktx and /dev/null differ diff --git a/interface/resources/meshes/mannequin/lambert1_Roughness.png b/interface/resources/meshes/mannequin/lambert1_Roughness.png new file mode 100644 index 0000000000..9efa372541 Binary files /dev/null and b/interface/resources/meshes/mannequin/lambert1_Roughness.png differ diff --git a/interface/resources/meshes/mannequin/lambert1_Roughness.ktx b/interface/resources/meshes/mannequin/lambert1_Roughness_bcn.ktx similarity index 99% rename from interface/resources/meshes/mannequin/lambert1_Roughness.ktx rename to interface/resources/meshes/mannequin/lambert1_Roughness_bcn.ktx index afe96d1631..628e6ce99b 100644 Binary files a/interface/resources/meshes/mannequin/lambert1_Roughness.ktx and b/interface/resources/meshes/mannequin/lambert1_Roughness_bcn.ktx differ diff --git a/interface/resources/meshes/mannequin/mannequin.baked.fbx b/interface/resources/meshes/mannequin/mannequin.baked.fbx index 2b0e1cf04b..63611b0b6c 100644 Binary files a/interface/resources/meshes/mannequin/mannequin.baked.fbx and b/interface/resources/meshes/mannequin/mannequin.baked.fbx differ diff --git a/interface/resources/qml/controls-uit/TextField.qml b/interface/resources/qml/controls-uit/TextField.qml index f94541897b..917068ac01 100644 --- a/interface/resources/qml/controls-uit/TextField.qml +++ b/interface/resources/qml/controls-uit/TextField.qml @@ -163,10 +163,18 @@ TextField { text: textField.label colorScheme: textField.colorScheme anchors.left: parent.left - anchors.right: parent.right + + Binding on anchors.right { + when: textField.right + value: textField.right + } + Binding on wrapMode { + when: textField.right + value: Text.WordWrap + } + anchors.bottom: parent.top anchors.bottomMargin: 3 - wrapMode: Text.WordWrap visible: label != "" } } diff --git a/interface/resources/qml/dialogs/+android/FileDialog.qml b/interface/resources/qml/dialogs/+android/FileDialog.qml index 548ab453a7..be6524d2b8 100644 --- a/interface/resources/qml/dialogs/+android/FileDialog.qml +++ b/interface/resources/qml/dialogs/+android/FileDialog.qml @@ -57,7 +57,7 @@ ModalWindow { property int iconSize: 40 property bool selectDirectory: false; - property bool showHidden: false; + property bool showHidden: true; // FIXME implement property bool multiSelect: false; property bool saveDialog: false; @@ -324,11 +324,14 @@ ModalWindow { showDirsFirst: true showDotAndDotDot: false showFiles: !root.selectDirectory + showHidden: root.showHidden Component.onCompleted: { showFiles = !root.selectDirectory + showHidden = root.showHidden } onFolderChanged: { + d.clearSelection(); fileTableModel.update(); // Update once the data from the folder change is available. } @@ -448,7 +451,7 @@ ModalWindow { rows = 0, i; - var newFilesModel = filesModelBuilder.createObject(root); + filesModel = filesModelBuilder.createObject(root); comparisonFunction = sortOrder === Qt.AscendingOrder ? function(a, b) { return a < b; } @@ -470,7 +473,7 @@ ModalWindow { while (lower < upper) { middle = Math.floor((lower + upper) / 2); var lessThan; - if (comparisonFunction(sortValue, newFilesModel.get(middle)[sortField])) { + if (comparisonFunction(sortValue, filesModel.get(middle)[sortField])) { lessThan = true; upper = middle; } else { @@ -479,7 +482,7 @@ ModalWindow { } } - newFilesModel.insert(lower, { + filesModel.insert(lower, { fileName: fileName, fileModified: (fileIsDir ? new Date(0) : model.getItem(i, "fileModified")), fileSize: model.getItem(i, "fileSize"), @@ -490,9 +493,6 @@ ModalWindow { rows++; } - filesModel = newFilesModel; - - d.clearSelection(); } } diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 154d66378b..6651af0db3 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -58,7 +58,7 @@ ModalWindow { property int iconSize: 40 property bool selectDirectory: false; - property bool showHidden: false; + property bool showHidden: true; // FIXME implement property bool multiSelect: false; property bool saveDialog: false; @@ -325,11 +325,14 @@ ModalWindow { showDirsFirst: true showDotAndDotDot: false showFiles: !root.selectDirectory + showHidden: root.showHidden Component.onCompleted: { showFiles = !root.selectDirectory + showHidden = root.showHidden } onFolderChanged: { + d.clearSelection(); fileTableModel.update(); // Update once the data from the folder change is available. } @@ -449,7 +452,7 @@ ModalWindow { rows = 0, i; - var newFilesModel = filesModelBuilder.createObject(root); + filesModel = filesModelBuilder.createObject(root); comparisonFunction = sortOrder === Qt.AscendingOrder ? function(a, b) { return a < b; } @@ -471,7 +474,7 @@ ModalWindow { while (lower < upper) { middle = Math.floor((lower + upper) / 2); var lessThan; - if (comparisonFunction(sortValue, newFilesModel.get(middle)[sortField])) { + if (comparisonFunction(sortValue, filesModel.get(middle)[sortField])) { lessThan = true; upper = middle; } else { @@ -480,7 +483,7 @@ ModalWindow { } } - newFilesModel.insert(lower, { + filesModel.insert(lower, { fileName: fileName, fileModified: (fileIsDir ? new Date(0) : model.getItem(i, "fileModified")), fileSize: model.getItem(i, "fileSize"), @@ -491,9 +494,6 @@ ModalWindow { rows++; } - filesModel = newFilesModel; - - d.clearSelection(); } } diff --git a/interface/resources/qml/dialogs/TabletFileDialog.qml b/interface/resources/qml/dialogs/TabletFileDialog.qml index db15337913..4de0460796 100644 --- a/interface/resources/qml/dialogs/TabletFileDialog.qml +++ b/interface/resources/qml/dialogs/TabletFileDialog.qml @@ -55,7 +55,7 @@ TabletModalWindow { property int iconSize: 40 property bool selectDirectory: false; - property bool showHidden: false; + property bool showHidden: true; // FIXME implement property bool multiSelect: false; property bool saveDialog: false; @@ -288,12 +288,15 @@ TabletModalWindow { showDirsFirst: true showDotAndDotDot: false showFiles: !root.selectDirectory + showHidden: root.showHidden Component.onCompleted: { showFiles = !root.selectDirectory + showHidden = root.showHidden } onFolderChanged: { - fileTableModel.update() + d.clearSelection(); + fileTableModel.update(); } function getItem(index, field) { @@ -411,7 +414,7 @@ TabletModalWindow { rows = 0, i; - var newFilesModel = filesModelBuilder.createObject(root); + filesModel = filesModelBuilder.createObject(root); comparisonFunction = sortOrder === Qt.AscendingOrder ? function(a, b) { return a < b; } @@ -433,7 +436,7 @@ TabletModalWindow { while (lower < upper) { middle = Math.floor((lower + upper) / 2); var lessThan; - if (comparisonFunction(sortValue, newFilesModel.get(middle)[sortField])) { + if (comparisonFunction(sortValue, filesModel.get(middle)[sortField])) { lessThan = true; upper = middle; } else { @@ -442,7 +445,7 @@ TabletModalWindow { } } - newFilesModel.insert(lower, { + filesModel.insert(lower, { fileName: fileName, fileModified: (fileIsDir ? new Date(0) : model.getItem(i, "fileModified")), fileSize: model.getItem(i, "fileSize"), @@ -453,9 +456,6 @@ TabletModalWindow { rows++; } - filesModel = newFilesModel; - - d.clearSelection(); } } diff --git a/interface/resources/qml/hifi/Feed.qml b/interface/resources/qml/hifi/Feed.qml index 3f3a47a297..4cd52a582d 100644 --- a/interface/resources/qml/hifi/Feed.qml +++ b/interface/resources/qml/hifi/Feed.qml @@ -16,10 +16,11 @@ import QtQuick 2.5 import QtGraphicalEffects 1.0 import "toolbars" import "../styles-uit" +import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. Column { id: root; - visible: false; + visible: !!suggestions.count; property int cardWidth: 212; property int cardHeight: 152; @@ -32,21 +33,37 @@ Column { property int stackedCardShadowHeight: 4; property int labelSize: 20; - property string metaverseServerUrl: ''; property string protocol: ''; property string actions: 'snapshot'; // sendToScript doesn't get wired until after everything gets created. So we have to queue fillDestinations on nextTick. property string labelText: actions; property string filter: ''; - onFilterChanged: filterChoicesByText(); property var goFunction: null; - property var rpc: null; + property var http: null; HifiConstants { id: hifi } - ListModel { id: suggestions; } + Component.onCompleted: suggestions.getFirstPage(); + HifiModels.PSFListModel { + id: suggestions; + http: root.http; + property var options: [ + 'include_actions=' + actions, + 'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'), + 'require_online=true', + 'protocol=' + encodeURIComponent(Window.protocolSignature()) + ]; + endpoint: '/api/v1/user_stories?' + options.join('&'); + itemsPerPage: 3; + processPage: function (data) { + return data.user_stories.map(makeModelData); + }; + listModelName: actions; + listView: scroll; + searchFilter: filter; + } function resolveUrl(url) { - return (url.indexOf('/') === 0) ? (metaverseServerUrl + url) : url; + return (url.indexOf('/') === 0) ? (Account.metaverseServerURL + url) : url; } function makeModelData(data) { // create a new obj from data // ListModel elements will only ever have those properties that are defined by the first obj that is added. @@ -55,16 +72,11 @@ Column { tags = data.tags || [data.action, data.username], description = data.description || "", thumbnail_url = data.thumbnail_url || ""; - if (actions === 'concurrency,snapshot') { - // A temporary hack for simulating announcements. We won't use this in production, but if requested, we'll use this data like announcements. - data.details.connections = 4; - data.action = 'announcement'; - } return { place_name: name, username: data.username || "", path: data.path || "", - created_at: data.created_at || "", + created_at: data.created_at || data.updated_at || "", // FIXME why aren't we getting created_at? action: data.action || "", thumbnail_url: resolveUrl(thumbnail_url), image_url: resolveUrl(data.details && data.details.image_url), @@ -74,125 +86,11 @@ Column { tags: tags, description: description, online_users: data.details.connections || data.details.concurrency || 0, - drillDownToPlace: false, - - searchText: [name].concat(tags, description || []).join(' ').toUpperCase() - } - } - property var allStories: []; - property var placeMap: ({}); // Used for making stacks. - property int requestId: 0; - function handleError(url, error, data, cb) { // cb(error) and answer truthy if needed, else falsey - if (!error && (data.status === 'success')) { - return; - } - if (!error) { // Create a message from the data - error = data.status + ': ' + data.error; - } - if (typeof(error) === 'string') { // Make a proper Error object - error = new Error(error); - } - error.message += ' in ' + url; // Include the url. - cb(error); - return true; - } - function getUserStoryPage(pageNumber, cb, cb1) { // cb(error) after all pages of domain data have been added to model - // If supplied, cb1 will be run after the first page IFF it is not the last, for responsiveness. - var options = [ - 'now=' + new Date().toISOString(), - 'include_actions=' + actions, - 'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'), - 'require_online=true', - 'protocol=' + protocol, - 'page=' + pageNumber - ]; - var url = metaverseBase + 'user_stories?' + options.join('&'); - var thisRequestId = ++requestId; - rpc('request', url, function (error, data) { - if (thisRequestId !== requestId) { - error = 'stale'; - } - if (handleError(url, error, data, cb)) { - return; // abandon stale requests - } - allStories = allStories.concat(data.user_stories.map(makeModelData)); - if ((data.current_page < data.total_pages) && (data.current_page <= 10)) { // just 10 pages = 100 stories for now - if ((pageNumber === 1) && cb1) { - cb1(); - } - return getUserStoryPage(pageNumber + 1, cb); - } - cb(); - }); - } - function fillDestinations() { // Public - console.debug('Feed::fillDestinations()') - - function report(label, error) { - console.log(label, actions, error || 'ok', allStories.length, 'filtered to', suggestions.count); - } - var filter = makeFilteredStoryProcessor(), counter = 0; - allStories = []; - suggestions.clear(); - placeMap = {}; - getUserStoryPage(1, function (error) { - allStories.slice(counter).forEach(filter); - report('user stories update', error); - root.visible = !!suggestions.count; - }, function () { // If there's more than a page, put what we have in the model right away, keeping track of how many are processed. - allStories.forEach(function (story) { - counter++; - filter(story); - root.visible = !!suggestions.count; - }); - report('user stories'); - }); - } - function identity(x) { - return x; - } - function makeFilteredStoryProcessor() { // answer a function(storyData) that adds it to suggestions if it matches - var words = filter.toUpperCase().split(/\s+/).filter(identity); - function suggestable(story) { - // We could filter out places we don't want to suggest, such as those where (story.place_name === AddressManager.placename) or (story.username === Account.username). - return true; - } - function matches(story) { - if (!words.length) { - return suggestable(story); - } - return words.every(function (word) { - return story.searchText.indexOf(word) >= 0; - }); - } - function addToSuggestions(place) { - var collapse = ((actions === 'concurrency,snapshot') && (place.action !== 'concurrency')) || (place.action === 'announcement'); - if (collapse) { - var existing = placeMap[place.place_name]; - if (existing) { - existing.drillDownToPlace = true; - return; - } - } - suggestions.append(place); - if (collapse) { - placeMap[place.place_name] = suggestions.get(suggestions.count - 1); - } else if (place.action === 'concurrency') { - suggestions.get(suggestions.count - 1).drillDownToPlace = true; // Don't change raw place object (in allStories). - } - } - return function (story) { - if (matches(story)) { - addToSuggestions(story); - } + // Server currently doesn't give isStacked (undefined). Could give bool. + drillDownToPlace: (data.isStacked === undefined) ? (data.action !== 'concurrency') : data.isStacked, + isStacked: !!data.isStacked }; } - function filterChoicesByText() { - suggestions.clear(); - placeMap = {}; - allStories.forEach(makeFilteredStoryProcessor()); - root.visible = !!suggestions.count; - } RalewayBold { id: label; @@ -208,6 +106,7 @@ Column { highlightMoveDuration: -1; highlightMoveVelocity: -1; currentIndex: -1; + onAtXEndChanged: { if (scroll.atXEnd && !scroll.atXBeginning) { suggestions.getNextPage(); } } spacing: 12; width: parent.width; @@ -227,6 +126,7 @@ Column { onlineUsers: model.online_users; storyId: model.metaverseId; drillDownToPlace: model.drillDownToPlace; + isStacked: model.isStacked; textPadding: root.textPadding; smallMargin: root.smallMargin; @@ -239,21 +139,4 @@ Column { unhoverThunk: function () { hovered = false } } } - NumberAnimation { - id: anim; - target: scroll; - property: "contentX"; - duration: 250; - } - function scrollToIndex(index) { - anim.running = false; - var pos = scroll.contentX; - var destPos; - scroll.positionViewAtIndex(index, ListView.Contain); - destPos = scroll.contentX; - anim.from = pos; - anim.to = destPos; - scroll.currentIndex = index; - anim.running = true; - } } diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index c97a802f10..c7c2174e9f 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -319,10 +319,10 @@ Item { visible: thisNameCard.userName !== ""; // Size width: parent.width - height: usernameTextPixelSize + 4 + height: paintedHeight // Anchors - anchors.top: isMyCard ? myDisplayName.bottom : pal.activeTab == "nearbyTab" ? displayNameContainer.bottom : undefined //(parent.height - displayNameTextPixelSize/2)); - anchors.verticalCenter: pal.activeTab == "connectionsTab" && !isMyCard ? avatarImage.verticalCenter : undefined + anchors.top: isMyCard ? myDisplayName.bottom : pal.activeTab == "nearbyTab" ? displayNameContainer.bottom : avatarImage.top //(parent.height - displayNameTextPixelSize/2)); + anchors.bottom: pal.activeTab === "connectionsTab" && !isMyCard ? avatarImage.bottom : undefined anchors.left: avatarImage.right; anchors.leftMargin: avatarImage.visible ? 5 : 0; anchors.rightMargin: 5; diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index d779b4ba42..8dcb76442b 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -18,6 +18,7 @@ import Qt.labs.settings 1.0 import "../styles-uit" import "../controls-uit" as HifiControlsUit import "../controls" as HifiControls +import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. // references HMD, Users, UserActivityLogger from root context @@ -37,13 +38,42 @@ Rectangle { property var myData: ({profileUrl: "", displayName: "", userName: "", audioLevel: 0.0, avgAudioLevel: 0.0, admin: true, placeName: "", connection: "", isPresent: true}); // valid dummy until set property var ignored: ({}); // Keep a local list of ignored avatars & their data. Necessary because HashMap is slow to respond after ignoring. property var nearbyUserModelData: []; // This simple list is essentially a mirror of the nearbyUserModel listModel without all the extra complexities. - property var connectionsUserModelData: []; // This simple list is essentially a mirror of the connectionsUserModel listModel without all the extra complexities. property bool iAmAdmin: false; property var activeTab: "nearbyTab"; property bool currentlyEditingDisplayName: false property bool punctuationMode: false; HifiConstants { id: hifi; } + RootHttpRequest { id: http; } + HifiModels.PSFListModel { + id: connectionsUserModel; + http: http; + endpoint: "/api/v1/users?filter=connections"; + property var sortColumn: connectionsTable.getColumn(connectionsTable.sortIndicatorColumn); + sortProperty: switch (sortColumn && sortColumn.role) { + case 'placeName': + 'location'; + break; + case 'connection': + 'is_friend'; + break; + default: + 'username'; + } + sortAscending: connectionsTable.sortIndicatorOrder === Qt.AscendingOrder; + itemsPerPage: 9; + listView: connectionsTable; + processPage: function (data) { + return data.users.map(function (user) { + return { + userName: user.username, + connection: user.connection, + profileUrl: user.images.thumbnail, + placeName: (user.location.root || user.location.domain || {}).name || '' + }; + }); + }; + } // The letterbox used for popup messages LetterboxMessage { @@ -106,16 +136,6 @@ Rectangle { }); return sessionIDs; } - function getSelectedConnectionsUserNames() { - var userNames = []; - connectionsTable.selection.forEach(function (userIndex) { - var datum = connectionsUserModelData[userIndex]; - if (datum) { - userNames.push(datum.userName); - } - }); - return userNames; - } function refreshNearbyWithFilter() { // We should just be able to set settings.filtered to inViewCheckbox.checked, but see #3249, so send to .js for saving. var userIds = getSelectedNearbySessionIDs(); @@ -232,9 +252,7 @@ Rectangle { anchors.fill: parent; onClicked: { if (activeTab != "connectionsTab") { - connectionsLoading.visible = false; - connectionsLoading.visible = true; - pal.sendToScript({method: 'refreshConnections'}); + connectionsUserModel.getFirstPage(); } activeTab = "connectionsTab"; connectionsHelpText.color = hifi.colors.blueAccent; @@ -258,11 +276,7 @@ Rectangle { id: reloadConnections; width: reloadConnections.height; glyph: hifi.glyphs.reload; - onClicked: { - connectionsLoading.visible = false; - connectionsLoading.visible = true; - pal.sendToScript({method: 'refreshConnections'}); - } + onClicked: connectionsUserModel.getFirstPage('delayRefresh'); } } // "CONNECTIONS" text @@ -472,7 +486,7 @@ Rectangle { visible: !isCheckBox && !isButton && !isAvgAudio; uuid: model ? model.sessionId : ""; selected: styleData.selected; - isReplicated: model.isReplicated; + isReplicated: model && model.isReplicated; isAdmin: model && model.admin; isPresent: model && model.isPresent; // Size @@ -702,7 +716,7 @@ Rectangle { anchors.top: parent.top; anchors.topMargin: 185; anchors.horizontalCenter: parent.horizontalCenter; - visible: true; + visible: !connectionsUserModel.retrievedAtLeastOnePage; onVisibleChanged: { if (visible) { connectionsTimeoutTimer.start(); @@ -747,14 +761,6 @@ Rectangle { headerVisible: true; sortIndicatorColumn: settings.connectionsSortIndicatorColumn; sortIndicatorOrder: settings.connectionsSortIndicatorOrder; - onSortIndicatorColumnChanged: { - settings.connectionsSortIndicatorColumn = sortIndicatorColumn; - sortConnectionsModel(); - } - onSortIndicatorOrderChanged: { - settings.connectionsSortIndicatorOrder = sortIndicatorOrder; - sortConnectionsModel(); - } TableViewColumn { id: connectionsUserNameHeader; @@ -779,8 +785,14 @@ Rectangle { resizable: false; } - model: ListModel { - id: connectionsUserModel; + model: connectionsUserModel; + Connections { + target: connectionsTable.flickableItem; + onAtYEndChanged: { + if (connectionsTable.flickableItem.atYEnd && !connectionsTable.flickableItem.atYBeginning) { + connectionsUserModel.getNextPage(); + } + } } // This Rectangle refers to each Row in the connectionsTable. @@ -859,12 +871,9 @@ Rectangle { checked: model && (model.connection === "friend"); boxSize: 24; onClicked: { - var newValue = model.connection !== "friend"; - connectionsUserModel.setProperty(model.userIndex, styleData.role, (newValue ? "friend" : "connection")); - connectionsUserModelData[model.userIndex][styleData.role] = newValue; // Defensive programming - pal.sendToScript({method: newValue ? 'addFriend' : 'removeFriend', params: model.userName}); + pal.sendToScript({method: checked ? 'addFriend' : 'removeFriend', params: model.userName}); - UserActivityLogger["palAction"](newValue ? styleData.role : "un-" + styleData.role, model.sessionId); + UserActivityLogger["palAction"](checked ? styleData.role : "un-" + styleData.role, model.sessionId); } } } @@ -1130,16 +1139,6 @@ Rectangle { sortModel(); reloadNearby.color = 0; break; - case 'connections': - var data = message.params; - if (pal.debug) { - console.log('Got connection data: ', JSON.stringify(data)); - } - connectionsUserModelData = data; - sortConnectionsModel(); - connectionsLoading.visible = false; - connectionsRefreshProblemText.visible = false; - break; case 'select': var sessionIds = message.params[0]; var selected = message.params[1]; @@ -1239,6 +1238,14 @@ Rectangle { reloadNearby.color = 2; } break; + case 'inspectionCertificate_resetCert': + // marketplaces.js sends out a signal to QML with that method when the tablet screen changes and it's not changed to a commerce-related screen. + // We want it to only be handled by the InspectionCertificate.qml, but there's not an easy way of doing that. + // As a part of a "cleanup inspectionCertificate_resetCert" ticket, we'll have to figure out less logspammy way of doing what has to be done. + break; + case 'http.response': + http.handleHttpResponse(message); + break; default: console.log('Unrecognized message:', JSON.stringify(message)); } @@ -1287,45 +1294,6 @@ Rectangle { nearbyTable.positionViewAtRow(newSelectedIndexes[0], ListView.Beginning); } } - function sortConnectionsModel() { - var column = connectionsTable.getColumn(connectionsTable.sortIndicatorColumn); - var sortProperty = column ? column.role : "userName"; - var before = (connectionsTable.sortIndicatorOrder === Qt.AscendingOrder) ? -1 : 1; - var after = -1 * before; - // get selection(s) before sorting - var selectedIDs = getSelectedConnectionsUserNames(); - connectionsUserModelData.sort(function (a, b) { - var aValue = a[sortProperty].toString().toLowerCase(), bValue = b[sortProperty].toString().toLowerCase(); - if (!aValue && !bValue) { - return 0; - } else if (!aValue) { - return after; - } else if (!bValue) { - return before; - } - switch (true) { - case (aValue < bValue): return before; - case (aValue > bValue): return after; - default: return 0; - } - }); - connectionsTable.selection.clear(); - - connectionsUserModel.clear(); - var userIndex = 0; - var newSelectedIndexes = []; - connectionsUserModelData.forEach(function (datum) { - datum.userIndex = userIndex++; - connectionsUserModel.append(datum); - if (selectedIDs.indexOf(datum.sessionId) != -1) { - newSelectedIndexes.push(datum.userIndex); - } - }); - if (newSelectedIndexes.length > 0) { - connectionsTable.selection.select(newSelectedIndexes); - connectionsTable.positionViewAtRow(newSelectedIndexes[0], ListView.Beginning); - } - } signal sendToScript(var message); function noticeSelection() { var userIds = []; diff --git a/interface/resources/qml/hifi/RootHttpRequest.qml b/interface/resources/qml/hifi/RootHttpRequest.qml new file mode 100644 index 0000000000..0355626996 --- /dev/null +++ b/interface/resources/qml/hifi/RootHttpRequest.qml @@ -0,0 +1,39 @@ +// +// RootHttpRequest.qml +// qml/hifi +// +// Create an item of this in the ROOT qml to be able to make http requests. +// Used by PSFListModel.qml +// +// Created by Howard Stearns on 5/29/2018 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 + +Item { + property var httpCalls: ({}); + property var httpCounter: 0; + // Public function for initiating an http request. + // REQUIRES parent to be root to have sendToScript! + function request(options, callback) { + console.debug('HttpRequest', JSON.stringify(options)); + httpCalls[httpCounter] = callback; + var message = {method: 'http.request', params: options, id: httpCounter++, jsonrpc: "2.0"}; + parent.sendToScript(message); + } + // REQUIRES that parent/root handle http.response message.method in fromScript, by calling this function. + function handleHttpResponse(message) { + var callback = httpCalls[message.id]; // FIXME: as different top level tablet apps gets loaded, the id repeats. We should drop old app callbacks without warning. + if (!callback) { + console.warn('No callback for', JSON.stringify(message)); + return; + } + delete httpCalls[message.id]; + console.debug('HttpRequest response', JSON.stringify(message)); + callback(message.error, message.response); + } +} diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index f25282c738..16c1b55930 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -42,7 +42,7 @@ Rectangle { property bool alreadyOwned: false; property int itemPrice: -1; property bool isCertified; - property string itemType; + property string itemType: "unknown"; property var itemTypesArray: ["entity", "wearable", "contentSet", "app", "avatar", "unknown"]; property var itemTypesText: ["entity", "wearable", "content set", "app", "avatar", "item"]; property var buttonTextNormal: ["REZ", "WEAR", "REPLACE CONTENT SET", "INSTALL", "WEAR", "REZ"]; @@ -98,9 +98,6 @@ Rectangle { } else { root.certificateId = result.data.certificate_id; root.itemHref = result.data.download_url; - if (result.data.categories.indexOf("Wearables") > -1) { - root.itemType = "wearable"; - } root.activeView = "checkoutSuccess"; UserActivityLogger.commercePurchaseSuccess(root.itemId, root.itemAuthor, root.itemPrice, !root.alreadyOwned); } @@ -170,9 +167,6 @@ Rectangle { root.activeView = "checkoutFailure"; } else { root.itemHref = result.data.download_url; - if (result.data.categories.indexOf("Wearables") > -1) { - root.itemType = "wearable"; - } root.activeView = "checkoutSuccess"; } } @@ -186,20 +180,6 @@ Rectangle { itemPreviewImage.source = "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/" + itemId + "/thumbnail/hifi-mp-" + itemId + ".jpg"; } - onItemHrefChanged: { - if (root.itemHref.indexOf(".fst") > -1) { - root.itemType = "avatar"; - } else if (root.itemHref.indexOf('.json.gz') > -1 || root.itemHref.indexOf('.content.zip') > -1) { - root.itemType = "contentSet"; - } else if (root.itemHref.indexOf('.app.json') > -1) { - root.itemType = "app"; - } else if (root.itemHref.indexOf('.json') > -1) { - root.itemType = "entity"; // "wearable" type handled later - } else { - root.itemType = "unknown"; - } - } - onItemTypeChanged: { if (root.itemType === "entity" || root.itemType === "wearable" || root.itemType === "contentSet" || root.itemType === "avatar" || root.itemType === "app") { @@ -1102,6 +1082,7 @@ Rectangle { root.referrer = message.params.referrer; root.itemAuthor = message.params.itemAuthor; root.itemEdition = message.params.itemEdition || -1; + root.itemType = message.params.itemType || "unknown"; refreshBuyUI(); break; default: diff --git a/interface/resources/qml/hifi/commerce/common/sendAsset/ConnectionItem.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/ConnectionItem.qml index 66a9f9a822..41eacd68d5 100644 --- a/interface/resources/qml/hifi/commerce/common/sendAsset/ConnectionItem.qml +++ b/interface/resources/qml/hifi/commerce/common/sendAsset/ConnectionItem.qml @@ -44,7 +44,7 @@ Item { Item { id: avatarImage; - visible: profileUrl !== "" && userName !== ""; + visible: profilePicUrl !== "" && userName !== ""; // Size anchors.verticalCenter: parent.verticalCenter; anchors.left: parent.left; diff --git a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml index c7c72e5f7c..3e4bae4780 100644 --- a/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml +++ b/interface/resources/qml/hifi/commerce/common/sendAsset/SendAsset.qml @@ -19,6 +19,7 @@ import "../../../../styles-uit" import "../../../../controls-uit" as HifiControlsUit import "../../../../controls" as HifiControls import "../" as HifiCommerceCommon +import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. Item { HifiConstants { id: hifi; } @@ -36,6 +37,8 @@ Item { property string assetName: ""; property string assetCertID: ""; property string sendingPubliclyEffectImage; + property var http; + property var listModelName; // This object is always used in a popup or full-screen Wallet section. // This MouseArea is used to prevent a user from being @@ -118,9 +121,7 @@ Item { if (root.currentActiveView === 'chooseRecipientConnection') { // Refresh connections model - connectionsLoading.visible = false; - connectionsLoading.visible = true; - sendSignalToParent({method: 'refreshConnections'}); + connectionsModel.getFirstPage(); } else if (root.currentActiveView === 'sendAssetHome') { Commerce.balance(); } else if (root.currentActiveView === 'chooseRecipientNearby') { @@ -392,11 +393,17 @@ Item { hoverEnabled: true; } - ListModel { + HifiModels.PSFListModel { id: connectionsModel; - } - ListModel { - id: filteredConnectionsModel; + http: root.http; + listModelName: root.listModelName; + endpoint: "/api/v1/users?filter=connections"; + itemsPerPage: 8; + listView: connectionsList; + processPage: function (data) { + return data.users; + }; + searchFilter: filterBar.text; } Rectangle { @@ -472,10 +479,6 @@ Item { anchors.fill: parent; centerPlaceholderGlyph: hifi.glyphs.search; - onTextChanged: { - buildFilteredConnectionsModel(); - } - onAccepted: { focus = false; } @@ -495,6 +498,7 @@ Item { AnimatedImage { id: connectionsLoading; + visible: !connectionsModel.retrievedAtLeastOnePage; source: "../../../../../icons/profilePicLoading.gif" width: 120; height: width; @@ -515,14 +519,15 @@ Item { } visible: !connectionsLoading.visible; clip: true; - model: filteredConnectionsModel; + model: connectionsModel; + onAtYEndChanged: if (connectionsList.atYEnd && !connectionsList.atYBeginning) { connectionsModel.getNextPage(); } snapMode: ListView.SnapToItem; // Anchors anchors.fill: parent; delegate: ConnectionItem { isSelected: connectionsList.currentIndex === index; - userName: model.userName; - profilePicUrl: model.profileUrl; + userName: model.username; + profilePicUrl: model.images.thumbnail; anchors.topMargin: 6; anchors.bottomMargin: 6; @@ -553,7 +558,7 @@ Item { // "Make a Connection" instructions Rectangle { id: connectionInstructions; - visible: connectionsModel.count === 0 && !connectionsLoading.visible; + visible: connectionsModel.count === 0 && !connectionsModel.searchFilter && !connectionsLoading.visible; anchors.fill: parent; color: "white"; @@ -1806,22 +1811,6 @@ Item { // FUNCTION DEFINITIONS START // - function updateConnections(connections) { - connectionsModel.clear(); - connectionsModel.append(connections); - buildFilteredConnectionsModel(); - connectionsLoading.visible = false; - } - - function buildFilteredConnectionsModel() { - filteredConnectionsModel.clear(); - for (var i = 0; i < connectionsModel.count; i++) { - if (connectionsModel.get(i).userName.toLowerCase().indexOf(filterBar.text.toLowerCase()) !== -1) { - filteredConnectionsModel.append(connectionsModel.get(i)); - } - } - } - function resetSendAssetData() { amountTextField.focus = false; optionalMessage.focus = false; diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index 19b57354dc..b43372da5c 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -163,7 +163,6 @@ Item { Rectangle { id: contextCard; - z: 2; anchors.left: parent.left; anchors.leftMargin: 30; anchors.top: parent.top; @@ -337,7 +336,6 @@ Item { Rectangle { id: permissionExplanationCard; - z: 1; anchors.left: parent.left; anchors.leftMargin: 30; anchors.top: parent.top; @@ -596,8 +594,8 @@ Item { anchors.fill: parent; hoverEnabled: enabled; onClicked: { - contextCard.z = 1; - permissionExplanationCard.z = 0; + contextCard.visible = true; + permissionExplanationCard.visible = false; root.sendToPurchases({ method: 'flipCard' }); } onEntered: { @@ -779,8 +777,8 @@ Item { noPermissionGlyph.color = hifi.colors.redAccent; } onClicked: { - contextCard.z = 0; - permissionExplanationCard.z = 1; + contextCard.visible = false; + permissionExplanationCard.visible = true; root.sendToPurchases({ method: 'flipCard' }); } } diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 8fe1ebe6c9..0d2acf4ec3 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -16,10 +16,12 @@ import QtQuick 2.5 import "../../../styles-uit" import "../../../controls-uit" as HifiControlsUit import "../../../controls" as HifiControls +import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. import "../wallet" as HifiWallet import "../common" as HifiCommerceCommon import "../inspectionCertificate" as HifiInspectionCertificate import "../common/sendAsset" as HifiSendAsset +import "../.." as HifiCommon // references XXX from root context @@ -34,12 +36,17 @@ Rectangle { property bool punctuationMode: false; property bool isShowingMyItems: false; property bool isDebuggingFirstUseTutorial: false; - property int pendingItemCount: 0; property string installedApps; property bool keyboardRaised: false; property int numUpdatesAvailable: 0; // Style color: hifi.colors.white; + function getPurchases() { + root.activeView = "purchasesMain"; + root.installedApps = Commerce.getInstalledApps(); + purchasesModel.getFirstPage(); + Commerce.getAvailableUpdates(); + } Connections { target: Commerce; @@ -62,10 +69,7 @@ Rectangle { if ((Settings.getValue("isFirstUseOfPurchases", true) || root.isDebuggingFirstUseTutorial) && root.activeView !== "firstUseTutorial") { root.activeView = "firstUseTutorial"; } else if (!Settings.getValue("isFirstUseOfPurchases", true) && root.activeView === "initialize") { - root.activeView = "purchasesMain"; - root.installedApps = Commerce.getInstalledApps(); - Commerce.inventory(); - Commerce.getAvailableUpdates(); + getPurchases(); } } else { console.log("ERROR in Purchases.qml: Unknown wallet status: " + walletStatus); @@ -81,39 +85,7 @@ Rectangle { } onInventoryResult: { - purchasesReceived = true; - - if (result.status !== 'success') { - console.log("Failed to get purchases", result.message); - } else if (!purchasesContentsList.dragging) { // Don't modify the view if the user's scrolling - var inventoryResult = processInventoryResult(result.data.assets); - - var currentIndex = purchasesContentsList.currentIndex === -1 ? 0 : purchasesContentsList.currentIndex; - purchasesModel.clear(); - purchasesModel.append(inventoryResult); - - root.pendingItemCount = 0; - for (var i = 0; i < purchasesModel.count; i++) { - if (purchasesModel.get(i).status === "pending") { - root.pendingItemCount++; - } - } - - if (previousPurchasesModel.count !== 0) { - checkIfAnyItemStatusChanged(); - } else { - // Fill statusChanged default value - // Not doing this results in the default being true... - for (var i = 0; i < purchasesModel.count; i++) { - purchasesModel.setProperty(i, "statusChanged", false); - } - } - previousPurchasesModel.append(inventoryResult); - - buildFilteredPurchasesModel(); - - purchasesContentsList.positionViewAtIndex(currentIndex, ListView.Beginning); - } + purchasesModel.handlePage(result.status !== "success" && result.message, result); } onAvailableUpdatesResult: { @@ -134,6 +106,10 @@ Rectangle { } } + onIsShowingMyItemsChanged: { + getPurchases(); + } + Timer { id: notSetUpTimer; interval: 200; @@ -172,8 +148,14 @@ Rectangle { } } + HifiCommon.RootHttpRequest { + id: http; + } + HifiSendAsset.SendAsset { id: sendAsset; + http: http; + listModelName: "Gift Connections"; z: 998; visible: root.activeView === "giftAsset"; anchors.fill: parent; @@ -183,9 +165,7 @@ Rectangle { Connections { onSendSignalToParent: { if (msg.method === 'sendAssetHome_back' || msg.method === 'closeSendAsset') { - root.activeView = "purchasesMain"; - Commerce.inventory(); - Commerce.getAvailableUpdates(); + getPurchases(); } else { sendToScript(msg); } @@ -449,10 +429,7 @@ Rectangle { case 'tutorial_skipClicked': case 'tutorial_finished': Settings.setValue("isFirstUseOfPurchases", false); - root.activeView = "purchasesMain"; - root.installedApps = Commerce.getInstalledApps(); - Commerce.inventory(); - Commerce.getAvailableUpdates(); + getPurchases(); break; } } @@ -528,7 +505,7 @@ Rectangle { }, { "displayName": "Content Set", - "filterName": "contentSet" + "filterName": "content_set" }, { "displayName": "Entity", @@ -540,7 +517,7 @@ Rectangle { }, { "displayName": "Updatable", - "filterName": "updatable" + "filterName": "updated" } ] filterBar.primaryFilterChoices.clear(); @@ -548,14 +525,12 @@ Rectangle { } onPrimaryFilter_displayNameChanged: { - buildFilteredPurchasesModel(); - purchasesContentsList.positionViewAtIndex(0, ListView.Beginning) + purchasesModel.tagsFilter = filterBar.primaryFilter_filterName; filterBar.previousPrimaryFilter = filterBar.primaryFilter_displayName; } onTextChanged: { - buildFilteredPurchasesModel(); - purchasesContentsList.positionViewAtIndex(0, ListView.Beginning) + purchasesModel.searchFilter = filterBar.text; filterBar.previousText = filterBar.text; } } @@ -574,24 +549,41 @@ Rectangle { anchors.topMargin: 16; } - ListModel { + HifiModels.PSFListModel { id: purchasesModel; - } - ListModel { - id: previousPurchasesModel; - } - HifiCommerceCommon.SortableListModel { - id: tempPurchasesModel; - } - HifiCommerceCommon.SortableListModel { - id: filteredPurchasesModel; + itemsPerPage: 6; + listModelName: 'purchases'; + getPage: function () { + console.debug('getPage', purchasesModel.listModelName, root.isShowingMyItems, filterBar.primaryFilter_filterName, purchasesModel.currentPageToRetrieve, purchasesModel.itemsPerPage); + Commerce.inventory( + root.isShowingMyItems ? "proofs" : "purchased", + filterBar.primaryFilter_filterName, + filterBar.text, + purchasesModel.currentPageToRetrieve, + purchasesModel.itemsPerPage + ); + } + processPage: function(data) { + purchasesReceived = true; // HRS FIXME? + data.assets.forEach(function (item) { + if (item.status.length > 1) { console.warn("Unrecognized inventory status", item); } + item.status = item.status[0]; + item.categories = item.categories.join(';'); + item.cardBackVisible = false; + item.isInstalled = root.installedApps.indexOf(item.id) > -1; + item.wornEntityID = ''; + }); + sendToScript({ method: 'purchases_updateWearables' }); + + return data.assets; + } } ListView { id: purchasesContentsList; - visible: (root.isShowingMyItems && filteredPurchasesModel.count !== 0) || (!root.isShowingMyItems && filteredPurchasesModel.count !== 0); + visible: purchasesModel.count !== 0; clip: true; - model: filteredPurchasesModel; + model: purchasesModel; snapMode: ListView.SnapToItem; // Anchors anchors.top: separator.bottom; @@ -608,13 +600,13 @@ Rectangle { itemEdition: model.edition_number; numberSold: model.number_sold; limitedRun: model.limited_run; - displayedItemCount: model.displayedItemCount; - cardBackVisible: model.cardBackVisible; - isInstalled: model.isInstalled; + displayedItemCount: 999; // For now (and maybe longer), we're going to display all the edition numbers. + cardBackVisible: model.cardBackVisible || false; + isInstalled: model.isInstalled || false; wornEntityID: model.wornEntityID; upgradeUrl: model.upgrade_url; upgradeTitle: model.upgrade_title; - itemType: model.itemType; + itemType: model.item_type; isShowingMyItems: root.isShowingMyItems; valid: model.valid; anchors.topMargin: 10; @@ -706,11 +698,11 @@ Rectangle { } else if (msg.method === "setFilterText") { filterBar.text = msg.filterText; } else if (msg.method === "flipCard") { - for (var i = 0; i < filteredPurchasesModel.count; i++) { + for (var i = 0; i < purchasesModel.count; i++) { if (i !== index || msg.closeAll) { - filteredPurchasesModel.setProperty(i, "cardBackVisible", false); + purchasesModel.setProperty(i, "cardBackVisible", false); } else { - filteredPurchasesModel.setProperty(i, "cardBackVisible", true); + purchasesModel.setProperty(i, "cardBackVisible", true); } } } else if (msg.method === "updateItemClicked") { @@ -761,7 +753,7 @@ Rectangle { lightboxPopup.button2text = "CONFIRM"; lightboxPopup.button2method = function() { Entities.deleteEntity(msg.wornEntityID); - filteredPurchasesModel.setProperty(index, 'wornEntityID', ''); + purchasesModel.setProperty(index, 'wornEntityID', ''); root.activeView = "giftAsset"; lightboxPopup.visible = false; }; @@ -773,6 +765,14 @@ Rectangle { } } } + + + onAtYEndChanged: { + if (purchasesContentsList.atYEnd && !purchasesContentsList.atYBeginning) { + console.log("User scrolled to the bottom of 'Purchases'."); + purchasesModel.getNextPage(); + } + } } Rectangle { @@ -953,146 +953,14 @@ Rectangle { // // FUNCTION DEFINITIONS START // - - function processInventoryResult(inventory) { - for (var i = 0; i < inventory.length; i++) { - if (inventory[i].status.length > 1) { - console.log("WARNING: Inventory result index " + i + " has a status of length >1!") - } - inventory[i].status = inventory[i].status[0]; - inventory[i].categories = inventory[i].categories.join(';'); - } - return inventory; - } - - function populateDisplayedItemCounts() { - var itemCountDictionary = {}; - var currentItemId; - for (var i = 0; i < filteredPurchasesModel.count; i++) { - currentItemId = filteredPurchasesModel.get(i).id; - if (itemCountDictionary[currentItemId] === undefined) { - itemCountDictionary[currentItemId] = 1; - } else { - itemCountDictionary[currentItemId]++; - } - } - - for (var i = 0; i < filteredPurchasesModel.count; i++) { - filteredPurchasesModel.setProperty(i, "displayedItemCount", itemCountDictionary[filteredPurchasesModel.get(i).id]); - } - } - - function sortByDate() { - filteredPurchasesModel.sortColumnName = "purchase_date"; - filteredPurchasesModel.isSortingDescending = true; - filteredPurchasesModel.valuesAreNumerical = true; - filteredPurchasesModel.quickSort(); - } - - function buildFilteredPurchasesModel() { - var sameItemCount = 0; - - tempPurchasesModel.clear(); - - for (var i = 0; i < purchasesModel.count; i++) { - if (purchasesModel.get(i).title.toLowerCase().indexOf(filterBar.text.toLowerCase()) !== -1) { - if (purchasesModel.get(i).status !== "confirmed" && !root.isShowingMyItems) { - tempPurchasesModel.insert(0, purchasesModel.get(i)); - } else if ((root.isShowingMyItems && purchasesModel.get(i).edition_number === "0") || - (!root.isShowingMyItems && purchasesModel.get(i).edition_number !== "0")) { - tempPurchasesModel.append(purchasesModel.get(i)); - } - } - } - - // primaryFilter filtering and adding of itemType property to model - var currentItemType, currentRootFileUrl, currentCategories; - for (var i = 0; i < tempPurchasesModel.count; i++) { - currentRootFileUrl = tempPurchasesModel.get(i).root_file_url; - currentCategories = tempPurchasesModel.get(i).categories; - - if (currentRootFileUrl.indexOf(".fst") > -1) { - currentItemType = "avatar"; - } else if (currentCategories.indexOf("Wearables") > -1) { - currentItemType = "wearable"; - } else if (currentRootFileUrl.endsWith('.json.gz') || currentRootFileUrl.endsWith('.content.zip')) { - currentItemType = "contentSet"; - } else if (currentRootFileUrl.endsWith('.app.json')) { - currentItemType = "app"; - } else if (currentRootFileUrl.endsWith('.json')) { - currentItemType = "entity"; - } else { - currentItemType = "unknown"; - } - if (filterBar.primaryFilter_displayName !== "" && - ((filterBar.primaryFilter_displayName === "Updatable" && tempPurchasesModel.get(i).upgrade_url === "") || - (filterBar.primaryFilter_displayName !== "Updatable" && filterBar.primaryFilter_filterName.toLowerCase() !== currentItemType.toLowerCase()))) { - tempPurchasesModel.remove(i); - i--; - } else { - tempPurchasesModel.setProperty(i, 'itemType', currentItemType); - } - } - - for (var i = 0; i < tempPurchasesModel.count; i++) { - if (!filteredPurchasesModel.get(i)) { - sameItemCount = -1; - break; - } else if (tempPurchasesModel.get(i).itemId === filteredPurchasesModel.get(i).itemId && - tempPurchasesModel.get(i).edition_number === filteredPurchasesModel.get(i).edition_number && - tempPurchasesModel.get(i).status === filteredPurchasesModel.get(i).status) { - sameItemCount++; - } - } - - if (sameItemCount !== tempPurchasesModel.count || - filterBar.text !== filterBar.previousText || - filterBar.primaryFilter !== filterBar.previousPrimaryFilter) { - filteredPurchasesModel.clear(); - var currentId; - for (var i = 0; i < tempPurchasesModel.count; i++) { - currentId = tempPurchasesModel.get(i).id; - filteredPurchasesModel.append(tempPurchasesModel.get(i)); - filteredPurchasesModel.setProperty(i, 'cardBackVisible', false); - filteredPurchasesModel.setProperty(i, 'isInstalled', ((root.installedApps).indexOf(currentId) > -1)); - filteredPurchasesModel.setProperty(i, 'wornEntityID', ''); - } - - sendToScript({ method: 'purchases_updateWearables' }); - populateDisplayedItemCounts(); - sortByDate(); - } - } - - function checkIfAnyItemStatusChanged() { - var currentPurchasesModelId, currentPurchasesModelEdition, currentPurchasesModelStatus; - var previousPurchasesModelStatus; - for (var i = 0; i < purchasesModel.count; i++) { - currentPurchasesModelId = purchasesModel.get(i).id; - currentPurchasesModelEdition = purchasesModel.get(i).edition_number; - currentPurchasesModelStatus = purchasesModel.get(i).status; - - for (var j = 0; j < previousPurchasesModel.count; j++) { - previousPurchasesModelStatus = previousPurchasesModel.get(j).status; - if (currentPurchasesModelId === previousPurchasesModel.get(j).id && - currentPurchasesModelEdition === previousPurchasesModel.get(j).edition_number && - currentPurchasesModelStatus !== previousPurchasesModelStatus) { - - purchasesModel.setProperty(i, "statusChanged", true); - } else { - purchasesModel.setProperty(i, "statusChanged", false); - } - } - } - } function updateCurrentlyWornWearables(wearables) { - for (var i = 0; i < filteredPurchasesModel.count; i++) { + for (var i = 0; i < purchasesModel.count; i++) { for (var j = 0; j < wearables.length; j++) { - if (filteredPurchasesModel.get(i).itemType === "wearable" && - wearables[j].entityCertID === filteredPurchasesModel.get(i).certificate_id && - wearables[j].entityEdition.toString() === filteredPurchasesModel.get(i).edition_number) { - filteredPurchasesModel.setProperty(i, 'wornEntityID', wearables[j].entityID); + if (purchasesModel.get(i).itemType === "wearable" && + wearables[j].entityCertID === purchasesModel.get(i).certificate_id && + wearables[j].entityEdition.toString() === purchasesModel.get(i).edition_number) { + purchasesModel.setProperty(i, 'wornEntityID', wearables[j].entityID); break; } } @@ -1149,7 +1017,7 @@ Rectangle { switch (message.method) { case 'updatePurchases': referrerURL = message.referrerURL || ""; - titleBarContainer.referrerURL = message.referrerURL; + titleBarContainer.referrerURL = message.referrerURL || ""; filterBar.text = message.filterText ? message.filterText : ""; break; case 'inspectionCertificate_setCertificateId': @@ -1168,6 +1036,9 @@ Rectangle { case 'updateWearables': updateCurrentlyWornWearables(message.wornWearables); break; + case 'http.response': + http.handleHttpResponse(message); + break; default: console.log('Unrecognized message from marketplaces.js:', JSON.stringify(message)); } diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index 86700f702e..603d7fb676 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -19,6 +19,7 @@ import "../../../controls-uit" as HifiControlsUit import "../../../controls" as HifiControls import "../common" as HifiCommerceCommon import "../common/sendAsset" +import "../.." as HifiCommon Rectangle { HifiConstants { id: hifi; } @@ -343,8 +344,14 @@ Rectangle { } } + HifiCommon.RootHttpRequest { + id: http; + } + SendAsset { id: sendMoney; + http: http; + listModelName: "Send Money Connections"; z: 997; visible: root.activeView === "sendMoney"; anchors.fill: parent; @@ -768,6 +775,13 @@ Rectangle { case 'updateSelectedRecipientUsername': sendMoney.fromScript(message); break; + case 'http.response': + http.handleHttpResponse(message); + break; + case 'palIsStale': + case 'avatarDisconnected': + // Because we don't have "channels" for sending messages to a specific QML object, the messages are broadcast to all QML Items. If an Item of yours happens to be visible when some script sends a message with a method you don't expect, you'll get "Unrecognized message..." logs. + break; default: console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); } diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index 7a14ee060f..4cf6db7889 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -18,27 +18,17 @@ import QtQuick.Controls 2.2 import "../../../styles-uit" import "../../../controls-uit" as HifiControlsUit import "../../../controls" as HifiControls +import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere. Item { HifiConstants { id: hifi; } id: root; - property bool initialHistoryReceived: false; - property bool historyRequestPending: true; - property bool noMoreHistoryData: false; - property int pendingCount: 0; - property int currentHistoryPage: 1; - property var pagesAlreadyAdded: new Array(); onVisibleChanged: { if (visible) { - transactionHistoryModel.clear(); Commerce.balance(); - initialHistoryReceived = false; - root.currentHistoryPage = 1; - root.noMoreHistoryData = false; - root.historyRequestPending = true; - Commerce.history(root.currentHistoryPage); + transactionHistoryModel.getFirstPage(); Commerce.getAvailableUpdates(); } else { refreshTimer.stop(); @@ -53,86 +43,7 @@ Item { } onHistoryResult : { - root.initialHistoryReceived = true; - root.historyRequestPending = false; - - if (result.status === 'success') { - var currentPage = parseInt(result.current_page); - - if (result.data.history.length === 0) { - root.noMoreHistoryData = true; - console.log("No more data to retrieve from Commerce.history() endpoint.") - } else if (root.currentHistoryPage === 1) { - var sameItemCount = 0; - tempTransactionHistoryModel.clear(); - - tempTransactionHistoryModel.append(result.data.history); - - for (var i = 0; i < tempTransactionHistoryModel.count; i++) { - if (!transactionHistoryModel.get(i)) { - sameItemCount = -1; - break; - } else if (tempTransactionHistoryModel.get(i).transaction_type === transactionHistoryModel.get(i).transaction_type && - tempTransactionHistoryModel.get(i).text === transactionHistoryModel.get(i).text) { - sameItemCount++; - } - } - - if (sameItemCount !== tempTransactionHistoryModel.count) { - transactionHistoryModel.clear(); - for (var i = 0; i < tempTransactionHistoryModel.count; i++) { - transactionHistoryModel.append(tempTransactionHistoryModel.get(i)); - } - calculatePendingAndInvalidated(); - } - } else { - if (root.pagesAlreadyAdded.indexOf(currentPage) !== -1) { - console.log("Page " + currentPage + " of history has already been added to the list."); - } else { - // First, add the history result to a temporary model - tempTransactionHistoryModel.clear(); - tempTransactionHistoryModel.append(result.data.history); - - // Make a note that we've already added this page to the model... - root.pagesAlreadyAdded.push(currentPage); - - var insertionIndex = 0; - // If there's nothing in the model right now, we don't need to modify insertionIndex. - if (transactionHistoryModel.count !== 0) { - var currentIteratorPage; - // Search through the whole transactionHistoryModel and look for the insertion point. - // The insertion point is found when the result page from the server is less than - // the page that the current item came from, OR when we've reached the end of the whole model. - for (var i = 0; i < transactionHistoryModel.count; i++) { - currentIteratorPage = transactionHistoryModel.get(i).resultIsFromPage; - - if (currentPage < currentIteratorPage) { - insertionIndex = i; - break; - } else if (i === transactionHistoryModel.count - 1) { - insertionIndex = i + 1; - break; - } - } - } - - // Go through the results we just got back from the server, setting the "resultIsFromPage" - // property of those results and adding them to the main model. - for (var i = 0; i < tempTransactionHistoryModel.count; i++) { - tempTransactionHistoryModel.setProperty(i, "resultIsFromPage", currentPage); - transactionHistoryModel.insert(i + insertionIndex, tempTransactionHistoryModel.get(i)) - } - - calculatePendingAndInvalidated(); - } - } - } - - // Only auto-refresh if the user hasn't scrolled - // and there is more data to grab - if (transactionHistory.atYBeginning && !root.noMoreHistoryData) { - refreshTimer.start(); - } + transactionHistoryModel.handlePage(null, result); } onAvailableUpdatesResult: { @@ -147,7 +58,7 @@ Item { Connections { target: GlobalServices onMyUsernameChanged: { - transactionHistoryModel.clear(); + transactionHistoryModel.resetModel(); usernameText.text = Account.username; } } @@ -235,9 +146,8 @@ Item { onTriggered: { if (transactionHistory.atYBeginning) { console.log("Refreshing 1st Page of Recent Activity..."); - root.historyRequestPending = true; Commerce.balance(); - Commerce.history(1); + transactionHistoryModel.getFirstPage("delayedClear"); } } } @@ -299,11 +209,42 @@ Item { } } - ListModel { - id: tempTransactionHistoryModel; - } - ListModel { + HifiModels.PSFListModel { id: transactionHistoryModel; + listModelName: "transaction history"; // For debugging. Alternatively, we could specify endpoint for that purpose, even though it's not used directly. + itemsPerPage: 6; + getPage: function () { + console.debug('getPage', transactionHistoryModel.listModelName, transactionHistoryModel.currentPageToRetrieve); + Commerce.history(transactionHistoryModel.currentPageToRetrieve, transactionHistoryModel.itemsPerPage); + } + processPage: function (data) { + console.debug('processPage', transactionHistoryModel.listModelName, JSON.stringify(data)); + var result, pending; // Set up or get the accumulator for pending. + if (transactionHistoryModel.currentPageToRetrieve == 1) { + pending = {transaction_type: "pendingCount", count: 0}; + result = [pending]; + } else { + pending = transactionHistoryModel.get(0); + result = []; + } + + // Either add to pending, or to result. + // Note that you only see a page of pending stuff until you scroll... + data.history.forEach(function (item) { + if (item.status === 'pending') { + pending.count++; + } else { + result = result.concat(item); + } + }); + + // Only auto-refresh if the user hasn't scrolled + // and there is more data to grab + if (transactionHistory.atYBeginning && data.history.length) { + refreshTimer.start(); + } + return result; + } } Item { anchors.top: recentActivityText.bottom; @@ -312,8 +253,8 @@ Item { anchors.left: parent.left; anchors.right: parent.right; - Item { - visible: transactionHistoryModel.count === 0 && root.initialHistoryReceived; + Item { // On empty history. We don't want to flash and then replace, so don't show until we know we're zero. + visible: transactionHistoryModel.count === 0 && transactionHistoryModel.currentPageToRetrieve < 0; anchors.centerIn: parent; width: parent.width - 12; height: parent.height; @@ -385,10 +326,10 @@ Item { model: transactionHistoryModel; delegate: Item { width: parent.width; - height: (model.transaction_type === "pendingCount" && root.pendingCount !== 0) ? 40 : ((model.status === "confirmed" || model.status === "invalidated") ? transactionText.height + 30 : 0); + height: (model.transaction_type === "pendingCount" && model.count !== 0) ? 40 : ((model.status === "confirmed" || model.status === "invalidated") ? transactionText.height + 30 : 0); Item { - visible: model.transaction_type === "pendingCount" && root.pendingCount !== 0; + visible: model.transaction_type === "pendingCount" && model.count !== 0; anchors.top: parent.top; anchors.left: parent.left; width: parent.width; @@ -397,7 +338,7 @@ Item { AnonymousProRegular { id: pendingCountText; anchors.fill: parent; - text: root.pendingCount + ' Transaction' + (root.pendingCount > 1 ? 's' : '') + ' Pending'; + text: model.count + ' Transaction' + (model.count > 1 ? 's' : '') + ' Pending'; size: 18; color: hifi.colors.blueAccent; verticalAlignment: Text.AlignVCenter; @@ -460,14 +401,9 @@ Item { } } onAtYEndChanged: { - if (transactionHistory.atYEnd) { + if (transactionHistory.atYEnd && !transactionHistory.atYBeginning) { console.log("User scrolled to the bottom of 'Recent Activity'."); - if (!root.historyRequestPending && !root.noMoreHistoryData) { - // Grab next page of results and append to model - root.historyRequestPending = true; - Commerce.history(++root.currentHistoryPage); - console.log("Fetching Page " + root.currentHistoryPage + " of Recent Activity..."); - } + transactionHistoryModel.getNextPage(); } } } @@ -506,40 +442,6 @@ Item { return year + '-' + month + '-' + day + '
' + drawnHour + ':' + min + amOrPm; } - - function calculatePendingAndInvalidated(startingPendingCount) { - var pendingCount = startingPendingCount ? startingPendingCount : 0; - for (var i = 0; i < transactionHistoryModel.count; i++) { - if (transactionHistoryModel.get(i).status === "pending") { - pendingCount++; - } - } - - root.pendingCount = pendingCount; - if (pendingCount > 0) { - transactionHistoryModel.insert(0, {"transaction_type": "pendingCount"}); - } - } - - // - // Function Name: fromScript() - // - // Relevant Variables: - // None - // - // Arguments: - // message: The message sent from the JavaScript. - // Messages are in format "{method, params}", like json-rpc. - // - // Description: - // Called when a message is received from a script. - // - function fromScript(message) { - switch (message.method) { - default: - console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); - } - } signal sendSignalToWallet(var msg); // diff --git a/interface/resources/qml/hifi/models/PSFListModel.qml b/interface/resources/qml/hifi/models/PSFListModel.qml new file mode 100644 index 0000000000..1bfa2f6ae0 --- /dev/null +++ b/interface/resources/qml/hifi/models/PSFListModel.qml @@ -0,0 +1,168 @@ +// +// PSFListModel.qml +// qml/hifi/commerce/common +// +// PSFListModel +// "PSF" stands for: +// - Paged +// - Sortable +// - Filterable +// +// Created by Zach Fox on 2018-05-15 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 + +ListModel { + id: root; + // Used when printing debug statements + property string listModelName: endpoint; + + // Parameters. Even if you override getPage, below, please set these for clarity and consistency, when applicable. + // E.g., your getPage function could refer to this sortKey, etc. + property string endpoint; + property string sortProperty; // Currently only handles sorting on one column, which fits with current needs and tables. + property bool sortAscending; + property string sortKey: !sortProperty ? '' : (sortProperty + "," + (sortAscending ? "ASC" : "DESC")); + property string searchFilter: ""; + property string tagsFilter; + + // QML fires the following changed handlers even when first instantiating the Item. So we need a guard against firing them too early. + property bool initialized: false; + Component.onCompleted: initialized = true; + onEndpointChanged: if (initialized) { getFirstPage('delayClear'); } + onSortKeyChanged: if (initialized) { getFirstPage('delayClear'); } + onSearchFilterChanged: if (initialized) { getFirstPage('delayClear'); } + onTagsFilterChanged: if (initialized) { getFirstPage('delayClear'); } + + property int itemsPerPage: 100; + + // State. + property int currentPageToRetrieve: 0; // 0 = before first page. -1 = we have them all. Otherwise 1-based page number. + property bool retrievedAtLeastOnePage: false; + // We normally clear on reset. But if we want to "refresh", we can delay clearing the model until we get a result. + // Not normally set directly, but rather by giving a truthy argument to getFirstPage(true); + property bool delayedClear: false; + function resetModel() { + if (!delayedClear) { root.clear(); } + currentPageToRetrieve = 1; + retrievedAtLeastOnePage = false; + } + + // Page processing. + + // Override to return one property of data, and/or to transform the elements. Must return an array of model elements. + property var processPage: function (data) { return data; } + + property var listView; // Optional. For debugging. + // Check consistency and call processPage. + function handlePage(error, response) { + var processed; + console.debug('handlePage', listModelName, additionalFirstPageRequested, error, JSON.stringify(response)); + function fail(message) { + console.warn("Warning page fail", listModelName, JSON.stringify(message)); + currentPageToRetrieve = -1; + requestPending = false; + delayedClear = false; + } + if (error || (response.status !== 'success')) { + return fail(error || response.status); + } + if (!requestPending) { + return fail("No request in flight."); + } + requestPending = false; + if (response.current_page && response.current_page !== currentPageToRetrieve) { // Not all endpoints specify this property. + return fail("Mismatched page, expected:" + currentPageToRetrieve); + } + processed = processPage(response.data || response); + if (response.total_pages && (response.total_pages === currentPageToRetrieve)) { + currentPageToRetrieve = -1; + } + + if (delayedClear) { + root.clear(); + delayedClear = false; + } + root.append(processed); // FIXME keep index steady, and apply any post sort + retrievedAtLeastOnePage = true; + // Suppose two properties change at once, and both of their change handlers request a new first page. + // (An example is when the a filter box gets cleared with text in it, so that the search and tags are both reset.) + // Or suppose someone just types new search text quicker than the server response. + // In these cases, we would have multiple requests in flight, and signal based responses aren't generally very good + // at matching up the right handler with the right message. Rather than require all the APIs to carefully handle such, + // and also to cut down on useless requests, we take care of that case here. + if (additionalFirstPageRequested) { + console.debug('deferred getFirstPage', listModelName); + additionalFirstPageRequested = false; + getFirstPage('delayedClear'); + } + } + function debugView(label) { + if (!listView) { return; } + console.debug(label, listModelName, 'perPage:', itemsPerPage, 'count:', listView.count, + 'index:', listView.currentIndex, 'section:', listView.currentSection, + 'atYBeginning:', listView.atYBeginning, 'atYEnd:', listView.atYEnd, + 'y:', listView.y, 'contentY:', listView.contentY); + } + + // Override either http or getPage. + property var http; // An Item that has a request function. + property var getPage: function () { // Any override MUST call handlePage(), above, even if results empty. + if (!http) { return console.warn("Neither http nor getPage was set for", listModelName); } + // If it is a path starting with slash, add the metaverseServer domain. + var url = /^\//.test(endpoint) ? (Account.metaverseServerURL + endpoint) : endpoint; + var parameters = [ + 'per_page=' + itemsPerPage, + 'page=' + currentPageToRetrieve + ]; + if (searchFilter) { + parameters.splice(parameters.length, 0, 'search=' + searchFilter); + } + if (sortKey) { + parameters.splice(parameters.length, 0, 'sort=' + sortKey); + } + + var parametersSeparator = /\?/.test(url) ? '&' : '?'; + url = url + parametersSeparator + parameters.join('&'); + console.debug('getPage', listModelName, currentPageToRetrieve); + http.request({uri: url}, handlePage); + } + + // Start the show by retrieving data according to `getPage()`. + // It can be custom-defined by this item's Parent. + property var getFirstPage: function (delayClear) { + if (requestPending) { + console.debug('deferring getFirstPage', listModelName); + additionalFirstPageRequested = true; + return; + } + delayedClear = !!delayClear; + resetModel(); + requestPending = true; + console.debug("getFirstPage", listModelName, currentPageToRetrieve); + getPage(); + } + property bool additionalFirstPageRequested: false; + property bool requestPending: false; // For de-bouncing getNextPage. + // This function, will get the _next_ page of data according to `getPage()`. + // It can be custom-defined by this item's Parent. Typical usage: + // ListView { + // id: theList + // model: thisPSFListModelId + // onAtYEndChanged: if (theList.atYEnd && !theList.atYBeginning) { thisPSFListModelId.getNextPage(); } + // ...} + property var getNextPage: function () { + if (requestPending || currentPageToRetrieve < 0) { + return; + } + currentPageToRetrieve++; + console.debug("getNextPage", listModelName, currentPageToRetrieve); + requestPending = true; + getPage(); + } +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml index dc67494e27..08f86770e6 100644 --- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -34,41 +34,16 @@ StackView { height: parent !== null ? parent.height : undefined property int cardWidth: 212; property int cardHeight: 152; - property string metaverseBase: addressBarDialog.metaverseServerUrl + "/api/v1/"; property var tablet: null; - // This version only implements rpc(method, parameters, callback(error, result)) calls initiated from here, not initiated from .js, nor "notifications". - property var rpcCalls: ({}); - property var rpcCounter: 0; + RootHttpRequest { id: http; } signal sendToScript(var message); - function rpc(method, parameters, callback) { - console.debug('TabletAddressDialog: rpc: method = ', method, 'parameters = ', parameters, 'callback = ', callback) - - rpcCalls[rpcCounter] = callback; - var message = {method: method, params: parameters, id: rpcCounter++, jsonrpc: "2.0"}; - sendToScript(message); - } function fromScript(message) { - if (message.method === 'refreshFeeds') { - var feeds = [happeningNow, places, snapshots]; - console.debug('TabletAddressDialog::fromScript: refreshFeeds', 'feeds = ', feeds); - - feeds.forEach(function(feed) { - feed.protocol = encodeURIComponent(message.protocolSignature); - Qt.callLater(feed.fillDestinations); - }); - - return; + switch (message.method) { + case 'http.response': + http.handleHttpResponse(message); + break; } - - var callback = rpcCalls[message.id]; - if (!callback) { - // FIXME: We often recieve very long messages here, the logging of which is drastically slowing down the main thread - //console.log('No callback for message fromScript', JSON.stringify(message)); - return; - } - delete rpcCalls[message.id]; - callback(message.error, message.result); } Component { id: tabletWebView; TabletWebView {} } @@ -346,12 +321,11 @@ StackView { width: parent.width; cardWidth: 312 + (2 * 4); cardHeight: 163 + (2 * 4); - metaverseServerUrl: addressBarDialog.metaverseServerUrl; labelText: 'HAPPENING NOW'; actions: 'announcement'; filter: addressLine.text; goFunction: goCard; - rpc: root.rpc; + http: http; } Feed { id: places; @@ -359,12 +333,11 @@ StackView { cardWidth: 210; cardHeight: 110 + messageHeight; messageHeight: 44; - metaverseServerUrl: addressBarDialog.metaverseServerUrl; labelText: 'PLACES'; actions: 'concurrency'; filter: addressLine.text; goFunction: goCard; - rpc: root.rpc; + http: http; } Feed { id: snapshots; @@ -373,12 +346,11 @@ StackView { cardHeight: 75 + messageHeight + 4; messageHeight: 32; textPadding: 6; - metaverseServerUrl: addressBarDialog.metaverseServerUrl; labelText: 'RECENT SNAPS'; actions: 'snapshot'; filter: addressLine.text; goFunction: goCard; - rpc: root.rpc; + http: http; } } } diff --git a/interface/resources/qml/hifi/tablet/TabletRoot.qml b/interface/resources/qml/hifi/tablet/TabletRoot.qml index 15db5d8f88..fa268ad6ee 100644 --- a/interface/resources/qml/hifi/tablet/TabletRoot.qml +++ b/interface/resources/qml/hifi/tablet/TabletRoot.qml @@ -65,6 +65,18 @@ Item { return false; } + function closeDialog() { + if (openMessage != null) { + openMessage.destroy(); + openMessage = null; + } + + if (openModal != null) { + openModal.destroy(); + openModal = null; + } + } + function isUrlLoaded(url) { if (currentApp >= 0) { var currentAppUrl = tabletApps.get(currentApp).appUrl; diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml index 08b0104fce..871d1c92a9 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml @@ -8,12 +8,12 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -import QtQuick 2.5 -import QtQuick.Controls 1.4 +import QtQuick 2.7 import Qt.labs.folderlistmodel 2.1 import Qt.labs.settings 1.0 -import QtQuick.Controls.Styles 1.4 import QtQuick.Dialogs 1.2 as OriginalDialogs +import QtQuick.Controls 1.4 as QQC1 +import QtQuick.Controls 2.3 import ".." import "../../../controls-uit" @@ -30,6 +30,8 @@ Rectangle { color: hifi.colors.baseGray; + property var filesModel: ListModel { } + Settings { category: "FileDialog" property alias width: root.width @@ -52,7 +54,7 @@ Rectangle { property int iconSize: 40 property bool selectDirectory: false; - property bool showHidden: false; + property bool showHidden: true; // FIXME implement property bool multiSelect: false; property bool saveDialog: false; @@ -149,7 +151,7 @@ Rectangle { ComboBox { id: pathSelector - anchors { + anchors { top: parent.top topMargin: hifi.dimensions.contentMargin.y left: navControls.right @@ -247,7 +249,9 @@ Rectangle { } currentSelectionUrl = helper.pathToUrl(fileTableView.model.get(row).filePath); - currentSelectionIsFolder = fileTableView.model.isFolder(row); + currentSelectionIsFolder = fileTableView.model !== filesModel ? + fileTableView.model.isFolder(row) : + fileTableModel.isFolder(row); if (root.selectDirectory || !currentSelectionIsFolder) { currentSelection.text = capitalizeDrive(helper.urlToPath(currentSelectionUrl)); } else { @@ -280,11 +284,14 @@ Rectangle { showDirsFirst: true showDotAndDotDot: false showFiles: !root.selectDirectory + showHidden: root.showHidden Component.onCompleted: { showFiles = !root.selectDirectory + showHidden = root.showHidden } onFolderChanged: { + d.clearSelection(); fileTableModel.update(); // Update once the data from the folder change is available. } @@ -325,7 +332,12 @@ Rectangle { } } - ListModel { + Component { + id: filesModelBuilder + ListModel { } + } + + QtObject { id: fileTableModel // FolderListModel has a couple of problems: @@ -377,7 +389,11 @@ Rectangle { if (row === -1) { return false; } - return get(row).fileIsDir; + return filesModel.get(row).fileIsDir; + } + + function get(row) { + return filesModel.get(row) } function update() { @@ -395,7 +411,7 @@ Rectangle { rows = 0, i; - clear(); + filesModel = filesModelBuilder.createObject(root); comparisonFunction = sortOrder === Qt.AscendingOrder ? function(a, b) { return a < b; } @@ -417,7 +433,7 @@ Rectangle { while (lower < upper) { middle = Math.floor((lower + upper) / 2); var lessThan; - if (comparisonFunction(sortValue, get(middle)[sortField])) { + if (comparisonFunction(sortValue, filesModel.get(middle)[sortField])) { lessThan = true; upper = middle; } else { @@ -426,7 +442,7 @@ Rectangle { } } - insert(lower, { + filesModel.insert(lower, { fileName: fileName, fileModified: (fileIsDir ? new Date(0) : model.getItem(i, "fileModified")), fileSize: model.getItem(i, "fileSize"), @@ -437,8 +453,6 @@ Rectangle { rows++; } - - d.clearSelection(); } } @@ -463,12 +477,12 @@ Rectangle { sortIndicatorOrder: Qt.AscendingOrder sortIndicatorVisible: true - model: fileTableModel + model: filesModel function updateSort() { - model.sortOrder = sortIndicatorOrder; - model.sortColumn = sortIndicatorColumn; - model.update(); + fileTableModel.sortOrder = sortIndicatorOrder; + fileTableModel.sortColumn = sortIndicatorColumn; + fileTableModel.update(); } onSortIndicatorColumnChanged: { updateSort(); } @@ -520,7 +534,7 @@ Rectangle { } } - TableViewColumn { + QQC1.TableViewColumn { id: fileNameColumn role: "fileName" title: "Name" @@ -528,7 +542,7 @@ Rectangle { movable: false resizable: true } - TableViewColumn { + QQC1.TableViewColumn { id: fileMofifiedColumn role: "fileModified" title: "Date" @@ -537,7 +551,7 @@ Rectangle { resizable: true visible: !selectDirectory } - TableViewColumn { + QQC1.TableViewColumn { role: "fileSize" title: "Size" width: fileTableView.width - fileNameColumn.width - fileMofifiedColumn.width @@ -552,11 +566,12 @@ Rectangle { } function navigateToCurrentRow() { + var currentModel = fileTableView.model !== filesModel ? fileTableView.model : fileTableModel var row = fileTableView.currentRow - var isFolder = model.isFolder(row); - var file = model.get(row).filePath; + var isFolder = currentModel.isFolder(row); + var file = currentModel.get(row).filePath; if (isFolder) { - fileTableView.model.folder = helper.pathToUrl(file); + currentModel.folder = helper.pathToUrl(file); } else { okAction.trigger(); } @@ -571,7 +586,8 @@ Rectangle { var newPrefix = prefix + event.text.toLowerCase(); var matchedIndex = -1; for (var i = 0; i < model.count; ++i) { - var name = model.get(i).fileName.toLowerCase(); + var name = model !== filesModel ? model.get(i).fileName.toLowerCase() : + filesModel.get(i).fileName.toLowerCase(); if (0 === name.indexOf(newPrefix)) { matchedIndex = i; break; diff --git a/interface/src/AboutUtil.cpp b/interface/src/AboutUtil.cpp index 5179897443..634e52b481 100644 --- a/interface/src/AboutUtil.cpp +++ b/interface/src/AboutUtil.cpp @@ -8,48 +8,46 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // + +#include "AboutUtil.h" #include #include -#include "AboutUtil.h" -#include "BuildInfo.h" #include +#include + +#include "BuildInfo.h" #include "DependencyManager.h" #include "scripting/HMDScriptingInterface.h" #include "Application.h" -#include AboutUtil::AboutUtil(QObject *parent) : QObject(parent) { - QLocale locale_; - m_DateConverted = QDate::fromString(BuildInfo::BUILD_TIME, "dd/MM/yyyy"). - toString(locale_.dateFormat(QLocale::ShortFormat)); + QLocale locale; + _dateConverted = QDate::fromString(BuildInfo::BUILD_TIME, "dd/MM/yyyy"). + toString(locale.dateFormat(QLocale::ShortFormat)); } -AboutUtil *AboutUtil::getInstance() -{ +AboutUtil *AboutUtil::getInstance() { static AboutUtil instance; return &instance; } -QString AboutUtil::buildDate() const -{ - return m_DateConverted; +QString AboutUtil::getBuildDate() const { + return _dateConverted; } -QString AboutUtil::buildVersion() const -{ +QString AboutUtil::getBuildVersion() const { return BuildInfo::VERSION; } -QString AboutUtil::qtVersion() const -{ +QString AboutUtil::getQtVersion() const { return qVersion(); } void AboutUtil::openUrl(const QString& url) const { auto tabletScriptingInterface = DependencyManager::get(); - auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + auto tablet = tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system"); auto hmd = DependencyManager::get(); auto offscreenUi = DependencyManager::get(); diff --git a/interface/src/AboutUtil.h b/interface/src/AboutUtil.h index 9b65b887b9..767e69842d 100644 --- a/interface/src/AboutUtil.h +++ b/interface/src/AboutUtil.h @@ -15,28 +15,41 @@ #include -class AboutUtil : public QObject { +/**jsdoc + * @namespace HifiAbout + * + * @hifi-interface + * @hifi-client-entity + * + * @property {string} buildDate + * @property {string} buildVersion + * @property {string} qtVersion + */ +class AboutUtil : public QObject { Q_OBJECT - Q_PROPERTY(QString buildDate READ buildDate CONSTANT) - Q_PROPERTY(QString buildVersion READ buildVersion CONSTANT) - Q_PROPERTY(QString qtVersion READ qtVersion CONSTANT) - - AboutUtil(QObject* parent = nullptr); + Q_PROPERTY(QString buildDate READ getBuildDate CONSTANT) + Q_PROPERTY(QString buildVersion READ getBuildVersion CONSTANT) + Q_PROPERTY(QString qtVersion READ getQtVersion CONSTANT) public: static AboutUtil* getInstance(); ~AboutUtil() {} - QString buildDate() const; - QString buildVersion() const; - QString qtVersion() const; + QString getBuildDate() const; + QString getBuildVersion() const; + QString getQtVersion() const; public slots: + + /**jsdoc + * @function HifiAbout.openUrl + * @param {string} url + */ void openUrl(const QString &url) const; private: - - QString m_DateConverted; + AboutUtil(QObject* parent = nullptr); + QString _dateConverted; }; #endif // hifi_AboutUtil_h diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 6a102f418b..acdcdef2cf 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -334,7 +334,11 @@ static const QString RENDER_FORWARD{ "HIFI_RENDER_FORWARD" }; static bool DISABLE_DEFERRED = QProcessEnvironment::systemEnvironment().contains(RENDER_FORWARD); #endif +#if !defined(Q_OS_ANDROID) static const int MAX_CONCURRENT_RESOURCE_DOWNLOADS = 16; +#else +static const int MAX_CONCURRENT_RESOURCE_DOWNLOADS = 4; +#endif // For processing on QThreadPool, we target a number of threads after reserving some // based on how many are being consumed by the application and the display plugin. However, @@ -693,8 +697,8 @@ private: }; /**jsdoc - *

The Controller.Hardware.Application object has properties representing Interface's state. The property - * values are integer IDs, uniquely identifying each output. Read-only. These can be mapped to actions or functions or + *

The Controller.Hardware.Application object has properties representing Interface's state. The property + * values are integer IDs, uniquely identifying each output. Read-only. These can be mapped to actions or functions or * Controller.Standard items in a {@link RouteObject} mapping (e.g., using the {@link RouteObject#when} method). * Each data value is either 1.0 for "true" or 0.0 for "false".

* @@ -717,7 +721,7 @@ private: * * *
NavigationFocusednumbernumberNot used.
- * @typedef Controller.Hardware-Application + * @typedef {object} Controller.Hardware-Application */ static const QString STATE_IN_HMD = "InHMD"; @@ -776,7 +780,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { static const auto SUPPRESS_SETTINGS_RESET = "--suppress-settings-reset"; bool suppressPrompt = cmdOptionExists(argc, const_cast(argv), SUPPRESS_SETTINGS_RESET); - // Ignore any previous crashes if running from command line with a test script. + // Ignore any previous crashes if running from command line with a test script. bool inTestMode { false }; for (int i = 0; i < argc; ++i) { QString parameter(argv[i]); @@ -798,15 +802,8 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { qApp->setProperty(hifi::properties::APP_LOCAL_DATA_PATH, cacheDir); } - // FIXME fix the OSX installer to install the resources.rcc binary instead of resource files and remove - // this conditional exclusion -#if !defined(Q_OS_OSX) { -#if defined(Q_OS_ANDROID) - const QString resourcesBinaryFile = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/resources.rcc"; -#else - const QString resourcesBinaryFile = QCoreApplication::applicationDirPath() + "/resources.rcc"; -#endif + const QString resourcesBinaryFile = PathUtils::getRccPath(); if (!QFile::exists(resourcesBinaryFile)) { throw std::runtime_error("Unable to find primary resources"); } @@ -814,7 +811,6 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { throw std::runtime_error("Unable to load primary resources"); } } -#endif // Tell the plugin manager about our statically linked plugins auto pluginManager = PluginManager::getInstance(); @@ -1112,7 +1108,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo qCDebug(interfaceapp) << "[VERSION] Build sequence:" << qPrintable(applicationVersion()); qCDebug(interfaceapp) << "[VERSION] MODIFIED_ORGANIZATION:" << BuildInfo::MODIFIED_ORGANIZATION; qCDebug(interfaceapp) << "[VERSION] VERSION:" << BuildInfo::VERSION; - qCDebug(interfaceapp) << "[VERSION] BUILD_BRANCH:" << BuildInfo::BUILD_BRANCH; + qCDebug(interfaceapp) << "[VERSION] BUILD_TYPE_STRING:" << BuildInfo::BUILD_TYPE_STRING; qCDebug(interfaceapp) << "[VERSION] BUILD_GLOBAL_SERVICES:" << BuildInfo::BUILD_GLOBAL_SERVICES; #if USE_STABLE_GLOBAL_SERVICES qCDebug(interfaceapp) << "[VERSION] We will use STABLE global services."; @@ -1369,11 +1365,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo initializeGL(); qCDebug(interfaceapp, "Initialized GL"); - // Initialize the display plugin architecture + // Initialize the display plugin architecture initializeDisplayPlugins(); qCDebug(interfaceapp, "Initialized Display"); - // Create the rendering engine. This can be slow on some machines due to lots of + // Create the rendering engine. This can be slow on some machines due to lots of // GPU pipeline creation. initializeRenderEngine(); qCDebug(interfaceapp, "Initialized Render Engine."); @@ -1417,7 +1413,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // In practice we shouldn't run across installs that don't have a known installer type. // Client or Client+Server installs should always have the installer.ini next to their // respective interface.exe, and Steam installs will be detected as such. If a user were - // to delete the installer.ini, though, and as an example, we won't know the context of the + // to delete the installer.ini, though, and as an example, we won't know the context of the // original install. constexpr auto INSTALLER_KEY_TYPE = "type"; constexpr auto INSTALLER_KEY_CAMPAIGN = "campaign"; @@ -1443,17 +1439,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // add firstRun flag from settings to launch event Setting::Handle firstRun { Settings::firstRun, true }; - // once the settings have been loaded, check if we need to flip the default for UserActivityLogger - auto& userActivityLogger = UserActivityLogger::getInstance(); - if (!userActivityLogger.isDisabledSettingSet()) { - // the user activity logger is opt-out for Interface - // but it's defaulted to disabled for other targets - // so we need to enable it here if it has never been disabled by the user - userActivityLogger.disable(false); - } - QString machineFingerPrint = uuidStringWithoutCurlyBraces(FingerprintUtils::getMachineFingerprint()); + auto& userActivityLogger = UserActivityLogger::getInstance(); if (userActivityLogger.isEnabled()) { // sessionRunTime will be reset soon by loadSettings. Grab it now to get previous session value. // The value will be 0 if the user blew away settings this session, which is both a feature and a bug. @@ -1465,6 +1453,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo { "tester", QProcessEnvironment::systemEnvironment().contains(TESTER) }, { "installer_campaign", installerCampaign }, { "installer_type", installerType }, + { "build_type", BuildInfo::BUILD_TYPE_STRING }, { "previousSessionCrashed", _previousSessionCrashed }, { "previousSessionRuntime", sessionRunTime.get() }, { "cpu_architecture", QSysInfo::currentCpuArchitecture() }, @@ -2182,7 +2171,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo if (testProperty.isValid()) { auto scriptEngines = DependencyManager::get(); const auto testScript = property(hifi::properties::TEST).toUrl(); - + // Set last parameter to exit interface when the test script finishes, if so requested scriptEngines->loadScript(testScript, false, false, false, false, quitWhenFinished); @@ -2399,7 +2388,7 @@ void Application::onAboutToQuit() { } } - // The active display plugin needs to be loaded before the menu system is active, + // The active display plugin needs to be loaded before the menu system is active, // so its persisted explicitly here Setting::Handle{ ACTIVE_DISPLAY_PLUGIN_SETTING_NAME }.set(getActiveDisplayPlugin()->getName()); @@ -2492,6 +2481,7 @@ void Application::cleanupBeforeQuit() { } _window->saveGeometry(); + _gpuContext->shutdown(); // Destroy third party processes after scripts have finished using them. #ifdef HAVE_DDE @@ -2609,10 +2599,55 @@ void Application::initializeGL() { _isGLInitialized = true; } - _glWidget->makeCurrent(); - glClearColor(0.2f, 0.2f, 0.2f, 1); - glClear(GL_COLOR_BUFFER_BIT); - _glWidget->swapBuffers(); + if (!_glWidget->makeCurrent()) { + qCWarning(interfaceapp, "Unable to make window context current"); + } + +#if !defined(DISABLE_QML) + // Build a shared canvas / context for the Chromium processes + { + // Disable signed distance field font rendering on ATI/AMD GPUs, due to + // https://highfidelity.manuscript.com/f/cases/13677/Text-showing-up-white-on-Marketplace-app + std::string vendor{ (const char*)glGetString(GL_VENDOR) }; + if ((vendor.find("AMD") != std::string::npos) || (vendor.find("ATI") != std::string::npos)) { + qputenv("QTWEBENGINE_CHROMIUM_FLAGS", QByteArray("--disable-distance-field-text")); + } + + // Chromium rendering uses some GL functions that prevent nSight from capturing + // frames, so we only create the shared context if nsight is NOT active. + if (!nsightActive()) { + _chromiumShareContext = new OffscreenGLCanvas(); + _chromiumShareContext->setObjectName("ChromiumShareContext"); + _chromiumShareContext->create(_glWidget->qglContext()); + if (!_chromiumShareContext->makeCurrent()) { + qCWarning(interfaceapp, "Unable to make chromium shared context current"); + } + qt_gl_set_global_share_context(_chromiumShareContext->getContext()); + _chromiumShareContext->doneCurrent(); + // Restore the GL widget context + if (!_glWidget->makeCurrent()) { + qCWarning(interfaceapp, "Unable to make window context current"); + } + } else { + qCWarning(interfaceapp) << "nSight detected, disabling chrome rendering"; + } + } +#endif + + // Build a shared canvas / context for the QML rendering + { + _qmlShareContext = new OffscreenGLCanvas(); + _qmlShareContext->setObjectName("QmlShareContext"); + _qmlShareContext->create(_glWidget->qglContext()); + if (!_qmlShareContext->makeCurrent()) { + qCWarning(interfaceapp, "Unable to make QML shared context current"); + } + OffscreenQmlSurface::setSharedContext(_qmlShareContext->getContext()); + _qmlShareContext->doneCurrent(); + if (!_glWidget->makeCurrent()) { + qCWarning(interfaceapp, "Unable to make window context current"); + } + } // Build an offscreen GL context for the main thread. _offscreenContext = new OffscreenGLCanvas(); @@ -2624,6 +2659,11 @@ void Application::initializeGL() { _offscreenContext->doneCurrent(); _offscreenContext->setThreadContext(); + _glWidget->makeCurrent(); + glClearColor(0.2f, 0.2f, 0.2f, 1); + glClear(GL_COLOR_BUFFER_BIT); + _glWidget->swapBuffers(); + // Move the GL widget context to the render event handler thread _renderEventHandler = new RenderEventHandler(_glWidget->qglContext()); if (!_offscreenContext->makeCurrent()) { @@ -2633,7 +2673,7 @@ void Application::initializeGL() { // Create the GPU backend // Requires the window context, because that's what's used in the actual rendering - // and the GPU backend will make things like the VAO which cannot be shared across + // and the GPU backend will make things like the VAO which cannot be shared across // contexts _glWidget->makeCurrent(); gpu::Context::init(); @@ -2656,7 +2696,7 @@ void Application::initializeDisplayPlugins() { auto lastActiveDisplayPluginName = activeDisplayPluginSetting.get(); auto defaultDisplayPlugin = displayPlugins.at(0); - // Once time initialization code + // Once time initialization code DisplayPluginPointer targetDisplayPlugin; foreach(auto displayPlugin, displayPlugins) { displayPlugin->setContext(_gpuContext); @@ -2669,7 +2709,7 @@ void Application::initializeDisplayPlugins() { } // The default display plugin needs to be activated first, otherwise the display plugin thread - // may be launched by an external plugin, which is bad + // may be launched by an external plugin, which is bad setDisplayPlugin(defaultDisplayPlugin); // Now set the desired plugin if it's not the same as the default plugin @@ -2746,40 +2786,6 @@ extern void setupPreferences(); static void addDisplayPluginToMenu(const DisplayPluginPointer& displayPlugin, int index, bool active = false); void Application::initializeUi() { - // Build a shared canvas / context for the Chromium processes -#if !defined(DISABLE_QML) - // Chromium rendering uses some GL functions that prevent nSight from capturing - // frames, so we only create the shared context if nsight is NOT active. - if (!nsightActive()) { - _chromiumShareContext = new OffscreenGLCanvas(); - _chromiumShareContext->setObjectName("ChromiumShareContext"); - _chromiumShareContext->create(_offscreenContext->getContext()); - if (!_chromiumShareContext->makeCurrent()) { - qCWarning(interfaceapp, "Unable to make chromium shared context current"); - } - qt_gl_set_global_share_context(_chromiumShareContext->getContext()); - _chromiumShareContext->doneCurrent(); - // Restore the GL widget context - _offscreenContext->makeCurrent(); - } else { - qCWarning(interfaceapp) << "nSIGHT detected, disabling chrome rendering"; - } -#endif - - // Build a shared canvas / context for the QML rendering - _qmlShareContext = new OffscreenGLCanvas(); - _qmlShareContext->setObjectName("QmlShareContext"); - _qmlShareContext->create(_offscreenContext->getContext()); - if (!_qmlShareContext->makeCurrent()) { - qCWarning(interfaceapp, "Unable to make QML shared context current"); - } - OffscreenQmlSurface::setSharedContext(_qmlShareContext->getContext()); - _qmlShareContext->doneCurrent(); - // Restore the GL widget context - _offscreenContext->makeCurrent(); - // Make sure all QML surfaces share the main thread GL context - OffscreenQmlSurface::setSharedContext(_offscreenContext->getContext()); - AddressBarDialog::registerType(); ErrorDialog::registerType(); LoginDialog::registerType(); @@ -3011,9 +3017,11 @@ void Application::onDesktopRootItemCreated(QQuickItem* rootItem) { auto surfaceContext = DependencyManager::get()->getSurfaceContext(); surfaceContext->setContextProperty("Stats", Stats::getInstance()); +#if !defined(Q_OS_ANDROID) auto offscreenUi = DependencyManager::get(); auto qml = PathUtils::qmlUrl("AvatarInputsBar.qml"); offscreenUi->show(qml, "AvatarInputsBar"); +#endif } void Application::updateCamera(RenderArgs& renderArgs, float deltaTime) { @@ -3642,7 +3650,6 @@ void Application::keyPressEvent(QKeyEvent* event) { _keysPressed.insert(event->key()); _controllerScriptingInterface->emitKeyPressEvent(event); // send events to any registered scripts - // if one of our scripts have asked to capture this event, then stop processing it if (_controllerScriptingInterface->isKeyCaptured(event)) { return; @@ -3668,9 +3675,21 @@ void Application::keyPressEvent(QKeyEvent* event) { } break; - case Qt::Key_1: - case Qt::Key_2: - case Qt::Key_3: + case Qt::Key_1: { + Menu* menu = Menu::getInstance(); + menu->triggerOption(MenuOption::FirstPerson); + break; + } + case Qt::Key_2: { + Menu* menu = Menu::getInstance(); + menu->triggerOption(MenuOption::FullscreenMirror); + break; + } + case Qt::Key_3: { + Menu* menu = Menu::getInstance(); + menu->triggerOption(MenuOption::ThirdPerson); + break; + } case Qt::Key_4: case Qt::Key_5: case Qt::Key_6: @@ -3727,6 +3746,13 @@ void Application::keyPressEvent(QKeyEvent* event) { } break; + case Qt::Key_R: + if (isMeta && !event->isAutoRepeat()) { + DependencyManager::get()->reloadAllScripts(); + DependencyManager::get()->clearCache(); + } + break; + case Qt::Key_Asterisk: Menu::getInstance()->triggerOption(MenuOption::DefaultSkybox); break; @@ -3792,68 +3818,6 @@ void Application::keyPressEvent(QKeyEvent* event) { Menu::getInstance()->triggerOption(MenuOption::Chat); break; -#if 0 - case Qt::Key_I: - if (isShifted) { - _myCamera.setEyeOffsetOrientation(glm::normalize( - glm::quat(glm::vec3(0.002f, 0, 0)) * _myCamera.getEyeOffsetOrientation())); - } else { - _myCamera.setEyeOffsetPosition(_myCamera.getEyeOffsetPosition() + glm::vec3(0, 0.001, 0)); - } - updateProjectionMatrix(); - break; - - case Qt::Key_K: - if (isShifted) { - _myCamera.setEyeOffsetOrientation(glm::normalize( - glm::quat(glm::vec3(-0.002f, 0, 0)) * _myCamera.getEyeOffsetOrientation())); - } else { - _myCamera.setEyeOffsetPosition(_myCamera.getEyeOffsetPosition() + glm::vec3(0, -0.001, 0)); - } - updateProjectionMatrix(); - break; - - case Qt::Key_J: - if (isShifted) { - QMutexLocker viewLocker(&_viewMutex); - _viewFrustum.setFocalLength(_viewFrustum.getFocalLength() - 0.1f); - } else { - _myCamera.setEyeOffsetPosition(_myCamera.getEyeOffsetPosition() + glm::vec3(-0.001, 0, 0)); - } - updateProjectionMatrix(); - break; - - case Qt::Key_M: - if (isShifted) { - QMutexLocker viewLocker(&_viewMutex); - _viewFrustum.setFocalLength(_viewFrustum.getFocalLength() + 0.1f); - } else { - _myCamera.setEyeOffsetPosition(_myCamera.getEyeOffsetPosition() + glm::vec3(0.001, 0, 0)); - } - updateProjectionMatrix(); - break; - - case Qt::Key_U: - if (isShifted) { - _myCamera.setEyeOffsetOrientation(glm::normalize( - glm::quat(glm::vec3(0, 0, -0.002f)) * _myCamera.getEyeOffsetOrientation())); - } else { - _myCamera.setEyeOffsetPosition(_myCamera.getEyeOffsetPosition() + glm::vec3(0, 0, -0.001)); - } - updateProjectionMatrix(); - break; - - case Qt::Key_Y: - if (isShifted) { - _myCamera.setEyeOffsetOrientation(glm::normalize( - glm::quat(glm::vec3(0, 0, 0.002f)) * _myCamera.getEyeOffsetOrientation())); - } else { - _myCamera.setEyeOffsetPosition(_myCamera.getEyeOffsetPosition() + glm::vec3(0, 0, 0.001)); - } - updateProjectionMatrix(); - break; -#endif - case Qt::Key_Slash: Menu::getInstance()->triggerOption(MenuOption::Stats); break; @@ -4236,7 +4200,7 @@ bool Application::acceptSnapshot(const QString& urlString) { static uint32_t _renderedFrameIndex { INVALID_FRAME }; bool Application::shouldPaint() const { - if (_aboutToQuit) { + if (_aboutToQuit || _window->isMinimized()) { return false; } @@ -4632,12 +4596,6 @@ void Application::idle() { _overlayConductor.update(secondsSinceLastUpdate); - auto myAvatar = getMyAvatar(); - if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON || _myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { - Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, myAvatar->getBoomLength() <= MyAvatar::ZOOM_MIN); - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, !(myAvatar->getBoomLength() <= MyAvatar::ZOOM_MIN)); - cameraMenuChanged(); - } _gameLoopCounter.increment(); } @@ -4802,12 +4760,15 @@ void Application::loadSettings() { // DONT CHECK IN //DependencyManager::get()->setAutomaticLODAdjust(false); - Menu::getInstance()->loadSettings(); + auto menu = Menu::getInstance(); + menu->loadSettings(); + + // override the menu option show overlays to always be true on startup + menu->setIsOptionChecked(MenuOption::Overlays, true); // If there is a preferred plugin, we probably messed it up with the menu settings, so fix it. auto pluginManager = PluginManager::getInstance(); auto plugins = pluginManager->getPreferredDisplayPlugins(); - auto menu = Menu::getInstance(); if (plugins.size() > 0) { for (auto plugin : plugins) { if (auto action = menu->getActionForOption(plugin->getName())) { @@ -5184,6 +5145,21 @@ void Application::cameraModeChanged() { cameraMenuChanged(); } +void Application::changeViewAsNeeded(float boomLength) { + // Switch between first and third person views as needed + // This is called when the boom length has changed + bool boomLengthGreaterThanMinimum = (boomLength > MyAvatar::ZOOM_MIN); + + if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON && boomLengthGreaterThanMinimum) { + Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, false); + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); + cameraMenuChanged(); + } else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON && !boomLengthGreaterThanMinimum) { + Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, true); + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, false); + cameraMenuChanged(); + } +} void Application::cameraMenuChanged() { auto menu = Menu::getInstance(); @@ -5462,7 +5438,7 @@ void Application::update(float deltaTime) { // process octree stats packets are sent in between full sends of a scene (this isn't currently true). // We keep physics disabled until we've received a full scene and everything near the avatar in that // scene is ready to compute its collision shape. - if (nearbyEntitiesAreReadyForPhysics()) { + if (nearbyEntitiesAreReadyForPhysics() && getMyAvatar()->isReadyForPhysics()) { _physicsEnabled = true; getMyAvatar()->updateMotionBehaviorFromMenu(); } @@ -5820,7 +5796,7 @@ void Application::update(float deltaTime) { viewIsDifferentEnough = true; } - + // if it's been a while since our last query or the view has significantly changed then send a query, otherwise suppress it static const std::chrono::seconds MIN_PERIOD_BETWEEN_QUERIES { 3 }; auto now = SteadyClock::now(); @@ -6188,7 +6164,9 @@ void Application::updateWindowTitle() const { auto nodeList = DependencyManager::get(); auto accountManager = DependencyManager::get(); - QString buildVersion = " (build " + applicationVersion() + ")"; + QString buildVersion = " - " + + (BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Stable ? QString("Version") : QString("Build")) + + " " + applicationVersion(); QString loginStatus = accountManager->isLoggedIn() ? "" : " (NOT LOGGED IN)"; @@ -7634,18 +7612,18 @@ void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRa }); } -void Application::takeSecondaryCameraSnapshot(const QString& filename) { - postLambdaEvent([filename, this] { +void Application::takeSecondaryCameraSnapshot(const bool& notify, const QString& filename) { + postLambdaEvent([notify, filename, this] { QString snapshotPath = DependencyManager::get()->saveSnapshot(getActiveDisplayPlugin()->getSecondaryCameraScreenshot(), filename, TestScriptingInterface::getInstance()->getTestResultsLocation()); - emit DependencyManager::get()->stillSnapshotTaken(snapshotPath, true); + emit DependencyManager::get()->stillSnapshotTaken(snapshotPath, notify); }); } -void Application::takeSecondaryCamera360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const QString& filename) { - postLambdaEvent([filename, cubemapOutputFormat, cameraPosition] { - DependencyManager::get()->save360Snapshot(cameraPosition, cubemapOutputFormat, filename); +void Application::takeSecondaryCamera360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const bool& notify, const QString& filename) { + postLambdaEvent([notify, filename, cubemapOutputFormat, cameraPosition] { + DependencyManager::get()->save360Snapshot(cameraPosition, cubemapOutputFormat, notify, filename); }); } @@ -7749,7 +7727,7 @@ void Application::sendLambdaEvent(const std::function& f) { } else { LambdaEvent event(f); QCoreApplication::sendEvent(this, &event); - } + } } void Application::initPlugins(const QStringList& arguments) { @@ -7972,7 +7950,7 @@ void Application::setDisplayPlugin(DisplayPluginPointer newDisplayPlugin) { } // FIXME don't have the application directly set the state of the UI, - // instead emit a signal that the display plugin is changing and let + // instead emit a signal that the display plugin is changing and let // the desktop lock itself. Reduces coupling between the UI and display // plugins auto offscreenUi = DependencyManager::get(); @@ -8081,7 +8059,6 @@ void Application::switchDisplayMode() { setActiveDisplayPlugin(DESKTOP_DISPLAY_PLUGIN_NAME); startHMDStandBySession(); } - emit activeDisplayPluginChanged(); } _previousHMDWornStatus = currentHMDWornStatus; } diff --git a/interface/src/Application.h b/interface/src/Application.h index ee97077002..236edf8bb0 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -281,8 +281,11 @@ public: float getGameLoopRate() const { return _gameLoopCounter.rate(); } void takeSnapshot(bool notify, bool includeAnimated = false, float aspectRatio = 0.0f, const QString& filename = QString()); - void takeSecondaryCameraSnapshot(const QString& filename = QString()); - void takeSecondaryCamera360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const QString& filename = QString()); + void takeSecondaryCameraSnapshot(const bool& notify, const QString& filename = QString()); + void takeSecondaryCamera360Snapshot(const glm::vec3& cameraPosition, + const bool& cubemapOutputFormat, + const bool& notify, + const QString& filename = QString()); void shareSnapshot(const QString& filename, const QUrl& href = QUrl("")); @@ -417,6 +420,8 @@ public slots: void updateVerboseLogging(); + void changeViewAsNeeded(float boomLength); + private slots: void onDesktopRootItemCreated(QQuickItem* qmlContext); void onDesktopRootContextCreated(QQmlContext* qmlContext); @@ -649,7 +654,7 @@ private: quint64 _lastFaceTrackerUpdate; render::ScenePointer _main3DScene{ new render::Scene(glm::vec3(-0.5f * (float)TREE_SCALE), (float)TREE_SCALE) }; - render::EnginePointer _renderEngine{ new render::Engine() }; + render::EnginePointer _renderEngine{ new render::RenderEngine() }; gpu::ContextPointer _gpuContext; // initialized during window creation mutable QMutex _renderArgsMutex{ QMutex::Recursive }; diff --git a/interface/src/Application_render.cpp b/interface/src/Application_render.cpp index 2a16e8c33c..2208b3187c 100644 --- a/interface/src/Application_render.cpp +++ b/interface/src/Application_render.cpp @@ -30,9 +30,6 @@ void Application::editRenderArgs(RenderArgsEditor editor) { void Application::paintGL() { // Some plugins process message events, allowing paintGL to be called reentrantly. - if (_aboutToQuit || _window->isMinimized()) { - return; - } _renderFrameCount++; _lastTimeRendered.start(); diff --git a/interface/src/Crashpad.cpp b/interface/src/Crashpad.cpp index 45f1d0778f..88651925d5 100644 --- a/interface/src/Crashpad.cpp +++ b/interface/src/Crashpad.cpp @@ -18,6 +18,7 @@ #if HAS_CRASHPAD #include +#include #include #include @@ -69,6 +70,8 @@ bool startCrashHandler() { annotations["token"] = BACKTRACE_TOKEN; annotations["format"] = "minidump"; annotations["version"] = BuildInfo::VERSION.toStdString(); + annotations["build_number"] = BuildInfo::BUILD_NUMBER.toStdString(); + annotations["build_type"] = BuildInfo::BUILD_TYPE_STRING.toStdString(); arguments.push_back("--no-rate-limit"); diff --git a/interface/src/FancyCamera.h b/interface/src/FancyCamera.h index bee21bad22..4ca073fb4f 100644 --- a/interface/src/FancyCamera.h +++ b/interface/src/FancyCamera.h @@ -25,7 +25,7 @@ class FancyCamera : public Camera { // FIXME: JSDoc 3.5.5 doesn't augment @property definitions. The following definition is repeated in Camera.h. /**jsdoc - * @property cameraEntity {Uuid} The ID of the entity that the camera position and orientation follow when the camera is in + * @property {Uuid} cameraEntity The ID of the entity that the camera position and orientation follow when the camera is in * entity mode. */ Q_PROPERTY(QUuid cameraEntity READ getCameraEntity WRITE setCameraEntity) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index f55c389a1f..d55490998d 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -103,16 +103,32 @@ Menu::Menu() { editMenu->addSeparator(); // Edit > Cut - addActionToQMenuAndActionHash(editMenu, "Cut", Qt::CTRL | Qt::Key_X); + auto cutAction = addActionToQMenuAndActionHash(editMenu, "Cut", QKeySequence::Cut); + connect(cutAction, &QAction::triggered, [] { + QKeyEvent* keyEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_X, Qt::ControlModifier); + QCoreApplication::postEvent(QCoreApplication::instance(), keyEvent); + }); // Edit > Copy - addActionToQMenuAndActionHash(editMenu, "Copy", Qt::CTRL | Qt::Key_C); + auto copyAction = addActionToQMenuAndActionHash(editMenu, "Copy", QKeySequence::Copy); + connect(copyAction, &QAction::triggered, [] { + QKeyEvent* keyEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_C, Qt::ControlModifier); + QCoreApplication::postEvent(QCoreApplication::instance(), keyEvent); + }); // Edit > Paste - addActionToQMenuAndActionHash(editMenu, "Paste", Qt::CTRL | Qt::Key_V); + auto pasteAction = addActionToQMenuAndActionHash(editMenu, "Paste", QKeySequence::Paste); + connect(pasteAction, &QAction::triggered, [] { + QKeyEvent* keyEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_V, Qt::ControlModifier); + QCoreApplication::postEvent(QCoreApplication::instance(), keyEvent); + }); // Edit > Delete - addActionToQMenuAndActionHash(editMenu, "Delete", Qt::Key_Delete); + auto deleteAction = addActionToQMenuAndActionHash(editMenu, "Delete", QKeySequence::Delete); + connect(deleteAction, &QAction::triggered, [] { + QKeyEvent* keyEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Delete, Qt::ControlModifier); + QCoreApplication::postEvent(QCoreApplication::instance(), keyEvent); + }); editMenu->addSeparator(); @@ -201,21 +217,21 @@ Menu::Menu() { // View > First Person auto firstPersonAction = cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash( - viewMenu, MenuOption::FirstPerson, Qt::Key_1, + viewMenu, MenuOption::FirstPerson, 0, true, qApp, SLOT(cameraMenuChanged()))); firstPersonAction->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(cameraModeGroup)); // View > Third Person auto thirdPersonAction = cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash( - viewMenu, MenuOption::ThirdPerson, Qt::Key_3, + viewMenu, MenuOption::ThirdPerson, 0, false, qApp, SLOT(cameraMenuChanged()))); thirdPersonAction->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(cameraModeGroup)); // View > Mirror auto viewMirrorAction = cameraModeGroup->addAction(addCheckableActionToQMenuAndActionHash( - viewMenu, MenuOption::FullscreenMirror, Qt::Key_2, + viewMenu, MenuOption::FullscreenMirror, 0, false, qApp, SLOT(cameraMenuChanged()))); viewMirrorAction->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(cameraModeGroup)); @@ -572,6 +588,10 @@ Menu::Menu() { }); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::FixGaze, 0, false); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ToggleHipsFollowing, 0, false, + avatar.get(), SLOT(setToggleHips(bool))); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AnimDebugDrawBaseOfSupport, 0, false, + avatar.get(), SLOT(setEnableDebugDrawBaseOfSupport(bool))); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AnimDebugDrawDefaultPose, 0, false, avatar.get(), SLOT(setEnableDebugDrawDefaultPose(bool))); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AnimDebugDrawAnimPose, 0, false, diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 8569911cbd..6fb089acd8 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -30,6 +30,7 @@ namespace MenuOption { const QString AddressBar = "Show Address Bar"; const QString Animations = "Animations..."; const QString AnimDebugDrawAnimPose = "Debug Draw Animation"; + const QString AnimDebugDrawBaseOfSupport = "Debug Draw Base of Support"; const QString AnimDebugDrawDefaultPose = "Debug Draw Default Pose"; const QString AnimDebugDrawPosition= "Debug Draw Position"; const QString AskToResetSettings = "Ask To Reset Settings on Start"; @@ -202,6 +203,7 @@ namespace MenuOption { const QString ThirdPerson = "Third Person"; const QString ThreePointCalibration = "3 Point Calibration"; const QString ThrottleFPSIfNotFocus = "Throttle FPS If Not Focus"; // FIXME - this value duplicated in Basic2DWindowOpenGLDisplayPlugin.cpp + const QString ToggleHipsFollowing = "Toggle Hips Following"; const QString ToolWindow = "Tool Window"; const QString TransmitterDrive = "Transmitter Drive"; const QString TurnWithHead = "Turn using Head"; diff --git a/interface/src/SecondaryCamera.cpp b/interface/src/SecondaryCamera.cpp index db51cf99c8..b9a767f700 100644 --- a/interface/src/SecondaryCamera.cpp +++ b/interface/src/SecondaryCamera.cpp @@ -9,11 +9,13 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "Application.h" #include "SecondaryCamera.h" -#include -#include + #include +#include +#include + +#include "Application.h" using RenderArgsPointer = std::shared_ptr; diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 094b3bb67b..4d133706e6 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -104,6 +104,9 @@ void AvatarManager::updateMyAvatar(float deltaTime) { PerformanceWarning warn(showWarnings, "AvatarManager::updateMyAvatar()"); _myAvatar->update(deltaTime); + render::Transaction transaction; + _myAvatar->updateRenderItem(transaction); + qApp->getMain3DScene()->enqueueTransaction(transaction); quint64 now = usecTimestampNow(); quint64 dt = now - _lastSendAvatarDataTime; @@ -465,13 +468,14 @@ void AvatarManager::updateAvatarRenderStatus(bool shouldRenderAvatars) { _shouldRender = shouldRenderAvatars; const render::ScenePointer& scene = qApp->getMain3DScene(); render::Transaction transaction; + auto avatarHashCopy = getHashCopy(); if (_shouldRender) { - for (auto avatarData : _avatarHash) { + for (auto avatarData : avatarHashCopy) { auto avatar = std::static_pointer_cast(avatarData); avatar->addToScene(avatar, scene, transaction); } } else { - for (auto avatarData : _avatarHash) { + for (auto avatarData : avatarHashCopy) { auto avatar = std::static_pointer_cast(avatarData); avatar->removeFromScene(avatar, scene, transaction); } @@ -511,7 +515,8 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersectionVector(const Pic glm::vec3 normDirection = glm::normalize(ray.direction); - for (auto avatarData : _avatarHash) { + auto avatarHashCopy = getHashCopy(); + for (auto avatarData : avatarHashCopy) { auto avatar = std::static_pointer_cast(avatarData); if ((avatarsToInclude.size() > 0 && !avatarsToInclude.contains(avatar->getID())) || (avatarsToDiscard.size() > 0 && avatarsToDiscard.contains(avatar->getID()))) { diff --git a/interface/src/avatar/AvatarMotionState.cpp b/interface/src/avatar/AvatarMotionState.cpp index beb7e34439..4c5aaacb95 100644 --- a/interface/src/avatar/AvatarMotionState.cpp +++ b/interface/src/avatar/AvatarMotionState.cpp @@ -21,17 +21,6 @@ AvatarMotionState::AvatarMotionState(AvatarSharedPointer avatar, const btCollisi _type = MOTIONSTATE_TYPE_AVATAR; } -void AvatarMotionState::handleEasyChanges(uint32_t& flags) { - ObjectMotionState::handleEasyChanges(flags); - if (flags & Simulation::DIRTY_PHYSICS_ACTIVATION && !_body->isActive()) { - _body->activate(); - } -} - -bool AvatarMotionState::handleHardAndEasyChanges(uint32_t& flags, PhysicsEngine* engine) { - return ObjectMotionState::handleHardAndEasyChanges(flags, engine); -} - AvatarMotionState::~AvatarMotionState() { assert(_avatar); _avatar = nullptr; @@ -57,9 +46,6 @@ PhysicsMotionType AvatarMotionState::computePhysicsMotionType() const { const btCollisionShape* AvatarMotionState::computeNewShape() { ShapeInfo shapeInfo; std::static_pointer_cast(_avatar)->computeShapeInfo(shapeInfo); - glm::vec3 halfExtents = shapeInfo.getHalfExtents(); - halfExtents.y = 0.0f; - _diameter = 2.0f * glm::length(halfExtents); return getShapeManager()->getShape(shapeInfo); } @@ -74,31 +60,25 @@ void AvatarMotionState::getWorldTransform(btTransform& worldTrans) const { worldTrans.setRotation(glmToBullet(getObjectRotation())); if (_body) { _body->setLinearVelocity(glmToBullet(getObjectLinearVelocity())); - _body->setAngularVelocity(glmToBullet(getObjectAngularVelocity())); + _body->setAngularVelocity(glmToBullet(getObjectLinearVelocity())); } } // virtual void AvatarMotionState::setWorldTransform(const btTransform& worldTrans) { + // HACK: The PhysicsEngine does not actually move OTHER avatars -- instead it slaves their local RigidBody to the transform + // as specified by a remote simulation. However, to give the remote simulation time to respond to our own objects we tie + // the other avatar's body to its true position with a simple spring. This is a HACK that will have to be improved later. const float SPRING_TIMESCALE = 0.5f; float tau = PHYSICS_ENGINE_FIXED_SUBSTEP / SPRING_TIMESCALE; btVector3 currentPosition = worldTrans.getOrigin(); - btVector3 offsetToTarget = glmToBullet(getObjectPosition()) - currentPosition; - float distance = offsetToTarget.length(); - if ((1.0f - tau) * distance > _diameter) { - // the avatar body is far from its target --> slam position - btTransform newTransform; - newTransform.setOrigin(currentPosition + offsetToTarget); - newTransform.setRotation(glmToBullet(getObjectRotation())); - _body->setWorldTransform(newTransform); - _body->setLinearVelocity(glmToBullet(getObjectLinearVelocity())); - _body->setAngularVelocity(glmToBullet(getObjectAngularVelocity())); - } else { - // the avatar body is near its target --> slam velocity - btVector3 velocity = glmToBullet(getObjectLinearVelocity()) + (1.0f / SPRING_TIMESCALE) * offsetToTarget; - _body->setLinearVelocity(velocity); - _body->setAngularVelocity(glmToBullet(getObjectAngularVelocity())); - } + btVector3 targetPosition = glmToBullet(getObjectPosition()); + btTransform newTransform; + newTransform.setOrigin((1.0f - tau) * currentPosition + tau * targetPosition); + newTransform.setRotation(glmToBullet(getObjectRotation())); + _body->setWorldTransform(newTransform); + _body->setLinearVelocity(glmToBullet(getObjectLinearVelocity())); + _body->setAngularVelocity(glmToBullet(getObjectLinearVelocity())); } // These pure virtual methods must be implemented for each MotionState type @@ -160,13 +140,8 @@ QUuid AvatarMotionState::getSimulatorID() const { } // virtual -void AvatarMotionState::computeCollisionGroupAndMask(int16_t& group, int16_t& mask) const { +void AvatarMotionState::computeCollisionGroupAndMask(int32_t& group, int32_t& mask) const { group = BULLET_COLLISION_GROUP_OTHER_AVATAR; mask = Physics::getDefaultCollisionMask(group); } -// virtual -float AvatarMotionState::getMass() const { - return std::static_pointer_cast(_avatar)->computeMass(); -} - diff --git a/interface/src/avatar/AvatarMotionState.h b/interface/src/avatar/AvatarMotionState.h index 73fb853312..07e8102752 100644 --- a/interface/src/avatar/AvatarMotionState.h +++ b/interface/src/avatar/AvatarMotionState.h @@ -23,9 +23,6 @@ class AvatarMotionState : public ObjectMotionState { public: AvatarMotionState(AvatarSharedPointer avatar, const btCollisionShape* shape); - virtual void handleEasyChanges(uint32_t& flags) override; - virtual bool handleHardAndEasyChanges(uint32_t& flags, PhysicsEngine* engine) override; - virtual PhysicsMotionType getMotionType() const override { return _motionType; } virtual uint32_t getIncomingDirtyFlags() override; @@ -65,9 +62,7 @@ public: void addDirtyFlags(uint32_t flags) { _dirtyFlags |= flags; } - virtual void computeCollisionGroupAndMask(int16_t& group, int16_t& mask) const override; - - virtual float getMass() const override; + virtual void computeCollisionGroupAndMask(int32_t& group, int32_t& mask) const override; friend class AvatarManager; friend class Avatar; @@ -81,7 +76,6 @@ protected: virtual const btCollisionShape* computeNewShape() override; AvatarSharedPointer _avatar; - float _diameter { 0.0f }; uint32_t _dirtyFlags; }; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 5fb4e80b80..d57905ee33 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -52,6 +52,7 @@ #include "MyHead.h" #include "MySkeletonModel.h" +#include "AnimUtil.h" #include "Application.h" #include "AvatarManager.h" #include "AvatarActionHold.h" @@ -422,12 +423,12 @@ void MyAvatar::update(float deltaTime) { } #ifdef DEBUG_DRAW_HMD_MOVING_AVERAGE - glm::vec3 p = transformPoint(getSensorToWorldMatrix(), getControllerPoseInAvatarFrame(controller::Pose::HEAD) * - glm::vec3(_headControllerFacingMovingAverage.x, 0.0f, _headControllerFacingMovingAverage.y)); - DebugDraw::getInstance().addMarker("facing-avg", getOrientation(), p, glm::vec4(1.0f)); - p = transformPoint(getSensorToWorldMatrix(), getHMDSensorPosition() + - glm::vec3(_headControllerFacing.x, 0.0f, _headControllerFacing.y)); - DebugDraw::getInstance().addMarker("facing", getOrientation(), p, glm::vec4(1.0f)); + auto sensorHeadPose = getControllerPoseInSensorFrame(controller::Action::HEAD); + glm::vec3 worldHeadPos = transformPoint(getSensorToWorldMatrix(), sensorHeadPose.getTranslation()); + glm::vec3 worldFacingAverage = transformVectorFast(getSensorToWorldMatrix(), glm::vec3(_headControllerFacingMovingAverage.x, 0.0f, _headControllerFacingMovingAverage.y)); + glm::vec3 worldFacing = transformVectorFast(getSensorToWorldMatrix(), glm::vec3(_headControllerFacing.x, 0.0f, _headControllerFacing.y)); + DebugDraw::getInstance().drawRay(worldHeadPos, worldHeadPos + worldFacing, glm::vec4(0.0f, 1.0f, 0.0f, 1.0f)); + DebugDraw::getInstance().drawRay(worldHeadPos, worldHeadPos + worldFacingAverage, glm::vec4(0.0f, 0.0f, 1.0f, 1.0f)); #endif if (_goToPending) { @@ -654,8 +655,8 @@ void MyAvatar::simulate(float deltaTime) { if (success) { moveOperator.addEntityToMoveList(entity, newCube); } - // send an edit packet to update the entity-server about the queryAABox. If it's an - // avatar-entity, don't. + // send an edit packet to update the entity-server about the queryAABox + // unless it is client-only if (packetSender && !entity->getClientOnly()) { EntityItemProperties properties = entity->getProperties(); properties.setQueryAACubeDirty(); @@ -663,6 +664,17 @@ void MyAvatar::simulate(float deltaTime) { packetSender->queueEditEntityMessage(PacketType::EntityEdit, entityTree, entity->getID(), properties); entity->setLastBroadcast(usecTimestampNow()); + + entity->forEachDescendant([&](SpatiallyNestablePointer descendant) { + EntityItemPointer entityDescendant = std::static_pointer_cast(descendant); + if (!entityDescendant->getClientOnly() && descendant->updateQueryAACube()) { + EntityItemProperties descendantProperties; + descendantProperties.setQueryAACube(descendant->getQueryAACube()); + descendantProperties.setLastEdited(now); + packetSender->queueEditEntityMessage(PacketType::EntityEdit, entityTree, entityDescendant->getID(), descendantProperties); + entityDescendant->setLastBroadcast(now); // for debug/physics status icons + } + }); } } }); @@ -701,7 +713,8 @@ void MyAvatar::updateFromHMDSensorMatrix(const glm::mat4& hmdSensorMatrix) { _hmdSensorOrientation = glmExtractRotation(hmdSensorMatrix); auto headPose = getControllerPoseInSensorFrame(controller::Action::HEAD); if (headPose.isValid()) { - _headControllerFacing = getFacingDir2D(headPose.rotation); + glm::quat bodyOrientation = computeBodyFacingFromHead(headPose.rotation, Vectors::UNIT_Y); + _headControllerFacing = getFacingDir2D(bodyOrientation); } else { _headControllerFacing = glm::vec2(1.0f, 0.0f); } @@ -1068,6 +1081,22 @@ float loadSetting(Settings& settings, const QString& name, float defaultValue) { return value; } +void MyAvatar::setToggleHips(bool followHead) { + _follow.setToggleHipsFollowing(followHead); +} + +void MyAvatar::FollowHelper::setToggleHipsFollowing(bool followHead) { + _toggleHipsFollowing = followHead; +} + +bool MyAvatar::FollowHelper::getToggleHipsFollowing() const { + return _toggleHipsFollowing; +} + +void MyAvatar::setEnableDebugDrawBaseOfSupport(bool isEnabled) { + _enableDebugDrawBaseOfSupport = isEnabled; +} + void MyAvatar::setEnableDebugDrawDefaultPose(bool isEnabled) { _enableDebugDrawDefaultPose = isEnabled; @@ -1127,7 +1156,11 @@ void MyAvatar::setEnableDebugDrawIKChains(bool isEnabled) { } void MyAvatar::setEnableMeshVisible(bool isEnabled) { - _skeletonModel->setVisibleInScene(isEnabled, qApp->getMain3DScene(), render::ItemKey::TAG_BITS_NONE, true); + return Avatar::setEnableMeshVisible(isEnabled); +} + +bool MyAvatar::getEnableMeshVisible() const { + return Avatar::getEnableMeshVisible(); } void MyAvatar::setEnableInverseKinematics(bool isEnabled) { @@ -1195,6 +1228,8 @@ void MyAvatar::loadData() { settings.endGroup(); setEnableMeshVisible(Menu::getInstance()->isOptionChecked(MenuOption::MeshVisible)); + _follow.setToggleHipsFollowing (Menu::getInstance()->isOptionChecked(MenuOption::ToggleHipsFollowing)); + setEnableDebugDrawBaseOfSupport(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawBaseOfSupport)); setEnableDebugDrawDefaultPose(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawDefaultPose)); setEnableDebugDrawAnimPose(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawAnimPose)); setEnableDebugDrawPosition(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawPosition)); @@ -1479,7 +1514,10 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { _skeletonModelChangeCount++; int skeletonModelChangeCount = _skeletonModelChangeCount; Avatar::setSkeletonModelURL(skeletonModelURL); - _skeletonModel->setVisibleInScene(true, qApp->getMain3DScene(), render::ItemKey::TAG_BITS_NONE, true); + _skeletonModel->setTagMask(render::hifi::TAG_NONE); + _skeletonModel->setGroupCulled(true); + _skeletonModel->setVisibleInScene(true, qApp->getMain3DScene()); + _headBoneSet.clear(); _cauterizationNeedsUpdate = true; @@ -2054,14 +2092,12 @@ void MyAvatar::preDisplaySide(const RenderArgs* renderArgs) { _attachmentData[i].jointName.compare("RightEye", Qt::CaseInsensitive) == 0 || _attachmentData[i].jointName.compare("HeadTop_End", Qt::CaseInsensitive) == 0 || _attachmentData[i].jointName.compare("Face", Qt::CaseInsensitive) == 0) { - uint8_t modelRenderTagBits = shouldDrawHead ? render::ItemKey::TAG_BITS_0 : render::ItemKey::TAG_BITS_NONE; - modelRenderTagBits |= render::ItemKey::TAG_BITS_1; - _attachmentModels[i]->setVisibleInScene(true, qApp->getMain3DScene(), - modelRenderTagBits, false); + uint8_t modelRenderTagBits = shouldDrawHead ? render::hifi::TAG_ALL_VIEWS : render::hifi::TAG_SECONDARY_VIEW; - uint8_t castShadowRenderTagBits = render::ItemKey::TAG_BITS_0 | render::ItemKey::TAG_BITS_1; - _attachmentModels[i]->setCanCastShadow(true, qApp->getMain3DScene(), - castShadowRenderTagBits, false); + _attachmentModels[i]->setTagMask(modelRenderTagBits); + _attachmentModels[i]->setGroupCulled(false); + _attachmentModels[i]->setCanCastShadow(true); + _attachmentModels[i]->setVisibleInScene(true, qApp->getMain3DScene()); } } } @@ -2081,6 +2117,31 @@ bool MyAvatar::shouldRenderHead(const RenderArgs* renderArgs) const { return !defaultMode || !firstPerson || !insideHead; } +void MyAvatar::setHasScriptedBlendshapes(bool hasScriptedBlendshapes) { + if (hasScriptedBlendshapes == _hasScriptedBlendShapes) { + return; + } + if (!hasScriptedBlendshapes) { + // send a forced avatarData update to make sure the script can send neutal blendshapes on unload + // without having to wait for the update loop, make sure _hasScriptedBlendShapes is still true + // before sending the update, or else it won't send the neutal blendshapes to the receiving clients + sendAvatarDataPacket(true); + } + _hasScriptedBlendShapes = hasScriptedBlendshapes; +} + +void MyAvatar::setHasProceduralBlinkFaceMovement(bool hasProceduralBlinkFaceMovement) { + _headData->setHasProceduralBlinkFaceMovement(hasProceduralBlinkFaceMovement); +} + +void MyAvatar::setHasProceduralEyeFaceMovement(bool hasProceduralEyeFaceMovement) { + _headData->setHasProceduralEyeFaceMovement(hasProceduralEyeFaceMovement); +} + +void MyAvatar::setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement) { + _headData->setHasAudioEnabledFaceMovement(hasAudioEnabledFaceMovement); +} + void MyAvatar::updateOrientation(float deltaTime) { // Smoothly rotate body with arrow keys @@ -2245,9 +2306,15 @@ void MyAvatar::updateActionMotor(float deltaTime) { _actionMotorVelocity = getSensorToWorldScale() * (_walkSpeed.get() * _walkSpeedScalar) * direction; } + float previousBoomLength = _boomLength; float boomChange = getDriveKey(ZOOM); _boomLength += 2.0f * _boomLength * boomChange + boomChange * boomChange; _boomLength = glm::clamp(_boomLength, ZOOM_MIN, ZOOM_MAX); + + // May need to change view if boom length has changed + if (previousBoomLength != _boomLength) { + qApp->changeViewAsNeeded(_boomLength); + } } void MyAvatar::updatePosition(float deltaTime) { @@ -2388,11 +2455,16 @@ void MyAvatar::restrictScaleFromDomainSettings(const QJsonObject& domainSettings if (_domainMinimumHeight > _domainMaximumHeight) { std::swap(_domainMinimumHeight, _domainMaximumHeight); } + // Set avatar current scale Settings settings; settings.beginGroup("Avatar"); _targetScale = loadSetting(settings, "scale", 1.0f); + // clamp the desired _targetScale by the domain limits NOW, don't try to gracefully animate. Because + // this might cause our avatar to become embedded in the terrain. + _targetScale = getDomainLimitedScale(); + qCDebug(interfaceapp) << "This domain requires a minimum avatar scale of " << _domainMinimumHeight << " and a maximum avatar scale of " << _domainMaximumHeight; @@ -2401,6 +2473,8 @@ void MyAvatar::restrictScaleFromDomainSettings(const QJsonObject& domainSettings setModelScale(_targetScale); rebuildCollisionShape(); settings.endGroup(); + + _haveReceivedHeightLimitsFromDomain = true; } void MyAvatar::leaveDomain() { @@ -2418,6 +2492,7 @@ void MyAvatar::saveAvatarScale() { void MyAvatar::clearScaleRestriction() { _domainMinimumHeight = MIN_AVATAR_HEIGHT; _domainMaximumHeight = MAX_AVATAR_HEIGHT; + _haveReceivedHeightLimitsFromDomain = false; } void MyAvatar::goToLocation(const QVariant& propertiesVar) { @@ -2535,8 +2610,12 @@ bool MyAvatar::safeLanding(const glm::vec3& position) { // If position is not reliably safe from being stuck by physics, answer true and place a candidate better position in betterPositionOut. bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& betterPositionOut) { + // We begin with utilities and tests. The Algorithm in four parts is below. - auto halfHeight = _characterController.getCapsuleHalfHeight() + _characterController.getCapsuleRadius(); + // NOTE: we use estimated avatar height here instead of the bullet capsule halfHeight, because + // the domain avatar height limiting might not have taken effect yet on the actual bullet shape. + auto halfHeight = 0.5f * getHeight(); + if (halfHeight == 0) { return false; // zero height avatar } @@ -2545,14 +2624,13 @@ bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& bette return false; // no entity tree } // More utilities. - const auto offset = getWorldOrientation() *_characterController.getCapsuleLocalOffset(); - const auto capsuleCenter = positionIn + offset; + const auto capsuleCenter = positionIn; const auto up = _worldUpDirection, down = -up; glm::vec3 upperIntersection, upperNormal, lowerIntersection, lowerNormal; EntityItemID upperId, lowerId; QVector include{}, ignore{}; auto mustMove = [&] { // Place bottom of capsule at the upperIntersection, and check again based on the capsule center. - betterPositionOut = upperIntersection + (up * halfHeight) - offset; + betterPositionOut = upperIntersection + (up * halfHeight); return true; }; auto findIntersection = [&](const glm::vec3& startPointIn, const glm::vec3& directionIn, glm::vec3& intersectionOut, EntityItemID& entityIdOut, glm::vec3& normalOut) { @@ -2572,7 +2650,7 @@ bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& bette EntityItemID entityID = entityTree->findRayIntersection(startPointIn, directionIn, include, ignore, visibleOnly, collidableOnly, precisionPicking, element, distance, face, normalOut, extraInfo, lockType, accurateResult); if (entityID.isNull()) { - return false; + return false; } intersectionOut = startPointIn + (directionIn * distance); entityIdOut = entityID; @@ -2598,7 +2676,7 @@ bool MyAvatar::requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& bette // I.e., we are in a clearing between two objects. if (isDown(upperNormal) && isUp(lowerNormal)) { auto spaceBetween = glm::distance(upperIntersection, lowerIntersection); - const float halfHeightFactor = 2.5f; // Until case 5003 is fixed (and maybe after?), we need a fudge factor. Also account for content modelers not being precise. + const float halfHeightFactor = 2.25f; // Until case 5003 is fixed (and maybe after?), we need a fudge factor. Also account for content modelers not being precise. if (spaceBetween > (halfHeightFactor * halfHeight)) { // There is room for us to fit in that clearing. If there wasn't, physics would oscilate us between the objects above and below. // We're now going to iterate upwards through successive upperIntersections, testing to see if we're contained within the top surface of some entity. @@ -2797,6 +2875,7 @@ glm::mat4 MyAvatar::deriveBodyFromHMDSensor() const { auto headPose = getControllerPoseInSensorFrame(controller::Action::HEAD); if (headPose.isValid()) { headPosition = headPose.translation; + // AJT: TODO: can remove this Y_180 headOrientation = headPose.rotation * Quaternions::Y_180; } const glm::quat headOrientationYawOnly = cancelOutRollAndPitch(headOrientation); @@ -2819,6 +2898,8 @@ glm::mat4 MyAvatar::deriveBodyFromHMDSensor() const { // eyeToNeck offset is relative full HMD orientation. // while neckToRoot offset is only relative to HMDs yaw. // Y_180 is necessary because rig is z forward and hmdOrientation is -z forward + + // AJT: TODO: can remove this Y_180, if we remove the higher level one. glm::vec3 headToNeck = headOrientation * Quaternions::Y_180 * (localNeck - localHead); glm::vec3 neckToRoot = headOrientationYawOnly * Quaternions::Y_180 * -localNeck; @@ -2828,6 +2909,202 @@ glm::mat4 MyAvatar::deriveBodyFromHMDSensor() const { return createMatFromQuatAndPos(headOrientationYawOnly, bodyPos); } +// ease in function for dampening cg movement +static float slope(float num) { + const float CURVE_CONSTANT = 1.0f; + float ret = 1.0f; + if (num > 0.0f) { + ret = 1.0f - (1.0f / (1.0f + CURVE_CONSTANT * num)); + } + return ret; +} + +// This function gives a soft clamp at the edge of the base of support +// dampenCgMovement returns the damped cg value in Avatar space. +// cgUnderHeadHandsAvatarSpace is also in Avatar space +// baseOfSupportScale is based on the height of the user +static glm::vec3 dampenCgMovement(glm::vec3 cgUnderHeadHandsAvatarSpace, float baseOfSupportScale) { + float distanceFromCenterZ = cgUnderHeadHandsAvatarSpace.z; + float distanceFromCenterX = cgUnderHeadHandsAvatarSpace.x; + + // In the forward direction we need a different scale because forward is in + // the direction of the hip extensor joint, which means bending usually happens + // well before reaching the edge of the base of support. + const float clampFront = DEFAULT_AVATAR_SUPPORT_BASE_FRONT * DEFAULT_AVATAR_FORWARD_DAMPENING_FACTOR * baseOfSupportScale; + float clampBack = DEFAULT_AVATAR_SUPPORT_BASE_BACK * DEFAULT_AVATAR_LATERAL_DAMPENING_FACTOR * baseOfSupportScale; + float clampLeft = DEFAULT_AVATAR_SUPPORT_BASE_LEFT * DEFAULT_AVATAR_LATERAL_DAMPENING_FACTOR * baseOfSupportScale; + float clampRight = DEFAULT_AVATAR_SUPPORT_BASE_RIGHT * DEFAULT_AVATAR_LATERAL_DAMPENING_FACTOR * baseOfSupportScale; + glm::vec3 dampedCg(0.0f, 0.0f, 0.0f); + + // find the damped z coord of the cg + if (cgUnderHeadHandsAvatarSpace.z < 0.0f) { + // forward displacement + dampedCg.z = slope(fabs(distanceFromCenterZ / clampFront)) * clampFront; + } else { + // backwards displacement + dampedCg.z = slope(fabs(distanceFromCenterZ / clampBack)) * clampBack; + } + + // find the damped x coord of the cg + if (cgUnderHeadHandsAvatarSpace.x > 0.0f) { + // right of center + dampedCg.x = slope(fabs(distanceFromCenterX / clampRight)) * clampRight; + } else { + // left of center + dampedCg.x = slope(fabs(distanceFromCenterX / clampLeft)) * clampLeft; + } + return dampedCg; +} + +// computeCounterBalance returns the center of gravity in Avatar space +glm::vec3 MyAvatar::computeCounterBalance() const { + struct JointMass { + QString name; + float weight; + glm::vec3 position; + JointMass() {}; + JointMass(QString n, float w, glm::vec3 p) { + name = n; + weight = w; + position = p; + } + }; + + // init the body part weights + JointMass cgHeadMass(QString("Head"), DEFAULT_AVATAR_HEAD_MASS, glm::vec3(0.0f, 0.0f, 0.0f)); + JointMass cgLeftHandMass(QString("LeftHand"), DEFAULT_AVATAR_LEFTHAND_MASS, glm::vec3(0.0f, 0.0f, 0.0f)); + JointMass cgRightHandMass(QString("RightHand"), DEFAULT_AVATAR_RIGHTHAND_MASS, glm::vec3(0.0f, 0.0f, 0.0f)); + glm::vec3 tposeHead = DEFAULT_AVATAR_HEAD_POS; + glm::vec3 tposeHips = glm::vec3(0.0f, 0.0f, 0.0f); + + if (_skeletonModel->getRig().indexOfJoint(cgHeadMass.name) != -1) { + cgHeadMass.position = getAbsoluteJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint(cgHeadMass.name)); + tposeHead = getAbsoluteDefaultJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint(cgHeadMass.name)); + } + if (_skeletonModel->getRig().indexOfJoint(cgLeftHandMass.name) != -1) { + cgLeftHandMass.position = getAbsoluteJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint(cgLeftHandMass.name)); + } else { + cgLeftHandMass.position = DEFAULT_AVATAR_LEFTHAND_POS; + } + if (_skeletonModel->getRig().indexOfJoint(cgRightHandMass.name) != -1) { + cgRightHandMass.position = getAbsoluteJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint(cgRightHandMass.name)); + } else { + cgRightHandMass.position = DEFAULT_AVATAR_RIGHTHAND_POS; + } + if (_skeletonModel->getRig().indexOfJoint("Hips") != -1) { + tposeHips = getAbsoluteDefaultJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint("Hips")); + } + + // find the current center of gravity position based on head and hand moments + glm::vec3 sumOfMoments = (cgHeadMass.weight * cgHeadMass.position) + (cgLeftHandMass.weight * cgLeftHandMass.position) + (cgRightHandMass.weight * cgRightHandMass.position); + float totalMass = cgHeadMass.weight + cgLeftHandMass.weight + cgRightHandMass.weight; + + glm::vec3 currentCg = (1.0f / totalMass) * sumOfMoments; + currentCg.y = 0.0f; + // dampening the center of gravity, in effect, limits the value to the perimeter of the base of support + float baseScale = 1.0f; + if (getUserEyeHeight() > 0.0f) { + baseScale = getUserEyeHeight() / DEFAULT_AVATAR_EYE_HEIGHT; + } + glm::vec3 desiredCg = dampenCgMovement(currentCg, baseScale); + + // compute hips position to maintain desiredCg + glm::vec3 counterBalancedForHead = (totalMass + DEFAULT_AVATAR_HIPS_MASS) * desiredCg; + counterBalancedForHead -= sumOfMoments; + glm::vec3 counterBalancedCg = (1.0f / DEFAULT_AVATAR_HIPS_MASS) * counterBalancedForHead; + + // find the height of the hips + glm::vec3 xzDiff((cgHeadMass.position.x - counterBalancedCg.x), 0.0f, (cgHeadMass.position.z - counterBalancedCg.z)); + float headMinusHipXz = glm::length(xzDiff); + float headHipDefault = glm::length(tposeHead - tposeHips); + float hipHeight = 0.0f; + if (headHipDefault > headMinusHipXz) { + hipHeight = sqrtf((headHipDefault * headHipDefault) - (headMinusHipXz * headMinusHipXz)); + } + counterBalancedCg.y = (cgHeadMass.position.y - hipHeight); + + // this is to be sure that the feet don't lift off the floor. + // add 5 centimeters to allow for going up on the toes. + if (counterBalancedCg.y > (tposeHips.y + 0.05f)) { + // if the height is higher than default hips, clamp to default hips + counterBalancedCg.y = tposeHips.y + 0.05f; + } + return counterBalancedCg; +} + +// this function matches the hips rotation to the new cghips-head axis +// headOrientation, headPosition and hipsPosition are in avatar space +// returns the matrix of the hips in Avatar space +static glm::mat4 computeNewHipsMatrix(glm::quat headOrientation, glm::vec3 headPosition, glm::vec3 hipsPosition) { + + glm::quat bodyOrientation = computeBodyFacingFromHead(headOrientation, Vectors::UNIT_Y); + + const float MIX_RATIO = 0.3f; + glm::quat hipsRot = safeLerp(Quaternions::IDENTITY, bodyOrientation, MIX_RATIO); + glm::vec3 hipsFacing = hipsRot * Vectors::UNIT_Z; + + glm::vec3 spineVec = headPosition - hipsPosition; + glm::vec3 u, v, w; + generateBasisVectors(glm::normalize(spineVec), hipsFacing, u, v, w); + return glm::mat4(glm::vec4(w, 0.0f), + glm::vec4(u, 0.0f), + glm::vec4(v, 0.0f), + glm::vec4(hipsPosition, 1.0f)); +} + +static void drawBaseOfSupport(float baseOfSupportScale, float footLocal, glm::mat4 avatarToWorld) { + // scale the base of support based on user height + float clampFront = DEFAULT_AVATAR_SUPPORT_BASE_FRONT * baseOfSupportScale; + float clampBack = DEFAULT_AVATAR_SUPPORT_BASE_BACK * baseOfSupportScale; + float clampLeft = DEFAULT_AVATAR_SUPPORT_BASE_LEFT * baseOfSupportScale; + float clampRight = DEFAULT_AVATAR_SUPPORT_BASE_RIGHT * baseOfSupportScale; + float floor = footLocal + 0.05f; + + // transform the base of support corners to world space + glm::vec3 frontRight = transformPoint(avatarToWorld, { clampRight, floor, clampFront }); + glm::vec3 frontLeft = transformPoint(avatarToWorld, { clampLeft, floor, clampFront }); + glm::vec3 backRight = transformPoint(avatarToWorld, { clampRight, floor, clampBack }); + glm::vec3 backLeft = transformPoint(avatarToWorld, { clampLeft, floor, clampBack }); + + // draw the borders + const glm::vec4 rayColor = { 1.0f, 0.0f, 0.0f, 1.0f }; + DebugDraw::getInstance().drawRay(backLeft, frontLeft, rayColor); + DebugDraw::getInstance().drawRay(backLeft, backRight, rayColor); + DebugDraw::getInstance().drawRay(backRight, frontRight, rayColor); + DebugDraw::getInstance().drawRay(frontLeft, frontRight, rayColor); +} + +// this function finds the hips position using a center of gravity model that +// balances the head and hands with the hips over the base of support +// returns the rotation (-z forward) and position of the Avatar in Sensor space +glm::mat4 MyAvatar::deriveBodyUsingCgModel() const { + glm::mat4 sensorToWorldMat = getSensorToWorldMatrix(); + glm::mat4 worldToSensorMat = glm::inverse(sensorToWorldMat); + auto headPose = getControllerPoseInSensorFrame(controller::Action::HEAD); + + glm::mat4 sensorHeadMat = createMatFromQuatAndPos(headPose.rotation * Quaternions::Y_180, headPose.translation); + + // convert into avatar space + glm::mat4 avatarToWorldMat = getTransform().getMatrix(); + glm::mat4 avatarHeadMat = glm::inverse(avatarToWorldMat) * sensorToWorldMat * sensorHeadMat; + + if (_enableDebugDrawBaseOfSupport) { + float scaleBaseOfSupport = getUserEyeHeight() / DEFAULT_AVATAR_EYE_HEIGHT; + glm::vec3 rightFootPositionLocal = getAbsoluteJointTranslationInObjectFrame(_skeletonModel->getRig().indexOfJoint("RightFoot")); + drawBaseOfSupport(scaleBaseOfSupport, rightFootPositionLocal.y, avatarToWorldMat); + } + + // get the new center of gravity + const glm::vec3 cgHipsPosition = computeCounterBalance(); + + // find the new hips rotation using the new head-hips axis as the up axis + glm::mat4 avatarHipsMat = computeNewHipsMatrix(glmExtractRotation(avatarHeadMat), extractTranslation(avatarHeadMat), cgHipsPosition); + + // convert hips from avatar to sensor space + // The Y_180 is to convert from z forward to -z forward. + return worldToSensorMat * avatarToWorldMat * avatarHipsMat; +} + float MyAvatar::getUserHeight() const { return _userHeight.get(); } @@ -2849,6 +3126,10 @@ float MyAvatar::getWalkSpeed() const { return _walkSpeed.get() * _walkSpeedScalar; } +bool MyAvatar::isReadyForPhysics() const { + return qApp->isServerlessMode() || _haveReceivedHeightLimitsFromDomain; +} + void MyAvatar::setSprintMode(bool sprint) { _walkSpeedScalar = sprint ? AVATAR_SPRINT_SPEED_SCALAR : AVATAR_WALK_SPEED_SCALAR; } @@ -2992,9 +3273,7 @@ void MyAvatar::FollowHelper::decrementTimeRemaining(float dt) { bool MyAvatar::FollowHelper::shouldActivateRotation(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const { const float FOLLOW_ROTATION_THRESHOLD = cosf(PI / 6.0f); // 30 degrees glm::vec2 bodyFacing = getFacingDir2D(currentBodyMatrix); - return glm::dot(-myAvatar.getHeadControllerFacingMovingAverage(), bodyFacing) < FOLLOW_ROTATION_THRESHOLD; - } bool MyAvatar::FollowHelper::shouldActivateHorizontal(const MyAvatar& myAvatar, const glm::mat4& desiredBodyMatrix, const glm::mat4& currentBodyMatrix) const { @@ -3065,11 +3344,19 @@ void MyAvatar::FollowHelper::prePhysicsUpdate(MyAvatar& myAvatar, const glm::mat AnimPose followWorldPose(currentWorldMatrix); + glm::quat currentHipsLocal = myAvatar.getAbsoluteJointRotationInObjectFrame(myAvatar.getJointIndex("Hips")); + const glm::quat hipsinWorldSpace = followWorldPose.rot() * (Quaternions::Y_180 * (currentHipsLocal)); + const glm::vec3 avatarUpWorld = glm::normalize(followWorldPose.rot()*(Vectors::UP)); + glm::quat resultingSwingInWorld; + glm::quat resultingTwistInWorld; + swingTwistDecomposition(hipsinWorldSpace, avatarUpWorld, resultingSwingInWorld, resultingTwistInWorld); + // remove scale present from sensorToWorldMatrix followWorldPose.scale() = glm::vec3(1.0f); if (isActive(Rotation)) { - followWorldPose.rot() = glmExtractRotation(desiredWorldMatrix); + //use the hmd reading for the hips follow + followWorldPose.rot() = glmExtractRotation(desiredWorldMatrix); } if (isActive(Horizontal)) { glm::vec3 desiredTranslation = extractTranslation(desiredWorldMatrix); @@ -3465,6 +3752,10 @@ void MyAvatar::updateHoldActions(const AnimPose& prePhysicsPose, const AnimPose& } } +bool MyAvatar::isRecenteringHorizontally() const { + return _follow.isActive(FollowHelper::Horizontal); +} + const MyHead* MyAvatar::getMyHead() const { return static_cast(getHead()); } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index fa6a675d99..1a6feb142a 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -86,6 +86,10 @@ class MyAvatar : public Avatar { * @property {number} audioListenerModeCamera=1 - The audio listening position is at the camera. Read-only. * @property {number} audioListenerModeCustom=2 - The audio listening position is at a the position specified by set by the * customListenPosition and customListenOrientation property values. Read-only. + * @property {boolean} hasScriptedBlendshapes=false - Blendshapes will be transmitted over the network if set to true. + * @property {boolean} hasProceduralBlinkFaceMovement=true - procedural blinking will be turned on if set to true. + * @property {boolean} hasProceduralEyeFaceMovement=true - procedural eye movement will be turned on if set to true. + * @property {boolean} hasAudioEnabledFaceMovement=true - If set to true, voice audio will move the mouth Blendshapes while MyAvatar.hasScriptedBlendshapes is enabled. * @property {Vec3} customListenPosition=Vec3.ZERO - The listening position used when the audioListenerMode * property value is audioListenerModeCustom. * @property {Quat} customListenOrientation=Quat.IDENTITY - The listening orientation used when the @@ -105,6 +109,9 @@ class MyAvatar : public Avatar { * by 30cm. Read-only. * @property {Pose} rightHandTipPose - The pose of the right hand as determined by the hand controllers, with the position * by 30cm. Read-only. + * @property {boolean} centerOfGravityModelEnabled=true - If true then the avatar hips are placed according to the center of + * gravity model that balance the center of gravity over the base of support of the feet. Setting the value false + * will result in the default behaviour where the hips are placed under the head. * @property {boolean} hmdLeanRecenterEnabled=true - If true then the avatar is re-centered to be under the * head's position. In room-scale VR, this behavior is what causes your avatar to follow your HMD as you walk around * the room. Setting the value false is useful if you want to pin the avatar to a fixed position. @@ -121,7 +128,7 @@ class MyAvatar : public Avatar { * while flying. * @property {number} hmdRollControlDeadZone=8 - The amount of HMD roll, in degrees, required before your avatar turns if * hmdRollControlEnabled is enabled. - * @property hmdRollControlRate {number} If hmdRollControlEnabled is true, this value determines the maximum turn rate of + * @property {number} hmdRollControlRate If hmdRollControlEnabled is true, this value determines the maximum turn rate of * your avatar when rolling your HMD in degrees per second. * @property {number} userHeight=1.75 - The height of the user in sensor space. * @property {number} userEyeHeight=1.65 - The estimated height of the user's eyes in sensor space. Read-only. @@ -184,6 +191,10 @@ class MyAvatar : public Avatar { Q_PROPERTY(AudioListenerMode audioListenerModeHead READ getAudioListenerModeHead) Q_PROPERTY(AudioListenerMode audioListenerModeCamera READ getAudioListenerModeCamera) Q_PROPERTY(AudioListenerMode audioListenerModeCustom READ getAudioListenerModeCustom) + Q_PROPERTY(bool hasScriptedBlendshapes READ getHasScriptedBlendshapes WRITE setHasScriptedBlendshapes) + Q_PROPERTY(bool hasProceduralBlinkFaceMovement READ getHasProceduralBlinkFaceMovement WRITE setHasProceduralBlinkFaceMovement) + Q_PROPERTY(bool hasProceduralEyeFaceMovement READ getHasProceduralEyeFaceMovement WRITE setHasProceduralEyeFaceMovement) + Q_PROPERTY(bool hasAudioEnabledFaceMovement READ getHasAudioEnabledFaceMovement WRITE setHasAudioEnabledFaceMovement) //TODO: make gravity feature work Q_PROPERTY(glm::vec3 gravity READ getGravity WRITE setGravity) Q_PROPERTY(glm::vec3 leftHandPosition READ getLeftHandPosition) @@ -199,6 +210,7 @@ class MyAvatar : public Avatar { Q_PROPERTY(float energy READ getEnergy WRITE setEnergy) Q_PROPERTY(bool isAway READ getIsAway WRITE setAway) + Q_PROPERTY(bool centerOfGravityModelEnabled READ getCenterOfGravityModelEnabled WRITE setCenterOfGravityModelEnabled) Q_PROPERTY(bool hmdLeanRecenterEnabled READ getHMDLeanRecenterEnabled WRITE setHMDLeanRecenterEnabled) Q_PROPERTY(bool collisionsEnabled READ getCollisionsEnabled WRITE setCollisionsEnabled) Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled) @@ -464,7 +476,7 @@ public: Q_INVOKABLE bool getClearOverlayWhenMoving() const { return _clearOverlayWhenMoving; } /**jsdoc * @function MyAvatar.setClearOverlayWhenMoving - * @returns {boolean} + * @param {boolean} on */ Q_INVOKABLE void setClearOverlayWhenMoving(bool on) { _clearOverlayWhenMoving = on; } @@ -480,7 +492,16 @@ public: */ Q_INVOKABLE QString getDominantHand() const { return _dominantHand; } - + /**jsdoc + * @function MyAvatar.setCenterOfGravityModelEnabled + * @param {boolean} enabled + */ + Q_INVOKABLE void setCenterOfGravityModelEnabled(bool value) { _centerOfGravityModelEnabled = value; } + /**jsdoc + * @function MyAvatar.getCenterOfGravityModelEnabled + * @returns {boolean} + */ + Q_INVOKABLE bool getCenterOfGravityModelEnabled() const { return _centerOfGravityModelEnabled; } /**jsdoc * @function MyAvatar.setHMDLeanRecenterEnabled * @param {boolean} enabled @@ -564,6 +585,13 @@ public: */ Q_INVOKABLE void triggerRotationRecenter(); + /**jsdoc + *The isRecenteringHorizontally function returns true if MyAvatar + *is translating the root of the Avatar to keep the center of gravity under the head. + *isActive(Horizontal) is returned. + *@function MyAvatar.isRecenteringHorizontally + */ + Q_INVOKABLE bool isRecenteringHorizontally() const; eyeContactTarget getEyeContactTarget(); @@ -956,10 +984,18 @@ public: void removeHoldAction(AvatarActionHold* holdAction); // thread-safe void updateHoldActions(const AnimPose& prePhysicsPose, const AnimPose& postUpdatePose); + // derive avatar body position and orientation from the current HMD Sensor location. - // results are in HMD frame + // results are in sensor frame (-z forward) glm::mat4 deriveBodyFromHMDSensor() const; + glm::vec3 computeCounterBalance() const; + + // derive avatar body position and orientation from using the current HMD Sensor location in relation to the previous + // location of the base of support of the avatar. + // results are in sensor frame (-z foward) + glm::mat4 deriveBodyUsingCgModel() const; + /**jsdoc * @function MyAvatar.isUp * @param {Vec3} direction @@ -987,6 +1023,8 @@ public: QVector getScriptUrls(); + bool isReadyForPhysics() const; + public slots: /**jsdoc @@ -1107,7 +1145,16 @@ public slots: */ Q_INVOKABLE void updateMotionBehaviorFromMenu(); - + /**jsdoc + * @function MyAvatar.setToggleHips + * @param {boolean} enabled + */ + void setToggleHips(bool followHead); + /**jsdoc + * @function MyAvatar.setEnableDebugDrawBaseOfSupport + * @param {boolean} enabled + */ + void setEnableDebugDrawBaseOfSupport(bool isEnabled); /**jsdoc * @function MyAvatar.setEnableDebugDrawDefaultPose * @param {boolean} enabled @@ -1159,7 +1206,7 @@ public slots: * @function MyAvatar.getEnableMeshVisible * @returns {boolean} true if your avatar's mesh is visible, otherwise false. */ - bool getEnableMeshVisible() const { return _skeletonModel->isVisible(); } + bool getEnableMeshVisible() const override; /**jsdoc * Set whether or not your avatar mesh is visible. @@ -1171,7 +1218,7 @@ public slots: * MyAvatar.setEnableMeshVisible(true); * }, 10000); */ - void setEnableMeshVisible(bool isEnabled); + virtual void setEnableMeshVisible(bool isEnabled) override; /**jsdoc * @function MyAvatar.setEnableInverseKinematics @@ -1341,6 +1388,14 @@ private: virtual bool shouldRenderHead(const RenderArgs* renderArgs) const override; void setShouldRenderLocally(bool shouldRender) { _shouldRender = shouldRender; setEnableMeshVisible(shouldRender); } bool getShouldRenderLocally() const { return _shouldRender; } + void setHasScriptedBlendshapes(bool hasScriptedBlendshapes); + bool getHasScriptedBlendshapes() const override { return _hasScriptedBlendShapes; } + void setHasProceduralBlinkFaceMovement(bool hasProceduralBlinkFaceMovement); + bool getHasProceduralBlinkFaceMovement() const override { return _headData->getHasProceduralBlinkFaceMovement(); } + void setHasProceduralEyeFaceMovement(bool hasProceduralEyeFaceMovement); + bool getHasProceduralEyeFaceMovement() const override { return _headData->getHasProceduralEyeFaceMovement(); } + void setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement); + bool getHasAudioEnabledFaceMovement() const override { return _headData->getHasAudioEnabledFaceMovement(); } bool isMyAvatar() const override { return true; } virtual int parseDataFromBuffer(const QByteArray& buffer) override; virtual glm::vec3 getSkeletonPosition() const override; @@ -1402,7 +1457,7 @@ private: SharedSoundPointer _collisionSound; MyCharacterController _characterController; - int16_t _previousCollisionGroup { BULLET_COLLISION_GROUP_MY_AVATAR }; + int32_t _previousCollisionGroup { BULLET_COLLISION_GROUP_MY_AVATAR }; AvatarWeakPointer _lookAtTargetAvatar; glm::vec3 _targetAvatarPosition; @@ -1449,6 +1504,7 @@ private: bool _hmdRollControlEnabled { true }; float _hmdRollControlDeadZone { ROLL_CONTROL_DEAD_ZONE_DEFAULT }; float _hmdRollControlRate { ROLL_CONTROL_RATE_DEFAULT }; + std::atomic _hasScriptedBlendShapes { false }; // working copy -- see AvatarData for thread-safe _sensorToWorldMatrixCache, used for outward facing access glm::mat4 _sensorToWorldMatrix { glm::mat4() }; @@ -1458,8 +1514,8 @@ private: glm::quat _hmdSensorOrientation; glm::vec3 _hmdSensorPosition; // cache head controller pose in sensor space - glm::vec2 _headControllerFacing; // facing vector in xz plane - glm::vec2 _headControllerFacingMovingAverage { 0, 0 }; // facing vector in xz plane + glm::vec2 _headControllerFacing; // facing vector in xz plane (sensor space) + glm::vec2 _headControllerFacingMovingAverage { 0.0f, 0.0f }; // facing vector in xz plane (sensor space) // cache of the current body position and orientation of the avatar's body, // in sensor space. @@ -1495,9 +1551,12 @@ private: void setForceActivateVertical(bool val); bool getForceActivateHorizontal() const; void setForceActivateHorizontal(bool val); - std::atomic _forceActivateRotation{ false }; - std::atomic _forceActivateVertical{ false }; - std::atomic _forceActivateHorizontal{ false }; + bool getToggleHipsFollowing() const; + void setToggleHipsFollowing(bool followHead); + std::atomic _forceActivateRotation { false }; + std::atomic _forceActivateVertical { false }; + std::atomic _forceActivateHorizontal { false }; + std::atomic _toggleHipsFollowing { true }; }; FollowHelper _follow; @@ -1510,6 +1569,7 @@ private: bool _prevShouldDrawHead; bool _rigEnabled { true }; + bool _enableDebugDrawBaseOfSupport { false }; bool _enableDebugDrawDefaultPose { false }; bool _enableDebugDrawAnimPose { false }; bool _enableDebugDrawHandControllers { false }; @@ -1532,6 +1592,7 @@ private: std::map _controllerPoseMap; mutable std::mutex _controllerPoseMapMutex; + bool _centerOfGravityModelEnabled { true }; bool _hmdLeanRecenterEnabled { true }; bool _sprint { false }; @@ -1568,6 +1629,8 @@ private: // load avatar scripts once when rig is ready bool _shouldLoadScripts { false }; + + bool _haveReceivedHeightLimitsFromDomain = { false }; }; QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode); diff --git a/interface/src/avatar/MyHead.cpp b/interface/src/avatar/MyHead.cpp index cad2f9e5d0..9b05a26c76 100644 --- a/interface/src/avatar/MyHead.cpp +++ b/interface/src/avatar/MyHead.cpp @@ -46,32 +46,18 @@ void MyHead::simulate(float deltaTime) { auto player = DependencyManager::get(); // Only use face trackers when not playing back a recording. if (!player->isPlaying()) { - FaceTracker* faceTracker = qApp->getActiveFaceTracker(); - _isFaceTrackerConnected = faceTracker != nullptr && !faceTracker->isMuted(); + auto faceTracker = qApp->getActiveFaceTracker(); + const bool hasActualFaceTrackerConnected = faceTracker && !faceTracker->isMuted(); + _isFaceTrackerConnected = hasActualFaceTrackerConnected || _owningAvatar->getHasScriptedBlendshapes(); if (_isFaceTrackerConnected) { - _transientBlendshapeCoefficients = faceTracker->getBlendshapeCoefficients(); - - if (typeid(*faceTracker) == typeid(DdeFaceTracker)) { - - if (Menu::getInstance()->isOptionChecked(MenuOption::UseAudioForMouth)) { - calculateMouthShapes(deltaTime); - - const int JAW_OPEN_BLENDSHAPE = 21; - const int MMMM_BLENDSHAPE = 34; - const int FUNNEL_BLENDSHAPE = 40; - const int SMILE_LEFT_BLENDSHAPE = 28; - const int SMILE_RIGHT_BLENDSHAPE = 29; - _transientBlendshapeCoefficients[JAW_OPEN_BLENDSHAPE] += _audioJawOpen; - _transientBlendshapeCoefficients[SMILE_LEFT_BLENDSHAPE] += _mouth4; - _transientBlendshapeCoefficients[SMILE_RIGHT_BLENDSHAPE] += _mouth4; - _transientBlendshapeCoefficients[MMMM_BLENDSHAPE] += _mouth2; - _transientBlendshapeCoefficients[FUNNEL_BLENDSHAPE] += _mouth3; - } - applyEyelidOffset(getFinalOrientationInWorldFrame()); + if (hasActualFaceTrackerConnected) { + _blendshapeCoefficients = faceTracker->getBlendshapeCoefficients(); } } + auto eyeTracker = DependencyManager::get(); _isEyeTrackerConnected = eyeTracker->isTracking(); + // if eye tracker is connected we should get the data here. } Parent::simulate(deltaTime); } diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp index f317f6b2c1..c15b00ca19 100644 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -45,7 +45,14 @@ static AnimPose computeHipsInSensorFrame(MyAvatar* myAvatar, bool isFlying) { return result; } - glm::mat4 hipsMat = myAvatar->deriveBodyFromHMDSensor(); + glm::mat4 hipsMat; + if (myAvatar->getCenterOfGravityModelEnabled()) { + // then we use center of gravity model + hipsMat = myAvatar->deriveBodyUsingCgModel(); + } else { + // otherwise use the default of putting the hips under the head + hipsMat = myAvatar->deriveBodyFromHMDSensor(); + } glm::vec3 hipsPos = extractTranslation(hipsMat); glm::quat hipsRot = glmExtractRotation(hipsMat); @@ -53,8 +60,11 @@ static AnimPose computeHipsInSensorFrame(MyAvatar* myAvatar, bool isFlying) { glm::mat4 avatarToSensorMat = worldToSensorMat * avatarToWorldMat; // dampen hips rotation, by mixing it with the avatar orientation in sensor space - const float MIX_RATIO = 0.5f; - hipsRot = safeLerp(glmExtractRotation(avatarToSensorMat), hipsRot, MIX_RATIO); + // turning this off for center of gravity model because it is already mixed in there + if (!(myAvatar->getCenterOfGravityModelEnabled())) { + const float MIX_RATIO = 0.5f; + hipsRot = safeLerp(glmExtractRotation(avatarToSensorMat), hipsRot, MIX_RATIO); + } if (isFlying) { // rotate the hips back to match the flying animation. @@ -73,6 +83,7 @@ static AnimPose computeHipsInSensorFrame(MyAvatar* myAvatar, bool isFlying) { hipsPos = headPos + tiltRot * (hipsPos - headPos); } + // AJT: TODO can we remove this? return AnimPose(hipsRot * Quaternions::Y_180, hipsPos); } @@ -170,6 +181,15 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { } } + bool isFlying = (myAvatar->getCharacterController()->getState() == CharacterController::State::Hover || myAvatar->getCharacterController()->computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS); + if (isFlying != _prevIsFlying) { + const float FLY_TO_IDLE_HIPS_TRANSITION_TIME = 0.5f; + _flyIdleTimer = FLY_TO_IDLE_HIPS_TRANSITION_TIME; + } else { + _flyIdleTimer -= deltaTime; + } + _prevIsFlying = isFlying; + // if hips are not under direct control, estimate the hips position. if (avatarHeadPose.isValid() && !(params.primaryControllerFlags[Rig::PrimaryControllerType_Hips] & (uint8_t)Rig::ControllerFlags::Enabled)) { bool isFlying = (myAvatar->getCharacterController()->getState() == CharacterController::State::Hover || myAvatar->getCharacterController()->computeCollisionGroup() == BULLET_COLLISION_GROUP_COLLISIONLESS); @@ -181,14 +201,28 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { AnimPose hips = computeHipsInSensorFrame(myAvatar, isFlying); + // timescale in seconds + const float TRANS_HORIZ_TIMESCALE = 0.15f; + const float TRANS_VERT_TIMESCALE = 0.01f; // We want the vertical component of the hips to follow quickly to prevent spine squash/stretch. + const float ROT_TIMESCALE = 0.15f; + const float FLY_IDLE_TRANSITION_TIMESCALE = 0.25f; + + float transHorizAlpha, transVertAlpha, rotAlpha; + if (_flyIdleTimer < 0.0f) { + transHorizAlpha = glm::min(deltaTime / TRANS_HORIZ_TIMESCALE, 1.0f); + transVertAlpha = glm::min(deltaTime / TRANS_VERT_TIMESCALE, 1.0f); + rotAlpha = glm::min(deltaTime / ROT_TIMESCALE, 1.0f); + } else { + transHorizAlpha = glm::min(deltaTime / FLY_IDLE_TRANSITION_TIMESCALE, 1.0f); + transVertAlpha = glm::min(deltaTime / FLY_IDLE_TRANSITION_TIMESCALE, 1.0f); + rotAlpha = glm::min(deltaTime / FLY_IDLE_TRANSITION_TIMESCALE, 1.0f); + } + // smootly lerp hips, in sensorframe, with different coeff for horiz and vertical translation. - const float ROT_ALPHA = 0.9f; - const float TRANS_HORIZ_ALPHA = 0.9f; - const float TRANS_VERT_ALPHA = 0.1f; float hipsY = hips.trans().y; - hips.trans() = lerp(hips.trans(), _prevHips.trans(), TRANS_HORIZ_ALPHA); - hips.trans().y = lerp(hipsY, _prevHips.trans().y, TRANS_VERT_ALPHA); - hips.rot() = safeLerp(hips.rot(), _prevHips.rot(), ROT_ALPHA); + hips.trans() = lerp(_prevHips.trans(), hips.trans(), transHorizAlpha); + hips.trans().y = lerp(_prevHips.trans().y, hipsY, transVertAlpha); + hips.rot() = safeLerp(_prevHips.rot(), hips.rot(), rotAlpha); _prevHips = hips; _prevHipsValid = true; diff --git a/interface/src/avatar/MySkeletonModel.h b/interface/src/avatar/MySkeletonModel.h index 252b6c293b..ebef9796a4 100644 --- a/interface/src/avatar/MySkeletonModel.h +++ b/interface/src/avatar/MySkeletonModel.h @@ -28,6 +28,8 @@ private: AnimPose _prevHips; // sensor frame bool _prevHipsValid { false }; + bool _prevIsFlying { false }; + float _flyIdleTimer { 0.0f }; std::map _jointRotationFrameOffsetMap; }; diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index f791ea25bc..69698e82a6 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -134,8 +134,14 @@ void Ledger::balance(const QStringList& keys) { keysQuery("balance", "balanceSuccess", "balanceFailure"); } -void Ledger::inventory(const QStringList& keys) { - keysQuery("inventory", "inventorySuccess", "inventoryFailure"); +void Ledger::inventory(const QString& editionFilter, const QString& typeFilter, const QString& titleFilter, const int& page, const int& perPage) { + QJsonObject params; + params["edition_filter"] = editionFilter; + params["type_filter"] = typeFilter; + params["title_filter"] = titleFilter; + params["page"] = page; + params["per_page"] = perPage; + keysQuery("inventory", "inventorySuccess", "inventoryFailure", params); } QString hfcString(const QJsonValue& sentValue, const QJsonValue& receivedValue) { @@ -260,9 +266,9 @@ void Ledger::historyFailure(QNetworkReply& reply) { failResponse("history", reply); } -void Ledger::history(const QStringList& keys, const int& pageNumber) { +void Ledger::history(const QStringList& keys, const int& pageNumber, const int& itemsPerPage) { QJsonObject params; - params["per_page"] = 100; + params["per_page"] = itemsPerPage; params["page"] = pageNumber; keysQuery("history", "historySuccess", "historyFailure", params); } diff --git a/interface/src/commerce/Ledger.h b/interface/src/commerce/Ledger.h index abc97bfe72..8a8fd2630a 100644 --- a/interface/src/commerce/Ledger.h +++ b/interface/src/commerce/Ledger.h @@ -28,8 +28,8 @@ public: void buy(const QString& hfc_key, int cost, const QString& asset_id, const QString& inventory_key, const bool controlled_failure = false); bool receiveAt(const QString& hfc_key, const QString& signing_key); void balance(const QStringList& keys); - void inventory(const QStringList& keys); - void history(const QStringList& keys, const int& pageNumber); + void inventory(const QString& editionFilter, const QString& typeFilter, const QString& titleFilter, const int& page, const int& perPage); + void history(const QStringList& keys, const int& pageNumber, const int& itemsPerPage); void account(); void updateLocation(const QString& asset_id, const QString& location, const bool& alsoUpdateSiblings = false, const bool controlledFailure = false); void certificateInfo(const QString& certificateId); diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 722f29ba2f..b960c0b703 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -105,21 +105,21 @@ void QmlCommerce::balance() { } } -void QmlCommerce::inventory() { +void QmlCommerce::inventory(const QString& editionFilter, const QString& typeFilter, const QString& titleFilter, const int& page, const int& perPage) { auto ledger = DependencyManager::get(); auto wallet = DependencyManager::get(); QStringList cachedPublicKeys = wallet->listPublicKeys(); if (!cachedPublicKeys.isEmpty()) { - ledger->inventory(cachedPublicKeys); + ledger->inventory(editionFilter, typeFilter, titleFilter, page, perPage); } } -void QmlCommerce::history(const int& pageNumber) { +void QmlCommerce::history(const int& pageNumber, const int& itemsPerPage) { auto ledger = DependencyManager::get(); auto wallet = DependencyManager::get(); QStringList cachedPublicKeys = wallet->listPublicKeys(); if (!cachedPublicKeys.isEmpty()) { - ledger->history(cachedPublicKeys, pageNumber); + ledger->history(cachedPublicKeys, pageNumber, itemsPerPage); } } @@ -227,10 +227,13 @@ QString QmlCommerce::getInstalledApps() { QString scriptURL = appFileJsonObject["scriptURL"].toString(); // If the script .app.json is on the user's local disk but the associated script isn't running - // for some reason, start that script again. + // for some reason (i.e. the user stopped it from Running Scripts), + // delete the .app.json from the user's local disk. if (!runningScripts.contains(scriptURL)) { - if ((DependencyManager::get()->loadScript(scriptURL.trimmed())).isNull()) { - qCDebug(commerce) << "Couldn't start script while checking installed apps."; + if (!appFile.remove()) { + qCWarning(commerce) + << "Couldn't delete local .app.json file (app's script isn't running). App filename is:" + << appFileName; } } } else { diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index 27e97fe7db..a0c6916799 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -73,8 +73,8 @@ protected: Q_INVOKABLE void buy(const QString& assetId, int cost, const bool controlledFailure = false); Q_INVOKABLE void balance(); - Q_INVOKABLE void inventory(); - Q_INVOKABLE void history(const int& pageNumber); + Q_INVOKABLE void inventory(const QString& editionFilter = QString(), const QString& typeFilter = QString(), const QString& titleFilter = QString(), const int& page = 1, const int& perPage = 20); + Q_INVOKABLE void history(const int& pageNumber, const int& itemsPerPage = 100); Q_INVOKABLE void generateKeyPair(); Q_INVOKABLE void account(); diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index 982adb4b5e..e003ae88a0 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -314,6 +314,7 @@ Wallet::Wallet() { auto nodeList = DependencyManager::get(); auto ledger = DependencyManager::get(); auto& packetReceiver = nodeList->getPacketReceiver(); + _passphrase = new QString(""); packetReceiver.registerListener(PacketType::ChallengeOwnership, this, "handleChallengeOwnershipPacket"); packetReceiver.registerListener(PacketType::ChallengeOwnershipRequest, this, "handleChallengeOwnershipPacket"); @@ -365,6 +366,10 @@ Wallet::~Wallet() { if (_securityImage) { delete _securityImage; } + + if (_passphrase) { + delete _passphrase; + } } bool Wallet::setPassphrase(const QString& passphrase) { @@ -531,7 +536,6 @@ bool Wallet::walletIsAuthenticatedWithPassphrase() { // be sure to add the public key so we don't do this over and over _publicKeys.push_back(publicKey.toBase64()); - DependencyManager::get()->setWalletStatus((uint)WalletStatus::WALLET_STATUS_READY); return true; } } @@ -610,7 +614,11 @@ void Wallet::updateImageProvider() { SecurityImageProvider* securityImageProvider; // inform offscreenUI security image provider - QQmlEngine* engine = DependencyManager::get()->getSurfaceContext()->engine(); + auto offscreenUI = DependencyManager::get(); + if (!offscreenUI) { + return; + } + QQmlEngine* engine = offscreenUI->getSurfaceContext()->engine(); securityImageProvider = reinterpret_cast(engine->imageProvider(SecurityImageProvider::PROVIDER_NAME)); securityImageProvider->setSecurityImage(_securityImage); diff --git a/interface/src/commerce/Wallet.h b/interface/src/commerce/Wallet.h index 8a7d6b8c07..665afd9a23 100644 --- a/interface/src/commerce/Wallet.h +++ b/interface/src/commerce/Wallet.h @@ -78,7 +78,7 @@ private: QByteArray _salt; QByteArray _iv; QByteArray _ckey; - QString* _passphrase { new QString("") }; + QString* _passphrase { nullptr }; bool _isOverridingServer { false }; bool writeWallet(const QString& newPassphrase = QString("")); diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 22db128f7e..cd8c052d63 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -81,6 +81,13 @@ int main(int argc, const char* argv[]) { // Instance UserActivityLogger now that the settings are loaded auto& ual = UserActivityLogger::getInstance(); + // once the settings have been loaded, check if we need to flip the default for UserActivityLogger + if (!ual.isDisabledSettingSet()) { + // the user activity logger is opt-out for Interface + // but it's defaulted to disabled for other targets + // so we need to enable it here if it has never been disabled by the user + ual.disable(false); + } qDebug() << "UserActivityLogger is enabled:" << ual.isEnabled(); if (ual.isEnabled()) { diff --git a/interface/src/raypick/PickScriptingInterface.cpp b/interface/src/raypick/PickScriptingInterface.cpp index 8da6e7c615..74459ca624 100644 --- a/interface/src/raypick/PickScriptingInterface.cpp +++ b/interface/src/raypick/PickScriptingInterface.cpp @@ -36,7 +36,7 @@ unsigned int PickScriptingInterface::createPick(const PickQuery::PickType type, * @typedef {object} Picks.RayPickProperties * @property {boolean} [enabled=false] If this Pick should start enabled or not. Disabled Picks do not updated their pick results. * @property {number} [filter=Picks.PICK_NOTHING] The filter for this Pick to use, constructed using filter flags combined using bitwise OR. - * @property {float} [maxDistance=0.0] The max distance at which this Pick will intersect. 0.0 = no max. < 0.0 is invalid. + * @property {number} [maxDistance=0.0] The max distance at which this Pick will intersect. 0.0 = no max. < 0.0 is invalid. * @property {string} [joint] Only for Joint or Mouse Ray Picks. If "Mouse", it will create a Ray Pick that follows the system mouse, in desktop or HMD. * If "Avatar", it will create a Joint Ray Pick that follows your avatar's head. Otherwise, it will create a Joint Ray Pick that follows the given joint, if it * exists on your current avatar. @@ -103,7 +103,7 @@ unsigned int PickScriptingInterface::createRayPick(const QVariant& properties) { * @property {number} [hand=-1] An integer. 0 == left, 1 == right. Invalid otherwise. * @property {boolean} [enabled=false] If this Pick should start enabled or not. Disabled Picks do not updated their pick results. * @property {number} [filter=Picks.PICK_NOTHING] The filter for this Pick to use, constructed using filter flags combined using bitwise OR. - * @property {float} [maxDistance=0.0] The max distance at which this Pick will intersect. 0.0 = no max. < 0.0 is invalid. + * @property {number} [maxDistance=0.0] The max distance at which this Pick will intersect. 0.0 = no max. < 0.0 is invalid. */ unsigned int PickScriptingInterface::createStylusPick(const QVariant& properties) { QVariantMap propMap = properties.toMap(); diff --git a/interface/src/raypick/PickScriptingInterface.h b/interface/src/raypick/PickScriptingInterface.h index a39aa3a4a1..0ee091716d 100644 --- a/interface/src/raypick/PickScriptingInterface.h +++ b/interface/src/raypick/PickScriptingInterface.h @@ -22,21 +22,22 @@ * @hifi-interface * @hifi-client-entity * - * @property PICK_NOTHING {number} A filter flag. Don't intersect with anything. Read-only. - * @property PICK_ENTITIES {number} A filter flag. Include entities when intersecting. Read-only. - * @property PICK_OVERLAYS {number} A filter flag. Include overlays when intersecting. Read-only. - * @property PICK_AVATARS {number} A filter flag. Include avatars when intersecting. Read-only. - * @property PICK_HUD {number} A filter flag. Include the HUD sphere when intersecting in HMD mode. Read-only. - * @property PICK_COARSE {number} A filter flag. Pick against coarse meshes, instead of exact meshes. Read-only. - * @property PICK_INCLUDE_INVISIBLE {number} A filter flag. Include invisible objects when intersecting. Read-only. - * @property PICK_INCLUDE_NONCOLLIDABLE {number} A filter flag. Include non-collidable objects when intersecting. + * @property {number} PICK_NOTHING A filter flag. Don't intersect with anything. Read-only. + * @property {number} PICK_ENTITIES A filter flag. Include entities when intersecting. Read-only. + * @property {number} PICK_OVERLAYS A filter flag. Include overlays when intersecting. Read-only. + * @property {number} PICK_AVATARS A filter flag. Include avatars when intersecting. Read-only. + * @property {number} PICK_HUD A filter flag. Include the HUD sphere when intersecting in HMD mode. Read-only. + * @property {number} PICK_COARSE A filter flag. Pick against coarse meshes, instead of exact meshes. Read-only. + * @property {number} PICK_INCLUDE_INVISIBLE A filter flag. Include invisible objects when intersecting. Read-only. + * @property {number} PICK_INCLUDE_NONCOLLIDABLE A filter flag. Include non-collidable objects when intersecting. * Read-only. - * @property PICK_ALL_INTERSECTIONS {number} Read-only. - * @property INTERSECTED_NONE {number} An intersection type. Intersected nothing with the given filter flags. Read-only. - * @property INTERSECTED_ENTITY {number} An intersection type. Intersected an entity. Read-only. - * @property INTERSECTED_OVERLAY {number} An intersection type. Intersected an overlay. Read-only. - * @property INTERSECTED_AVATAR {number} An intersection type. Intersected an avatar. Read-only. - * @property INTERSECTED_HUD {number} An intersection type. Intersected the HUD sphere. Read-only. + * @property {number} PICK_ALL_INTERSECTIONS Read-only. + * @property {number} INTERSECTED_NONE An intersection type. Intersected nothing with the given filter flags. + * Read-only. + * @property {number} INTERSECTED_ENTITY An intersection type. Intersected an entity. Read-only. + * @property {number} INTERSECTED_OVERLAY An intersection type. Intersected an overlay. Read-only. + * @property {number} INTERSECTED_AVATAR An intersection type. Intersected an avatar. Read-only. + * @property {number} INTERSECTED_HUD An intersection type. Intersected the HUD sphere. Read-only. * @property {number} perFrameTimeBudget - The max number of usec to spend per frame updating Pick results. Read-only. */ @@ -99,11 +100,11 @@ public: /**jsdoc * An intersection result for a Ray Pick. * - * @typedef {Object} RayPickResult + * @typedef {object} RayPickResult * @property {number} type The intersection type. * @property {boolean} intersects If there was a valid intersection (type != INTERSECTED_NONE) * @property {Uuid} objectID The ID of the intersected object. Uuid.NULL for the HUD or invalid intersections. - * @property {float} distance The distance to the intersection point from the origin of the ray. + * @property {number} distance The distance to the intersection point from the origin of the ray. * @property {Vec3} intersection The intersection point in world-space. * @property {Vec3} surfaceNormal The surface normal at the intersected point. All NANs if type == INTERSECTED_HUD. * @property {Variant} extraInfo Additional intersection details when available for Model objects. @@ -113,11 +114,11 @@ public: /**jsdoc * An intersection result for a Stylus Pick. * - * @typedef {Object} StylusPickResult + * @typedef {object} StylusPickResult * @property {number} type The intersection type. * @property {boolean} intersects If there was a valid intersection (type != INTERSECTED_NONE) * @property {Uuid} objectID The ID of the intersected object. Uuid.NULL for the HUD or invalid intersections. - * @property {float} distance The distance to the intersection point from the origin of the ray. + * @property {number} distance The distance to the intersection point from the origin of the ray. * @property {Vec3} intersection The intersection point in world-space. * @property {Vec3} surfaceNormal The surface normal at the intersected point. All NANs if type == INTERSECTED_HUD. * @property {Variant} extraInfo Additional intersection details when available for Model objects. diff --git a/interface/src/raypick/PointerScriptingInterface.cpp b/interface/src/raypick/PointerScriptingInterface.cpp index b7ac899c8d..4e953a5cb8 100644 --- a/interface/src/raypick/PointerScriptingInterface.cpp +++ b/interface/src/raypick/PointerScriptingInterface.cpp @@ -68,14 +68,14 @@ unsigned int PointerScriptingInterface::createStylus(const QVariant& properties) * A set of properties used to define the visual aspect of a Ray Pointer in the case that the Pointer is not intersecting something. Same as a {@link Pointers.RayPointerRenderState}, * but with an additional distance field. * - * @typedef {Object} Pointers.DefaultRayPointerRenderState + * @typedef {object} Pointers.DefaultRayPointerRenderState * @augments Pointers.RayPointerRenderState * @property {number} distance The distance at which to render the end of this Ray Pointer, if one is defined. */ /**jsdoc * A set of properties used to define the visual aspect of a Ray Pointer in the case that the Pointer is intersecting something. * - * @typedef {Object} Pointers.RayPointerRenderState + * @typedef {object} Pointers.RayPointerRenderState * @property {string} name The name of this render state, used by {@link Pointers.setRenderState} and {@link Pointers.editRenderState} * @property {Overlays.OverlayProperties} [start] All of the properties you would normally pass to {@link Overlays.addOverlay}, plus the type (as a type field). * An overlay to represent the beginning of the Ray Pointer, if desired. @@ -87,7 +87,7 @@ unsigned int PointerScriptingInterface::createStylus(const QVariant& properties) /**jsdoc * A trigger mechanism for Ray Pointers. * - * @typedef {Object} Pointers.Trigger + * @typedef {object} Pointers.Trigger * @property {Controller.Standard|Controller.Actions|function} action This can be a built-in Controller action, like Controller.Standard.LTClick, or a function that evaluates to >= 1.0 when you want to trigger button. * @property {string} button Which button to trigger. "Primary", "Secondary", "Tertiary", and "Focus" are currently supported. Only "Primary" will trigger clicks on web surfaces. If "Focus" is triggered, * it will try to set the entity or overlay focus to the object at which the Pointer is aimed. Buttons besides the first three will still trigger events, but event.button will be "None". diff --git a/interface/src/raypick/PointerScriptingInterface.h b/interface/src/raypick/PointerScriptingInterface.h index 49eb40504d..628af84790 100644 --- a/interface/src/raypick/PointerScriptingInterface.h +++ b/interface/src/raypick/PointerScriptingInterface.h @@ -136,7 +136,7 @@ public: * Sets the length of this Pointer. No effect on Stylus Pointers. * @function Pointers.setLength * @param {number} uid The ID of the Pointer, as returned by {@link Pointers.createPointer}. - * @param {float} length The desired length of the Pointer. + * @param {number} length The desired length of the Pointer. */ Q_INVOKABLE void setLength(unsigned int uid, float length) const { DependencyManager::get()->setLength(uid, length); } diff --git a/interface/src/scripting/ControllerScriptingInterface.h b/interface/src/scripting/ControllerScriptingInterface.h index e16383e234..051a372aad 100644 --- a/interface/src/scripting/ControllerScriptingInterface.h +++ b/interface/src/scripting/ControllerScriptingInterface.h @@ -105,9 +105,6 @@ class ScriptEngine; *
  • {@link Controller.getValue|getValue}
  • *
  • {@link Controller.getAxisValue|getAxisValue}
  • *
  • {@link Controller.getPoseValue|getgetPoseValue}
  • - *
  • {@link Controller.getButtonValue|getButtonValue} for a particular device
  • - *
  • {@link Controller.getAxisValue(0)|getAxisValue} for a particular device
  • - *
  • {@link Controller.getPoseValue(0)|getPoseValue} for a particular device
  • *
  • {@link Controller.getActionValue|getActionValue}
  • * * diff --git a/interface/src/scripting/TestScriptingInterface.cpp b/interface/src/scripting/TestScriptingInterface.cpp index 700994c517..430441226f 100644 --- a/interface/src/scripting/TestScriptingInterface.cpp +++ b/interface/src/scripting/TestScriptingInterface.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -186,3 +187,7 @@ void TestScriptingInterface::saveObject(QVariant variant, const QString& filenam file.write(jsonData); file.close(); } + +void TestScriptingInterface::showMaximized() { + qApp->getWindow()->showMaximized(); +} \ No newline at end of file diff --git a/interface/src/scripting/TestScriptingInterface.h b/interface/src/scripting/TestScriptingInterface.h index 5666417727..c47e39d1f3 100644 --- a/interface/src/scripting/TestScriptingInterface.h +++ b/interface/src/scripting/TestScriptingInterface.h @@ -27,70 +27,128 @@ public slots: /**jsdoc * Exits the application + * @function Test.quit */ void quit(); /**jsdoc * Waits for all texture transfers to be complete + * @function Test.waitForTextureIdle */ void waitForTextureIdle(); /**jsdoc * Waits for all pending downloads to be complete + * @function Test.waitForDownloadIdle */ void waitForDownloadIdle(); /**jsdoc * Waits for all file parsing operations to be complete + * @function Test.waitForProcessingIdle */ void waitForProcessingIdle(); /**jsdoc * Waits for all pending downloads, parsing and texture transfers to be complete + * @function Test.waitIdle */ void waitIdle(); + /**jsdoc + * Waits for establishment of connection to server + * @function Test.waitForConnection + * @param {int} maxWaitMs [default=10000] - Number of milliseconds to wait + */ bool waitForConnection(qint64 maxWaitMs = 10000); + /**jsdoc + * Waits a specific number of milliseconds + * @function Test.wait + * @param {int} milliseconds - Number of milliseconds to wait + */ void wait(int milliseconds); + /**jsdoc + * Waits for all pending downloads, parsing and texture transfers to be complete + * @function Test.loadTestScene + * @param {string} sceneFile - URL of scene to load + */ bool loadTestScene(QString sceneFile); + /**jsdoc + * Clears all caches + * @function Test.clear + */ void clear(); /**jsdoc * Start recording Chrome compatible tracing events * logRules can be used to specify a set of logging category rules to limit what gets captured + * @function Test.startTracing + * @param {string} logrules [defaultValue=""] - See implementation for explanation */ bool startTracing(QString logrules = ""); /**jsdoc * Stop recording Chrome compatible tracing events and serialize recorded events to a file * Using a filename with a .gz extension will automatically compress the output file + * @function Test.stopTracing + * @param {string} filename - Name of file to save to + * @returns {bool} True if successful. */ bool stopTracing(QString filename); + /**jsdoc + * Starts a specific trace event + * @function Test.startTraceEvent + * @param {string} name - Name of event + */ void startTraceEvent(QString name); + /**jsdoc + * Stop a specific name event + * Using a filename with a .gz extension will automatically compress the output file + * @function Test.endTraceEvent + * @param {string} filename - Name of event + */ void endTraceEvent(QString name); /**jsdoc * Write detailed timing stats of next physics stepSimulation() to filename + * @function Test.savePhysicsSimulationStats + * @param {string} filename - Name of file to save to */ void savePhysicsSimulationStats(QString filename); + /**jsdoc + * Profiles a specific function + * @function Test.savePhysicsSimulationStats + * @param {string} name - Name used to reference the function + * @param {function} function - Function to profile + */ Q_INVOKABLE void profileRange(const QString& name, QScriptValue function); /**jsdoc * Clear all caches (menu command Reload Content) + * @function Test.clearCaches */ void clearCaches(); /**jsdoc * Save a JSON object to a file in the test results location + * @function Test.saveObject + * @param {string} name - Name of the object + * @param {string} filename - Name of file to save to */ void saveObject(QVariant v, const QString& filename); + /**jsdoc + * Maximizes the window + * @function Test.showMaximized + */ + void showMaximized(); + private: bool waitForCondition(qint64 maxWaitMs, std::function condition); QString _testResultsLocation; diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 6f6e83842c..0aea7a02c5 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -446,12 +446,12 @@ void WindowScriptingInterface::takeSnapshot(bool notify, bool includeAnimated, f qApp->takeSnapshot(notify, includeAnimated, aspectRatio, filename); } -void WindowScriptingInterface::takeSecondaryCameraSnapshot(const QString& filename) { - qApp->takeSecondaryCameraSnapshot(filename); +void WindowScriptingInterface::takeSecondaryCameraSnapshot(const bool& notify, const QString& filename) { + qApp->takeSecondaryCameraSnapshot(notify, filename); } -void WindowScriptingInterface::takeSecondaryCamera360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const QString& filename) { - qApp->takeSecondaryCamera360Snapshot(cameraPosition, cubemapOutputFormat, filename); +void WindowScriptingInterface::takeSecondaryCamera360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const bool& notify, const QString& filename) { + qApp->takeSecondaryCamera360Snapshot(cameraPosition, cubemapOutputFormat, notify, filename); } void WindowScriptingInterface::shareSnapshot(const QString& path, const QUrl& href) { @@ -522,7 +522,7 @@ int WindowScriptingInterface::openMessageBox(QString title, QString text, int bu * RestoreDefaults 0x8000000 "Restore Defaults" * * - * @typedef Window.MessageBoxButton + * @typedef {number} Window.MessageBoxButton */ int WindowScriptingInterface::createMessageBox(QString title, QString text, int buttons, int defaultButton) { auto messageBox = DependencyManager::get()->createMessageBox(OffscreenUi::ICON_INFORMATION, title, text, diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 1d06f33ec0..29eb6421e1 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -318,7 +318,6 @@ public slots: * {@link Window.stillSnapshotTaken|stillSnapshotTaken} is emitted; when a still image plus moving images are captured, * {@link Window.processingGifStarted|processingGifStarted} and {@link Window.processingGifCompleted|processingGifCompleted} * are emitted. The path to store the snapshots and the length of the animated GIF to capture are specified in Settings > - * NOTE: to provide a non-default value - all previous parameters must be provided. * General > Snapshots. * @function Window.takeSnapshot * @param {boolean} [notify=true] - This value is passed on through the {@link Window.stillSnapshotTaken|stillSnapshotTaken} @@ -328,7 +327,7 @@ public slots: * @param {number} [aspectRatio=0] - The width/height ratio of the snapshot required. If the value is 0 the * full resolution is used (window dimensions in desktop mode; HMD display dimensions in HMD mode), otherwise one of the * dimensions is adjusted in order to match the aspect ratio. - * @param {string} [filename=""] - If this parameter is not given, the image will be saved as 'hifi-snap-by--YYYY-MM-DD_HH-MM-SS'. + * @param {string} [filename=""] - If this parameter is not given, the image will be saved as "hifi-snap-by-<user name>-YYYY-MM-DD_HH-MM-SS". * If this parameter is "" then the image will be saved as ".jpg". * Otherwise, the image will be saved to this filename, with an appended ".jpg". * @@ -360,29 +359,29 @@ public slots: /**jsdoc * Takes a still snapshot of the current view from the secondary camera that can be set up through the {@link Render} API. - * NOTE: to provide a non-default value - all previous parameters must be provided. * @function Window.takeSecondaryCameraSnapshot - * @param {string} [filename=""] - If this parameter is not given, the image will be saved as 'hifi-snap-by--YYYY-MM-DD_HH-MM-SS'. + * @param {boolean} [notify=true] - This value is passed on through the {@link Window.stillSnapshotTaken|stillSnapshotTaken} + * signal. + * @param {string} [filename=""] - If this parameter is not given, the image will be saved as "hifi-snap-by-<user name>-YYYY-MM-DD_HH-MM-SS". * If this parameter is "" then the image will be saved as ".jpg". * Otherwise, the image will be saved to this filename, with an appended ".jpg". - * - * var filename = QString(); */ - void takeSecondaryCameraSnapshot(const QString& filename = QString()); + void takeSecondaryCameraSnapshot(const bool& notify = true, const QString& filename = QString()); /**jsdoc - * Takes a 360 snapshot given a position of the secondary camera (which does not need to have been previously set up). - * @function Window.takeSecondaryCameraSnapshot - * @param {vec3} [cameraPosition] - The (x, y, z) position of the camera for the 360 snapshot - * @param {boolean} [cubemapOutputFormat=false] - If true then the snapshot is saved as a cube map image, - * otherwise is saved as an equirectangular image. - * @param {string} [filename=""] - If this parameter is not given, the image will be saved as 'hifi-snap-by--YYYY-MM-DD_HH-MM-SS'. - * If this parameter is "" then the image will be saved as ".jpg". - * Otherwise, the image will be saved to this filename, with an appended ".jpg". - * - * var filename = QString(); - */ - void takeSecondaryCamera360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat = false, const QString& filename = QString()); + * Takes a 360° snapshot at a given position for the secondary camera. The secondary camera does not need to have been + * set up. + * @function Window.takeSecondaryCamera360Snapshot + * @param {Vec3} cameraPosition - The position of the camera for the snapshot. + * @param {boolean} [cubemapOutputFormat=false] - If true then the snapshot is saved as a cube map image, + * otherwise is saved as an equirectangular image. + * @param {boolean} [notify=true] - This value is passed on through the {@link Window.stillSnapshotTaken|stillSnapshotTaken} + * signal. + * @param {string} [filename=""] - If this parameter is not supplied, the image will be saved as "hifi-snap-by-<user name>-YYYY-MM-DD_HH-MM-SS". + * If this parameter is "" then the image will be saved as ".jpg". + * Otherwise, the image will be saved to this filename, with an appended ".jpg". + */ + void takeSecondaryCamera360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat = false, const bool& notify = true, const QString& filename = QString()); /**jsdoc * Emit a {@link Window.connectionAdded|connectionAdded} or a {@link Window.connectionError|connectionError} signal that @@ -470,7 +469,7 @@ public slots: * * * - * @typedef Window.DisplayTexture + * @typedef {string} Window.DisplayTexture */ bool setDisplayTexture(const QString& name); @@ -523,16 +522,21 @@ public slots: int openMessageBox(QString title, QString text, int buttons, int defaultButton); /**jsdoc - * Open the given resource in the Interface window or in a web browser depending on the url scheme + * Open a URL in the Interface window or other application, depending on the URL's scheme. If the URL starts with + * hifi:// then that URL is navigated to in Interface, otherwise the URL is opened in the application the OS + * associates with the URL's scheme (e.g., a Web browser for http://). * @function Window.openUrl - * @param {string} url - The resource to open + * @param {string} url - The URL to open. */ void openUrl(const QUrl& url); /**jsdoc - * (Android only) Open the requested Activity and optionally back to the scene when the activity is done + * Open an Android activity and optionally return back to the scene when the activity is completed. Android only. * @function Window.openAndroidActivity - * @param {string} activityName - The name of the activity to open. One of "Home", "Login" or "Privacy Policy" + * @param {string} activityName - The name of the activity to open: one of "Home", "Login", or + * "Privacy Policy". + * @param {boolean} backToScene - If true, the user is automatically returned back to the scene when the + * activity is completed. */ void openAndroidActivity(const QString& activityName, const bool backToScene); @@ -607,7 +611,8 @@ signals: void stillSnapshotTaken(const QString& pathStillSnapshot, bool notify); /**jsdoc - * Triggered when a still equirectangular snapshot has been taken by calling {@link Window.takeSecondaryCamera360Snapshot|takeSecondaryCamera360Snapshot} + * Triggered when a still 360° snapshot has been taken by calling + * {@link Window.takeSecondaryCamera360Snapshot|takeSecondaryCamera360Snapshot}. * @function Window.snapshot360Taken * @param {string} pathStillSnapshot - The path and name of the snapshot image file. * @param {boolean} notify - The value of the notify parameter that {@link Window.takeSecondaryCamera360Snapshot|takeSecondaryCamera360Snapshot} diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index d01e7d6671..83601a2797 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -93,10 +93,18 @@ void DialogsManager::setDomainConnectionFailureVisibility(bool visible) { static const QUrl url("dialogs/TabletConnectionFailureDialog.qml"); auto hmd = DependencyManager::get(); if (visible) { + _dialogCreatedWhileShown = tablet->property("tabletShown").toBool(); tablet->initialScreen(url); if (!hmd->getShouldShowTablet()) { hmd->openTablet(); } + } else if (tablet->isPathLoaded(url)) { + tablet->closeDialog(); + tablet->gotoHomeScreen(); + if (!_dialogCreatedWhileShown) { + hmd->closeTablet(); + } + _dialogCreatedWhileShown = false; } } } diff --git a/interface/src/ui/DialogsManager.h b/interface/src/ui/DialogsManager.h index f17ac39a7e..0633dec573 100644 --- a/interface/src/ui/DialogsManager.h +++ b/interface/src/ui/DialogsManager.h @@ -80,6 +80,7 @@ private: QPointer _octreeStatsDialog; QPointer _testingDialog; QPointer _domainConnectionDialog; + bool _dialogCreatedWhileShown { false }; bool _addressBarVisible { false }; }; diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp index e7e3c91d13..d131bb3467 100644 --- a/interface/src/ui/OverlayConductor.cpp +++ b/interface/src/ui/OverlayConductor.cpp @@ -18,7 +18,6 @@ #include "InterfaceLogging.h" OverlayConductor::OverlayConductor() { - } OverlayConductor::~OverlayConductor() { @@ -33,8 +32,8 @@ bool OverlayConductor::headOutsideOverlay() const { glm::vec3 uiPos = uiTransform.getTranslation(); glm::vec3 uiForward = uiTransform.getRotation() * glm::vec3(0.0f, 0.0f, -1.0f); - const float MAX_COMPOSITOR_DISTANCE = 0.99f; // If you're 1m from center of ui sphere, you're at the surface. - const float MAX_COMPOSITOR_ANGLE = 180.0f; // rotation check is effectively disabled + const float MAX_COMPOSITOR_DISTANCE = 0.99f; // If you're 1m from center of ui sphere, you're at the surface. + const float MAX_COMPOSITOR_ANGLE = 180.0f; // rotation check is effectively disabled if (glm::distance(uiPos, hmdPos) > MAX_COMPOSITOR_DISTANCE || glm::dot(uiForward, hmdForward) < cosf(glm::radians(MAX_COMPOSITOR_ANGLE))) { return true; @@ -43,10 +42,9 @@ bool OverlayConductor::headOutsideOverlay() const { } bool OverlayConductor::updateAvatarIsAtRest() { - auto myAvatar = DependencyManager::get()->getMyAvatar(); - const quint64 REST_ENABLE_TIME_USECS = 1000 * 1000; // 1 s + const quint64 REST_ENABLE_TIME_USECS = 1000 * 1000; // 1 s const quint64 REST_DISABLE_TIME_USECS = 200 * 1000; // 200 ms const float AT_REST_THRESHOLD = 0.01f; @@ -69,31 +67,6 @@ bool OverlayConductor::updateAvatarIsAtRest() { return _currentAtRest; } -bool OverlayConductor::updateAvatarHasDriveInput() { - auto myAvatar = DependencyManager::get()->getMyAvatar(); - - const quint64 DRIVE_ENABLE_TIME_USECS = 200 * 1000; // 200 ms - const quint64 DRIVE_DISABLE_TIME_USECS = 1000 * 1000; // 1 s - - bool desiredDriving = myAvatar->hasDriveInput(); - if (desiredDriving != _desiredDriving) { - // start timer - _desiredDrivingTimer = usecTimestampNow() + (desiredDriving ? DRIVE_ENABLE_TIME_USECS : DRIVE_DISABLE_TIME_USECS); - } - - _desiredDriving = desiredDriving; - - if (_desiredDrivingTimer != 0 && usecTimestampNow() > _desiredDrivingTimer) { - // timer expired - // change state! - _currentDriving = _desiredDriving; - // disable timer - _desiredDrivingTimer = 0; - } - - return _currentDriving; -} - void OverlayConductor::centerUI() { // place the overlay at the current hmd position in sensor space auto camMat = cancelOutRollAndPitch(qApp->getHMDSensorPose()); @@ -115,20 +88,19 @@ void OverlayConductor::update(float dt) { _hmdMode = false; } - bool prevDriving = _currentDriving; - bool isDriving = updateAvatarHasDriveInput(); - bool drivingChanged = prevDriving != isDriving; bool isAtRest = updateAvatarIsAtRest(); + bool isMoving = !isAtRest; + bool shouldRecenter = false; - if (_flags & SuppressedByDrive) { - if (!isDriving) { - _flags &= ~SuppressedByDrive; - shouldRecenter = true; + if (_flags & SuppressedByMove) { + if (!isMoving) { + _flags &= ~SuppressedByMove; + shouldRecenter = true; } } else { - if (myAvatar->getClearOverlayWhenMoving() && drivingChanged && isDriving) { - _flags |= SuppressedByDrive; + if (myAvatar->getClearOverlayWhenMoving() && isMoving) { + _flags |= SuppressedByMove; } } @@ -143,7 +115,6 @@ void OverlayConductor::update(float dt) { } } - bool targetVisible = Menu::getInstance()->isOptionChecked(MenuOption::Overlays) && (0 == (_flags & SuppressMask)); if (targetVisible != currentVisible) { offscreenUi->setPinned(!targetVisible); diff --git a/interface/src/ui/OverlayConductor.h b/interface/src/ui/OverlayConductor.h index cdd596a7bc..cf69c32fc5 100644 --- a/interface/src/ui/OverlayConductor.h +++ b/interface/src/ui/OverlayConductor.h @@ -23,23 +23,17 @@ public: private: bool headOutsideOverlay() const; - bool updateAvatarHasDriveInput(); bool updateAvatarIsAtRest(); enum SupressionFlags { - SuppressedByDrive = 0x01, + SuppressedByMove = 0x01, SuppressedByHead = 0x02, SuppressMask = 0x03, }; - uint8_t _flags { SuppressedByDrive }; + uint8_t _flags { SuppressedByMove }; bool _hmdMode { false }; - // used by updateAvatarHasDriveInput - uint64_t _desiredDrivingTimer { 0 }; - bool _desiredDriving { false }; - bool _currentDriving { false }; - // used by updateAvatarIsAtRest uint64_t _desiredAtRestTimer { 0 }; bool _desiredAtRest { true }; diff --git a/interface/src/ui/Snapshot.cpp b/interface/src/ui/Snapshot.cpp index b2c0ee7ce7..2b306ace91 100644 --- a/interface/src/ui/Snapshot.cpp +++ b/interface/src/ui/Snapshot.cpp @@ -122,8 +122,9 @@ static const glm::quat CAMERA_ORIENTATION_LEFT(glm::quat(glm::radians(glm::vec3( static const glm::quat CAMERA_ORIENTATION_BACK(glm::quat(glm::radians(glm::vec3(0.0f, 180.0f, 0.0f)))); static const glm::quat CAMERA_ORIENTATION_RIGHT(glm::quat(glm::radians(glm::vec3(0.0f, 270.0f, 0.0f)))); static const glm::quat CAMERA_ORIENTATION_UP(glm::quat(glm::radians(glm::vec3(90.0f, 0.0f, 0.0f)))); -void Snapshot::save360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const QString& filename) { +void Snapshot::save360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const bool& notify, const QString& filename) { _snapshotFilename = filename; + _notify360 = notify; _cubemapOutputFormat = cubemapOutputFormat; SecondaryCameraJobConfig* secondaryCameraRenderConfig = static_cast(qApp->getRenderEngine()->getConfiguration()->getConfig("SecondaryCamera")); @@ -247,7 +248,8 @@ void Snapshot::convertToCubemap() { painter.end(); - emit DependencyManager::get()->snapshot360Taken(saveSnapshot(outputImage, _snapshotFilename), true); + emit DependencyManager::get()->snapshot360Taken(saveSnapshot(outputImage, _snapshotFilename), + _notify360); } void Snapshot::convertToEquirectangular() { @@ -327,7 +329,8 @@ void Snapshot::convertToEquirectangular() { } } - emit DependencyManager::get()->snapshot360Taken(saveSnapshot(outputImage, _snapshotFilename), true); + emit DependencyManager::get()->snapshot360Taken(saveSnapshot(outputImage, _snapshotFilename), + _notify360); } QTemporaryFile* Snapshot::saveTempSnapshot(QImage image) { diff --git a/interface/src/ui/Snapshot.h b/interface/src/ui/Snapshot.h index 2bac857a97..8fc05775bd 100644 --- a/interface/src/ui/Snapshot.h +++ b/interface/src/ui/Snapshot.h @@ -50,7 +50,10 @@ class Snapshot : public QObject, public Dependency { public: Snapshot(); QString saveSnapshot(QImage image, const QString& filename, const QString& pathname = QString()); - void save360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const QString& filename); + void save360Snapshot(const glm::vec3& cameraPosition, + const bool& cubemapOutputFormat, + const bool& notify, + const QString& filename); QTemporaryFile* saveTempSnapshot(QImage image); SnapshotMetaData* parseSnapshotData(QString snapshotPath); @@ -89,6 +92,7 @@ private: const QString& userSelectedFilename = QString(), const QString& userSelectedPathname = QString()); QString _snapshotFilename; + bool _notify360; bool _cubemapOutputFormat; QTimer _snapshotTimer; qint16 _snapshotIndex; diff --git a/interface/src/ui/SnapshotUploader.cpp b/interface/src/ui/SnapshotUploader.cpp index 67902c1a35..4613871d25 100644 --- a/interface/src/ui/SnapshotUploader.cpp +++ b/interface/src/ui/SnapshotUploader.cpp @@ -35,6 +35,7 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { QString thumbnailUrl = dataObject.value("thumbnail_url").toString(); QString imageUrl = dataObject.value("image_url").toString(); QString snapshotID = dataObject.value("id").toString(); + QString originalImageFileName = dataObject.value("original_image_file_name").toString(); auto addressManager = DependencyManager::get(); QString placeName = _inWorldLocation.authority(); // We currently only upload shareable places, in which case this is just host. QString currentPath = _inWorldLocation.path(); @@ -48,6 +49,7 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { detailsObject.insert("shareable_url", dataObject.value("shareable_url").toString()); } detailsObject.insert("snapshot_id", snapshotID); + detailsObject.insert("original_image_file_name", originalImageFileName); QString pickledDetails = QJsonDocument(detailsObject).toJson(); userStoryObject.insert("details", pickledDetails); userStoryObject.insert("thumbnail_url", thumbnailUrl); diff --git a/interface/src/ui/overlays/Base3DOverlay.cpp b/interface/src/ui/overlays/Base3DOverlay.cpp index f4efd1301d..f59b513bd5 100644 --- a/interface/src/ui/overlays/Base3DOverlay.cpp +++ b/interface/src/ui/overlays/Base3DOverlay.cpp @@ -349,3 +349,23 @@ void Base3DOverlay::setVisible(bool visible) { Parent::setVisible(visible); notifyRenderVariableChange(); } + +render::ItemKey Base3DOverlay::getKey() { + auto builder = render::ItemKey::Builder(Overlay::getKey()); + + if (getDrawInFront()) { + builder.withLayer(render::hifi::LAYER_3D_FRONT); + } else if (getDrawHUDLayer()) { + builder.withLayer(render::hifi::LAYER_3D_HUD); + } else { + builder.withoutLayer(); + } + + builder.withoutViewSpace(); + + if (isTransparent()) { + builder.withTransparent(); + } + + return builder.build(); +} \ No newline at end of file diff --git a/interface/src/ui/overlays/Base3DOverlay.h b/interface/src/ui/overlays/Base3DOverlay.h index ab83a64273..2a63a6cb67 100644 --- a/interface/src/ui/overlays/Base3DOverlay.h +++ b/interface/src/ui/overlays/Base3DOverlay.h @@ -35,6 +35,7 @@ public: // getters virtual bool is3D() const override { return true; } + virtual render::ItemKey getKey() override; virtual uint32_t fetchMetaSubItems(render::ItemIDs& subItems) const override { subItems.push_back(getRenderItemID()); return (uint32_t) subItems.size(); } virtual scriptable::ScriptableModelBase getScriptableModel() override { return scriptable::ScriptableModelBase(); } diff --git a/interface/src/ui/overlays/Billboard3DOverlay.cpp b/interface/src/ui/overlays/Billboard3DOverlay.cpp index 960f0de095..ecade70ef8 100644 --- a/interface/src/ui/overlays/Billboard3DOverlay.cpp +++ b/interface/src/ui/overlays/Billboard3DOverlay.cpp @@ -46,6 +46,13 @@ bool Billboard3DOverlay::applyTransformTo(Transform& transform, bool force) { return transformChanged; } +void Billboard3DOverlay::update(float duration) { + if (isFacingAvatar()) { + _renderVariableDirty = true; + } + Parent::update(duration); +} + Transform Billboard3DOverlay::evalRenderTransform() { Transform transform = getTransform(); bool transformChanged = applyTransformTo(transform, true); diff --git a/interface/src/ui/overlays/Billboard3DOverlay.h b/interface/src/ui/overlays/Billboard3DOverlay.h index 6b3aa40451..174bc23bc8 100644 --- a/interface/src/ui/overlays/Billboard3DOverlay.h +++ b/interface/src/ui/overlays/Billboard3DOverlay.h @@ -18,6 +18,7 @@ class Billboard3DOverlay : public Planar3DOverlay, public PanelAttachable, public Billboardable { Q_OBJECT + using Parent = Planar3DOverlay; public: Billboard3DOverlay() {} @@ -26,6 +27,8 @@ public: void setProperties(const QVariantMap& properties) override; QVariant getProperty(const QString& property) override; + void update(float duration) override; + protected: virtual bool applyTransformTo(Transform& transform, bool force = false) override; diff --git a/interface/src/ui/overlays/Image3DOverlay.cpp b/interface/src/ui/overlays/Image3DOverlay.cpp index df93245922..6e9946e935 100644 --- a/interface/src/ui/overlays/Image3DOverlay.cpp +++ b/interface/src/ui/overlays/Image3DOverlay.cpp @@ -51,11 +51,6 @@ void Image3DOverlay::update(float deltatime) { _texture = DependencyManager::get()->getTexture(_url); _textureIsLoaded = false; } - if (usecTimestampNow() > _transformExpiry) { - Transform transform = getTransform(); - applyTransformTo(transform); - setTransform(transform); - } Parent::update(deltatime); } diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index 27e3bd0e2d..875352ebb4 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -100,26 +100,40 @@ void ModelOverlay::update(float deltatime) { processMaterials(); emit DependencyManager::get()->modelAddedToScene(getID(), NestableType::Overlay, _model); } + bool metaDirty = false; if (_visibleDirty) { _visibleDirty = false; // don't show overlays in mirrors or spectator-cam unless _isVisibleInSecondaryCamera is true - _model->setVisibleInScene(getVisible(), scene, - render::ItemKey::TAG_BITS_0 | - (_isVisibleInSecondaryCamera ? render::ItemKey::TAG_BITS_1 : render::ItemKey::TAG_BITS_NONE), - false); + uint8_t modelRenderTagMask = (_isVisibleInSecondaryCamera ? render::hifi::TAG_ALL_VIEWS : render::hifi::TAG_MAIN_VIEW); + _model->setTagMask(modelRenderTagMask, scene); + _model->setVisibleInScene(getVisible(), scene); + metaDirty = true; } if (_drawInFrontDirty) { _drawInFrontDirty = false; _model->setLayeredInFront(getDrawInFront(), scene); + metaDirty = true; } if (_drawInHUDDirty) { _drawInHUDDirty = false; _model->setLayeredInHUD(getDrawHUDLayer(), scene); + metaDirty = true; + } + if (_groupCulledDirty) { + _groupCulledDirty = false; + _model->setGroupCulled(_isGroupCulled, scene); + metaDirty = true; + } + if (metaDirty) { + transaction.updateItem(getRenderItemID(), [](Overlay& data) {}); } scene->enqueueTransaction(transaction); if (!_texturesLoaded && _model->getGeometry() && _model->getGeometry()->areTexturesLoaded()) { _texturesLoaded = true; + if (!_modelTextures.isEmpty()) { + _model->setTextures(_modelTextures); + } _model->updateRenderItems(); } } @@ -150,13 +164,24 @@ void ModelOverlay::setVisible(bool visible) { } void ModelOverlay::setDrawInFront(bool drawInFront) { - Base3DOverlay::setDrawInFront(drawInFront); - _drawInFrontDirty = true; + if (drawInFront != getDrawInFront()) { + Base3DOverlay::setDrawInFront(drawInFront); + _drawInFrontDirty = true; + } } void ModelOverlay::setDrawHUDLayer(bool drawHUDLayer) { - Base3DOverlay::setDrawHUDLayer(drawHUDLayer); - _drawInHUDDirty = true; + if (drawHUDLayer != getDrawHUDLayer()) { + Base3DOverlay::setDrawHUDLayer(drawHUDLayer); + _drawInHUDDirty = true; + } +} + +void ModelOverlay::setGroupCulled(bool groupCulled) { + if (groupCulled != _isGroupCulled) { + _isGroupCulled = groupCulled; + _groupCulledDirty = true; + } } void ModelOverlay::setProperties(const QVariantMap& properties) { @@ -207,8 +232,12 @@ void ModelOverlay::setProperties(const QVariantMap& properties) { if (texturesValue.isValid() && texturesValue.canConvert(QVariant::Map)) { _texturesLoaded = false; QVariantMap textureMap = texturesValue.toMap(); - QMetaObject::invokeMethod(_model.get(), "setTextures", Qt::AutoConnection, - Q_ARG(const QVariantMap&, textureMap)); + _modelTextures = textureMap; + } + + auto groupCulledValue = properties["isGroupCulled"]; + if (groupCulledValue.isValid() && groupCulledValue.canConvert(QVariant::Bool)) { + setGroupCulled(groupCulledValue.toBool()); } // jointNames is read-only. @@ -348,6 +377,8 @@ vectorType ModelOverlay::mapJoints(mapFunction function) const { * {@link Overlays.findRayIntersection|findRayIntersection} ignores the overlay. * @property {boolean} drawInFront=false - If true, the overlay is rendered in front of other overlays that don't * have drawInFront set to true, and in front of entities. + * @property {boolean} isGroupCulled=false - If true, the mesh parts of the model are LOD culled as a group. + * If false, separate mesh parts will be LOD culled individually. * @property {boolean} grabbable=false - Signal to grabbing scripts whether or not this overlay can be grabbed. * @property {Uuid} parentID=null - The avatar, entity, or overlay that the overlay is parented to. * @property {number} parentJointIndex=65535 - Integer value specifying the skeleton joint that the overlay is attached to if @@ -712,3 +743,11 @@ scriptable::ScriptableModelBase ModelOverlay::getScriptableModel() { } return result; } + +render::ItemKey ModelOverlay::getKey() { + auto builder = render::ItemKey::Builder(Base3DOverlay::getKey()); + if (_isGroupCulled) { + builder.withMetaCullGroup(); + } + return builder.build(); +} \ No newline at end of file diff --git a/interface/src/ui/overlays/ModelOverlay.h b/interface/src/ui/overlays/ModelOverlay.h index 3ef3f23fec..334c9c06f1 100644 --- a/interface/src/ui/overlays/ModelOverlay.h +++ b/interface/src/ui/overlays/ModelOverlay.h @@ -33,6 +33,7 @@ public: virtual uint32_t fetchMetaSubItems(render::ItemIDs& subItems) const override; + render::ItemKey getKey() override; void clearSubRenderItemIDs(); void setSubRenderItemIDs(const render::ItemIDs& ids); @@ -63,6 +64,7 @@ public: void setVisible(bool visible) override; void setDrawInFront(bool drawInFront) override; void setDrawHUDLayer(bool drawHUDLayer) override; + void setGroupCulled(bool groupCulled); void addMaterial(graphics::MaterialLayer material, const std::string& parentMaterialName) override; void removeMaterial(graphics::MaterialPointer material, const std::string& parentMaterialName) override; @@ -121,6 +123,8 @@ private: bool _visibleDirty { true }; bool _drawInFrontDirty { false }; bool _drawInHUDDirty { false }; + bool _isGroupCulled { false }; + bool _groupCulledDirty { false }; void processMaterials(); diff --git a/interface/src/ui/overlays/Overlay.cpp b/interface/src/ui/overlays/Overlay.cpp index 2c0c7c71b6..faa15ee2b4 100644 --- a/interface/src/ui/overlays/Overlay.cpp +++ b/interface/src/ui/overlays/Overlay.cpp @@ -244,4 +244,21 @@ void Overlay::addMaterial(graphics::MaterialLayer material, const std::string& p void Overlay::removeMaterial(graphics::MaterialPointer material, const std::string& parentMaterialName) { std::lock_guard lock(_materialsLock); _materials[parentMaterialName].remove(material); +} + +render::ItemKey Overlay::getKey() { + auto builder = render::ItemKey::Builder().withTypeShape(); + + builder.withViewSpace(); + builder.withLayer(render::hifi::LAYER_2D); + + if (!getVisible()) { + builder.withInvisible(); + } + + // always visible in primary view. if isVisibleInSecondaryCamera, also draw in secondary view + render::hifi::Tag viewTagBits = getIsVisibleInSecondaryCamera() ? render::hifi::TAG_ALL_VIEWS : render::hifi::TAG_MAIN_VIEW; + builder.withTagBits(viewTagBits); + + return builder.build(); } \ No newline at end of file diff --git a/interface/src/ui/overlays/Overlay.h b/interface/src/ui/overlays/Overlay.h index 2f27d50f7e..45fc77a452 100644 --- a/interface/src/ui/overlays/Overlay.h +++ b/interface/src/ui/overlays/Overlay.h @@ -40,6 +40,7 @@ public: virtual void update(float deltatime) {} virtual void render(RenderArgs* args) = 0; + virtual render::ItemKey getKey(); virtual AABox getBounds() const = 0; virtual bool supportsGetProperty() const { return true; } @@ -132,7 +133,6 @@ private: namespace render { template <> const ItemKey payloadGetKey(const Overlay::Pointer& overlay); template <> const Item::Bound payloadGetBound(const Overlay::Pointer& overlay); - template <> int payloadGetLayer(const Overlay::Pointer& overlay); template <> void payloadRender(const Overlay::Pointer& overlay, RenderArgs* args); template <> const ShapeKey shapeGetShapeKey(const Overlay::Pointer& overlay); template <> uint32_t metaFetchMetaSubItems(const Overlay::Pointer& overlay, ItemIDs& subItems); diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index 3ff782da99..3debf74f26 100644 --- a/interface/src/ui/overlays/Overlays.h +++ b/interface/src/ui/overlays/Overlays.h @@ -53,7 +53,7 @@ const OverlayID UNKNOWN_OVERLAY_ID = OverlayID(); * @property {number} distance - The distance from the {@link PickRay} origin to the intersection point. * @property {Vec3} surfaceNormal - The normal of the overlay surface at the intersection point. * @property {Vec3} intersection - The position of the intersection point. - * @property {Object} extraInfo Additional intersection details, if available. + * @property {object} extraInfo Additional intersection details, if available. */ class RayToOverlayIntersectionResult { public: @@ -482,7 +482,7 @@ public slots: /**jsdoc * Check if there is an overlay of a given ID. - * @function Overlays.isAddedOverly + * @function Overlays.isAddedOverlay * @param {Uuid} overlayID - The ID to check. * @returns {boolean} true if an overlay with the given ID exists, false otherwise. */ diff --git a/interface/src/ui/overlays/OverlaysPayload.cpp b/interface/src/ui/overlays/OverlaysPayload.cpp index 185547a333..37fadef0b4 100644 --- a/interface/src/ui/overlays/OverlaysPayload.cpp +++ b/interface/src/ui/overlays/OverlaysPayload.cpp @@ -32,48 +32,11 @@ namespace render { template <> const ItemKey payloadGetKey(const Overlay::Pointer& overlay) { - auto builder = ItemKey::Builder().withTypeShape(); - if (overlay->is3D()) { - auto overlay3D = std::static_pointer_cast(overlay); - if (overlay3D->getDrawInFront() || overlay3D->getDrawHUDLayer()) { - builder.withLayered(); - } - if (overlay->isTransparent()) { - builder.withTransparent(); - } - } else { - builder.withViewSpace(); - } - - if (!overlay->getVisible()) { - builder.withInvisible(); - } - - // always visible in primary view. if isVisibleInSecondaryCamera, also draw in secondary view - uint32_t viewTaskBits = render::ItemKey::TAG_BITS_0 | - (overlay->getIsVisibleInSecondaryCamera() ? render::ItemKey::TAG_BITS_1 : render::ItemKey::TAG_BITS_NONE); - - builder.withTagBits(viewTaskBits); - - return builder.build(); + return overlay->getKey(); } template <> const Item::Bound payloadGetBound(const Overlay::Pointer& overlay) { return overlay->getBounds(); } - template <> int payloadGetLayer(const Overlay::Pointer& overlay) { - if (overlay->is3D()) { - auto overlay3D = std::dynamic_pointer_cast(overlay); - if (overlay3D->getDrawInFront()) { - return Item::LAYER_3D_FRONT; - } else if (overlay3D->getDrawHUDLayer()) { - return Item::LAYER_3D_HUD; - } else { - return Item::LAYER_3D; - } - } else { - return Item::LAYER_2D; - } - } template <> void payloadRender(const Overlay::Pointer& overlay, RenderArgs* args) { if (args) { overlay->render(args); @@ -83,7 +46,6 @@ namespace render { return overlay->getShapeKey(); } - template <> uint32_t metaFetchMetaSubItems(const Overlay::Pointer& overlay, ItemIDs& subItems) { return overlay->fetchMetaSubItems(subItems); } diff --git a/interface/src/ui/overlays/Text3DOverlay.cpp b/interface/src/ui/overlays/Text3DOverlay.cpp index 9c920efb93..b128ce7df7 100644 --- a/interface/src/ui/overlays/Text3DOverlay.cpp +++ b/interface/src/ui/overlays/Text3DOverlay.cpp @@ -83,15 +83,6 @@ xColor Text3DOverlay::getBackgroundColor() { return result; } -void Text3DOverlay::update(float deltatime) { - if (usecTimestampNow() > _transformExpiry) { - Transform transform = getTransform(); - applyTransformTo(transform); - setTransform(transform); - } - Parent::update(deltatime); -} - void Text3DOverlay::render(RenderArgs* args) { if (!_renderVisible || !getParentVisible()) { return; // do nothing if we're not visible @@ -306,13 +297,4 @@ QSizeF Text3DOverlay::textSize(const QString& text) const { float pointToWorldScale = (maxHeight / FIXED_FONT_SCALING_RATIO) * _lineHeight; return QSizeF(extents.x, extents.y) * pointToWorldScale; -} - -bool Text3DOverlay::findRayIntersection(const glm::vec3 &origin, const glm::vec3 &direction, float &distance, - BoxFace &face, glm::vec3& surfaceNormal) { - Transform transform = getTransform(); - applyTransformTo(transform, true); - setTransform(transform); - return Billboard3DOverlay::findRayIntersection(origin, direction, distance, face, surfaceNormal); -} - +} \ No newline at end of file diff --git a/interface/src/ui/overlays/Text3DOverlay.h b/interface/src/ui/overlays/Text3DOverlay.h index daa5fdc804..21163101d0 100644 --- a/interface/src/ui/overlays/Text3DOverlay.h +++ b/interface/src/ui/overlays/Text3DOverlay.h @@ -30,8 +30,6 @@ public: ~Text3DOverlay(); virtual void render(RenderArgs* args) override; - virtual void update(float deltatime) override; - virtual const render::ShapeKey getShapeKey() override; // getters @@ -60,9 +58,6 @@ public: QSizeF textSize(const QString& test) const; // Meters - virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, - BoxFace& face, glm::vec3& surfaceNormal) override; - virtual Text3DOverlay* createClone() const override; private: diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index c678e3d2a2..8af818edc6 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -259,7 +259,6 @@ void Web3DOverlay::setupQmlSurface() { _webSurface->getSurfaceContext()->setContextProperty("Web3DOverlay", this); _webSurface->getSurfaceContext()->setContextProperty("Window", DependencyManager::get().data()); _webSurface->getSurfaceContext()->setContextProperty("Reticle", qApp->getApplicationCompositor().getReticleInterface()); - _webSurface->getSurfaceContext()->setContextProperty("desktop", DependencyManager::get()->getDesktop()); _webSurface->getSurfaceContext()->setContextProperty("HiFiAbout", AboutUtil::getInstance()); // Override min fps for tablet UI, for silky smooth scrolling diff --git a/libraries/animation/src/AnimUtil.cpp b/libraries/animation/src/AnimUtil.cpp index 65c605b5ba..acb90126fc 100644 --- a/libraries/animation/src/AnimUtil.cpp +++ b/libraries/animation/src/AnimUtil.cpp @@ -9,7 +9,9 @@ // #include "AnimUtil.h" -#include "GLMHelpers.h" +#include +#include +#include // TODO: use restrict keyword // TODO: excellent candidate for simd vectorization. @@ -107,3 +109,44 @@ AnimPose boneLookAt(const glm::vec3& target, const AnimPose& bone) { glm::vec4(bone.trans(), 1.0f)); return AnimPose(lookAt); } + +// This will attempt to determine the proper body facing of a characters body +// assumes headRot is z-forward and y-up. +// and returns a bodyRot that is also z-forward and y-up +glm::quat computeBodyFacingFromHead(const glm::quat& headRot, const glm::vec3& up) { + + glm::vec3 bodyUp = glm::normalize(up); + + // initially take the body facing from the head. + glm::vec3 headUp = headRot * Vectors::UNIT_Y; + glm::vec3 headForward = headRot * Vectors::UNIT_Z; + glm::vec3 headLeft = headRot * Vectors::UNIT_X; + const float NOD_THRESHOLD = cosf(glm::radians(45.0f)); + const float TILT_THRESHOLD = cosf(glm::radians(30.0f)); + + glm::vec3 bodyForward = headForward; + + float nodDot = glm::dot(headForward, bodyUp); + float tiltDot = glm::dot(headLeft, bodyUp); + + if (fabsf(tiltDot) < TILT_THRESHOLD) { // if we are not tilting too much + if (nodDot < -NOD_THRESHOLD) { // head is looking downward + // the body should face in the same direction as the top the head. + bodyForward = headUp; + } else if (nodDot > NOD_THRESHOLD) { // head is looking upward + // the body should face away from the top of the head. + bodyForward = -headUp; + } + } + + // cancel out upward component + bodyForward = glm::normalize(bodyForward - nodDot * bodyUp); + + glm::vec3 u, v, w; + generateBasisVectors(bodyForward, bodyUp, u, v, w); + + // create matrix from orthogonal basis vectors + glm::mat4 bodyMat(glm::vec4(w, 0.0f), glm::vec4(v, 0.0f), glm::vec4(u, 0.0f), glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); + + return glmExtractRotation(bodyMat); +} diff --git a/libraries/animation/src/AnimUtil.h b/libraries/animation/src/AnimUtil.h index f2cceb361b..3cd7f4b6fb 100644 --- a/libraries/animation/src/AnimUtil.h +++ b/libraries/animation/src/AnimUtil.h @@ -33,4 +33,9 @@ inline glm::quat safeLerp(const glm::quat& a, const glm::quat& b, float alpha) { AnimPose boneLookAt(const glm::vec3& target, const AnimPose& bone); +// This will attempt to determine the proper body facing of a characters body +// assumes headRot is z-forward and y-up. +// and returns a bodyRot that is also z-forward and y-up +glm::quat computeBodyFacingFromHead(const glm::quat& headRot, const glm::vec3& up); + #endif diff --git a/libraries/animation/src/AnimationCache.h b/libraries/animation/src/AnimationCache.h index d8f8a13cde..4b0a8901f5 100644 --- a/libraries/animation/src/AnimationCache.h +++ b/libraries/animation/src/AnimationCache.h @@ -70,7 +70,7 @@ public: * @function AnimationCache.prefetch * @param {string} url - URL of the resource to prefetch. * @param {object} [extra=null] - * @returns {Resource} + * @returns {ResourceObject} */ /**jsdoc @@ -79,7 +79,7 @@ public: * @param {string} url - URL of the resource to load. * @param {string} [fallback=""] - Fallback URL if load of the desired URL fails. * @param {} [extra=null] - * @returns {Resource} + * @returns {object} */ @@ -87,7 +87,7 @@ public: * Returns animation resource for particular animation. * @function AnimationCache.getAnimation * @param {string} url - URL to load. - * @returns {Resource} animation + * @returns {AnimationObject} animation */ Q_INVOKABLE AnimationPointer getAnimation(const QString& url) { return getAnimation(QUrl(url)); } Q_INVOKABLE AnimationPointer getAnimation(const QUrl& url); @@ -104,6 +104,17 @@ private: Q_DECLARE_METATYPE(AnimationPointer) +/**jsdoc + * @class AnimationObject + * + * @hifi-interface + * @hifi-client-entity + * @hifi-server-entity + * @hifi-assignment-client + * + * @property {string[]} jointNames + * @property {FBXAnimationFrame[]} frames + */ /// An animation loaded from the network. class Animation : public Resource { Q_OBJECT @@ -118,9 +129,16 @@ public: virtual bool isLoaded() const override; - + /**jsdoc + * @function AnimationObject.getJointNames + * @returns {string[]} + */ Q_INVOKABLE QStringList getJointNames() const; + /**jsdoc + * @function AnimationObject.getFrames + * @returns {FBXAnimationFrame[]} + */ Q_INVOKABLE QVector getFrames() const; const QVector& getFramesReference() const; diff --git a/libraries/audio/src/Sound.h b/libraries/audio/src/Sound.h index 69dbf5a913..4cfdac7792 100644 --- a/libraries/audio/src/Sound.h +++ b/libraries/audio/src/Sound.h @@ -77,6 +77,17 @@ private: typedef QSharedPointer SharedSoundPointer; +/**jsdoc + * @class SoundObject + * + * @hifi-interface + * @hifi-client-entity + * @hifi-server-entity + * @hifi-assignment-client + * + * @property {boolean} downloaded + * @property {number} duration + */ class SoundScriptingInterface : public QObject { Q_OBJECT @@ -90,6 +101,10 @@ public: bool isReady() const { return _sound->isReady(); } float getDuration() { return _sound->getDuration(); } +/**jsdoc + * @function SoundObject.ready + * @returns {Signal} + */ signals: void ready(); diff --git a/libraries/audio/src/SoundCache.h b/libraries/audio/src/SoundCache.h index 347f324353..4352b1d459 100644 --- a/libraries/audio/src/SoundCache.h +++ b/libraries/audio/src/SoundCache.h @@ -64,7 +64,7 @@ public: * @function SoundCache.prefetch * @param {string} url - URL of the resource to prefetch. * @param {object} [extra=null] - * @returns {Resource} + * @returns {ResourceObject} */ /**jsdoc @@ -73,14 +73,14 @@ public: * @param {string} url - URL of the resource to load. * @param {string} [fallback=""] - Fallback URL if load of the desired URL fails. * @param {} [extra=null] - * @returns {Resource} + * @returns {object} */ /**jsdoc * @function SoundCache.getSound * @param {string} url - * @returns {object} + * @returns {SoundObject} */ Q_INVOKABLE SharedSoundPointer getSound(const QUrl& url); protected: diff --git a/libraries/auto-updater/src/AutoUpdater.cpp b/libraries/auto-updater/src/AutoUpdater.cpp index e58ac067a6..6749cd9e10 100644 --- a/libraries/auto-updater/src/AutoUpdater.cpp +++ b/libraries/auto-updater/src/AutoUpdater.cpp @@ -11,6 +11,8 @@ #include "AutoUpdater.h" +#include + #include #include #include @@ -157,10 +159,8 @@ void AutoUpdater::parseLatestVersionData() { } void AutoUpdater::checkVersionAndNotify() { - if (QCoreApplication::applicationVersion() == "dev" || - QCoreApplication::applicationVersion().contains("PR") || - _builds.empty()) { - // No version checking is required in dev builds or when no build + if (BuildInfo::BUILD_TYPE != BuildInfo::BuildType::Stable || _builds.empty()) { + // No version checking is required in nightly/PR/dev builds or when no build // data was found for the platform return; } @@ -196,4 +196,4 @@ void AutoUpdater::appendBuildData(int versionNumber, thisBuildDetails.insert("pullRequestNumber", pullRequestNumber); _builds.insert(versionNumber, thisBuildDetails); -} \ No newline at end of file +} diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index da829b23e4..9682c81697 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -52,10 +52,15 @@ const glm::vec3 HAND_TO_PALM_OFFSET(0.0f, 0.12f, 0.08f); namespace render { template <> const ItemKey payloadGetKey(const AvatarSharedPointer& avatar) { - return ItemKey::Builder::opaqueShape().withTypeMeta().withTagBits(ItemKey::TAG_BITS_0 | ItemKey::TAG_BITS_1).withMetaCullGroup(); + ItemKey::Builder keyBuilder = ItemKey::Builder::opaqueShape().withTypeMeta().withTagBits(render::hifi::TAG_ALL_VIEWS).withMetaCullGroup(); + auto avatarPtr = static_pointer_cast(avatar); + if (!avatarPtr->getEnableMeshVisible()) { + keyBuilder.withInvisible(); + } + return keyBuilder.build(); } template <> const Item::Bound payloadGetBound(const AvatarSharedPointer& avatar) { - return static_pointer_cast(avatar)->getBounds(); + return static_pointer_cast(avatar)->getRenderBounds(); } template <> void payloadRender(const AvatarSharedPointer& avatar, RenderArgs* args) { auto avatarPtr = static_pointer_cast(avatar); @@ -164,6 +169,11 @@ AABox Avatar::getBounds() const { return _skeletonModel->getRenderableMeshBound(); } + +AABox Avatar::getRenderBounds() const { + return _renderBound; +} + void Avatar::animateScaleChanges(float deltaTime) { if (_isAnimatingScale) { @@ -569,16 +579,26 @@ static TextRenderer3D* textRenderer(TextRendererType type) { void Avatar::addToScene(AvatarSharedPointer self, const render::ScenePointer& scene, render::Transaction& transaction) { auto avatarPayload = new render::Payload(self); - auto avatarPayloadPointer = Avatar::PayloadPointer(avatarPayload); - + auto avatarPayloadPointer = std::shared_ptr>(avatarPayload); if (_renderItemID == render::Item::INVALID_ITEM_ID) { _renderItemID = scene->allocateID(); } + // INitialize the _render bound as we are creating the avatar render item + _renderBound = getBounds(); transaction.resetItem(_renderItemID, avatarPayloadPointer); _skeletonModel->addToScene(scene, transaction); + _skeletonModel->setTagMask(render::hifi::TAG_ALL_VIEWS); + _skeletonModel->setGroupCulled(true); + _skeletonModel->setCanCastShadow(true); + _skeletonModel->setVisibleInScene(_isMeshVisible, scene); + processMaterials(); for (auto& attachmentModel : _attachmentModels) { attachmentModel->addToScene(scene, transaction); + attachmentModel->setTagMask(render::hifi::TAG_ALL_VIEWS); + attachmentModel->setGroupCulled(false); + attachmentModel->setCanCastShadow(true); + attachmentModel->setVisibleInScene(_isMeshVisible, scene); } _mustFadeIn = true; @@ -637,7 +657,15 @@ void Avatar::removeFromScene(AvatarSharedPointer self, const render::ScenePointe void Avatar::updateRenderItem(render::Transaction& transaction) { if (render::Item::isValidID(_renderItemID)) { - transaction.updateItem>(_renderItemID, [](render::Payload& p) {}); + auto renderBound = getBounds(); + transaction.updateItem(_renderItemID, + [renderBound](AvatarData& avatar) { + auto avatarPtr = dynamic_cast(&avatar); + if (avatarPtr) { + avatarPtr->_renderBound = renderBound; + } + } + ); } } @@ -759,6 +787,18 @@ void Avatar::render(RenderArgs* renderArgs) { } } + +void Avatar::setEnableMeshVisible(bool isEnabled) { + if (_isMeshVisible != isEnabled) { + _isMeshVisible = isEnabled; + _needMeshVisibleSwitch = true; + } +} + +bool Avatar::getEnableMeshVisible() const { + return _isMeshVisible; +} + void Avatar::fixupModelsInScene(const render::ScenePointer& scene) { bool canTryFade{ false }; @@ -770,6 +810,12 @@ void Avatar::fixupModelsInScene(const render::ScenePointer& scene) { if (_skeletonModel->isRenderable() && _skeletonModel->needsFixupInScene()) { _skeletonModel->removeFromScene(scene, transaction); _skeletonModel->addToScene(scene, transaction); + + _skeletonModel->setTagMask(render::hifi::TAG_ALL_VIEWS); + _skeletonModel->setGroupCulled(true); + _skeletonModel->setCanCastShadow(true); + _skeletonModel->setVisibleInScene(_isMeshVisible, scene); + processMaterials(); canTryFade = true; _isAnimatingScale = true; @@ -778,9 +824,25 @@ void Avatar::fixupModelsInScene(const render::ScenePointer& scene) { if (attachmentModel->isRenderable() && attachmentModel->needsFixupInScene()) { attachmentModel->removeFromScene(scene, transaction); attachmentModel->addToScene(scene, transaction); + + attachmentModel->setTagMask(render::hifi::TAG_ALL_VIEWS); + attachmentModel->setGroupCulled(false); + attachmentModel->setCanCastShadow(true); + attachmentModel->setVisibleInScene(_isMeshVisible, scene); } } + if (_needMeshVisibleSwitch) { + _skeletonModel->setVisibleInScene(_isMeshVisible, scene); + for (auto attachmentModel : _attachmentModels) { + if (attachmentModel->isRenderable()) { + attachmentModel->setVisibleInScene(_isMeshVisible, scene); + } + } + updateRenderItem(transaction); + _needMeshVisibleSwitch = false; + } + if (_mustFadeIn && canTryFade) { // Do it now to be sure all the sub items are ready and the fade is sent to them too fade(transaction, render::Transition::USER_ENTER_DOMAIN); @@ -799,6 +861,7 @@ bool Avatar::shouldRenderHead(const RenderArgs* renderArgs) const { return true; } +// virtual void Avatar::simulateAttachments(float deltaTime) { assert(_attachmentModels.size() == _attachmentModelsTexturesLoaded.size()); PerformanceTimer perfTimer("attachments"); @@ -1481,14 +1544,12 @@ void Avatar::updateDisplayNameAlpha(bool showDisplayName) { } } +// virtual void Avatar::computeShapeInfo(ShapeInfo& shapeInfo) { float uniformScale = getModelScale(); - float radius = uniformScale * _skeletonModel->getBoundingCapsuleRadius(); - float height = uniformScale * _skeletonModel->getBoundingCapsuleHeight(); - shapeInfo.setCapsuleY(radius, 0.5f * height); - - glm::vec3 offset = uniformScale * _skeletonModel->getBoundingCapsuleOffset(); - shapeInfo.setOffset(offset); + shapeInfo.setCapsuleY(uniformScale * _skeletonModel->getBoundingCapsuleRadius(), + 0.5f * uniformScale * _skeletonModel->getBoundingCapsuleHeight()); + shapeInfo.setOffset(uniformScale * _skeletonModel->getBoundingCapsuleOffset()); } void Avatar::getCapsule(glm::vec3& start, glm::vec3& end, float& radius) { @@ -1511,8 +1572,9 @@ float Avatar::computeMass() { return _density * TWO_PI * radius * radius * (glm::length(end - start) + 2.0f * radius / 3.0f); } +// virtual void Avatar::rebuildCollisionShape() { - addPhysicsFlags(Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS); + addPhysicsFlags(Simulation::DIRTY_SHAPE); } void Avatar::setPhysicsCallback(AvatarPhysicsCallback cb) { diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 01114b5f6d..10c1d9ead2 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -74,7 +74,6 @@ public: virtual void instantiableAvatar() = 0; typedef render::Payload Payload; - typedef std::shared_ptr PayloadPointer; void init(); void updateAvatarEntities(); @@ -292,7 +291,7 @@ public: */ /**jsdoc * Information about a single joint in an Avatar's skeleton hierarchy. - * @typedef MyAvatar.SkeletonJoint + * @typedef {object} MyAvatar.SkeletonJoint * @property {string} name - Joint name. * @property {number} index - Joint index. * @property {number} parentIndex - Index of this joint's parent (-1 if no parent). @@ -322,6 +321,7 @@ public: bool hasNewJointData() const { return _hasNewJointData; } float getBoundingRadius() const; + AABox getRenderBounds() const; // THis call is accessible from rendering thread only to report the bounding box of the avatar during the frame. void addToScene(AvatarSharedPointer self, const render::ScenePointer& scene); void ensureInScene(AvatarSharedPointer self, const render::ScenePointer& scene); @@ -356,6 +356,10 @@ public: virtual void setAvatarEntityDataChanged(bool value) override; + // Show hide the model representation of the avatar + virtual void setEnableMeshVisible(bool isEnabled); + virtual bool getEnableMeshVisible() const; + void addMaterial(graphics::MaterialLayer material, const std::string& parentMaterialName) override; void removeMaterial(graphics::MaterialPointer material, const std::string& parentMaterialName) override; @@ -532,6 +536,10 @@ protected: std::mutex _materialsLock; void processMaterials(); + + AABox _renderBound; + bool _isMeshVisible{ true }; + bool _needMeshVisibleSwitch{ true }; }; #endif // hifi_Avatar_h diff --git a/libraries/avatars-renderer/src/avatars-renderer/Head.cpp b/libraries/avatars-renderer/src/avatars-renderer/Head.cpp index 256b3bf8a6..5800c1404b 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Head.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Head.cpp @@ -20,6 +20,7 @@ #include #include #include +#include "Logging.h" #include "Avatar.h" @@ -58,25 +59,30 @@ void Head::simulate(float deltaTime) { _longTermAverageLoudness = glm::mix(_longTermAverageLoudness, _averageLoudness, glm::min(deltaTime / AUDIO_LONG_TERM_AVERAGING_SECS, 1.0f)); } - if (!_isFaceTrackerConnected) { - if (!_isEyeTrackerConnected) { - // Update eye saccades - const float AVERAGE_MICROSACCADE_INTERVAL = 1.0f; - const float AVERAGE_SACCADE_INTERVAL = 6.0f; - const float MICROSACCADE_MAGNITUDE = 0.002f; - const float SACCADE_MAGNITUDE = 0.04f; - const float NOMINAL_FRAME_RATE = 60.0f; + if (!_isEyeTrackerConnected) { + // Update eye saccades + const float AVERAGE_MICROSACCADE_INTERVAL = 1.0f; + const float AVERAGE_SACCADE_INTERVAL = 6.0f; + const float MICROSACCADE_MAGNITUDE = 0.002f; + const float SACCADE_MAGNITUDE = 0.04f; + const float NOMINAL_FRAME_RATE = 60.0f; - if (randFloat() < deltaTime / AVERAGE_MICROSACCADE_INTERVAL) { - _saccadeTarget = MICROSACCADE_MAGNITUDE * randVector(); - } else if (randFloat() < deltaTime / AVERAGE_SACCADE_INTERVAL) { - _saccadeTarget = SACCADE_MAGNITUDE * randVector(); - } - _saccade += (_saccadeTarget - _saccade) * pow(0.5f, NOMINAL_FRAME_RATE * deltaTime); - } else { - _saccade = glm::vec3(); + if (randFloat() < deltaTime / AVERAGE_MICROSACCADE_INTERVAL) { + _saccadeTarget = MICROSACCADE_MAGNITUDE * randVector(); + } else if (randFloat() < deltaTime / AVERAGE_SACCADE_INTERVAL) { + _saccadeTarget = SACCADE_MAGNITUDE * randVector(); } + _saccade += (_saccadeTarget - _saccade) * pow(0.5f, NOMINAL_FRAME_RATE * deltaTime); + } else { + _saccade = glm::vec3(); + } + const float BLINK_SPEED = 10.0f; + const float BLINK_SPEED_VARIABILITY = 1.0f; + const float BLINK_START_VARIABILITY = 0.25f; + const float FULLY_OPEN = 0.0f; + const float FULLY_CLOSED = 1.0f; + if (getHasProceduralBlinkFaceMovement()) { // Detect transition from talking to not; force blink after that and a delay bool forceBlink = false; const float TALKING_LOUDNESS = 100.0f; @@ -88,29 +94,12 @@ void Head::simulate(float deltaTime) { forceBlink = true; } - // Update audio attack data for facial animation (eyebrows and mouth) - float audioAttackAveragingRate = (10.0f - deltaTime * NORMAL_HZ) / 10.0f; // --> 0.9 at 60 Hz - _audioAttack = audioAttackAveragingRate * _audioAttack + - (1.0f - audioAttackAveragingRate) * fabs((audioLoudness - _longTermAverageLoudness) - _lastLoudness); - _lastLoudness = (audioLoudness - _longTermAverageLoudness); - - const float BROW_LIFT_THRESHOLD = 100.0f; - if (_audioAttack > BROW_LIFT_THRESHOLD) { - _browAudioLift += sqrtf(_audioAttack) * 0.01f; - } - _browAudioLift = glm::clamp(_browAudioLift *= 0.7f, 0.0f, 1.0f); - - const float BLINK_SPEED = 10.0f; - const float BLINK_SPEED_VARIABILITY = 1.0f; - const float BLINK_START_VARIABILITY = 0.25f; - const float FULLY_OPEN = 0.0f; - const float FULLY_CLOSED = 1.0f; if (_leftEyeBlinkVelocity == 0.0f && _rightEyeBlinkVelocity == 0.0f) { // no blinking when brows are raised; blink less with increasing loudness const float BASE_BLINK_RATE = 15.0f / 60.0f; const float ROOT_LOUDNESS_TO_BLINK_INTERVAL = 0.25f; if (forceBlink || (_browAudioLift < EPSILON && shouldDo(glm::max(1.0f, sqrt(fabs(_averageLoudness - _longTermAverageLoudness)) * - ROOT_LOUDNESS_TO_BLINK_INTERVAL) / BASE_BLINK_RATE, deltaTime))) { + ROOT_LOUDNESS_TO_BLINK_INTERVAL) / BASE_BLINK_RATE, deltaTime))) { _leftEyeBlinkVelocity = BLINK_SPEED + randFloat() * BLINK_SPEED_VARIABILITY; _rightEyeBlinkVelocity = BLINK_SPEED + randFloat() * BLINK_SPEED_VARIABILITY; if (randFloat() < 0.5f) { @@ -136,22 +125,45 @@ void Head::simulate(float deltaTime) { _rightEyeBlinkVelocity = 0.0f; } } + } else { + _rightEyeBlink = FULLY_OPEN; + _leftEyeBlink = FULLY_OPEN; + } // use data to update fake Faceshift blendshape coefficients + if (getHasAudioEnabledFaceMovement()) { + // Update audio attack data for facial animation (eyebrows and mouth) + float audioAttackAveragingRate = (10.0f - deltaTime * NORMAL_HZ) / 10.0f; // --> 0.9 at 60 Hz + _audioAttack = audioAttackAveragingRate * _audioAttack + + (1.0f - audioAttackAveragingRate) * fabs((audioLoudness - _longTermAverageLoudness) - _lastLoudness); + _lastLoudness = (audioLoudness - _longTermAverageLoudness); + const float BROW_LIFT_THRESHOLD = 100.0f; + if (_audioAttack > BROW_LIFT_THRESHOLD) { + _browAudioLift += sqrtf(_audioAttack) * 0.01f; + } + _browAudioLift = glm::clamp(_browAudioLift *= 0.7f, 0.0f, 1.0f); calculateMouthShapes(deltaTime); - FaceTracker::updateFakeCoefficients(_leftEyeBlink, - _rightEyeBlink, - _browAudioLift, - _audioJawOpen, - _mouth2, - _mouth3, - _mouth4, - _transientBlendshapeCoefficients); - - applyEyelidOffset(getOrientation()); } else { - _saccade = glm::vec3(); + _audioJawOpen = 0.0f; + _browAudioLift = 0.0f; + _mouth2 = 0.0f; + _mouth3 = 0.0f; + _mouth4 = 0.0f; + _mouthTime = 0.0f; + } + + FaceTracker::updateFakeCoefficients(_leftEyeBlink, + _rightEyeBlink, + _browAudioLift, + _audioJawOpen, + _mouth2, + _mouth3, + _mouth4, + _transientBlendshapeCoefficients); + + if (getHasProceduralEyeFaceMovement()) { + applyEyelidOffset(getOrientation()); } _leftEyePosition = _rightEyePosition = getPosition(); diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp index 428f86f0ab..de8c02f10e 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp @@ -35,7 +35,7 @@ SkeletonModel::SkeletonModel(Avatar* owningAvatar, QObject* parent) : _useDualQuaternionSkinning = true; // Avatars all cast shadow - _canCastShadow = true; + setCanCastShadow(true); assert(_owningAvatar); } diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 7a28686f8c..b5186ba8f4 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -300,14 +300,15 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent tranlationChangedSince(lastSentTime) || parentInfoChangedSince(lastSentTime)); - hasFaceTrackerInfo = !dropFaceTracking && hasFaceTracker() && (sendAll || faceTrackerInfoChangedSince(lastSentTime)); + hasFaceTrackerInfo = !dropFaceTracking && (hasFaceTracker() || getHasScriptedBlendshapes()) && + (sendAll || faceTrackerInfoChangedSince(lastSentTime)); hasJointData = sendAll || !sendMinimum; hasJointDefaultPoseFlags = hasJointData; } const size_t byteArraySize = AvatarDataPacket::MAX_CONSTANT_HEADER_SIZE + - (hasFaceTrackerInfo ? AvatarDataPacket::maxFaceTrackerInfoSize(_headData->getNumSummedBlendshapeCoefficients()) : 0) + + (hasFaceTrackerInfo ? AvatarDataPacket::maxFaceTrackerInfoSize(_headData->getBlendshapeCoefficients().size()) : 0) + (hasJointData ? AvatarDataPacket::maxJointDataSize(_jointData.size()) : 0) + (hasJointDefaultPoseFlags ? AvatarDataPacket::maxJointDefaultPoseFlagsSize(_jointData.size()) : 0); @@ -442,7 +443,7 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent auto startSection = destinationBuffer; auto data = reinterpret_cast(destinationBuffer); - uint8_t flags { 0 }; + uint16_t flags { 0 }; setSemiNibbleAt(flags, KEY_STATE_START_BIT, _keyState); @@ -450,20 +451,33 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent bool isFingerPointing = _handState & IS_FINGER_POINTING_FLAG; setSemiNibbleAt(flags, HAND_STATE_START_BIT, _handState & ~IS_FINGER_POINTING_FLAG); if (isFingerPointing) { - setAtBit(flags, HAND_STATE_FINGER_POINTING_BIT); + setAtBit16(flags, HAND_STATE_FINGER_POINTING_BIT); } // face tracker state if (_headData->_isFaceTrackerConnected) { - setAtBit(flags, IS_FACE_TRACKER_CONNECTED); + setAtBit16(flags, IS_FACE_TRACKER_CONNECTED); } // eye tracker state if (_headData->_isEyeTrackerConnected) { - setAtBit(flags, IS_EYE_TRACKER_CONNECTED); + setAtBit16(flags, IS_EYE_TRACKER_CONNECTED); } // referential state if (!parentID.isNull()) { - setAtBit(flags, HAS_REFERENTIAL); + setAtBit16(flags, HAS_REFERENTIAL); } + // audio face movement + if (_headData->getHasAudioEnabledFaceMovement()) { + setAtBit16(flags, AUDIO_ENABLED_FACE_MOVEMENT); + } + // procedural eye face movement + if (_headData->getHasProceduralEyeFaceMovement()) { + setAtBit16(flags, PROCEDURAL_EYE_FACE_MOVEMENT); + } + // procedural blink face movement + if (_headData->getHasProceduralBlinkFaceMovement()) { + setAtBit16(flags, PROCEDURAL_BLINK_FACE_MOVEMENT); + } + data->flags = flags; destinationBuffer += sizeof(AvatarDataPacket::AdditionalFlags); @@ -506,8 +520,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent if (hasFaceTrackerInfo) { auto startSection = destinationBuffer; auto faceTrackerInfo = reinterpret_cast(destinationBuffer); - const auto& blendshapeCoefficients = _headData->getSummedBlendshapeCoefficients(); - + const auto& blendshapeCoefficients = _headData->getBlendshapeCoefficients(); + // note: we don't use the blink and average loudness, we just use the numBlendShapes and + // compute the procedural info on the client side. faceTrackerInfo->leftEyeBlink = _headData->_leftEyeBlink; faceTrackerInfo->rightEyeBlink = _headData->_rightEyeBlink; faceTrackerInfo->averageLoudness = _headData->_averageLoudness; @@ -972,7 +987,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { PACKET_READ_CHECK(AdditionalFlags, sizeof(AvatarDataPacket::AdditionalFlags)); auto data = reinterpret_cast(sourceBuffer); - uint8_t bitItems = data->flags; + uint16_t bitItems = data->flags; // key state, stored as a semi-nibble in the bitItems auto newKeyState = (KeyState)getSemiNibbleAt(bitItems, KEY_STATE_START_BIT); @@ -980,26 +995,38 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { // hand state, stored as a semi-nibble plus a bit in the bitItems // we store the hand state as well as other items in a shared bitset. The hand state is an octal, but is split // into two sections to maintain backward compatibility. The bits are ordered as such (0-7 left to right). - // +---+-----+-----+--+ - // |x,x|H0,H1|x,x,x|H2| - // +---+-----+-----+--+ + // AA 6/1/18 added three more flags bits 8,9, and 10 for procedural audio, blink, and eye saccade enabled + // +---+-----+-----+--+--+--+--+-----+ + // |x,x|H0,H1|x,x,x|H2|Au|Bl|Ey|xxxxx| + // +---+-----+-----+--+--+--+--+-----+ // Hand state - H0,H1,H2 is found in the 3rd, 4th, and 8th bits auto newHandState = getSemiNibbleAt(bitItems, HAND_STATE_START_BIT) - + (oneAtBit(bitItems, HAND_STATE_FINGER_POINTING_BIT) ? IS_FINGER_POINTING_FLAG : 0); + + (oneAtBit16(bitItems, HAND_STATE_FINGER_POINTING_BIT) ? IS_FINGER_POINTING_FLAG : 0); - auto newFaceTrackerConnected = oneAtBit(bitItems, IS_FACE_TRACKER_CONNECTED); - auto newEyeTrackerConnected = oneAtBit(bitItems, IS_EYE_TRACKER_CONNECTED); + auto newFaceTrackerConnected = oneAtBit16(bitItems, IS_FACE_TRACKER_CONNECTED); + auto newEyeTrackerConnected = oneAtBit16(bitItems, IS_EYE_TRACKER_CONNECTED); + auto newHasAudioEnabledFaceMovement = oneAtBit16(bitItems, AUDIO_ENABLED_FACE_MOVEMENT); + auto newHasProceduralEyeFaceMovement = oneAtBit16(bitItems, PROCEDURAL_EYE_FACE_MOVEMENT); + auto newHasProceduralBlinkFaceMovement = oneAtBit16(bitItems, PROCEDURAL_BLINK_FACE_MOVEMENT); + + bool keyStateChanged = (_keyState != newKeyState); bool handStateChanged = (_handState != newHandState); bool faceStateChanged = (_headData->_isFaceTrackerConnected != newFaceTrackerConnected); bool eyeStateChanged = (_headData->_isEyeTrackerConnected != newEyeTrackerConnected); - bool somethingChanged = keyStateChanged || handStateChanged || faceStateChanged || eyeStateChanged; + bool audioEnableFaceMovementChanged = (_headData->getHasAudioEnabledFaceMovement() != newHasAudioEnabledFaceMovement); + bool proceduralEyeFaceMovementChanged = (_headData->getHasProceduralEyeFaceMovement() != newHasProceduralEyeFaceMovement); + bool proceduralBlinkFaceMovementChanged = (_headData->getHasProceduralBlinkFaceMovement() != newHasProceduralBlinkFaceMovement); + bool somethingChanged = keyStateChanged || handStateChanged || faceStateChanged || eyeStateChanged || audioEnableFaceMovementChanged || proceduralEyeFaceMovementChanged || proceduralBlinkFaceMovementChanged; _keyState = newKeyState; _handState = newHandState; _headData->_isFaceTrackerConnected = newFaceTrackerConnected; _headData->_isEyeTrackerConnected = newEyeTrackerConnected; + _headData->setHasAudioEnabledFaceMovement(newHasAudioEnabledFaceMovement); + _headData->setHasProceduralEyeFaceMovement(newHasProceduralEyeFaceMovement); + _headData->setHasProceduralBlinkFaceMovement(newHasProceduralBlinkFaceMovement); sourceBuffer += sizeof(AvatarDataPacket::AdditionalFlags); @@ -1060,23 +1087,21 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { PACKET_READ_CHECK(FaceTrackerInfo, sizeof(AvatarDataPacket::FaceTrackerInfo)); auto faceTrackerInfo = reinterpret_cast(sourceBuffer); - sourceBuffer += sizeof(AvatarDataPacket::FaceTrackerInfo); - - _headData->_leftEyeBlink = faceTrackerInfo->leftEyeBlink; - _headData->_rightEyeBlink = faceTrackerInfo->rightEyeBlink; - _headData->_averageLoudness = faceTrackerInfo->averageLoudness; - _headData->_browAudioLift = faceTrackerInfo->browAudioLift; - int numCoefficients = faceTrackerInfo->numBlendshapeCoefficients; const int coefficientsSize = sizeof(float) * numCoefficients; + sourceBuffer += sizeof(AvatarDataPacket::FaceTrackerInfo); + PACKET_READ_CHECK(FaceTrackerCoefficients, coefficientsSize); _headData->_blendshapeCoefficients.resize(numCoefficients); // make sure there's room for the copy! - _headData->_transientBlendshapeCoefficients.resize(numCoefficients); + //only copy the blendshapes to headData, not the procedural face info memcpy(_headData->_blendshapeCoefficients.data(), sourceBuffer, coefficientsSize); sourceBuffer += coefficientsSize; + int numBytesRead = sourceBuffer - startSection; _faceTrackerRate.increment(numBytesRead); _faceTrackerUpdateRate.increment(); + } else { + _headData->_blendshapeCoefficients.fill(0, _headData->_blendshapeCoefficients.size()); } if (hasJointData) { @@ -2363,7 +2388,7 @@ glm::vec3 AvatarData::getAbsoluteJointTranslationInObjectFrame(int index) const } /**jsdoc - * @typedef AttachmentData + * @typedef {object} AttachmentData * @property {string} modelUrl * @property {string} jointName * @property {Vec3} translation diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 62a14ec51e..51b3257ba2 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -79,20 +79,30 @@ const quint32 AVATAR_MOTION_SCRIPTABLE_BITS = // Bitset of state flags - we store the key state, hand state, Faceshift, eye tracking, and existence of // referential data in this bit set. The hand state is an octal, but is split into two sections to maintain // backward compatibility. The bits are ordered as such (0-7 left to right). -// +-----+-----+-+-+-+--+ -// |K0,K1|H0,H1|F|E|R|H2| -// +-----+-----+-+-+-+--+ +// AA 6/1/18 added three more flags bits 8,9, and 10 for procedural audio, blink, and eye saccade enabled +// +// +-----+-----+-+-+-+--+--+--+--+-----+ +// |K0,K1|H0,H1|F|E|R|H2|Au|Bl|Ey|xxxxx| +// +-----+-----+-+-+-+--+--+--+--+-----+ +// // Key state - K0,K1 is found in the 1st and 2nd bits // Hand state - H0,H1,H2 is found in the 3rd, 4th, and 8th bits // Face tracker - F is found in the 5th bit // Eye tracker - E is found in the 6th bit // Referential Data - R is found in the 7th bit +// Procedural audio to mouth movement is enabled 8th bit +// Procedural Blink is enabled 9th bit +// Procedural Eyelid is enabled 10th bit + const int KEY_STATE_START_BIT = 0; // 1st and 2nd bits const int HAND_STATE_START_BIT = 2; // 3rd and 4th bits const int IS_FACE_TRACKER_CONNECTED = 4; // 5th bit const int IS_EYE_TRACKER_CONNECTED = 5; // 6th bit (was CHAT_CIRCLING) const int HAS_REFERENTIAL = 6; // 7th bit const int HAND_STATE_FINGER_POINTING_BIT = 7; // 8th bit +const int AUDIO_ENABLED_FACE_MOVEMENT = 8; // 9th bit +const int PROCEDURAL_EYE_FACE_MOVEMENT = 9; // 10th bit +const int PROCEDURAL_BLINK_FACE_MOVEMENT = 10; // 11th bit const char HAND_STATE_NULL = 0; @@ -200,9 +210,9 @@ namespace AvatarDataPacket { static_assert(sizeof(SensorToWorldMatrix) == SENSOR_TO_WORLD_SIZE, "AvatarDataPacket::SensorToWorldMatrix size doesn't match."); PACKED_BEGIN struct AdditionalFlags { - uint8_t flags; // additional flags: hand state, key state, eye tracking + uint16_t flags; // additional flags: hand state, key state, eye tracking } PACKED_END; - const size_t ADDITIONAL_FLAGS_SIZE = 1; + const size_t ADDITIONAL_FLAGS_SIZE = 2; static_assert(sizeof(AdditionalFlags) == ADDITIONAL_FLAGS_SIZE, "AvatarDataPacket::AdditionalFlags size doesn't match."); // only present if HAS_REFERENTIAL flag is set in AvatarInfo.flags @@ -501,6 +511,11 @@ public: float getDomainLimitedScale() const; + virtual bool getHasScriptedBlendshapes() const { return false; } + virtual bool getHasProceduralBlinkFaceMovement() const { return true; } + virtual bool getHasProceduralEyeFaceMovement() const { return true; } + virtual bool getHasAudioEnabledFaceMovement() const { return false; } + /**jsdoc * Returns the minimum scale allowed for this avatar in the current domain. * This value can change as the user changes avatars or when changing domains. @@ -578,8 +593,7 @@ public: * @param {Quat} rotation - The rotation of the joint relative to its parent. * @param {Vec3} translation - The translation of the joint relative to its parent. * @example Set your avatar to it's default T-pose for a while.
    - * Avatar in T-pose - * + * Avatar in T-pose * // Set all joint translations and rotations to defaults. * var i, length, rotation, translation; * for (i = 0, length = MyAvatar.getJointNames().length; i < length; i++) { @@ -680,8 +694,7 @@ public: * @param {string} name - The name of the joint. * @param {Quat} rotation - The rotation of the joint relative to its parent. * @example Set your avatar to its default T-pose then rotate its right arm.
    - * Avatar in T-pose with arm rotated + * Avatar in T-pose with arm rotated * // Set all joint translations and rotations to defaults. * var i, length, rotation, translation; * for (i = 0, length = MyAvatar.getJointNames().length; i < length; i++) { @@ -713,8 +726,7 @@ public: * @param {Vec3} translation - The translation of the joint relative to its parent. * @example Stretch your avatar's neck. Depending on the avatar you are using, you will either see a gap between * the head and body or you will see the neck stretched.
    - * Avatar with neck stretched + * Avatar with neck stretched * // Stretch your avatar's neck. * MyAvatar.setJointTranslation("Neck", { x: 0, y: 25, z: 0 }); * @@ -798,8 +810,7 @@ public: * @param {Quat[]} jointRotations - The rotations for all joints in the avatar. The values are in the same order as the * array returned by {@link MyAvatar.getJointNames} or {@link Avatar.getJointNames}. * @example Set your avatar to its default T-pose then rotate its right arm.
    - * Avatar in T-pose - * + * Avatar in T-pose * // Set all joint translations and rotations to defaults. * var i, length, rotation, translation; * for (i = 0, length = MyAvatar.getJointNames().length; i < length; i++) { diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 829c98a418..974ae92432 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -82,6 +82,7 @@ AvatarSharedPointer AvatarHashMap::addAvatar(const QUuid& sessionUUID, const QWe avatar->setSessionUUID(sessionUUID); avatar->setOwningAvatarMixer(mixerWeakPointer); + // addAvatar is only called from newOrExistingAvatar, which already locks _hashLock _avatarHash.insert(sessionUUID, avatar); emit avatarAddedEvent(sessionUUID); diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index 6747025de0..ef6f7845eb 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -46,7 +46,7 @@ class AvatarHashMap : public QObject, public Dependency { public: AvatarHash getHashCopy() { QReadLocker lock(&_hashLock); return _avatarHash; } const AvatarHash getHashCopy() const { QReadLocker lock(&_hashLock); return _avatarHash; } - int size() { return _avatarHash.size(); } + int size() { QReadLocker lock(&_hashLock); return _avatarHash.size(); } // Currently, your own avatar will be included as the null avatar id. @@ -152,8 +152,6 @@ protected: virtual void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason = KillAvatarReason::NoReason); AvatarHash _avatarHash; - // "Case-based safety": Most access to the _avatarHash is on the same thread. Write access is protected by a write-lock. - // If you read from a different thread, you must read-lock the _hashLock. (Scripted write access is not supported). mutable QReadWriteLock _hashLock; private: diff --git a/libraries/avatars/src/HeadData.h b/libraries/avatars/src/HeadData.h index bcc2cacde5..f9c4b52139 100644 --- a/libraries/avatars/src/HeadData.h +++ b/libraries/avatars/src/HeadData.h @@ -69,6 +69,24 @@ public: } bool lookAtPositionChangedSince(quint64 time) { return _lookAtPositionChanged >= time; } + bool getHasProceduralEyeFaceMovement() const { return _hasProceduralEyeFaceMovement; } + + void setHasProceduralEyeFaceMovement(const bool hasProceduralEyeFaceMovement) { + _hasProceduralEyeFaceMovement = hasProceduralEyeFaceMovement; + } + + bool getHasProceduralBlinkFaceMovement() const { return _hasProceduralBlinkFaceMovement; } + + void setHasProceduralBlinkFaceMovement(const bool hasProceduralBlinkFaceMovement) { + _hasProceduralBlinkFaceMovement = hasProceduralBlinkFaceMovement; + } + + bool getHasAudioEnabledFaceMovement() const { return _hasAudioEnabledFaceMovement; } + + void setHasAudioEnabledFaceMovement(const bool hasAudioEnabledFaceMovement) { + _hasAudioEnabledFaceMovement = hasAudioEnabledFaceMovement; + } + friend class AvatarData; QJsonObject toJson() const; @@ -83,6 +101,9 @@ protected: glm::vec3 _lookAtPosition; quint64 _lookAtPositionChanged { 0 }; + bool _hasAudioEnabledFaceMovement { true }; + bool _hasProceduralBlinkFaceMovement { true }; + bool _hasProceduralEyeFaceMovement { true }; bool _isFaceTrackerConnected { false }; bool _isEyeTrackerConnected { false }; float _leftEyeBlink { 0.0f }; diff --git a/libraries/baking/src/TextureBaker.cpp b/libraries/baking/src/TextureBaker.cpp index b6957a2712..2b50f6be97 100644 --- a/libraries/baking/src/TextureBaker.cpp +++ b/libraries/baking/src/TextureBaker.cpp @@ -157,18 +157,19 @@ void TextureBaker::processTexture() { return; } - const char* name = khronos::gl::texture::toString(memKTX->_header.getGLInternaFormat()); - if (name == nullptr) { - handleError("Could not determine internal format for compressed KTX: " + _textureURL.toString()); - return; - } // attempt to write the baked texture to the destination file path - { + if (memKTX->_header.isCompressed()) { + const char* name = khronos::gl::texture::toString(memKTX->_header.getGLInternaFormat()); + if (name == nullptr) { + handleError("Could not determine internal format for compressed KTX: " + _textureURL.toString()); + return; + } + const char* data = reinterpret_cast(memKTX->_storage->data()); const size_t length = memKTX->_storage->size(); - auto fileName = _baseFilename + BAKED_TEXTURE_BCN_SUFFIX; + auto fileName = _baseFilename + "_" + name + ".ktx"; auto filePath = _outputDirectory.absoluteFilePath(fileName); QFile bakedTextureFile { filePath }; if (!bakedTextureFile.open(QIODevice::WriteOnly) || bakedTextureFile.write(data, length) == -1) { @@ -177,6 +178,18 @@ void TextureBaker::processTexture() { } _outputFiles.push_back(filePath); meta.availableTextureTypes[memKTX->_header.getGLInternaFormat()] = _metaTexturePathPrefix + fileName; + } else { + const char* data = reinterpret_cast(memKTX->_storage->data()); + const size_t length = memKTX->_storage->size(); + + auto fileName = _baseFilename + ".ktx"; + auto filePath = _outputDirectory.absoluteFilePath(fileName); + QFile ktxTextureFile { filePath }; + if (!ktxTextureFile.open(QIODevice::WriteOnly) || ktxTextureFile.write(data, length) == -1) { + handleError("Could not write ktx texture for " + _textureURL.toString()); + return; + } + _outputFiles.push_back(filePath); } diff --git a/libraries/controllers/src/controllers/Actions.cpp b/libraries/controllers/src/controllers/Actions.cpp index 978b0888ba..6923ef4b98 100644 --- a/libraries/controllers/src/controllers/Actions.cpp +++ b/libraries/controllers/src/controllers/Actions.cpp @@ -307,7 +307,7 @@ namespace controller { * action. * * - * @typedef Controller.Actions + * @typedef {object} Controller.Actions */ // Device functions Input::NamedVector ActionsDevice::getAvailableInputs() const { diff --git a/libraries/controllers/src/controllers/InputDevice.h b/libraries/controllers/src/controllers/InputDevice.h index 30a58eb2f0..1e626e6a3c 100644 --- a/libraries/controllers/src/controllers/InputDevice.h +++ b/libraries/controllers/src/controllers/InputDevice.h @@ -79,7 +79,7 @@ enum Hand { * {@link Controller.Hardware-Vive}. * * - * @typedef Controller.Hardware + * @typedef {object} Controller.Hardware * @example List all the currently available Controller.Hardware properties. * function printProperties(string, item) { * print(string); diff --git a/libraries/controllers/src/controllers/ScriptingInterface.cpp b/libraries/controllers/src/controllers/ScriptingInterface.cpp index 16db22401f..f49b41cbe6 100644 --- a/libraries/controllers/src/controllers/ScriptingInterface.cpp +++ b/libraries/controllers/src/controllers/ScriptingInterface.cpp @@ -92,28 +92,16 @@ namespace controller { return userInputMapper->getValue(Input((uint32_t)source)); } - float ScriptingInterface::getButtonValue(StandardButtonChannel source, uint16_t device) const { - return getValue(Input(device, source, ChannelType::BUTTON).getID()); - } - float ScriptingInterface::getAxisValue(int source) const { auto userInputMapper = DependencyManager::get(); return userInputMapper->getValue(Input((uint32_t)source)); } - float ScriptingInterface::getAxisValue(StandardAxisChannel source, uint16_t device) const { - return getValue(Input(device, source, ChannelType::AXIS).getID()); - } - Pose ScriptingInterface::getPoseValue(const int& source) const { auto userInputMapper = DependencyManager::get(); return userInputMapper->getPose(Input((uint32_t)source)); } - Pose ScriptingInterface::getPoseValue(StandardPoseChannel source, uint16_t device) const { - return getPoseValue(Input(device, source, ChannelType::POSE).getID()); - } - QVector ScriptingInterface::getAllActions() { return DependencyManager::get()->getAllActions(); } diff --git a/libraries/controllers/src/controllers/ScriptingInterface.h b/libraries/controllers/src/controllers/ScriptingInterface.h index dacb0c8568..b0004bc12d 100644 --- a/libraries/controllers/src/controllers/ScriptingInterface.h +++ b/libraries/controllers/src/controllers/ScriptingInterface.h @@ -206,43 +206,6 @@ namespace controller { */ Q_INVOKABLE Pose getPoseValue(const int& source) const; - /**jsdoc - * Get the value of a button on a particular device. - * @function Controller.getButtonValue - * @param {StandardButtonChannel} source - The button to get the value of. - * @param {number} [device=0] - The ID of the hardware device to get the value from. The default value of - * 0 corresponds to Standard. - * @returns {number} The current value of the button if the parameters are valid, otherwise 0. - * @deprecated This function no longer works. - */ - // FIXME: This function causes a JavaScript crash: https://highfidelity.manuscript.com/f/cases/edit/14139 - Q_INVOKABLE float getButtonValue(StandardButtonChannel source, uint16_t device = 0) const; - - /**jsdoc - * Get the value of an axis control on a particular device. - * @function Controller.getAxisValue - * @variation 0 - * @param {StandardAxisChannel} source - The axis to get the value of. - * @param {number} [device=0] - The ID of the hardware device to get the value from. The default value of - * 0 corresponds to Standard. - * @returns {number} The current value of the axis if the parameters are valid, otherwise 0. - * @deprecated This function no longer works. - */ - Q_INVOKABLE float getAxisValue(StandardAxisChannel source, uint16_t device = 0) const; - - /**jsdoc - * Get the value of an pose control on a particular device. - * @function Controller.getPoseValue - * @variation 0 - * @param {StandardPoseChannel} source - The pose to get the value of. - * @param {number} [device=0] - The ID of the hardware device to get the value from. The default value of - * 0 corresponds to Standard. - * @returns {Pose} The current value of the controller pose output if the parameters are valid, otherwise an invalid - * pose with Pose.valid == false. - * @deprecated This function no longer works. - */ - Q_INVOKABLE Pose getPoseValue(StandardPoseChannel source, uint16_t device = 0) const; - /**jsdoc * Triggers a haptic pulse on connected and enabled devices that have the capability. * @function Controller.triggerHapticPulse diff --git a/libraries/controllers/src/controllers/StandardController.cpp b/libraries/controllers/src/controllers/StandardController.cpp index 471943400d..e1733d2524 100644 --- a/libraries/controllers/src/controllers/StandardController.cpp +++ b/libraries/controllers/src/controllers/StandardController.cpp @@ -231,7 +231,7 @@ void StandardController::focusOutEvent() { * * * - * @typedef Controller.Standard + * @typedef {object} Controller.Standard */ Input::NamedVector StandardController::getAvailableInputs() const { static Input::NamedVector availableInputs { diff --git a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp index 09b9b7f8f9..d8b8cbd54a 100644 --- a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp @@ -151,11 +151,9 @@ void Basic2DWindowOpenGLDisplayPlugin::compositeExtra() { batch.setModelTransform(stickTransform); batch.draw(gpu::TRIANGLE_STRIP, 4); - if (!virtualPadManager.getLeftVirtualPad()->isBeingTouched()) { - batch.setResourceTexture(0, _virtualPadJumpBtnTexture); - batch.setModelTransform(jumpTransform); - batch.draw(gpu::TRIANGLE_STRIP, 4); - } + batch.setResourceTexture(0, _virtualPadJumpBtnTexture); + batch.setModelTransform(jumpTransform); + batch.draw(gpu::TRIANGLE_STRIP, 4); }); } #endif diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 354d3242a9..513f955e9e 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -46,6 +46,8 @@ const char* SRGB_TO_LINEAR_FRAG = R"SCRIBE( +// OpenGLDisplayPlugin_present.frag + uniform sampler2D colorMap; in vec2 varTexCoord0; diff --git a/libraries/embedded-webserver/src/HTTPConnection.cpp b/libraries/embedded-webserver/src/HTTPConnection.cpp index 12da599575..f65cd87f6e 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.cpp +++ b/libraries/embedded-webserver/src/HTTPConnection.cpp @@ -11,6 +11,8 @@ #include "HTTPConnection.h" +#include + #include #include #include @@ -29,11 +31,92 @@ const char* HTTPConnection::StatusCode404 = "404 Not Found"; const char* HTTPConnection::StatusCode500 = "500 Internal server error"; const char* HTTPConnection::DefaultContentType = "text/plain; charset=ISO-8859-1"; -HTTPConnection::HTTPConnection (QTcpSocket* socket, HTTPManager* parentManager) : + +class MemoryStorage : public HTTPConnection::Storage { +public: + static std::unique_ptr make(qint64 size); + virtual ~MemoryStorage() = default; + + const QByteArray& content() const override { return _array; } + qint64 bytesLeftToWrite() const override { return _array.size() - _bytesWritten; } + void write(const QByteArray& data) override; + +private: + MemoryStorage(qint64 size) { _array.resize(size); } + + QByteArray _array; + qint64 _bytesWritten { 0 }; +}; + +std::unique_ptr MemoryStorage::make(qint64 size) { + return std::unique_ptr(new MemoryStorage(size)); +} + +void MemoryStorage::write(const QByteArray& data) { + assert(data.size() <= bytesLeftToWrite()); + memcpy(_array.data() + _bytesWritten, data.data(), data.size()); + _bytesWritten += data.size(); +} + + +class FileStorage : public HTTPConnection::Storage { +public: + static std::unique_ptr make(qint64 size); + virtual ~FileStorage(); + + const QByteArray& content() const override { return _wrapperArray; }; + qint64 bytesLeftToWrite() const override { return _mappedMemorySize - _bytesWritten; } + void write(const QByteArray& data) override; + +private: + FileStorage(std::unique_ptr file, uchar* mapped, qint64 size); + + // Byte array is const because any edit will trigger a deep copy + // and pull all the data we want to keep on disk in memory. + const QByteArray _wrapperArray; + std::unique_ptr _file; + + uchar* const _mappedMemoryAddress { nullptr }; + const qint64 _mappedMemorySize { 0 }; + qint64 _bytesWritten { 0 }; +}; + +std::unique_ptr FileStorage::make(qint64 size) { + auto file = std::unique_ptr(new QTemporaryFile()); + file->open(); // Open for resize + file->resize(size); + auto mapped = file->map(0, size); // map the entire file + + return std::unique_ptr(new FileStorage(std::move(file), mapped, size)); +} + +// Use QByteArray::fromRawData to avoid a new allocation and access the already existing +// memory directly as long as all operations on the array are const. +FileStorage::FileStorage(std::unique_ptr file, uchar* mapped, qint64 size) : + _wrapperArray(QByteArray::fromRawData(reinterpret_cast(mapped), size)), + _file(std::move(file)), + _mappedMemoryAddress(mapped), + _mappedMemorySize(size) +{ +} + +FileStorage::~FileStorage() { + _file->unmap(_mappedMemoryAddress); + _file->close(); +} + +void FileStorage::write(const QByteArray& data) { + assert(data.size() <= bytesLeftToWrite()); + // We write directly to the mapped memory + memcpy(_mappedMemoryAddress + _bytesWritten, data.data(), data.size()); + _bytesWritten += data.size(); +} + + +HTTPConnection::HTTPConnection(QTcpSocket* socket, HTTPManager* parentManager) : QObject(parentManager), _parentManager(parentManager), _socket(socket), - _stream(socket), _address(socket->peerAddress()) { // take over ownership of the socket @@ -62,7 +145,7 @@ QHash HTTPConnection::parseUrlEncodedForm() { return QHash(); } - QUrlQuery form { _requestContent }; + QUrlQuery form { _requestContent->content() }; QHash pairs; for (auto pair : form.queryItems()) { auto key = QUrl::fromPercentEncoding(pair.first.toLatin1().replace('+', ' ')); @@ -97,7 +180,7 @@ QList HTTPConnection::parseFormData() const { QByteArray end = "\r\n--" + boundary + "--\r\n"; QList data; - QBuffer buffer(const_cast(&_requestContent)); + QBuffer buffer(const_cast(&_requestContent->content())); buffer.open(QIODevice::ReadOnly); while (buffer.canReadLine()) { QByteArray line = buffer.readLine().trimmed(); @@ -107,12 +190,13 @@ QList HTTPConnection::parseFormData() const { QByteArray line = buffer.readLine().trimmed(); if (line.isEmpty()) { // content starts after this line - int idx = _requestContent.indexOf(end, buffer.pos()); + int idx = _requestContent->content().indexOf(end, buffer.pos()); if (idx == -1) { qWarning() << "Missing end boundary." << _address; return data; } - datum.second = _requestContent.mid(buffer.pos(), idx - buffer.pos()); + datum.second = QByteArray::fromRawData(_requestContent->content().data() + buffer.pos(), + idx - buffer.pos()); data.append(datum); buffer.seek(idx + end.length()); @@ -256,7 +340,24 @@ void HTTPConnection::readHeaders() { _parentManager->handleHTTPRequest(this, _requestUrl); } else { - _requestContent.resize(clength.toInt()); + bool success = false; + auto length = clength.toInt(&success); + if (!success) { + qWarning() << "Invalid header." << _address << trimmed; + respond("400 Bad Request", "The header was malformed."); + return; + } + + // Storing big requests in memory gets expensive, especially on servers + // with limited memory. So we store big requests in a temporary file on disk + // and map it to faster read/write access. + static const int MAX_CONTENT_SIZE_IN_MEMORY = 10 * 1000 * 1000; + if (length < MAX_CONTENT_SIZE_IN_MEMORY) { + _requestContent = MemoryStorage::make(length); + } else { + _requestContent = FileStorage::make(length); + } + connect(_socket, SIGNAL(readyRead()), SLOT(readContent())); // read any content immediately available @@ -285,12 +386,13 @@ void HTTPConnection::readHeaders() { } void HTTPConnection::readContent() { - int size = _requestContent.size(); - if (_socket->bytesAvailable() < size) { - return; - } - _socket->read(_requestContent.data(), size); - _socket->disconnect(this, SLOT(readContent())); + auto size = std::min(_socket->bytesAvailable(), _requestContent->bytesLeftToWrite()); - _parentManager->handleHTTPRequest(this, _requestUrl.path()); + _requestContent->write(_socket->read(size)); + + if (_requestContent->bytesLeftToWrite() == 0) { + _socket->disconnect(this, SLOT(readContent())); + + _parentManager->handleHTTPRequest(this, _requestUrl.path()); + } } diff --git a/libraries/embedded-webserver/src/HTTPConnection.h b/libraries/embedded-webserver/src/HTTPConnection.h index e4d23e3c90..4b42acf296 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.h +++ b/libraries/embedded-webserver/src/HTTPConnection.h @@ -16,14 +16,14 @@ #ifndef hifi_HTTPConnection_h #define hifi_HTTPConnection_h -#include #include -#include #include #include +#include #include #include #include +#include #include #include @@ -57,52 +57,63 @@ public: /// WebSocket close status codes. enum ReasonCode { NoReason = 0, NormalClosure = 1000, GoingAway = 1001 }; + class Storage { + public: + Storage() = default; + virtual ~Storage() = default; + + virtual const QByteArray& content() const = 0; + + virtual qint64 bytesLeftToWrite() const = 0; + virtual void write(const QByteArray& data) = 0; + }; + /// Initializes the connection. - HTTPConnection (QTcpSocket* socket, HTTPManager* parentManager); + HTTPConnection(QTcpSocket* socket, HTTPManager* parentManager); /// Destroys the connection. - virtual ~HTTPConnection (); + virtual ~HTTPConnection(); /// Returns a pointer to the underlying socket, to which WebSocket message bodies should be written. - QTcpSocket* socket () const { return _socket; } + QTcpSocket* socket() const { return _socket; } /// Returns the request operation. - QNetworkAccessManager::Operation requestOperation () const { return _requestOperation; } + QNetworkAccessManager::Operation requestOperation() const { return _requestOperation; } /// Returns a reference to the request URL. - const QUrl& requestUrl () const { return _requestUrl; } + const QUrl& requestUrl() const { return _requestUrl; } /// Returns a copy of the request header value. If it does not exist, it will return a default constructed QByteArray. QByteArray requestHeader(const QString& key) const { return _requestHeaders.value(key.toLower().toLocal8Bit()); } /// Returns a reference to the request content. - const QByteArray& requestContent () const { return _requestContent; } + const QByteArray& requestContent() const { return _requestContent->content(); } /// Parses the request content as form data, returning a list of header/content pairs. - QList parseFormData () const; + QList parseFormData() const; /// Parses the request content as a url encoded form, returning a hash of key/value pairs. /// Duplicate keys are not supported. QHash parseUrlEncodedForm(); /// Sends a response and closes the connection. - void respond (const char* code, const QByteArray& content = QByteArray(), + void respond(const char* code, const QByteArray& content = QByteArray(), const char* contentType = DefaultContentType, const Headers& headers = Headers()); - void respond (const char* code, std::unique_ptr device, + void respond(const char* code, std::unique_ptr device, const char* contentType = DefaultContentType, const Headers& headers = Headers()); protected slots: /// Reads the request line. - void readRequest (); + void readRequest(); /// Reads the headers. - void readHeaders (); + void readHeaders(); /// Reads the content. - void readContent (); + void readContent(); protected: void respondWithStatusAndHeaders(const char* code, const char* contentType, const Headers& headers, qint64 size); @@ -112,9 +123,6 @@ protected: /// The underlying socket. QTcpSocket* _socket; - - /// The data stream for writing to the socket. - QDataStream _stream; /// The stored address. QHostAddress _address; @@ -132,7 +140,7 @@ protected: QByteArray _lastRequestHeader; /// The content of the request. - QByteArray _requestContent; + std::unique_ptr _requestContent; /// Response content std::unique_ptr _responseDevice; diff --git a/libraries/embedded-webserver/src/HTTPManager.cpp b/libraries/embedded-webserver/src/HTTPManager.cpp index db6fc8e084..4a75994e12 100644 --- a/libraries/embedded-webserver/src/HTTPManager.cpp +++ b/libraries/embedded-webserver/src/HTTPManager.cpp @@ -24,8 +24,7 @@ const int SOCKET_ERROR_EXIT_CODE = 2; const int SOCKET_CHECK_INTERVAL_IN_MS = 30000; -HTTPManager::HTTPManager(const QHostAddress& listenAddress, quint16 port, const QString& documentRoot, HTTPRequestHandler* requestHandler, QObject* parent) : - QTcpServer(parent), +HTTPManager::HTTPManager(const QHostAddress& listenAddress, quint16 port, const QString& documentRoot, HTTPRequestHandler* requestHandler) : _listenAddress(listenAddress), _documentRoot(documentRoot), _requestHandler(requestHandler), diff --git a/libraries/embedded-webserver/src/HTTPManager.h b/libraries/embedded-webserver/src/HTTPManager.h index cb76eed9f2..597f6921cc 100644 --- a/libraries/embedded-webserver/src/HTTPManager.h +++ b/libraries/embedded-webserver/src/HTTPManager.h @@ -33,7 +33,7 @@ class HTTPManager : public QTcpServer, public HTTPRequestHandler { Q_OBJECT public: /// Initializes the manager. - HTTPManager(const QHostAddress& listenAddress, quint16 port, const QString& documentRoot, HTTPRequestHandler* requestHandler = NULL, QObject* parent = 0); + HTTPManager(const QHostAddress& listenAddress, quint16 port, const QString& documentRoot, HTTPRequestHandler* requestHandler = nullptr); bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false) override; diff --git a/libraries/embedded-webserver/src/HTTPSConnection.h b/libraries/embedded-webserver/src/HTTPSConnection.h index 7b53dc0063..3e6c752a08 100644 --- a/libraries/embedded-webserver/src/HTTPSConnection.h +++ b/libraries/embedded-webserver/src/HTTPSConnection.h @@ -23,4 +23,4 @@ protected slots: void handleSSLErrors(const QList& errors); }; -#endif // hifi_HTTPSConnection_h \ No newline at end of file +#endif // hifi_HTTPSConnection_h diff --git a/libraries/embedded-webserver/src/HTTPSManager.cpp b/libraries/embedded-webserver/src/HTTPSManager.cpp index 8ba44f98ac..95339a0dd6 100644 --- a/libraries/embedded-webserver/src/HTTPSManager.cpp +++ b/libraries/embedded-webserver/src/HTTPSManager.cpp @@ -16,8 +16,8 @@ #include "HTTPSConnection.h" HTTPSManager::HTTPSManager(QHostAddress listenAddress, quint16 port, const QSslCertificate& certificate, const QSslKey& privateKey, - const QString& documentRoot, HTTPSRequestHandler* requestHandler, QObject* parent) : - HTTPManager(listenAddress, port, documentRoot, requestHandler, parent), + const QString& documentRoot, HTTPSRequestHandler* requestHandler) : + HTTPManager(listenAddress, port, documentRoot, requestHandler), _certificate(certificate), _privateKey(privateKey), _sslRequestHandler(requestHandler) diff --git a/libraries/embedded-webserver/src/HTTPSManager.h b/libraries/embedded-webserver/src/HTTPSManager.h index 2d3cc9ed62..9cea32862c 100644 --- a/libraries/embedded-webserver/src/HTTPSManager.h +++ b/libraries/embedded-webserver/src/HTTPSManager.h @@ -31,7 +31,7 @@ public: const QSslCertificate& certificate, const QSslKey& privateKey, const QString& documentRoot, - HTTPSRequestHandler* requestHandler = NULL, QObject* parent = 0); + HTTPSRequestHandler* requestHandler = nullptr); void setCertificate(const QSslCertificate& certificate) { _certificate = certificate; } void setPrivateKey(const QSslKey& privateKey) { _privateKey = privateKey; } @@ -48,4 +48,4 @@ private: HTTPSRequestHandler* _sslRequestHandler; }; -#endif // hifi_HTTPSManager_h \ No newline at end of file +#endif // hifi_HTTPSManager_h diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp index b61f24972a..ae4c13d96f 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp @@ -157,16 +157,20 @@ Item::Bound EntityRenderer::getBound() { return _bound; } +render::hifi::Tag EntityRenderer::getTagMask() const { + return _isVisibleInSecondaryCamera ? render::hifi::TAG_ALL_VIEWS : render::hifi::TAG_MAIN_VIEW; +} + ItemKey EntityRenderer::getKey() { if (isTransparent()) { - return ItemKey::Builder::transparentShape().withTypeMeta().withTagBits(render::ItemKey::TAG_BITS_0 | render::ItemKey::TAG_BITS_1); + return ItemKey::Builder::transparentShape().withTypeMeta().withTagBits(getTagMask()); } // This allows shapes to cast shadows if (_canCastShadow) { - return ItemKey::Builder::opaqueShape().withTypeMeta().withTagBits(render::ItemKey::TAG_BITS_0 | render::ItemKey::TAG_BITS_1).withShadowCaster(); + return ItemKey::Builder::opaqueShape().withTypeMeta().withTagBits(getTagMask()).withShadowCaster(); } else { - return ItemKey::Builder::opaqueShape().withTypeMeta().withTagBits(render::ItemKey::TAG_BITS_0 | render::ItemKey::TAG_BITS_1); + return ItemKey::Builder::opaqueShape().withTypeMeta().withTagBits(getTagMask()); } } @@ -380,6 +384,7 @@ void EntityRenderer::doRenderUpdateSynchronous(const ScenePointer& scene, Transa _moving = entity->isMovingRelativeToParent(); _visible = entity->getVisible(); + setIsVisibleInSecondaryCamera(entity->isVisibleInSecondaryCamera()); _canCastShadow = entity->getCanCastShadow(); _cauterized = entity->getCauterized(); _needsRenderUpdate = false; diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index ada57c8ab0..e1ce2ed39e 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -18,6 +18,7 @@ #include "AbstractViewStateInterface.h" #include "EntitiesRendererLogging.h" #include +#include class EntityTreeRenderer; @@ -74,6 +75,7 @@ protected: virtual Item::Bound getBound() override; virtual void render(RenderArgs* args) override final; virtual uint32_t metaFetchMetaSubItems(ItemIDs& subItems) override; + virtual render::hifi::Tag getTagMask() const; // Returns true if the item in question needs to have updateInScene called because of internal rendering state changes virtual bool needsRenderUpdate() const; @@ -97,6 +99,8 @@ protected: bool isFading() const { return _isFading; } virtual bool isTransparent() const { return _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) < 1.0f : false; } inline bool isValidRenderItem() const { return _renderItemID != Item::INVALID_ITEM_ID; } + + virtual void setIsVisibleInSecondaryCamera(bool value) { _isVisibleInSecondaryCamera = value; } template T withReadLockResult(const std::function& f) { @@ -129,6 +133,7 @@ protected: bool _isFading{ _entitiesShouldFadeFunction() }; bool _prevIsTransparent { false }; bool _visible { false }; + bool _isVisibleInSecondaryCamera { false }; bool _canCastShadow { false }; bool _cauterized { false }; bool _moving { false }; diff --git a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp index 7cea841bf0..eabcb68e4f 100644 --- a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp @@ -43,7 +43,7 @@ void MaterialEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& ItemKey MaterialEntityRenderer::getKey() { ItemKey::Builder builder; - builder.withTypeShape().withTagBits(render::ItemKey::TAG_BITS_0 | render::ItemKey::TAG_BITS_1); + builder.withTypeShape().withTagBits(getTagMask()); if (!_visible) { builder.withInvisible(); diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index c4fa71a488..a91534668c 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -1060,9 +1060,9 @@ ModelEntityRenderer::ModelEntityRenderer(const EntityItemPointer& entity) : Pare void ModelEntityRenderer::setKey(bool didVisualGeometryRequestSucceed) { if (didVisualGeometryRequestSucceed) { - _itemKey = ItemKey::Builder().withTypeMeta().withTagBits(render::ItemKey::TAG_BITS_0 | render::ItemKey::TAG_BITS_1); + _itemKey = ItemKey::Builder().withTypeMeta().withTagBits(getTagMask()); } else { - _itemKey = ItemKey::Builder().withTypeMeta().withTypeShape().withTagBits(render::ItemKey::TAG_BITS_0 | render::ItemKey::TAG_BITS_1); + _itemKey = ItemKey::Builder().withTypeMeta().withTypeShape().withTagBits(getTagMask()); } } @@ -1070,6 +1070,13 @@ ItemKey ModelEntityRenderer::getKey() { return _itemKey; } +render::hifi::Tag ModelEntityRenderer::getTagMask() const { + // Default behavior for model is to not be visible in main view if cauterized (aka parented to the avatar's neck joint) + return _cauterized ? + (_isVisibleInSecondaryCamera ? render::hifi::TAG_SECONDARY_VIEW : render::hifi::TAG_NONE) : + Parent::getTagMask(); // calculate which views to be shown in +} + uint32_t ModelEntityRenderer::metaFetchMetaSubItems(ItemIDs& subItems) { if (_model) { auto metaSubItems = _subRenderItemIDs; @@ -1329,6 +1336,7 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce emit DependencyManager::get()-> modelAddedToScene(entity->getEntityItemID(), NestableType::Entity, _model); } + _didLastVisualGeometryRequestSucceed = didVisualGeometryRequestSucceed; }); connect(model.get(), &Model::requestRenderUpdate, this, &ModelEntityRenderer::requestRenderUpdate); connect(entity.get(), &RenderableModelEntityItem::requestCollisionGeometryUpdate, this, &ModelEntityRenderer::flagForCollisionGeometryUpdate); @@ -1386,21 +1394,22 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce entity->updateModelBounds(); entity->stopModelOverrideIfNoParent(); - // Default behavior for model is to not be visible in main view if cauterized (aka parented to the avatar's neck joint) - uint32_t viewTaskBits = _cauterized ? - render::ItemKey::TAG_BITS_1 : // draw in every view except the main one (view zero) - render::ItemKey::TAG_BITS_ALL; // draw in all views - - if (model->isVisible() != _visible || model->getViewTagBits() != viewTaskBits) { + render::hifi::Tag tagMask = getTagMask(); + if (model->isVisible() != _visible) { // FIXME: this seems like it could be optimized if we tracked our last known visible state in // the renderable item. As it stands now the model checks it's visible/invisible state // so most of the time we don't do anything in this function. - model->setVisibleInScene(_visible, scene, viewTaskBits, false); + model->setVisibleInScene(_visible, scene); } + + if (model->getTagMask() != tagMask) { + model->setTagMask(tagMask, scene); + } + // TODO? early exit here when not visible? if (model->canCastShadow() != _canCastShadow) { - model->setCanCastShadow(_canCastShadow, scene, viewTaskBits, false); + model->setCanCastShadow(_canCastShadow, scene); } if (_needsCollisionGeometryUpdate) { @@ -1473,6 +1482,11 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce } } +void ModelEntityRenderer::setIsVisibleInSecondaryCamera(bool value) { + Parent::setIsVisibleInSecondaryCamera(value); + setKey(_didLastVisualGeometryRequestSucceed); +} + void ModelEntityRenderer::flagForCollisionGeometryUpdate() { _needsCollisionGeometryUpdate = true; emit requestRenderUpdate(); diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index 68bc70c8a9..f1748ca069 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -164,6 +164,10 @@ protected: void flagForCollisionGeometryUpdate(); void setCollisionMeshKey(const void* key); + render::hifi::Tag getTagMask() const override; + + void setIsVisibleInSecondaryCamera(bool value) override; + private: void animate(const TypedEntityPointer& entity); void mapJoints(const TypedEntityPointer& entity, const QStringList& modelJointNames); @@ -202,6 +206,8 @@ private: render::ItemKey _itemKey { render::ItemKey::Builder().withTypeMeta() }; + bool _didLastVisualGeometryRequestSucceed { true }; + void processMaterials(); }; diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp index ee77646920..881c39c0bd 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp @@ -147,9 +147,9 @@ void ParticleEffectEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEn ItemKey ParticleEffectEntityRenderer::getKey() { if (_visible) { - return ItemKey::Builder::transparentShape().withTagBits(render::ItemKey::TAG_BITS_0 | render::ItemKey::TAG_BITS_1); + return ItemKey::Builder::transparentShape().withTagBits(getTagMask()); } else { - return ItemKey::Builder().withInvisible().withTagBits(render::ItemKey::TAG_BITS_0 | render::ItemKey::TAG_BITS_1).build(); + return ItemKey::Builder().withInvisible().withTagBits(getTagMask()).build(); } } diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp index d571eac35c..7cab57123d 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp @@ -112,7 +112,7 @@ PolyLineEntityRenderer::PolyLineEntityRenderer(const EntityItemPointer& entity) } ItemKey PolyLineEntityRenderer::getKey() { - return ItemKey::Builder::transparentShape().withTypeMeta().withTagBits(render::ItemKey::TAG_BITS_0 | render::ItemKey::TAG_BITS_1); + return ItemKey::Builder::transparentShape().withTypeMeta().withTagBits(getTagMask()); } ShapeKey PolyLineEntityRenderer::getShapeKey() { diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h index 70c87dca6f..7077ae799b 100644 --- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h @@ -169,7 +169,7 @@ public: } protected: - virtual ItemKey getKey() override { return ItemKey::Builder::opaqueShape().withTagBits(render::ItemKey::TAG_BITS_0 | render::ItemKey::TAG_BITS_1); } + virtual ItemKey getKey() override { return ItemKey::Builder::opaqueShape().withTagBits(getTagMask()); } virtual ShapeKey getShapeKey() override; virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override; virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override; diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index 1d34837a58..69068b81d2 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -139,7 +139,7 @@ bool ShapeEntityRenderer::isTransparent() const { ItemKey ShapeEntityRenderer::getKey() { ItemKey::Builder builder; - builder.withTypeShape().withTypeMeta().withTagBits(render::ItemKey::TAG_BITS_0 | render::ItemKey::TAG_BITS_1); + builder.withTypeShape().withTypeMeta().withTagBits(getTagMask()); withReadLock([&] { if (isTransparent()) { diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index 5062162b6e..c5035431f6 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -269,7 +269,7 @@ void ZoneEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointe ItemKey ZoneEntityRenderer::getKey() { - return ItemKey::Builder().withTypeMeta().withTagBits(render::ItemKey::TAG_BITS_0 | render::ItemKey::TAG_BITS_1).build(); + return ItemKey::Builder().withTypeMeta().withTagBits(getTagMask()).build(); } bool ZoneEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const { diff --git a/libraries/entities-renderer/src/textured_particle.slv b/libraries/entities-renderer/src/textured_particle.slv index cab76227c4..1d4261b1cc 100644 --- a/libraries/entities-renderer/src/textured_particle.slv +++ b/libraries/entities-renderer/src/textured_particle.slv @@ -37,8 +37,8 @@ layout(std140) uniform particleBuffer { ParticleUniforms particle; }; -in vec3 inPosition; -in vec2 inColor; // This is actual Lifetime + Seed +layout(location=0) in vec3 inPosition; +layout(location=2) in vec2 inColor; // This is actual Lifetime + Seed out vec4 varColor; out vec2 varTexcoord; diff --git a/libraries/entities/src/AnimationPropertyGroup.cpp b/libraries/entities/src/AnimationPropertyGroup.cpp index 43c6b7a6a5..2db85eb7ac 100644 --- a/libraries/entities/src/AnimationPropertyGroup.cpp +++ b/libraries/entities/src/AnimationPropertyGroup.cpp @@ -46,7 +46,7 @@ bool operator!=(const AnimationPropertyGroup& a, const AnimationPropertyGroup& b /**jsdoc * The AnimationProperties are used to configure an animation. - * @typedef Entities.AnimationProperties + * @typedef {object} Entities.AnimationProperties * @property {string} url="" - The URL of the FBX file that has the animation. * @property {number} fps=30 - The speed in frames/s that the animation is played at. * @property {number} firstFrame=0 - The first frame to play in the animation. diff --git a/libraries/entities/src/EntityEditPacketSender.cpp b/libraries/entities/src/EntityEditPacketSender.cpp index 6c6c4b37d0..9ca102d016 100644 --- a/libraries/entities/src/EntityEditPacketSender.cpp +++ b/libraries/entities/src/EntityEditPacketSender.cpp @@ -154,3 +154,11 @@ void EntityEditPacketSender::queueEraseEntityMessage(const EntityItemID& entityI queueOctreeEditMessage(PacketType::EntityErase, bufferOut); } } + +void EntityEditPacketSender::queueCloneEntityMessage(const EntityItemID& entityIDToClone, const EntityItemID& newEntityID) { + QByteArray bufferOut(NLPacket::maxPayloadSize(PacketType::EntityClone), 0); + + if (EntityItemProperties::encodeCloneEntityMessage(entityIDToClone, newEntityID, bufferOut)) { + queueOctreeEditMessage(PacketType::EntityClone, bufferOut); + } +} diff --git a/libraries/entities/src/EntityEditPacketSender.h b/libraries/entities/src/EntityEditPacketSender.h index bf79bb9203..31f91707b8 100644 --- a/libraries/entities/src/EntityEditPacketSender.h +++ b/libraries/entities/src/EntityEditPacketSender.h @@ -38,6 +38,7 @@ public: void queueEraseEntityMessage(const EntityItemID& entityItemID); + void queueCloneEntityMessage(const EntityItemID& entityIDToClone, const EntityItemID& newEntityID); // My server type is the model server virtual char getMyNodeType() const override { return NodeType::EntityServer; } diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index d35c01a51b..70881fbc40 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -124,6 +124,13 @@ EntityPropertyFlags EntityItem::getEntityProperties(EncodeBitstreamParams& param requestedProperties += PROP_LAST_EDITED_BY; + requestedProperties += PROP_CLONEABLE; + requestedProperties += PROP_CLONE_LIFETIME; + requestedProperties += PROP_CLONE_LIMIT; + requestedProperties += PROP_CLONE_DYNAMIC; + requestedProperties += PROP_CLONE_AVATAR_ENTITY; + requestedProperties += PROP_CLONE_ORIGIN_ID; + return requestedProperties; } @@ -288,6 +295,13 @@ OctreeElement::AppendState EntityItem::appendEntityData(OctreePacketData* packet APPEND_ENTITY_PROPERTY(PROP_QUERY_AA_CUBE, getQueryAACube()); APPEND_ENTITY_PROPERTY(PROP_LAST_EDITED_BY, getLastEditedBy()); + APPEND_ENTITY_PROPERTY(PROP_CLONEABLE, getCloneable()); + APPEND_ENTITY_PROPERTY(PROP_CLONE_LIFETIME, getCloneLifetime()); + APPEND_ENTITY_PROPERTY(PROP_CLONE_LIMIT, getCloneLimit()); + APPEND_ENTITY_PROPERTY(PROP_CLONE_DYNAMIC, getCloneDynamic()); + APPEND_ENTITY_PROPERTY(PROP_CLONE_AVATAR_ENTITY, getCloneAvatarEntity()); + APPEND_ENTITY_PROPERTY(PROP_CLONE_ORIGIN_ID, getCloneOriginID()); + appendSubclassData(packetData, params, entityTreeElementExtraEncodeData, requestedProperties, propertyFlags, @@ -803,7 +817,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef READ_ENTITY_PROPERTY(PROP_VISIBLE, bool, setVisible); READ_ENTITY_PROPERTY(PROP_CAN_CAST_SHADOW, bool, setCanCastShadow); READ_ENTITY_PROPERTY(PROP_COLLISIONLESS, bool, setCollisionless); - READ_ENTITY_PROPERTY(PROP_COLLISION_MASK, uint8_t, setCollisionMask); + READ_ENTITY_PROPERTY(PROP_COLLISION_MASK, uint16_t, setCollisionMask); READ_ENTITY_PROPERTY(PROP_DYNAMIC, bool, setDynamic); READ_ENTITY_PROPERTY(PROP_LOCKED, bool, setLocked); READ_ENTITY_PROPERTY(PROP_USER_DATA, QString, setUserData); @@ -848,6 +862,13 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef READ_ENTITY_PROPERTY(PROP_LAST_EDITED_BY, QUuid, setLastEditedBy); + READ_ENTITY_PROPERTY(PROP_CLONEABLE, bool, setCloneable); + READ_ENTITY_PROPERTY(PROP_CLONE_LIFETIME, float, setCloneLifetime); + READ_ENTITY_PROPERTY(PROP_CLONE_LIMIT, float, setCloneLimit); + READ_ENTITY_PROPERTY(PROP_CLONE_DYNAMIC, bool, setCloneDynamic); + READ_ENTITY_PROPERTY(PROP_CLONE_AVATAR_ENTITY, bool, setCloneAvatarEntity); + READ_ENTITY_PROPERTY(PROP_CLONE_ORIGIN_ID, QUuid, setCloneOriginID); + bytesRead += readEntitySubclassDataFromBuffer(dataAt, (bytesLeftToRead - bytesRead), args, propertyFlags, overwriteLocalData, somethingChanged); @@ -1275,6 +1296,13 @@ EntityItemProperties EntityItem::getProperties(EntityPropertyFlags desiredProper COPY_ENTITY_PROPERTY_TO_PROPERTIES(lastEditedBy, getLastEditedBy); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(cloneable, getCloneable); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(cloneLifetime, getCloneLifetime); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(cloneLimit, getCloneLimit); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(cloneDynamic, getCloneDynamic); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(cloneAvatarEntity, getCloneAvatarEntity); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(cloneOriginID, getCloneOriginID); + properties._defaultSettings = false; return properties; @@ -1355,6 +1383,7 @@ bool EntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(visible, setVisible); SET_ENTITY_PROPERTY_FROM_PROPERTIES(canCastShadow, setCanCastShadow); SET_ENTITY_PROPERTY_FROM_PROPERTIES(userData, setUserData); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(isVisibleInSecondaryCamera, setIsVisibleInSecondaryCamera); // Certifiable Properties SET_ENTITY_PROPERTY_FROM_PROPERTIES(itemName, setItemName); @@ -1382,6 +1411,13 @@ bool EntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(lastEditedBy, setLastEditedBy); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(cloneable, setCloneable); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(cloneLifetime, setCloneLifetime); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(cloneLimit, setCloneLimit); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(cloneDynamic, setCloneDynamic); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(cloneAvatarEntity, setCloneAvatarEntity); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(cloneOriginID, setCloneOriginID); + if (updateQueryAACube()) { somethingChanged = true; } @@ -1814,7 +1850,7 @@ void EntityItem::setCollisionless(bool value) { }); } -void EntityItem::setCollisionMask(uint8_t value) { +void EntityItem::setCollisionMask(uint16_t value) { withWriteLock([&] { if ((_collisionMask & ENTITY_COLLISION_MASK_DEFAULT) != (value & ENTITY_COLLISION_MASK_DEFAULT)) { _collisionMask = (value & ENTITY_COLLISION_MASK_DEFAULT); @@ -1879,7 +1915,7 @@ void EntityItem::setCreated(quint64 value) { }); } -void EntityItem::computeCollisionGroupAndFinalMask(int16_t& group, int16_t& mask) const { +void EntityItem::computeCollisionGroupAndFinalMask(int32_t& group, int32_t& mask) const { if (_collisionless) { group = BULLET_COLLISION_GROUP_COLLISIONLESS; mask = 0; @@ -1892,7 +1928,7 @@ void EntityItem::computeCollisionGroupAndFinalMask(int16_t& group, int16_t& mask group = BULLET_COLLISION_GROUP_STATIC; } - uint8_t userMask = getCollisionMask(); + uint16_t userMask = getCollisionMask(); if ((bool)(userMask & USER_COLLISION_GROUP_MY_AVATAR) != (bool)(userMask & USER_COLLISION_GROUP_OTHER_AVATAR)) { @@ -1906,7 +1942,7 @@ void EntityItem::computeCollisionGroupAndFinalMask(int16_t& group, int16_t& mask if ((bool)(_flags & Simulation::SPECIAL_FLAGS_NO_BOOTSTRAPPING)) { userMask &= ~USER_COLLISION_GROUP_MY_AVATAR; } - mask = Physics::getDefaultCollisionMask(group) & (int16_t)(userMask); + mask = Physics::getDefaultCollisionMask(group) & (int32_t)(userMask); } } @@ -2725,6 +2761,28 @@ void EntityItem::setVisible(bool value) { } } +bool EntityItem::isVisibleInSecondaryCamera() const { + bool result; + withReadLock([&] { + result = _isVisibleInSecondaryCamera; + }); + return result; +} + +void EntityItem::setIsVisibleInSecondaryCamera(bool value) { + bool changed = false; + withWriteLock([&] { + if (_isVisibleInSecondaryCamera != value) { + changed = true; + _isVisibleInSecondaryCamera = value; + } + }); + + if (changed) { + emit requestRenderUpdate(); + } +} + bool EntityItem::getCanCastShadow() const { bool result; withReadLock([&] { @@ -2760,8 +2818,8 @@ bool EntityItem::getCollisionless() const { return result; } -uint8_t EntityItem::getCollisionMask() const { - uint8_t result; +uint16_t EntityItem::getCollisionMask() const { + uint16_t result; withReadLock([&] { result = _collisionMask; }); @@ -2975,3 +3033,118 @@ std::unordered_map EntityItem::getMaterial } return toReturn; } + +bool EntityItem::getCloneable() const { + bool result; + withReadLock([&] { + result = _cloneable; + }); + return result; +} + +void EntityItem::setCloneable(bool value) { + withWriteLock([&] { + _cloneable = value; + }); +} + +float EntityItem::getCloneLifetime() const { + float result; + withReadLock([&] { + result = _cloneLifetime; + }); + return result; +} + +void EntityItem::setCloneLifetime(float value) { + withWriteLock([&] { + _cloneLifetime = value; + }); +} + +float EntityItem::getCloneLimit() const { + float result; + withReadLock([&] { + result = _cloneLimit; + }); + return result; +} + +void EntityItem::setCloneLimit(float value) { + withWriteLock([&] { + _cloneLimit = value; + }); +} + +bool EntityItem::getCloneDynamic() const { + bool result; + withReadLock([&] { + result = _cloneDynamic; + }); + return result; +} + +void EntityItem::setCloneDynamic(bool value) { + withWriteLock([&] { + _cloneDynamic = value; + }); +} + +bool EntityItem::getCloneAvatarEntity() const { + bool result; + withReadLock([&] { + result = _cloneAvatarEntity; + }); + return result; +} + +void EntityItem::setCloneAvatarEntity(bool value) { + withWriteLock([&] { + _cloneAvatarEntity = value; + }); +} + +const QUuid EntityItem::getCloneOriginID() const { + QUuid result; + withReadLock([&] { + result = _cloneOriginID; + }); + return result; +} + +void EntityItem::setCloneOriginID(const QUuid& value) { + withWriteLock([&] { + _cloneOriginID = value; + }); +} + +void EntityItem::addCloneID(const QUuid& cloneID) { + withWriteLock([&] { + if (!_cloneIDs.contains(cloneID)) { + _cloneIDs.append(cloneID); + } + }); +} + +void EntityItem::removeCloneID(const QUuid& cloneID) { + withWriteLock([&] { + int index = _cloneIDs.indexOf(cloneID); + if (index >= 0) { + _cloneIDs.removeAt(index); + } + }); +} + +const QVector EntityItem::getCloneIDs() const { + QVector result; + withReadLock([&] { + result = _cloneIDs; + }); + return result; +} + +void EntityItem::setCloneIDs(const QVector& cloneIDs) { + withWriteLock([&] { + _cloneIDs = cloneIDs; + }); +} diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index a88250a133..0acf8dbbc1 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -277,6 +277,9 @@ public: bool getVisible() const; void setVisible(bool value); + bool isVisibleInSecondaryCamera() const; + void setIsVisibleInSecondaryCamera(bool value); + bool getCanCastShadow() const; void setCanCastShadow(bool value); @@ -288,10 +291,10 @@ public: bool getCollisionless() const; void setCollisionless(bool value); - uint8_t getCollisionMask() const; - void setCollisionMask(uint8_t value); + uint16_t getCollisionMask() const; + void setCollisionMask(uint16_t value); - void computeCollisionGroupAndFinalMask(int16_t& group, int16_t& mask) const; + void computeCollisionGroupAndFinalMask(int32_t& group, int32_t& mask) const; bool getDynamic() const; void setDynamic(bool value); @@ -341,6 +344,19 @@ public: quint32 getStaticCertificateVersion() const; void setStaticCertificateVersion(const quint32&); + bool getCloneable() const; + void setCloneable(bool value); + float getCloneLifetime() const; + void setCloneLifetime(float value); + float getCloneLimit() const; + void setCloneLimit(float value); + bool getCloneDynamic() const; + void setCloneDynamic(bool value); + bool getCloneAvatarEntity() const; + void setCloneAvatarEntity(bool value); + const QUuid getCloneOriginID() const; + void setCloneOriginID(const QUuid& value); + // TODO: get rid of users of getRadius()... float getRadius() const; @@ -494,6 +510,11 @@ public: void setSimulationOwnershipExpiry(uint64_t expiry) { _simulationOwnershipExpiry = expiry; } uint64_t getSimulationOwnershipExpiry() const { return _simulationOwnershipExpiry; } + void addCloneID(const QUuid& cloneID); + void removeCloneID(const QUuid& cloneID); + const QVector getCloneIDs() const; + void setCloneIDs(const QVector& cloneIDs); + signals: void requestRenderUpdate(); @@ -560,9 +581,10 @@ protected: glm::vec3 _registrationPoint { ENTITY_ITEM_DEFAULT_REGISTRATION_POINT }; float _angularDamping { ENTITY_ITEM_DEFAULT_ANGULAR_DAMPING }; bool _visible { ENTITY_ITEM_DEFAULT_VISIBLE }; + bool _isVisibleInSecondaryCamera { ENTITY_ITEM_DEFAULT_VISIBLE_IN_SECONDARY_CAMERA }; bool _canCastShadow{ ENTITY_ITEM_DEFAULT_CAN_CAST_SHADOW }; bool _collisionless { ENTITY_ITEM_DEFAULT_COLLISIONLESS }; - uint8_t _collisionMask { ENTITY_COLLISION_MASK_DEFAULT }; + uint16_t _collisionMask { ENTITY_COLLISION_MASK_DEFAULT }; bool _dynamic { ENTITY_ITEM_DEFAULT_DYNAMIC }; bool _locked { ENTITY_ITEM_DEFAULT_LOCKED }; QString _userData { ENTITY_ITEM_DEFAULT_USER_DATA }; @@ -648,6 +670,14 @@ protected: bool _cauterized { false }; // if true, don't draw because it would obscure 1st-person camera + bool _cloneable { ENTITY_ITEM_DEFAULT_CLONEABLE }; + float _cloneLifetime { ENTITY_ITEM_DEFAULT_CLONE_LIFETIME }; + float _cloneLimit { ENTITY_ITEM_DEFAULT_CLONE_LIMIT }; + bool _cloneDynamic { ENTITY_ITEM_DEFAULT_CLONE_DYNAMIC }; + bool _cloneAvatarEntity { ENTITY_ITEM_DEFAULT_CLONE_AVATAR_ENTITY }; + QUuid _cloneOriginID; + QVector _cloneIDs; + private: std::unordered_map _materials; std::mutex _materialsLock; diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 4d7c114176..84e248b74d 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -130,7 +130,7 @@ void buildStringToMaterialMappingModeLookup() { addMaterialMappingMode(PROJECTED); } -QString getCollisionGroupAsString(uint8_t group) { +QString getCollisionGroupAsString(uint16_t group) { switch (group) { case USER_COLLISION_GROUP_DYNAMIC: return "dynamic"; @@ -146,7 +146,7 @@ QString getCollisionGroupAsString(uint8_t group) { return ""; } -uint8_t getCollisionGroupAsBitMask(const QStringRef& name) { +uint16_t getCollisionGroupAsBitMask(const QStringRef& name) { if (0 == name.compare(QString("dynamic"))) { return USER_COLLISION_GROUP_DYNAMIC; } else if (0 == name.compare(QString("static"))) { @@ -164,7 +164,7 @@ uint8_t getCollisionGroupAsBitMask(const QStringRef& name) { QString EntityItemProperties::getCollisionMaskAsString() const { QString maskString(""); for (int i = 0; i < NUM_USER_COLLISION_GROUPS; ++i) { - uint8_t group = 0x01 << i; + uint16_t group = 0x0001 << i; if (group & _collisionMask) { maskString.append(getCollisionGroupAsString(group)); maskString.append(','); @@ -175,7 +175,7 @@ QString EntityItemProperties::getCollisionMaskAsString() const { void EntityItemProperties::setCollisionMaskFromString(const QString& maskString) { QVector groups = maskString.splitRef(','); - uint8_t mask = 0x00; + uint16_t mask = 0x0000; for (auto groupName : groups) { mask |= getCollisionGroupAsBitMask(groupName); } @@ -368,6 +368,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_MATERIAL_MAPPING_SCALE, materialMappingScale); CHECK_PROPERTY_CHANGE(PROP_MATERIAL_MAPPING_ROT, materialMappingRot); CHECK_PROPERTY_CHANGE(PROP_MATERIAL_DATA, materialData); + CHECK_PROPERTY_CHANGE(PROP_VISIBLE_IN_SECONDARY_CAMERA, isVisibleInSecondaryCamera); // Certifiable Properties CHECK_PROPERTY_CHANGE(PROP_ITEM_NAME, itemName); @@ -436,6 +437,13 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_DPI, dpi); CHECK_PROPERTY_CHANGE(PROP_RELAY_PARENT_JOINTS, relayParentJoints); + CHECK_PROPERTY_CHANGE(PROP_CLONEABLE, cloneable); + CHECK_PROPERTY_CHANGE(PROP_CLONE_LIFETIME, cloneLifetime); + CHECK_PROPERTY_CHANGE(PROP_CLONE_LIMIT, cloneLimit); + CHECK_PROPERTY_CHANGE(PROP_CLONE_DYNAMIC, cloneDynamic); + CHECK_PROPERTY_CHANGE(PROP_CLONE_AVATAR_ENTITY, cloneAvatarEntity); + CHECK_PROPERTY_CHANGE(PROP_CLONE_ORIGIN_ID, cloneOriginID); + changedProperties += _animation.getChangedProperties(); changedProperties += _keyLight.getChangedProperties(); changedProperties += _ambientLight.getChangedProperties(); @@ -479,10 +487,11 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * @property {boolean} locked=false - Whether or not the entity can be edited or deleted. If true then the * entity's properties other than locked cannot be changed, and the entity cannot be deleted. * @property {boolean} visible=true - Whether or not the entity is rendered. If true then the entity is rendered. - * @property {boolean} canCastShadows=true - Whether or not the entity casts shadows. Currently applicable only to + * @property {boolean} canCastShadow=true - Whether or not the entity can cast a shadow. Currently applicable only to * {@link Entities.EntityType|Model} and {@link Entities.EntityType|Shape} entities. Shadows are cast if inside a * {@link Entities.EntityType|Zone} entity with castShadows enabled in its * {@link Entities.EntityProperties-Zone|keyLight} property. + * @property {boolean} isVisibleInSecondaryCamera=true - Whether or not the entity is rendered in the secondary camera. If true then the entity is rendered. * * @property {Vec3} position=0,0,0 - The position of the entity. * @property {Quat} rotation=0,0,0,1 - The orientation of the entity with respect to world coordinates. @@ -588,6 +597,15 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * @property {Entities.RenderInfo} renderInfo - Information on the cost of rendering the entity. Currently information is only * provided for Model entities. Read-only. * + * @property {boolean} cloneable=false - If true then the entity can be cloned via {@link Entities.cloneEntity}. + * @property {number} cloneLifetime=300 - The entity lifetime for clones created from this entity. + * @property {number} cloneLimit=0 - The total number of clones of this entity that can exist in the domain at any given time. + * @property {boolean} cloneDynamic=false - If true then clones created from this entity will have their + * dynamic property set to true. + * @property {boolean} cloneAvatarEntity=false - If true then clones created from this entity will be created as + * avatar entities: their clientOnly property will be set to true. + * @property {Uuid} cloneOriginID - The ID of the entity that this entity was cloned from. + * * @property {string} itemName="" - Certifiable name of the Marketplace item. * @property {string} itemDescription="" - Certifiable description of the Marketplace item. * @property {string} itemCategories="" - Certifiable category of the Marketplace item. @@ -1219,6 +1237,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LOCKED, locked); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_USER_DATA, userData); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ALPHA, alpha); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_VISIBLE_IN_SECONDARY_CAMERA, isVisibleInSecondaryCamera); // Certifiable Properties COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ITEM_NAME, itemName); @@ -1391,7 +1410,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool /**jsdoc * The axis-aligned bounding box of an entity. - * @typedef Entities.BoundingBox + * @typedef {object} Entities.BoundingBox * @property {Vec3} brn - The bottom right near (minimum axes values) corner of the AA box. * @property {Vec3} tfl - The top far left (maximum axes values) corner of the AA box. * @property {Vec3} center - The center of the AA box. @@ -1430,6 +1449,13 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CLIENT_ONLY, clientOnly); // Gettable but not settable except at entity creation COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_OWNING_AVATAR_ID, owningAvatarID); // Gettable but not settable + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CLONEABLE, cloneable); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CLONE_LIFETIME, cloneLifetime); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CLONE_LIMIT, cloneLimit); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CLONE_DYNAMIC, cloneDynamic); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CLONE_AVATAR_ENTITY, cloneAvatarEntity); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CLONE_ORIGIN_ID, cloneOriginID); + // Rendering info if (!skipDefaults && !strictSemantics) { QScriptValue renderInfo = engine->newObject(); @@ -1506,7 +1532,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(localRenderAlpha, float, setLocalRenderAlpha); COPY_PROPERTY_FROM_QSCRIPTVALUE(collisionless, bool, setCollisionless); COPY_PROPERTY_FROM_QSCRIPTVALUE_GETTER(ignoreForCollisions, bool, setCollisionless, getCollisionless); // legacy support - COPY_PROPERTY_FROM_QSCRIPTVALUE(collisionMask, uint8_t, setCollisionMask); + COPY_PROPERTY_FROM_QSCRIPTVALUE(collisionMask, uint16_t, setCollisionMask); COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(collidesWith, CollisionMask); COPY_PROPERTY_FROM_QSCRIPTVALUE_GETTER(collisionsWillMove, bool, setDynamic, getDynamic); // legacy support COPY_PROPERTY_FROM_QSCRIPTVALUE(dynamic, bool, setDynamic); @@ -1551,6 +1577,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(materialMappingScale, glmVec2, setMaterialMappingScale); COPY_PROPERTY_FROM_QSCRIPTVALUE(materialMappingRot, float, setMaterialMappingRot); COPY_PROPERTY_FROM_QSCRIPTVALUE(materialData, QString, setMaterialData); + COPY_PROPERTY_FROM_QSCRIPTVALUE(isVisibleInSecondaryCamera, bool, setIsVisibleInSecondaryCamera); // Certifiable Properties COPY_PROPERTY_FROM_QSCRIPTVALUE(itemName, QString, setItemName); @@ -1642,6 +1669,13 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(dpi, uint16_t, setDPI); + COPY_PROPERTY_FROM_QSCRIPTVALUE(cloneable, bool, setCloneable); + COPY_PROPERTY_FROM_QSCRIPTVALUE(cloneLifetime, float, setCloneLifetime); + COPY_PROPERTY_FROM_QSCRIPTVALUE(cloneLimit, float, setCloneLimit); + COPY_PROPERTY_FROM_QSCRIPTVALUE(cloneDynamic, bool, setCloneDynamic); + COPY_PROPERTY_FROM_QSCRIPTVALUE(cloneAvatarEntity, bool, setCloneAvatarEntity); + COPY_PROPERTY_FROM_QSCRIPTVALUE(cloneOriginID, QUuid, setCloneOriginID); + _lastEdited = usecTimestampNow(); } @@ -1793,6 +1827,13 @@ void EntityItemProperties::merge(const EntityItemProperties& other) { COPY_PROPERTY_IF_CHANGED(dpi); + COPY_PROPERTY_IF_CHANGED(cloneable); + COPY_PROPERTY_IF_CHANGED(cloneLifetime); + COPY_PROPERTY_IF_CHANGED(cloneLimit); + COPY_PROPERTY_IF_CHANGED(cloneDynamic); + COPY_PROPERTY_IF_CHANGED(cloneAvatarEntity); + COPY_PROPERTY_IF_CHANGED(cloneOriginID); + _lastEdited = usecTimestampNow(); } @@ -1868,7 +1909,7 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue ADD_PROPERTY_TO_MAP(PROP_ANGULAR_VELOCITY, AngularVelocity, angularVelocity, glm::vec3); ADD_PROPERTY_TO_MAP(PROP_ANGULAR_DAMPING, AngularDamping, angularDamping, float); ADD_PROPERTY_TO_MAP(PROP_COLLISIONLESS, Collisionless, collisionless, bool); - ADD_PROPERTY_TO_MAP(PROP_DYNAMIC, unused, ignoreForCollisions, unused); // legacy support + ADD_PROPERTY_TO_MAP(PROP_COLLISIONLESS, unused, ignoreForCollisions, unused); // legacy support ADD_PROPERTY_TO_MAP(PROP_COLLISION_MASK, unused, collisionMask, unused); ADD_PROPERTY_TO_MAP(PROP_COLLISION_MASK, unused, collidesWith, unused); ADD_PROPERTY_TO_MAP(PROP_DYNAMIC, unused, collisionsWillMove, unused); // legacy support @@ -1916,6 +1957,8 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue ADD_PROPERTY_TO_MAP(PROP_MATERIAL_MAPPING_ROT, MaterialMappingRot, materialMappingRot, float); ADD_PROPERTY_TO_MAP(PROP_MATERIAL_DATA, MaterialData, materialData, QString); + ADD_PROPERTY_TO_MAP(PROP_VISIBLE_IN_SECONDARY_CAMERA, IsVisibleInSecondaryCamera, isVisibleInSecondaryCamera, bool); + // Certifiable Properties ADD_PROPERTY_TO_MAP(PROP_ITEM_NAME, ItemName, itemName, QString); ADD_PROPERTY_TO_MAP(PROP_ITEM_DESCRIPTION, ItemDescription, itemDescription, QString); @@ -2017,6 +2060,13 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue ADD_PROPERTY_TO_MAP(PROP_DPI, DPI, dpi, uint16_t); + ADD_PROPERTY_TO_MAP(PROP_CLONEABLE, Cloneable, cloneable, bool); + ADD_PROPERTY_TO_MAP(PROP_CLONE_LIFETIME, CloneLifetime, cloneLifetime, float); + ADD_PROPERTY_TO_MAP(PROP_CLONE_LIMIT, CloneLimit, cloneLimit, float); + ADD_PROPERTY_TO_MAP(PROP_CLONE_DYNAMIC, CloneDynamic, cloneDynamic, bool); + ADD_PROPERTY_TO_MAP(PROP_CLONE_AVATAR_ENTITY, CloneAvatarEntity, cloneAvatarEntity, bool); + ADD_PROPERTY_TO_MAP(PROP_CLONE_ORIGIN_ID, CloneOriginID, cloneOriginID, QUuid); + // FIXME - these are not yet handled //ADD_PROPERTY_TO_MAP(PROP_CREATED, Created, created, quint64); @@ -2331,6 +2381,12 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy APPEND_ENTITY_PROPERTY(PROP_ENTITY_INSTANCE_NUMBER, properties.getEntityInstanceNumber()); APPEND_ENTITY_PROPERTY(PROP_CERTIFICATE_ID, properties.getCertificateID()); APPEND_ENTITY_PROPERTY(PROP_STATIC_CERTIFICATE_VERSION, properties.getStaticCertificateVersion()); + + APPEND_ENTITY_PROPERTY(PROP_CLONEABLE, properties.getCloneable()); + APPEND_ENTITY_PROPERTY(PROP_CLONE_LIFETIME, properties.getCloneLifetime()); + APPEND_ENTITY_PROPERTY(PROP_CLONE_LIMIT, properties.getCloneLimit()); + APPEND_ENTITY_PROPERTY(PROP_CLONE_DYNAMIC, properties.getCloneDynamic()); + APPEND_ENTITY_PROPERTY(PROP_CLONE_AVATAR_ENTITY, properties.getCloneAvatarEntity()); } if (propertyCount > 0) { @@ -2535,7 +2591,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_VISIBLE, bool, setVisible); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CAN_CAST_SHADOW, bool, setCanCastShadow); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_COLLISIONLESS, bool, setCollisionless); - READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_COLLISION_MASK, uint8_t, setCollisionMask); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_COLLISION_MASK, uint16_t, setCollisionMask); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_DYNAMIC, bool, setDynamic); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_LOCKED, bool, setLocked); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_USER_DATA, QString, setUserData); @@ -2701,6 +2757,12 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CERTIFICATE_ID, QString, setCertificateID); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_STATIC_CERTIFICATE_VERSION, quint32, setStaticCertificateVersion); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CLONEABLE, bool, setCloneable); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CLONE_LIFETIME, float, setCloneLifetime); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CLONE_LIMIT, float, setCloneLimit); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CLONE_DYNAMIC, bool, setCloneDynamic); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CLONE_AVATAR_ENTITY, bool, setCloneAvatarEntity); + return valid; } @@ -2780,6 +2842,52 @@ bool EntityItemProperties::encodeEraseEntityMessage(const EntityItemID& entityIt return true; } +bool EntityItemProperties::encodeCloneEntityMessage(const EntityItemID& entityIDToClone, const EntityItemID& newEntityID, QByteArray& buffer) { + char* copyAt = buffer.data(); + int outputLength = 0; + + if (buffer.size() < (int)(NUM_BYTES_RFC4122_UUID * 2)) { + qCDebug(entities) << "ERROR - encodeCloneEntityMessage() called with buffer that is too small!"; + return false; + } + + memcpy(copyAt, entityIDToClone.toRfc4122().constData(), NUM_BYTES_RFC4122_UUID); + copyAt += NUM_BYTES_RFC4122_UUID; + outputLength += NUM_BYTES_RFC4122_UUID; + + memcpy(copyAt, newEntityID.toRfc4122().constData(), NUM_BYTES_RFC4122_UUID); + copyAt += NUM_BYTES_RFC4122_UUID; + outputLength += NUM_BYTES_RFC4122_UUID; + + buffer.resize(outputLength); + + return true; +} + +bool EntityItemProperties::decodeCloneEntityMessage(const QByteArray& buffer, int& processedBytes, EntityItemID& entityIDToClone, EntityItemID& newEntityID) { + const unsigned char* packetData = (const unsigned char*)buffer.constData(); + const unsigned char* dataAt = packetData; + size_t packetLength = buffer.size(); + processedBytes = 0; + + if (NUM_BYTES_RFC4122_UUID * 2 > packetLength) { + qCDebug(entities) << "EntityItemProperties::processEraseMessageDetails().... bailing because not enough bytes in buffer"; + return false; // bail to prevent buffer overflow + } + + QByteArray encodedID = buffer.mid((int)processedBytes, NUM_BYTES_RFC4122_UUID); + entityIDToClone = QUuid::fromRfc4122(encodedID); + dataAt += encodedID.size(); + processedBytes += encodedID.size(); + + encodedID = buffer.mid((int)processedBytes, NUM_BYTES_RFC4122_UUID); + newEntityID = QUuid::fromRfc4122(encodedID); + dataAt += encodedID.size(); + processedBytes += encodedID.size(); + + return true; +} + void EntityItemProperties::markAllChanged() { _lastEditedByChanged = true; _simulationOwnerChanged = true; @@ -2941,6 +3049,15 @@ void EntityItemProperties::markAllChanged() { _dpiChanged = true; _relayParentJointsChanged = true; + + _cloneableChanged = true; + _cloneLifetimeChanged = true; + _cloneLimitChanged = true; + _cloneDynamicChanged = true; + _cloneAvatarEntityChanged = true; + _cloneOriginIDChanged = true; + + _isVisibleInSecondaryCameraChanged = true; } // The minimum bounding box for the entity. @@ -3219,6 +3336,9 @@ QList EntityItemProperties::listChangedProperties() { if (materialDataChanged()) { out += "materialData"; } + if (isVisibleInSecondaryCameraChanged()) { + out += "isVisibleInSecondaryCamera"; + } // Certifiable Properties if (itemNameChanged()) { @@ -3373,6 +3493,25 @@ QList EntityItemProperties::listChangedProperties() { out += "isUVModeStretch"; } + if (cloneableChanged()) { + out += "cloneable"; + } + if (cloneLifetimeChanged()) { + out += "cloneLifetime"; + } + if (cloneLimitChanged()) { + out += "cloneLimit"; + } + if (cloneDynamicChanged()) { + out += "cloneDynamic"; + } + if (cloneAvatarEntityChanged()) { + out += "cloneAvatarEntity"; + } + if (cloneOriginIDChanged()) { + out += "cloneOriginID"; + } + getAnimation().listChangedProperties(out); getKeyLight().listChangedProperties(out); getAmbientLight().listChangedProperties(out); @@ -3536,3 +3675,18 @@ bool EntityItemProperties::verifyStaticCertificateProperties() { // I.e., if we can verify that the certificateID was produced by High Fidelity signing the static certificate hash. return verifySignature(EntityItem::_marketplacePublicKey, getStaticCertificateHash(), QByteArray::fromBase64(getCertificateID().toUtf8())); } + +void EntityItemProperties::convertToCloneProperties(const EntityItemID& entityIDToClone) { + setName(getName() + "-clone-" + entityIDToClone.toString()); + setLocked(false); + setLifetime(getCloneLifetime()); + setDynamic(getCloneDynamic()); + setClientOnly(getCloneAvatarEntity()); + setCreated(usecTimestampNow()); + setLastEdited(usecTimestampNow()); + setCloneable(ENTITY_ITEM_DEFAULT_CLONEABLE); + setCloneLifetime(ENTITY_ITEM_DEFAULT_CLONE_LIFETIME); + setCloneLimit(ENTITY_ITEM_DEFAULT_CLONE_LIMIT); + setCloneDynamic(ENTITY_ITEM_DEFAULT_CLONE_DYNAMIC); + setCloneAvatarEntity(ENTITY_ITEM_DEFAULT_CLONE_AVATAR_ENTITY); +} diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 39ea2e0bdd..e46eb73910 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -148,7 +148,7 @@ public: DEFINE_PROPERTY_REF(PROP_ANGULAR_VELOCITY, AngularVelocity, angularVelocity, glm::vec3, ENTITY_ITEM_DEFAULT_ANGULAR_VELOCITY); DEFINE_PROPERTY(PROP_ANGULAR_DAMPING, AngularDamping, angularDamping, float, ENTITY_ITEM_DEFAULT_ANGULAR_DAMPING); DEFINE_PROPERTY(PROP_COLLISIONLESS, Collisionless, collisionless, bool, ENTITY_ITEM_DEFAULT_COLLISIONLESS); - DEFINE_PROPERTY(PROP_COLLISION_MASK, CollisionMask, collisionMask, uint8_t, ENTITY_COLLISION_MASK_DEFAULT); + DEFINE_PROPERTY(PROP_COLLISION_MASK, CollisionMask, collisionMask, uint16_t, ENTITY_COLLISION_MASK_DEFAULT); DEFINE_PROPERTY(PROP_DYNAMIC, Dynamic, dynamic, bool, ENTITY_ITEM_DEFAULT_DYNAMIC); DEFINE_PROPERTY(PROP_IS_SPOTLIGHT, IsSpotlight, isSpotlight, bool, LightEntityItem::DEFAULT_IS_SPOTLIGHT); DEFINE_PROPERTY(PROP_INTENSITY, Intensity, intensity, float, LightEntityItem::DEFAULT_INTENSITY); @@ -233,6 +233,8 @@ public: DEFINE_PROPERTY_REF(PROP_MATERIAL_MAPPING_ROT, MaterialMappingRot, materialMappingRot, float, 0); DEFINE_PROPERTY_REF(PROP_MATERIAL_DATA, MaterialData, materialData, QString, ""); + DEFINE_PROPERTY(PROP_VISIBLE_IN_SECONDARY_CAMERA, IsVisibleInSecondaryCamera, isVisibleInSecondaryCamera, bool, ENTITY_ITEM_DEFAULT_VISIBLE_IN_SECONDARY_CAMERA); + // Certifiable Properties - related to Proof of Purchase certificates DEFINE_PROPERTY_REF(PROP_ITEM_NAME, ItemName, itemName, QString, ENTITY_ITEM_DEFAULT_ITEM_NAME); DEFINE_PROPERTY_REF(PROP_ITEM_DESCRIPTION, ItemDescription, itemDescription, QString, ENTITY_ITEM_DEFAULT_ITEM_DESCRIPTION); @@ -272,6 +274,13 @@ public: DEFINE_PROPERTY_REF(PROP_SERVER_SCRIPTS, ServerScripts, serverScripts, QString, ENTITY_ITEM_DEFAULT_SERVER_SCRIPTS); DEFINE_PROPERTY(PROP_RELAY_PARENT_JOINTS, RelayParentJoints, relayParentJoints, bool, ENTITY_ITEM_DEFAULT_RELAY_PARENT_JOINTS); + DEFINE_PROPERTY(PROP_CLONEABLE, Cloneable, cloneable, bool, ENTITY_ITEM_DEFAULT_CLONEABLE); + DEFINE_PROPERTY(PROP_CLONE_LIFETIME, CloneLifetime, cloneLifetime, float, ENTITY_ITEM_DEFAULT_CLONE_LIFETIME); + DEFINE_PROPERTY(PROP_CLONE_LIMIT, CloneLimit, cloneLimit, float, ENTITY_ITEM_DEFAULT_CLONE_LIMIT); + DEFINE_PROPERTY(PROP_CLONE_DYNAMIC, CloneDynamic, cloneDynamic, bool, ENTITY_ITEM_DEFAULT_CLONE_DYNAMIC); + DEFINE_PROPERTY(PROP_CLONE_AVATAR_ENTITY, CloneAvatarEntity, cloneAvatarEntity, bool, ENTITY_ITEM_DEFAULT_CLONE_AVATAR_ENTITY); + DEFINE_PROPERTY_REF(PROP_CLONE_ORIGIN_ID, CloneOriginID, cloneOriginID, QUuid, ENTITY_ITEM_DEFAULT_CLONE_ORIGIN_ID); + static QString getComponentModeString(uint32_t mode); static QString getComponentModeAsString(uint32_t mode); @@ -294,6 +303,8 @@ public: QByteArray& buffer, EntityPropertyFlags requestedProperties, EntityPropertyFlags& didntFitProperties); static bool encodeEraseEntityMessage(const EntityItemID& entityItemID, QByteArray& buffer); + static bool encodeCloneEntityMessage(const EntityItemID& entityIDToClone, const EntityItemID& newEntityID, QByteArray& buffer); + static bool decodeCloneEntityMessage(const QByteArray& buffer, int& processedBytes, EntityItemID& entityIDToClone, EntityItemID& newEntityID); static bool decodeEntityEditPacket(const unsigned char* data, int bytesToRead, int& processedBytes, EntityItemID& entityID, EntityItemProperties& properties); @@ -369,6 +380,8 @@ public: bool verifyStaticCertificateProperties(); static bool verifySignature(const QString& key, const QByteArray& text, const QByteArray& signature); + void convertToCloneProperties(const EntityItemID& entityIDToClone); + protected: QString getCollisionMaskAsString() const; void setCollisionMaskFromString(const QString& maskString); @@ -515,6 +528,13 @@ inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) { DEBUG_PROPERTY_IF_CHANGED(debug, properties, AmbientLightMode, ambientLightMode, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, SkyboxMode, skyboxMode, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, Cloneable, cloneable, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, CloneLifetime, cloneLifetime, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, CloneLimit, cloneLimit, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, CloneDynamic, cloneDynamic, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, CloneAvatarEntity, cloneAvatarEntity, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, CloneOriginID, cloneOriginID, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, VoxelVolumeSize, voxelVolumeSize, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, VoxelData, voxelData, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, VoxelSurfaceStyle, voxelSurfaceStyle, ""); diff --git a/libraries/entities/src/EntityItemPropertiesDefaults.h b/libraries/entities/src/EntityItemPropertiesDefaults.h index d2ddd687dd..6c39e90c91 100644 --- a/libraries/entities/src/EntityItemPropertiesDefaults.h +++ b/libraries/entities/src/EntityItemPropertiesDefaults.h @@ -46,6 +46,7 @@ const quint32 ENTITY_ITEM_DEFAULT_STATIC_CERTIFICATE_VERSION = 0; const float ENTITY_ITEM_DEFAULT_ALPHA = 1.0f; const float ENTITY_ITEM_DEFAULT_LOCAL_RENDER_ALPHA = 1.0f; const bool ENTITY_ITEM_DEFAULT_VISIBLE = true; +const bool ENTITY_ITEM_DEFAULT_VISIBLE_IN_SECONDARY_CAMERA = true; const bool ENTITY_ITEM_DEFAULT_CAN_CAST_SHADOW { true }; const QString ENTITY_ITEM_DEFAULT_SCRIPT = QString(""); @@ -97,4 +98,11 @@ const QUuid ENTITY_ITEM_DEFAULT_LAST_EDITED_BY = QUuid(); const bool ENTITY_ITEM_DEFAULT_RELAY_PARENT_JOINTS = false; +const bool ENTITY_ITEM_DEFAULT_CLONEABLE = false; +const float ENTITY_ITEM_DEFAULT_CLONE_LIFETIME = 300.0f; +const int ENTITY_ITEM_DEFAULT_CLONE_LIMIT = 0; +const bool ENTITY_ITEM_DEFAULT_CLONE_DYNAMIC = false; +const bool ENTITY_ITEM_DEFAULT_CLONE_AVATAR_ENTITY = false; +const QUuid ENTITY_ITEM_DEFAULT_CLONE_ORIGIN_ID = QUuid(); + #endif // hifi_EntityItemPropertiesDefaults_h diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index 99a5f287ea..d43a991f22 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -204,6 +204,13 @@ enum EntityPropertyList { PROP_CERTIFICATE_ID, PROP_STATIC_CERTIFICATE_VERSION, + PROP_CLONEABLE, + PROP_CLONE_LIFETIME, + PROP_CLONE_LIMIT, + PROP_CLONE_DYNAMIC, + PROP_CLONE_AVATAR_ENTITY, + PROP_CLONE_ORIGIN_ID, + PROP_HAZE_MODE, PROP_KEYLIGHT_COLOR, @@ -242,6 +249,8 @@ enum EntityPropertyList { PROP_MATERIAL_MAPPING_ROT, PROP_MATERIAL_DATA, + PROP_VISIBLE_IN_SECONDARY_CAMERA, // not sent over the wire, only used locally + //////////////////////////////////////////////////////////////////////////////////////////////////// // ATTENTION: add new properties to end of list just ABOVE this line PROP_AFTER_LAST_ITEM, diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 7c16214a78..98407a74c6 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -258,33 +258,9 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties propertiesWithSimID = convertPropertiesFromScriptSemantics(propertiesWithSimID, scalesWithParent); propertiesWithSimID.setDimensionsInitialized(properties.dimensionsChanged()); - EntityItemID id = EntityItemID(QUuid::createUuid()); - + EntityItemID id; // If we have a local entity tree set, then also update it. - bool success = true; - if (_entityTree) { - _entityTree->withWriteLock([&] { - EntityItemPointer entity = _entityTree->addEntity(id, propertiesWithSimID); - if (entity) { - if (propertiesWithSimID.queryAACubeRelatedPropertyChanged()) { - // due to parenting, the server may not know where something is in world-space, so include the bounding cube. - bool success; - AACube queryAACube = entity->getQueryAACube(success); - if (success) { - propertiesWithSimID.setQueryAACube(queryAACube); - } - } - - entity->setLastBroadcast(usecTimestampNow()); - // since we're creating this object we will immediately volunteer to own its simulation - entity->flagForOwnershipBid(VOLUNTEER_SIMULATION_PRIORITY); - propertiesWithSimID.setLastEdited(entity->getLastEdited()); - } else { - qCDebug(entities) << "script failed to add new Entity to local Octree"; - success = false; - } - }); - } + bool success = addLocalEntityCopy(propertiesWithSimID, id); // queue the packet if (success) { @@ -295,6 +271,37 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties } } +bool EntityScriptingInterface::addLocalEntityCopy(EntityItemProperties& properties, EntityItemID& id, bool isClone) { + bool success = true; + id = EntityItemID(QUuid::createUuid()); + + if (_entityTree) { + _entityTree->withWriteLock([&] { + EntityItemPointer entity = _entityTree->addEntity(id, properties, isClone); + if (entity) { + if (properties.queryAACubeRelatedPropertyChanged()) { + // due to parenting, the server may not know where something is in world-space, so include the bounding cube. + bool success; + AACube queryAACube = entity->getQueryAACube(success); + if (success) { + properties.setQueryAACube(queryAACube); + } + } + + entity->setLastBroadcast(usecTimestampNow()); + // since we're creating this object we will immediately volunteer to own its simulation + entity->flagForOwnershipBid(VOLUNTEER_SIMULATION_PRIORITY); + properties.setLastEdited(entity->getLastEdited()); + } else { + qCDebug(entities) << "script failed to add new Entity to local Octree"; + success = false; + } + }); + } + + return success; +} + QUuid EntityScriptingInterface::addModelEntity(const QString& name, const QString& modelUrl, const QString& textures, const QString& shapeType, bool dynamic, bool collisionless, const glm::vec3& position, const glm::vec3& gravity) { @@ -320,6 +327,28 @@ QUuid EntityScriptingInterface::addModelEntity(const QString& name, const QStrin return addEntity(properties); } +QUuid EntityScriptingInterface::cloneEntity(QUuid entityIDToClone) { + EntityItemID newEntityID; + EntityItemProperties properties = getEntityProperties(entityIDToClone); + bool cloneAvatarEntity = properties.getCloneAvatarEntity(); + properties.convertToCloneProperties(entityIDToClone); + + if (cloneAvatarEntity) { + return addEntity(properties, true); + } else { + // setLastEdited timestamp to 0 to ensure this entity gets updated with the properties + // from the server-created entity, don't change this unless you know what you are doing + properties.setLastEdited(0); + bool success = addLocalEntityCopy(properties, newEntityID, true); + if (success) { + getEntityPacketSender()->queueCloneEntityMessage(entityIDToClone, newEntityID); + return newEntityID; + } else { + return QUuid(); + } + } +} + EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identity) { EntityPropertyFlags noSpecificProperties; return getEntityProperties(identity, noSpecificProperties); @@ -1659,15 +1688,21 @@ QVector EntityScriptingInterface::getChildrenIDs(const QUuid& parentID) { if (!_entityTree) { return result; } - - EntityItemPointer entity = _entityTree->findEntityByEntityItemID(parentID); - if (!entity) { - qCDebug(entities) << "EntityScriptingInterface::getChildrenIDs - no entity with ID" << parentID; - return result; - } - _entityTree->withReadLock([&] { - entity->forEachChild([&](SpatiallyNestablePointer child) { + QSharedPointer parentFinder = DependencyManager::get(); + if (!parentFinder) { + return; + } + bool success; + SpatiallyNestableWeakPointer parentWP = parentFinder->find(parentID, success); + if (!success) { + return; + } + SpatiallyNestablePointer parent = parentWP.lock(); + if (!parent) { + return; + } + parent->forEachChild([&](SpatiallyNestablePointer child) { result.push_back(child->getID()); }); }); diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 7e47d9e2d4..50df825e5f 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -224,6 +224,16 @@ public slots: Q_INVOKABLE QUuid addModelEntity(const QString& name, const QString& modelUrl, const QString& textures, const QString& shapeType, bool dynamic, bool collisionless, const glm::vec3& position, const glm::vec3& gravity); + /**jsdoc + * Create a clone of an entity. A clone can be created by a client that doesn't have rez permissions in the current domain. + * The entity must have its cloneable property set to true. The clone has a modified name, other + * properties set per its clone related-properties, and its clone-related properties are set to defaults. + * @function Entities.cloneEntity + * @param {Uuid} entityID - The ID of the entity to clone. + * @returns {Uuid} The ID of the new entity if successfully cloned, otherwise {@link Uuid|Uuid.NULL}. + */ + Q_INVOKABLE QUuid cloneEntity(QUuid entityIDToClone); + /**jsdoc * Get the properties of an entity. * @function Entities.getEntityProperties @@ -1216,12 +1226,11 @@ public slots: /**jsdoc - * Get the IDs of entities, overlays, and avatars that are directly parented to an entity. To get all descendants of an - * entity, recurse on the IDs returned by the function. + * Get the IDs of entities, overlays, and avatars that are directly parented to an entity, overlay, or avatar model. Recurse on the IDs returned by the function to get all descendants of an entity, overlay, or avatar. * @function Entities.getChildrenIDs - * @param {Uuid} parentID - The ID of the entity to get the children IDs of. + * @param {Uuid} parentID - The ID of the entity, overlay, or avatar to get the children IDs of. * @returns {Uuid[]} An array of entity, overlay, and avatar IDs that are parented directly to the parentID - * entity. Does not include children's children, etc. The array is empty if no children can be found or + * entity, overlay, or avatar. Does not include children's children, etc. The array is empty if no children can be found or * parentID cannot be found. * @example Report the children of an entity. * function createEntity(description, position, parent) { @@ -1875,6 +1884,7 @@ private: bool polyVoxWorker(QUuid entityID, std::function actor); bool setPoints(QUuid entityID, std::function actor); void queueEntityMessage(PacketType packetType, EntityItemID entityID, const EntityItemProperties& properties); + bool addLocalEntityCopy(EntityItemProperties& propertiesWithSimID, EntityItemID& id, bool isClone = false); EntityItemPointer checkForTreeEntityAndTypeMatch(const QUuid& entityID, EntityTypes::EntityType entityType = EntityTypes::Unknown); diff --git a/libraries/entities/src/EntitySimulation.cpp b/libraries/entities/src/EntitySimulation.cpp index d034ddedbe..ba088cb7fd 100644 --- a/libraries/entities/src/EntitySimulation.cpp +++ b/libraries/entities/src/EntitySimulation.cpp @@ -65,6 +65,7 @@ void EntitySimulation::prepareEntityForDelete(EntityItemPointer entity) { removeEntityInternal(entity); if (entity->getElement()) { _deadEntities.insert(entity); + _entityTree->cleanupCloneIDs(entity->getEntityItemID()); } } } diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 3149527216..3d96107071 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -228,6 +228,7 @@ bool EntityTree::handlesEditPacketType(PacketType packetType) const { // we handle these types of "edit" packets switch (packetType) { case PacketType::EntityAdd: + case PacketType::EntityClone: case PacketType::EntityEdit: case PacketType::EntityErase: case PacketType::EntityPhysics: @@ -492,7 +493,7 @@ bool EntityTree::updateEntity(EntityItemPointer entity, const EntityItemProperti return true; } -EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const EntityItemProperties& properties) { +EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const EntityItemProperties& properties, bool isClone) { EntityItemPointer result = NULL; EntityItemProperties props = properties; @@ -504,7 +505,7 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti if (!properties.getClientOnly() && getIsClient() && !nodeList->getThisNodeCanRez() && !nodeList->getThisNodeCanRezTmp() && - !nodeList->getThisNodeCanRezCertified() && !nodeList->getThisNodeCanRezTmpCertified() && !_serverlessDomain) { + !nodeList->getThisNodeCanRezCertified() && !nodeList->getThisNodeCanRezTmpCertified() && !_serverlessDomain && !isClone) { return nullptr; } @@ -592,6 +593,7 @@ void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ign return; } + cleanupCloneIDs(entityID); unhookChildAvatar(entityID); emit deletingEntity(entityID); emit deletingEntityPointer(existingEntity.get()); @@ -625,6 +627,28 @@ void EntityTree::unhookChildAvatar(const EntityItemID entityID) { }); } +void EntityTree::cleanupCloneIDs(const EntityItemID& entityID) { + EntityItemPointer entity = findEntityByEntityItemID(entityID); + if (entity) { + // remove clone ID from it's clone origin's clone ID list if clone origin exists + const QUuid& cloneOriginID = entity->getCloneOriginID(); + if (!cloneOriginID.isNull()) { + EntityItemPointer cloneOrigin = findEntityByID(cloneOriginID); + if (cloneOrigin) { + cloneOrigin->removeCloneID(entityID); + } + } + // clear the clone origin ID on any clones that this entity had + const QVector& cloneIDs = entity->getCloneIDs(); + foreach(const QUuid& cloneChildID, cloneIDs) { + EntityItemPointer cloneChild = findEntityByEntityItemID(cloneChildID); + if (cloneChild) { + cloneChild->setCloneOriginID(QUuid()); + } + } + } +} + void EntityTree::deleteEntities(QSet entityIDs, bool force, bool ignoreWarnings) { // NOTE: callers must lock the tree before using this method DeleteEntityOperator theOperator(getThisPointer()); @@ -653,6 +677,7 @@ void EntityTree::deleteEntities(QSet entityIDs, bool force, bool i } // tell our delete operator about this entityID + cleanupCloneIDs(entityID); unhookChildAvatar(entityID); theOperator.addEntityIDToDeleteList(entityID); emit deletingEntity(entityID); @@ -1392,6 +1417,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c int processedBytes = 0; bool isAdd = false; + bool isClone = false; // we handle these types of "edit" packets switch (message.getType()) { case PacketType::EntityErase: { @@ -1400,6 +1426,9 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c break; } + case PacketType::EntityClone: + isClone = true; // fall through to next case + // FALLTHRU case PacketType::EntityAdd: isAdd = true; // fall through to next case // FALLTHRU @@ -1422,8 +1451,22 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c EntityItemProperties properties; startDecode = usecTimestampNow(); - bool validEditPacket = EntityItemProperties::decodeEntityEditPacket(editData, maxLength, processedBytes, - entityItemID, properties); + bool validEditPacket = false; + EntityItemID entityIDToClone; + EntityItemPointer entityToClone; + if (isClone) { + QByteArray buffer = QByteArray::fromRawData(reinterpret_cast(editData), maxLength); + validEditPacket = EntityItemProperties::decodeCloneEntityMessage(buffer, processedBytes, entityIDToClone, entityItemID); + if (validEditPacket) { + entityToClone = findEntityByEntityItemID(entityIDToClone); + if (entityToClone) { + properties = entityToClone->getProperties(); + } + } + } else { + validEditPacket = EntityItemProperties::decodeEntityEditPacket(editData, maxLength, processedBytes, entityItemID, properties); + } + endDecode = usecTimestampNow(); EntityItemPointer existingEntity; @@ -1491,24 +1534,26 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c } - if ((isAdd || properties.lifetimeChanged()) && - ((!senderNode->getCanRez() && senderNode->getCanRezTmp()) || - (!senderNode->getCanRezCertified() && senderNode->getCanRezTmpCertified()))) { - // this node is only allowed to rez temporary entities. if need be, cap the lifetime. - if (properties.getLifetime() == ENTITY_ITEM_IMMORTAL_LIFETIME || - properties.getLifetime() > _maxTmpEntityLifetime) { - properties.setLifetime(_maxTmpEntityLifetime); + if (!isClone) { + if ((isAdd || properties.lifetimeChanged()) && + ((!senderNode->getCanRez() && senderNode->getCanRezTmp()) || + (!senderNode->getCanRezCertified() && senderNode->getCanRezTmpCertified()))) { + // this node is only allowed to rez temporary entities. if need be, cap the lifetime. + if (properties.getLifetime() == ENTITY_ITEM_IMMORTAL_LIFETIME || + properties.getLifetime() > _maxTmpEntityLifetime) { + properties.setLifetime(_maxTmpEntityLifetime); + bumpTimestamp(properties); + } + } + + if (isAdd && properties.getLocked() && !senderNode->isAllowedEditor()) { + // if a node can't change locks, don't allow it to create an already-locked entity -- automatically + // clear the locked property and allow the unlocked entity to be created. + properties.setLocked(false); bumpTimestamp(properties); } } - if (isAdd && properties.getLocked() && !senderNode->isAllowedEditor()) { - // if a node can't change locks, don't allow it to create an already-locked entity -- automatically - // clear the locked property and allow the unlocked entity to be created. - properties.setLocked(false); - bumpTimestamp(properties); - } - // If we got a valid edit packet, then it could be a new entity or it could be an update to // an existing entity... handle appropriately if (validEditPacket) { @@ -1566,17 +1611,32 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c } else if (isAdd) { bool failedAdd = !allowed; bool isCertified = !properties.getCertificateID().isEmpty(); + bool isCloneable = properties.getCloneable(); + int cloneLimit = properties.getCloneLimit(); if (!allowed) { qCDebug(entities) << "Filtered entity add. ID:" << entityItemID; - } else if (!isCertified && !senderNode->getCanRez() && !senderNode->getCanRezTmp()) { + } else if (!isClone && !isCertified && !senderNode->getCanRez() && !senderNode->getCanRezTmp()) { failedAdd = true; qCDebug(entities) << "User without 'uncertified rez rights' [" << senderNode->getUUID() << "] attempted to add an uncertified entity with ID:" << entityItemID; - } else if (isCertified && !senderNode->getCanRezCertified() && !senderNode->getCanRezTmpCertified()) { + } else if (!isClone && isCertified && !senderNode->getCanRezCertified() && !senderNode->getCanRezTmpCertified()) { failedAdd = true; qCDebug(entities) << "User without 'certified rez rights' [" << senderNode->getUUID() << "] attempted to add a certified entity with ID:" << entityItemID; + } else if (isClone && isCertified) { + failedAdd = true; + qCDebug(entities) << "User attempted to clone certified entity from entity ID:" << entityIDToClone; + } else if (isClone && !isCloneable) { + failedAdd = true; + qCDebug(entities) << "User attempted to clone non-cloneable entity from entity ID:" << entityIDToClone; + } else if (isClone && entityToClone && entityToClone->getCloneIDs().size() >= cloneLimit && cloneLimit != 0) { + failedAdd = true; + qCDebug(entities) << "User attempted to clone entity ID:" << entityIDToClone << " which reached it's cloneable limit."; } else { + if (isClone) { + properties.convertToCloneProperties(entityIDToClone); + } + // this is a new entity... assign a new entityID properties.setCreated(properties.getLastEdited()); properties.setLastEditedBy(senderNode->getUUID()); @@ -1597,10 +1657,15 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c } } + if (newEntity && isClone) { + entityToClone->addCloneID(newEntity->getEntityItemID()); + newEntity->setCloneOriginID(entityIDToClone); + } + if (newEntity) { newEntity->markAsChangedOnServer(); notifyNewlyCreatedEntity(*newEntity, senderNode); - + startLogging = usecTimestampNow(); if (wantEditLogging()) { qCDebug(entities) << "User [" << senderNode->getUUID() << "] added entity. ID:" @@ -1927,6 +1992,7 @@ int EntityTree::processEraseMessage(ReceivedMessage& message, const SharedNodePo if (shouldEraseEntity(entityID, sourceNode)) { entityItemIDsToDelete << entityItemID; + cleanupCloneIDs(entityItemID); } } deleteEntities(entityItemIDsToDelete, true, true); @@ -1976,6 +2042,7 @@ int EntityTree::processEraseMessageDetails(const QByteArray& dataByteArray, cons if (shouldEraseEntity(entityID, sourceNode)) { entityItemIDsToDelete << entityItemID; + cleanupCloneIDs(entityItemID); } } @@ -2322,6 +2389,8 @@ bool EntityTree::readFromMap(QVariantMap& map) { return false; } + QMap> cloneIDs; + bool success = true; foreach (QVariant entityVariant, entitiesQList) { // QVariantMap --> QScriptValue --> EntityItemProperties --> Entity @@ -2409,11 +2478,43 @@ bool EntityTree::readFromMap(QVariantMap& map) { } } + // Convert old cloneable entities so they use cloneableData instead of userData + if (contentVersion < (int)EntityVersion::CloneableData) { + QJsonObject userData = QJsonDocument::fromJson(properties.getUserData().toUtf8()).object(); + QJsonObject grabbableKey = userData["grabbableKey"].toObject(); + QJsonValue cloneable = grabbableKey["cloneable"]; + if (cloneable.isBool() && cloneable.toBool()) { + QJsonValue cloneLifetime = grabbableKey["cloneLifetime"]; + QJsonValue cloneLimit = grabbableKey["cloneLimit"]; + QJsonValue cloneDynamic = grabbableKey["cloneDynamic"]; + QJsonValue cloneAvatarEntity = grabbableKey["cloneAvatarEntity"]; + + // This is cloneable, we need to convert the properties + properties.setCloneable(true); + properties.setCloneLifetime(cloneLifetime.toInt()); + properties.setCloneLimit(cloneLimit.toInt()); + properties.setCloneDynamic(cloneDynamic.toBool()); + properties.setCloneAvatarEntity(cloneAvatarEntity.toBool()); + } + } + EntityItemPointer entity = addEntity(entityItemID, properties); if (!entity) { qCDebug(entities) << "adding Entity failed:" << entityItemID << properties.getType(); success = false; } + + const QUuid& cloneOriginID = entity->getCloneOriginID(); + if (!cloneOriginID.isNull()) { + cloneIDs[cloneOriginID].push_back(entity->getEntityItemID()); + } + } + + for (const auto& entityID : cloneIDs.keys()) { + auto entity = findEntityByID(entityID); + if (entity) { + entity->setCloneIDs(cloneIDs.value(entityID)); + } } return success; diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index ee9fb10554..d5097b9a12 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -110,13 +110,14 @@ public: // The newer API... void postAddEntity(EntityItemPointer entityItem); - EntityItemPointer addEntity(const EntityItemID& entityID, const EntityItemProperties& properties); + EntityItemPointer addEntity(const EntityItemID& entityID, const EntityItemProperties& properties, bool isClone = false); // use this method if you only know the entityID bool updateEntity(const EntityItemID& entityID, const EntityItemProperties& properties, const SharedNodePointer& senderNode = SharedNodePointer(nullptr)); // check if the avatar is a child of this entity, If so set the avatar parentID to null void unhookChildAvatar(const EntityItemID entityID); + void cleanupCloneIDs(const EntityItemID& entityID); void deleteEntity(const EntityItemID& entityID, bool force = false, bool ignoreWarnings = true); void deleteEntities(QSet entityIDs, bool force = false, bool ignoreWarnings = true); diff --git a/libraries/entities/src/KeyLightPropertyGroup.h b/libraries/entities/src/KeyLightPropertyGroup.h index 2be33787de..b966b78fc7 100644 --- a/libraries/entities/src/KeyLightPropertyGroup.h +++ b/libraries/entities/src/KeyLightPropertyGroup.h @@ -35,7 +35,7 @@ class ReadBitstreamToTreeParams; * @property {Vec3} direction=0,-1,0 - The direction the light is shining. * @property {boolean} castShadows=false - If true then shadows are cast. Shadows are cast by avatars, plus * {@link Entities.EntityType|Model} and {@link Entities.EntityType|Shape} entities that have their - * {@link Entities.EntityProperties|canCastShadows} property set to true. + * {@link Entities.EntityProperties|canCastShadow} property set to true. */ class KeyLightPropertyGroup : public PropertyGroup { public: diff --git a/libraries/fbx/src/FBX.h b/libraries/fbx/src/FBX.h index 239908f86c..fc94236c96 100644 --- a/libraries/fbx/src/FBX.h +++ b/libraries/fbx/src/FBX.h @@ -258,6 +258,11 @@ public: QHash texcoordSetMap; }; +/**jsdoc + * @typedef {object} FBXAnimationFrame + * @property {Quat[]} rotations + * @property {Vec3[]} translations + */ /// A single animation frame extracted from an FBX document. class FBXAnimationFrame { public: diff --git a/libraries/fbx/src/GLTFReader.cpp b/libraries/fbx/src/GLTFReader.cpp index f322c2319e..1fa4b3873e 100644 --- a/libraries/fbx/src/GLTFReader.cpp +++ b/libraries/fbx/src/GLTFReader.cpp @@ -1174,7 +1174,7 @@ bool GLTFReader::addArrayOfType(const QByteArray& bin, int byteOffset, int byteL break; } case GLTFAccessorComponentType::UNSIGNED_INT: { - readArray(bin, byteOffset, byteLength, outarray, accessorType); + readArray(bin, byteOffset, byteLength, outarray, accessorType); break; } case GLTFAccessorComponentType::UNSIGNED_SHORT: { diff --git a/libraries/fbx/src/GLTFReader.h b/libraries/fbx/src/GLTFReader.h index 3554594768..28c1d8282f 100644 --- a/libraries/fbx/src/GLTFReader.h +++ b/libraries/fbx/src/GLTFReader.h @@ -190,7 +190,7 @@ namespace GLTFBufferViewTarget { struct GLTFBufferView { int buffer; //required int byteLength; //required - int byteOffset; + int byteOffset { 0 }; int target; QMap defined; void dump() { @@ -470,7 +470,7 @@ namespace GLTFAccessorComponentType { } struct GLTFAccessor { int bufferView; - int byteOffset; + int byteOffset { 0 }; int componentType; //required int count; //required int type; //required diff --git a/libraries/gl/src/gl/GLShaders.cpp b/libraries/gl/src/gl/GLShaders.cpp index ecd6fe3323..9bfe214fcf 100644 --- a/libraries/gl/src/gl/GLShaders.cpp +++ b/libraries/gl/src/gl/GLShaders.cpp @@ -2,15 +2,64 @@ #include "GLLogging.h" -namespace gl { +#include +#include +#include +#include +#include +#include + +using namespace gl; + +void Uniform::load(GLuint glprogram, int index) { + const GLint NAME_LENGTH = 256; + GLchar glname[NAME_LENGTH]; + GLint length = 0; + glGetActiveUniform(glprogram, index, NAME_LENGTH, &length, &size, &type, glname); + name = std::string(glname, length); + location = glGetUniformLocation(glprogram, glname); +} + +Uniforms gl::loadUniforms(GLuint glprogram) { + GLint uniformsCount = 0; + glGetProgramiv(glprogram, GL_ACTIVE_UNIFORMS, &uniformsCount); + + Uniforms result; + result.resize(uniformsCount); + for (int i = 0; i < uniformsCount; i++) { + result[i].load(glprogram, i); + } + return result; +} #ifdef SEPARATE_PROGRAM - bool compileShader(GLenum shaderDomain, const std::string& shaderSource, const std::string& defines, GLuint &shaderObject, GLuint &programObject, std::string& message) { +bool gl::compileShader(GLenum shaderDomain, + const std::string& shaderSource, + GLuint& shaderObject, + GLuint& programObject, + std::string& message) { + return compileShader(shaderDomain, std::vector{ shaderSource }, shaderObject, programObject, message); +} #else - bool compileShader(GLenum shaderDomain, const std::string& shaderSource, const std::string& defines, GLuint &shaderObject, std::string& message) { +bool gl::compileShader(GLenum shaderDomain, const std::string& shaderSource, GLuint& shaderObject, std::string& message) { + return compileShader(shaderDomain, std::vector{ shaderSource }, shaderObject, message); +} #endif - if (shaderSource.empty()) { + +#ifdef SEPARATE_PROGRAM +bool gl::compileShader(GLenum shaderDomain, + const std::string& shaderSource, + GLuint& shaderObject, + GLuint& programObject, + std::string& message) { +#else +bool gl::compileShader(GLenum shaderDomain, + const std::vector& shaderSources, + GLuint& shaderObject, + std::string& message) { +#endif + if (shaderSources.empty()) { qCDebug(glLogging) << "GLShader::compileShader - no GLSL shader source code ? so failed to create"; return false; } @@ -23,9 +72,11 @@ namespace gl { } // Assign the source - const int NUM_SOURCE_STRINGS = 2; - const GLchar* srcstr[] = { defines.c_str(), shaderSource.c_str() }; - glShaderSource(glshader, NUM_SOURCE_STRINGS, srcstr, NULL); + std::vector cstrs; + for (const auto& str : shaderSources) { + cstrs.push_back(str.c_str()); + } + glShaderSource(glshader, static_cast(cstrs.size()), cstrs.data(), NULL); // Compile ! glCompileShader(glshader); @@ -66,7 +117,7 @@ namespace gl { qCCritical(glLogging) << "GLShader::compileShader - failed to compile the gl shader object:"; int lineNumber = 0; - for (auto s : srcstr) { + for (const auto& s : cstrs) { QString str(s); QStringList lines = str.split("\n"); for (auto& line : lines) { @@ -142,7 +193,7 @@ namespace gl { return true; } -GLuint compileProgram(const std::vector& glshaders, std::string& message, std::vector& binary) { +GLuint gl::compileProgram(const std::vector& glshaders, std::string& message, CachedShader& cachedShader) { // A brand new program: GLuint glprogram = glCreateProgram(); if (!glprogram) { @@ -150,14 +201,21 @@ GLuint compileProgram(const std::vector& glshaders, std::string& message return 0; } - // glProgramParameteri(glprogram, GL_PROGRAM_, GL_TRUE); - // Create the program from the sub shaders - for (auto so : glshaders) { - glAttachShader(glprogram, so); - } + bool binaryLoaded = false; - // Link! - glLinkProgram(glprogram); + if (glshaders.empty() && cachedShader) { + glProgramBinary(glprogram, cachedShader.format, cachedShader.binary.data(), (GLsizei)cachedShader.binary.size()); + binaryLoaded = true; + } else { + // glProgramParameteri(glprogram, GL_PROGRAM_, GL_TRUE); + // Create the program from the sub shaders + for (auto so : glshaders) { + glAttachShader(glprogram, so); + } + + // Link! + glLinkProgram(glprogram); + } GLint linked = 0; glGetProgramiv(glprogram, GL_LINK_STATUS, &linked); @@ -205,25 +263,73 @@ GLuint compileProgram(const std::vector& glshaders, std::string& message } // If linked get the binaries - if (linked) { + if (linked && !binaryLoaded) { GLint binaryLength = 0; glGetProgramiv(glprogram, GL_PROGRAM_BINARY_LENGTH, &binaryLength); - if (binaryLength > 0) { - GLint numBinFormats = 0; - glGetIntegerv(GL_NUM_PROGRAM_BINARY_FORMATS, &numBinFormats); - if (numBinFormats > 0) { - binary.resize(binaryLength); - std::vector binFormats(numBinFormats); - glGetIntegerv(GL_NUM_PROGRAM_BINARY_FORMATS, binFormats.data()); - - GLenum programBinFormat; - glGetProgramBinary(glprogram, binaryLength, NULL, &programBinFormat, binary.data()); - } + cachedShader.binary.resize(binaryLength); + glGetProgramBinary(glprogram, binaryLength, NULL, &cachedShader.format, cachedShader.binary.data()); } } return glprogram; } +const QString& getShaderCacheFile() { + static const QString SHADER_CACHE_FOLDER{ "shaders" }; + static const QString SHADER_CACHE_FILE_NAME{ "cache.json" }; + static const QString SHADER_CACHE_FILE = FileUtils::standardPath(SHADER_CACHE_FOLDER) + SHADER_CACHE_FILE_NAME; + return SHADER_CACHE_FILE; +} + +static const char* SHADER_JSON_TYPE_KEY = "type"; +static const char* SHADER_JSON_SOURCE_KEY = "source"; +static const char* SHADER_JSON_DATA_KEY = "data"; + +void gl::loadShaderCache(ShaderCache& cache) { + QString shaderCacheFile = getShaderCacheFile(); + if (QFileInfo(shaderCacheFile).exists()) { + QString json = FileUtils::readFile(shaderCacheFile); + auto root = QJsonDocument::fromJson(json.toUtf8()).object(); + for (const auto& qhash : root.keys()) { + auto programObject = root[qhash].toObject(); + QByteArray qbinary = QByteArray::fromBase64(programObject[SHADER_JSON_DATA_KEY].toString().toUtf8()); + std::string hash = qhash.toStdString(); + auto& cachedShader = cache[hash]; + cachedShader.binary.resize(qbinary.size()); + memcpy(cachedShader.binary.data(), qbinary.data(), qbinary.size()); + cachedShader.format = (GLenum)programObject[SHADER_JSON_TYPE_KEY].toInt(); + cachedShader.source = programObject[SHADER_JSON_SOURCE_KEY].toString().toStdString(); + } + } +} + +void gl::saveShaderCache(const ShaderCache& cache) { + QByteArray json; + { + QVariantMap variantMap; + for (const auto& entry : cache) { + const auto& key = entry.first; + const auto& type = entry.second.format; + const auto& binary = entry.second.binary; + QVariantMap qentry; + qentry[SHADER_JSON_TYPE_KEY] = QVariant(type); + qentry[SHADER_JSON_SOURCE_KEY] = QString(entry.second.source.c_str()); + qentry[SHADER_JSON_DATA_KEY] = QByteArray{ binary.data(), (int)binary.size() }.toBase64(); + variantMap[key.c_str()] = qentry; + } + json = QJsonDocument::fromVariant(variantMap).toJson(QJsonDocument::Indented); + } + + if (!json.isEmpty()) { + QString shaderCacheFile = getShaderCacheFile(); + QFile saveFile(shaderCacheFile); + saveFile.open(QFile::WriteOnly | QFile::Text | QFile::Truncate); + saveFile.write(json); + saveFile.close(); + } +} + +std::string gl::getShaderHash(const std::string& shaderSource) { + return QCryptographicHash::hash(QByteArray(shaderSource.c_str()), QCryptographicHash::Md5).toBase64().toStdString(); } diff --git a/libraries/gl/src/gl/GLShaders.h b/libraries/gl/src/gl/GLShaders.h index fc070d7659..e6c11b4eb3 100644 --- a/libraries/gl/src/gl/GLShaders.h +++ b/libraries/gl/src/gl/GLShaders.h @@ -14,15 +14,47 @@ #include #include +#include namespace gl { + + struct Uniform { + std::string name; + GLint size{ -1 }; + GLenum type{ GL_FLOAT }; + GLint location{ -1 }; + void load(GLuint glprogram, int index); + }; + + using Uniforms = std::vector; + + Uniforms loadUniforms(GLuint glprogram); + + struct CachedShader { + GLenum format{ 0 }; + std::string source; + std::vector binary; + inline operator bool() const { + return format != 0 && !binary.empty(); + } + }; + + using ShaderCache = std::unordered_map; + + std::string getShaderHash(const std::string& shaderSource); + void loadShaderCache(ShaderCache& cache); + void saveShaderCache(const ShaderCache& cache); + + #ifdef SEPARATE_PROGRAM - bool compileShader(GLenum shaderDomain, const std::string& shaderSource, const std::string& defines, GLuint &shaderObject, GLuint &programObject, std::string& message); + bool compileShader(GLenum shaderDomain, const std::string& shaderSource, GLuint &shaderObject, GLuint &programObject, std::string& message); + bool compileShader(GLenum shaderDomain, const std::vector& shaderSources, GLuint &shaderObject, GLuint &programObject, std::string& message); #else - bool compileShader(GLenum shaderDomain, const std::string& shaderSource, const std::string& defines, GLuint &shaderObject, std::string& message); + bool compileShader(GLenum shaderDomain, const std::string& shaderSource, GLuint &shaderObject, std::string& message); + bool compileShader(GLenum shaderDomain, const std::vector& shaderSources, GLuint &shaderObject, std::string& message); #endif - GLuint compileProgram(const std::vector& glshaders, std::string& message, std::vector& binary); + GLuint compileProgram(const std::vector& glshaders, std::string& message, CachedShader& binary); } diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl-common/src/gpu/gl/GLBackend.cpp index f484de57f1..2321342eb4 100644 --- a/libraries/gpu-gl-common/src/gpu/gl/GLBackend.cpp +++ b/libraries/gpu-gl-common/src/gpu/gl/GLBackend.cpp @@ -124,13 +124,16 @@ void GLBackend::init() { GLBackend::GLBackend() { _pipeline._cameraCorrectionBuffer._buffer->flush(); glGetIntegerv(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &_uboAlignment); + initShaderBinaryCache(); } +GLBackend::~GLBackend() {} -GLBackend::~GLBackend() { +void GLBackend::shutdown() { killInput(); killTransform(); killTextureManagementStage(); + killShaderBinaryCache(); } void GLBackend::renderPassTransfer(const Batch& batch) { diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLBackend.h b/libraries/gpu-gl-common/src/gpu/gl/GLBackend.h index 32c75d0363..622c8f1081 100644 --- a/libraries/gpu-gl-common/src/gpu/gl/GLBackend.h +++ b/libraries/gpu-gl-common/src/gpu/gl/GLBackend.h @@ -23,6 +23,7 @@ #include #include +#include #include #include @@ -71,6 +72,9 @@ public: virtual ~GLBackend(); + // Shutdown rendering and persist any required resources + void shutdown() override; + void setCameraCorrection(const Mat4& correction, const Mat4& prevRenderView, bool reset = false); void render(const Batch& batch) final override; @@ -455,6 +459,13 @@ protected: virtual GLShader* compileBackendProgram(const Shader& program, const Shader::CompilationHandler& handler); virtual GLShader* compileBackendShader(const Shader& shader, const Shader::CompilationHandler& handler); virtual std::string getBackendShaderHeader() const = 0; + // For a program, this will return a string containing all the source files (without any + // backend headers or defines). For a vertex, fragment or geometry shader, this will + // return the fully customized shader with all the version and backend specific + // preprocessor directives + // The program string returned can be used as a key for a cache of shader binaries + // The shader strings can be reliably sent to the low level `compileShader` functions + virtual std::string getShaderSource(const Shader& shader, int version) final; virtual void makeProgramBindings(ShaderObject& shaderObject); class ElementResource { public: @@ -465,12 +476,12 @@ protected: ElementResource getFormatFromGLUniform(GLenum gltype); static const GLint UNUSED_SLOT {-1}; static bool isUnusedSlot(GLint binding) { return (binding == UNUSED_SLOT); } - virtual int makeUniformSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, + virtual int makeUniformSlots(const ShaderObject& program, const Shader::BindingSet& slotBindings, Shader::SlotSet& uniforms, Shader::SlotSet& textures, Shader::SlotSet& samplers); - virtual int makeUniformBlockSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& buffers); - virtual int makeResourceBufferSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& resourceBuffers) = 0; - virtual int makeInputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& inputs); - virtual int makeOutputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& outputs); + virtual int makeUniformBlockSlots(const ShaderObject& program, const Shader::BindingSet& slotBindings, Shader::SlotSet& buffers); + virtual int makeResourceBufferSlots(const ShaderObject& program, const Shader::BindingSet& slotBindings, Shader::SlotSet& resourceBuffers) = 0; + virtual int makeInputSlots(const ShaderObject& program, const Shader::BindingSet& slotBindings, Shader::SlotSet& inputs); + virtual int makeOutputSlots(const ShaderObject& program, const Shader::BindingSet& slotBindings, Shader::SlotSet& outputs); // Synchronize the state cache of this Backend with the actual real state of the GL Context @@ -489,6 +500,19 @@ protected: void resetStages(); + // Stores cached binary versions of the shaders for quicker startup on subsequent runs + // Note that shaders in the cache can still fail to load due to hardware or driver + // changes that invalidate the cached binary, in which case we fall back on compiling + // the source again + struct ShaderBinaryCache { + std::mutex _mutex; + std::vector _formats; + std::unordered_map _binaries; + } _shaderBinaryCache; + + virtual void initShaderBinaryCache(); + virtual void killShaderBinaryCache(); + struct TextureManagementStageState { bool _sparseCapable { false }; GLTextureTransferEnginePointer _transferEngine; diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLBackendShader.cpp b/libraries/gpu-gl-common/src/gpu/gl/GLBackendShader.cpp index bf36c134de..af6a0df297 100644 --- a/libraries/gpu-gl-common/src/gpu/gl/GLBackendShader.cpp +++ b/libraries/gpu-gl-common/src/gpu/gl/GLBackendShader.cpp @@ -11,6 +11,8 @@ using namespace gpu; using namespace gpu::gl; +using CachedShader = ::gl::CachedShader; + // Shader domain static const size_t NUM_SHADER_DOMAINS = 3; @@ -68,9 +70,45 @@ static const std::array VERSION_DEFINES { { stereoVersion } }; +static std::string getShaderTypeString(Shader::Type type) { + switch (type) { + case Shader::Type::VERTEX: + return "vertex"; + case Shader::Type::PIXEL: + return "pixel"; + case Shader::Type::GEOMETRY: + return "geometry"; + case Shader::Type::PROGRAM: + return "program"; + default: + qFatal("Unexpected shader type %d", type); + Q_UNREACHABLE(); + } +} + +std::string GLBackend::getShaderSource(const Shader& shader, int version) { + if (shader.isProgram()) { + std::string result; + result.append("// VERSION " + std::to_string(version)); + for (const auto& subShader : shader.getShaders()) { + result.append("//-------- "); + result.append(getShaderTypeString(subShader->getType())); + result.append("\n"); + result.append(subShader->getSource().getCode()); + } + return result; + } + + std::string shaderDefines = getBackendShaderHeader() + "\n" + + (supportsBindless() ? textureTableVersion : "\n") + + DOMAIN_DEFINES[shader.getType()] + "\n" + + VERSION_DEFINES[version]; + + return shaderDefines + "\n" + shader.getSource().getCode(); +} + GLShader* GLBackend::compileBackendShader(const Shader& shader, const Shader::CompilationHandler& handler) { // Any GLSLprogram ? normally yes... - const std::string& shaderSource = shader.getSource().getCode(); GLenum shaderDomain = SHADER_DOMAINS[shader.getType()]; GLShader::ShaderObjects shaderObjects; Shader::CompilationLogs compilationLogs(GLShader::NumVersions); @@ -78,11 +116,7 @@ GLShader* GLBackend::compileBackendShader(const Shader& shader, const Shader::Co for (int version = 0; version < GLShader::NumVersions; version++) { auto& shaderObject = shaderObjects[version]; - - std::string shaderDefines = getBackendShaderHeader() + "\n" - + (supportsBindless() ? textureTableVersion : "\n") - + DOMAIN_DEFINES[shader.getType()] + "\n" - + VERSION_DEFINES[version]; + auto shaderSource = getShaderSource(shader, version); if (handler) { bool retest = true; std::string currentSrc = shaderSource; @@ -90,7 +124,7 @@ GLShader* GLBackend::compileBackendShader(const Shader& shader, const Shader::Co // The retest bool is set to false as soon as the compilation succeed to wexit the while loop. // The handler tells us if we should retry or not while returning a modified version of the source. while (retest) { - bool result = ::gl::compileShader(shaderDomain, currentSrc, shaderDefines, shaderObject.glshader, compilationLogs[version].message); + bool result = ::gl::compileShader(shaderDomain, currentSrc, shaderObject.glshader, compilationLogs[version].message); compilationLogs[version].compiled = result; if (!result) { std::string newSrc; @@ -101,7 +135,7 @@ GLShader* GLBackend::compileBackendShader(const Shader& shader, const Shader::Co } } } else { - compilationLogs[version].compiled = ::gl::compileShader(shaderDomain, shaderSource, shaderDefines, shaderObject.glshader, compilationLogs[version].message); + compilationLogs[version].compiled = ::gl::compileShader(shaderDomain, shaderSource, shaderObject.glshader, compilationLogs[version].message); } if (!compilationLogs[version].compiled) { @@ -120,43 +154,80 @@ GLShader* GLBackend::compileBackendShader(const Shader& shader, const Shader::Co return object; } +std::atomic gpuBinaryShadersLoaded; + GLShader* GLBackend::compileBackendProgram(const Shader& program, const Shader::CompilationHandler& handler) { if (!program.isProgram()) { return nullptr; } GLShader::ShaderObjects programObjects; - program.incrementCompilationAttempt(); Shader::CompilationLogs compilationLogs(GLShader::NumVersions); for (int version = 0; version < GLShader::NumVersions; version++) { auto& programObject = programObjects[version]; + auto programSource = getShaderSource(program, version); + auto hash = ::gl::getShaderHash(programSource); - // Let's go through every shaders and make sure they are ready to go - std::vector< GLuint > shaderGLObjects; - for (auto subShader : program.getShaders()) { - auto object = GLShader::sync((*this), *subShader, handler); - if (object) { - shaderGLObjects.push_back(object->_shaderObjects[version].glshader); - } else { - qCWarning(gpugllogging) << "GLBackend::compileBackendProgram - One of the shaders of the program is not compiled?"; - compilationLogs[version].compiled = false; - compilationLogs[version].message = std::string("Failed to compile, one of the shaders of the program is not compiled ?"); - program.setCompilationLogs(compilationLogs); - return nullptr; + CachedShader cachedBinary; + { + Lock shaderCacheLock{ _shaderBinaryCache._mutex }; + if (_shaderBinaryCache._binaries.count(hash) != 0) { + cachedBinary = _shaderBinaryCache._binaries[hash]; + } + } + + + GLuint glprogram = 0; + + // If we have a cached binary program, try to load it instead of compiling the individual shaders + if (cachedBinary) { + glprogram = ::gl::compileProgram({}, compilationLogs[version].message, cachedBinary); + if (0 != glprogram) { + ++gpuBinaryShadersLoaded; + } + } + + // If we have no program, then either no cached binary, or the binary failed to load (perhaps a GPU driver update invalidated the cache) + if (0 == glprogram) { + cachedBinary = CachedShader(); + { + std::unique_lock shaderCacheLock{ _shaderBinaryCache._mutex }; + _shaderBinaryCache._binaries.erase(hash); + } + // Let's go through every shaders and make sure they are ready to go + std::vector shaderGLObjects; + shaderGLObjects.reserve(program.getShaders().size()); + for (auto subShader : program.getShaders()) { + auto object = GLShader::sync((*this), *subShader, handler); + if (object) { + shaderGLObjects.push_back(object->_shaderObjects[version].glshader); + } else { + qCWarning(gpugllogging) << "GLBackend::compileBackendProgram - One of the shaders of the program is not compiled?"; + compilationLogs[version].compiled = false; + compilationLogs[version].message = std::string("Failed to compile, one of the shaders of the program is not compiled ?"); + program.setCompilationLogs(compilationLogs); + return nullptr; + } + } + + glprogram = ::gl::compileProgram(shaderGLObjects, compilationLogs[version].message, cachedBinary); + if (cachedBinary) { + cachedBinary.source = programSource; + std::unique_lock shaderCacheLock{ _shaderBinaryCache._mutex }; + _shaderBinaryCache._binaries[hash] = cachedBinary; } } - GLuint glprogram = ::gl::compileProgram(shaderGLObjects, compilationLogs[version].message, compilationLogs[version].binary); if (glprogram == 0) { qCWarning(gpugllogging) << "GLBackend::compileBackendProgram - Program didn't link:\n" << compilationLogs[version].message.c_str(); program.setCompilationLogs(compilationLogs); return nullptr; } + compilationLogs[version].compiled = true; programObject.glprogram = glprogram; - makeProgramBindings(programObject); } // Compilation feedback @@ -338,20 +409,15 @@ GLBackend::ElementResource GLBackend::getFormatFromGLUniform(GLenum gltype) { }; -int GLBackend::makeUniformSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, +int GLBackend::makeUniformSlots(const ShaderObject& shaderProgram, const Shader::BindingSet& slotBindings, Shader::SlotSet& uniforms, Shader::SlotSet& textures, Shader::SlotSet& samplers) { - GLint uniformsCount = 0; + auto& glprogram = shaderProgram.glprogram; - glGetProgramiv(glprogram, GL_ACTIVE_UNIFORMS, &uniformsCount); - - for (int i = 0; i < uniformsCount; i++) { - const GLint NAME_LENGTH = 256; - GLchar name[NAME_LENGTH]; - GLint length = 0; - GLint size = 0; - GLenum type = 0; - glGetActiveUniform(glprogram, i, NAME_LENGTH, &length, &size, &type, name); - GLint location = glGetUniformLocation(glprogram, name); + for (const auto& uniform : shaderProgram.uniforms) { + const auto& type = uniform.type; + const auto& location = uniform.location; + const auto& size = uniform.size; + const auto& name = uniform.name; const GLint INVALID_UNIFORM_LOCATION = -1; // Try to make sense of the gltype @@ -359,8 +425,8 @@ int GLBackend::makeUniformSlots(GLuint glprogram, const Shader::BindingSet& slot // The uniform as a standard var type if (location != INVALID_UNIFORM_LOCATION) { + auto sname = uniform.name; // Let's make sure the name doesn't contains an array element - std::string sname(name); auto foundBracket = sname.find_first_of('['); if (foundBracket != std::string::npos) { // std::string arrayname = sname.substr(0, foundBracket); @@ -397,10 +463,11 @@ int GLBackend::makeUniformSlots(GLuint glprogram, const Shader::BindingSet& slot } } - return uniformsCount; + return static_cast(shaderProgram.uniforms.size()); } -int GLBackend::makeUniformBlockSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& buffers) { +int GLBackend::makeUniformBlockSlots(const ShaderObject& shaderProgram, const Shader::BindingSet& slotBindings, Shader::SlotSet& buffers) { + const auto& glprogram = shaderProgram.glprogram; GLint buffersCount = 0; glGetProgramiv(glprogram, GL_ACTIVE_UNIFORM_BLOCKS, &buffersCount); @@ -479,7 +546,8 @@ int GLBackend::makeUniformBlockSlots(GLuint glprogram, const Shader::BindingSet& return buffersCount; } -int GLBackend::makeInputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& inputs) { +int GLBackend::makeInputSlots(const ShaderObject& shaderProgram, const Shader::BindingSet& slotBindings, Shader::SlotSet& inputs) { + const auto& glprogram = shaderProgram.glprogram; GLint inputsCount = 0; glGetProgramiv(glprogram, GL_ACTIVE_ATTRIBUTES, &inputsCount); @@ -501,7 +569,7 @@ int GLBackend::makeInputSlots(GLuint glprogram, const Shader::BindingSet& slotBi return inputsCount; } -int GLBackend::makeOutputSlots(GLuint glprogram, const Shader::BindingSet& slotBindings, Shader::SlotSet& outputs) { +int GLBackend::makeOutputSlots(const ShaderObject& shaderProgram, const Shader::BindingSet& slotBindings, Shader::SlotSet& outputs) { /* GLint outputsCount = 0; glGetProgramiv(glprogram, GL_ACTIVE_, &outputsCount); @@ -525,67 +593,19 @@ void GLBackend::makeProgramBindings(ShaderObject& shaderObject) { if (!shaderObject.glprogram) { return; } - GLuint glprogram = shaderObject.glprogram; - GLint loc = -1; - - //Check for gpu specific attribute slotBindings - loc = glGetAttribLocation(glprogram, "inPosition"); - if (loc >= 0 && loc != gpu::Stream::POSITION) { - glBindAttribLocation(glprogram, gpu::Stream::POSITION, "inPosition"); - } - - loc = glGetAttribLocation(glprogram, "inNormal"); - if (loc >= 0 && loc != gpu::Stream::NORMAL) { - glBindAttribLocation(glprogram, gpu::Stream::NORMAL, "inNormal"); - } - - loc = glGetAttribLocation(glprogram, "inColor"); - if (loc >= 0 && loc != gpu::Stream::COLOR) { - glBindAttribLocation(glprogram, gpu::Stream::COLOR, "inColor"); - } - - loc = glGetAttribLocation(glprogram, "inTexCoord0"); - if (loc >= 0 && loc != gpu::Stream::TEXCOORD) { - glBindAttribLocation(glprogram, gpu::Stream::TEXCOORD, "inTexCoord0"); - } - - loc = glGetAttribLocation(glprogram, "inTangent"); - if (loc >= 0 && loc != gpu::Stream::TANGENT) { - glBindAttribLocation(glprogram, gpu::Stream::TANGENT, "inTangent"); - } - - char attribName[] = "inTexCoordn"; - for (auto i = 0; i < 4; i++) { - auto streamId = gpu::Stream::TEXCOORD1 + i; - - attribName[strlen(attribName) - 1] = '1' + i; - loc = glGetAttribLocation(glprogram, attribName); - if (loc >= 0 && loc != streamId) { - glBindAttribLocation(glprogram, streamId, attribName); - } - } - - loc = glGetAttribLocation(glprogram, "inSkinClusterIndex"); - if (loc >= 0 && loc != gpu::Stream::SKIN_CLUSTER_INDEX) { - glBindAttribLocation(glprogram, gpu::Stream::SKIN_CLUSTER_INDEX, "inSkinClusterIndex"); - } - - loc = glGetAttribLocation(glprogram, "inSkinClusterWeight"); - if (loc >= 0 && loc != gpu::Stream::SKIN_CLUSTER_WEIGHT) { - glBindAttribLocation(glprogram, gpu::Stream::SKIN_CLUSTER_WEIGHT, "inSkinClusterWeight"); - } - - loc = glGetAttribLocation(glprogram, "_drawCallInfo"); - if (loc >= 0 && loc != gpu::Stream::DRAW_CALL_INFO) { - glBindAttribLocation(glprogram, gpu::Stream::DRAW_CALL_INFO, "_drawCallInfo"); - } - - // Link again to take into account the assigned attrib location - glLinkProgram(glprogram); - - GLint linked = 0; - glGetProgramiv(glprogram, GL_LINK_STATUS, &linked); - if (!linked) { - qCWarning(gpugllogging) << "GLShader::makeBindings - failed to link after assigning slotBindings?"; - } +} + + +void GLBackend::initShaderBinaryCache() { + GLint numBinFormats = 0; + glGetIntegerv(GL_NUM_PROGRAM_BINARY_FORMATS, &numBinFormats); + if (numBinFormats > 0) { + _shaderBinaryCache._formats.resize(numBinFormats); + glGetIntegerv(GL_PROGRAM_BINARY_FORMATS, _shaderBinaryCache._formats.data()); + } + ::gl::loadShaderCache(_shaderBinaryCache._binaries); +} + +void GLBackend::killShaderBinaryCache() { + ::gl::saveShaderCache(_shaderBinaryCache._binaries); } diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLShader.cpp b/libraries/gpu-gl-common/src/gpu/gl/GLShader.cpp index 010a7c479c..0a527185ef 100644 --- a/libraries/gpu-gl-common/src/gpu/gl/GLShader.cpp +++ b/libraries/gpu-gl-common/src/gpu/gl/GLShader.cpp @@ -68,22 +68,23 @@ bool GLShader::makeProgram(GLBackend& backend, Shader& shader, const Shader::Bin for (int version = 0; version < GLShader::NumVersions; version++) { auto& shaderObject = object->_shaderObjects[version]; if (shaderObject.glprogram) { + shaderObject.uniforms = ::gl::loadUniforms(shaderObject.glprogram); Shader::SlotSet buffers; - backend.makeUniformBlockSlots(shaderObject.glprogram, slotBindings, buffers); + backend.makeUniformBlockSlots(shaderObject, slotBindings, buffers); Shader::SlotSet uniforms; Shader::SlotSet textures; Shader::SlotSet samplers; - backend.makeUniformSlots(shaderObject.glprogram, slotBindings, uniforms, textures, samplers); + backend.makeUniformSlots(shaderObject, slotBindings, uniforms, textures, samplers); Shader::SlotSet resourceBuffers; - backend.makeResourceBufferSlots(shaderObject.glprogram, slotBindings, resourceBuffers); + backend.makeResourceBufferSlots(shaderObject, slotBindings, resourceBuffers); Shader::SlotSet inputs; - backend.makeInputSlots(shaderObject.glprogram, slotBindings, inputs); + backend.makeInputSlots(shaderObject, slotBindings, inputs); Shader::SlotSet outputs; - backend.makeOutputSlots(shaderObject.glprogram, slotBindings, outputs); + backend.makeOutputSlots(shaderObject, slotBindings, outputs); // Define the public slots only from the default version if (version == 0) { diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLShader.h b/libraries/gpu-gl-common/src/gpu/gl/GLShader.h index 3259982e93..0ba77e50c6 100644 --- a/libraries/gpu-gl-common/src/gpu/gl/GLShader.h +++ b/libraries/gpu-gl-common/src/gpu/gl/GLShader.h @@ -9,14 +9,17 @@ #define hifi_gpu_gl_GLShader_h #include "GLShared.h" +#include namespace gpu { namespace gl { struct ShaderObject { + using Uniforms = ::gl::Uniforms; GLuint glshader { 0 }; GLuint glprogram { 0 }; GLint transformCameraSlot { -1 }; GLint transformObjectSlot { -1 }; + Uniforms uniforms; }; class GLShader : public GPUObject { diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h index 23dcac0d8d..e840b9fe78 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h +++ b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h @@ -173,7 +173,7 @@ protected: std::string getBackendShaderHeader() const override; void makeProgramBindings(ShaderObject& shaderObject) override; - int makeResourceBufferSlots(GLuint glprogram, const Shader::BindingSet& slotBindings,Shader::SlotSet& resourceBuffers) override; + int makeResourceBufferSlots(const ShaderObject& shaderProgram, const Shader::BindingSet& slotBindings,Shader::SlotSet& resourceBuffers) override; }; diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendShader.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendShader.cpp index 64c9033cf7..0fa1b1bf42 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendShader.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendShader.cpp @@ -22,20 +22,13 @@ std::string GL41Backend::getBackendShaderHeader() const { return header; } -int GL41Backend::makeResourceBufferSlots(GLuint glprogram, const Shader::BindingSet& slotBindings,Shader::SlotSet& resourceBuffers) { +int GL41Backend::makeResourceBufferSlots(const ShaderObject& shaderProgram, const Shader::BindingSet& slotBindings,Shader::SlotSet& resourceBuffers) { GLint ssboCount = 0; - GLint uniformsCount = 0; - - glGetProgramiv(glprogram, GL_ACTIVE_UNIFORMS, &uniformsCount); - - for (int i = 0; i < uniformsCount; i++) { - const GLint NAME_LENGTH = 256; - GLchar name[NAME_LENGTH]; - GLint length = 0; - GLint size = 0; - GLenum type = 0; - glGetActiveUniform(glprogram, i, NAME_LENGTH, &length, &size, &type, name); - GLint location = glGetUniformLocation(glprogram, name); + const auto& glprogram = shaderProgram.glprogram; + for (const auto& uniform : shaderProgram.uniforms) { + const auto& name = uniform.name; + const auto& type = uniform.type; + const auto& location = uniform.location; const GLint INVALID_UNIFORM_LOCATION = -1; // Try to make sense of the gltype diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp index 503e1df922..a255cc5878 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp @@ -31,6 +31,10 @@ bool GL41Backend::supportedTextureFormat(const gpu::Element& format) { case gpu::Semantic::COMPRESSED_EAC_RED_SIGNED: case gpu::Semantic::COMPRESSED_EAC_XY: case gpu::Semantic::COMPRESSED_EAC_XY_SIGNED: + // The ARB_texture_compression_bptc extension is not supported on 4.1 + // See https://www.g-truc.net/doc/OpenGL%204%20Hardware%20Matrix.pdf + case gpu::Semantic::COMPRESSED_BC6_RGB: + case gpu::Semantic::COMPRESSED_BC7_SRGBA: return false; default: return true; diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h index 0db9271f57..cb7ddce930 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h @@ -274,7 +274,7 @@ protected: // Shader Stage std::string getBackendShaderHeader() const override; void makeProgramBindings(ShaderObject& shaderObject) override; - int makeResourceBufferSlots(GLuint glprogram, const Shader::BindingSet& slotBindings,Shader::SlotSet& resourceBuffers) override; + int makeResourceBufferSlots(const ShaderObject& shaderProgram, const Shader::BindingSet& slotBindings,Shader::SlotSet& resourceBuffers) override; // Texture Management Stage void initTextureManagementStage() override; diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendShader.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendShader.cpp index 44e439df55..a95426a06d 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendShader.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendShader.cpp @@ -27,7 +27,8 @@ std::string GL45Backend::getBackendShaderHeader() const { return header; } -int GL45Backend::makeResourceBufferSlots(GLuint glprogram, const Shader::BindingSet& slotBindings,Shader::SlotSet& resourceBuffers) { +int GL45Backend::makeResourceBufferSlots(const ShaderObject& shaderProgram, const Shader::BindingSet& slotBindings,Shader::SlotSet& resourceBuffers) { + const auto& glprogram = shaderProgram.glprogram; GLint buffersCount = 0; glGetProgramInterfaceiv(glprogram, GL_SHADER_STORAGE_BLOCK, GL_ACTIVE_RESOURCES, &buffersCount); diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp index 558f221705..fda7ac22dd 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp @@ -33,6 +33,8 @@ using namespace gpu::gl45; bool GL45Backend::supportedTextureFormat(const gpu::Element& format) { switch (format.getSemantic()) { + // ETC textures are actually required by the OpenGL spec as of 4.3, but aren't always supported by hardware + // They'll be recompressed by OpenGL, which will be slow or have poor quality, so disable them for now case gpu::Semantic::COMPRESSED_ETC2_RGB: case gpu::Semantic::COMPRESSED_ETC2_SRGB: case gpu::Semantic::COMPRESSED_ETC2_RGB_PUNCHTHROUGH_ALPHA: diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackend.h b/libraries/gpu-gles/src/gpu/gles/GLESBackend.h index cb8e4abb29..9656d29ac5 100644 --- a/libraries/gpu-gles/src/gpu/gles/GLESBackend.h +++ b/libraries/gpu-gles/src/gpu/gles/GLESBackend.h @@ -164,7 +164,7 @@ protected: std::string getBackendShaderHeader() const override; void makeProgramBindings(ShaderObject& shaderObject) override; - int makeResourceBufferSlots(GLuint glprogram, const Shader::BindingSet& slotBindings,Shader::SlotSet& resourceBuffers) override; + int makeResourceBufferSlots(const ShaderObject& shaderObject, const Shader::BindingSet& slotBindings,Shader::SlotSet& resourceBuffers) override; }; diff --git a/libraries/gpu-gles/src/gpu/gles/GLESBackendShader.cpp b/libraries/gpu-gles/src/gpu/gles/GLESBackendShader.cpp index 7e8056ba79..34caa97696 100644 --- a/libraries/gpu-gles/src/gpu/gles/GLESBackendShader.cpp +++ b/libraries/gpu-gles/src/gpu/gles/GLESBackendShader.cpp @@ -25,20 +25,15 @@ std::string GLESBackend::getBackendShaderHeader() const { return header; } -int GLESBackend::makeResourceBufferSlots(GLuint glprogram, const Shader::BindingSet& slotBindings,Shader::SlotSet& resourceBuffers) { +int GLESBackend::makeResourceBufferSlots(const ShaderObject& shaderObject, const Shader::BindingSet& slotBindings,Shader::SlotSet& resourceBuffers) { GLint ssboCount = 0; - GLint uniformsCount = 0; + GLint uniformsCount = 0; + const auto& glprogram = shaderObject.glprogram; - glGetProgramiv(glprogram, GL_ACTIVE_UNIFORMS, &uniformsCount); - - for (int i = 0; i < uniformsCount; i++) { - const GLint NAME_LENGTH = 256; - GLchar name[NAME_LENGTH]; - GLint length = 0; - GLint size = 0; - GLenum type = 0; - glGetActiveUniform(glprogram, i, NAME_LENGTH, &length, &size, &type, name); - GLint location = glGetUniformLocation(glprogram, name); + for (const auto& uniform : shaderObject.uniforms) { + const auto& type = uniform.type; + const auto& location = uniform.location; + const auto& name = uniform.name; const GLint INVALID_UNIFORM_LOCATION = -1; // Try to make sense of the gltype diff --git a/libraries/gpu/src/gpu/Context.cpp b/libraries/gpu/src/gpu/Context.cpp index 909ed23669..bb6b27626a 100644 --- a/libraries/gpu/src/gpu/Context.cpp +++ b/libraries/gpu/src/gpu/Context.cpp @@ -53,6 +53,13 @@ Context::~Context() { _batchPool.clear(); } +void Context::shutdown() { + if (_backend) { + _backend->shutdown(); + _backend.reset(); + } +} + const std::string& Context::getBackendVersion() const { return _backend->getVersion(); } diff --git a/libraries/gpu/src/gpu/Context.h b/libraries/gpu/src/gpu/Context.h index 4683f442e0..4560ea5526 100644 --- a/libraries/gpu/src/gpu/Context.h +++ b/libraries/gpu/src/gpu/Context.h @@ -54,6 +54,7 @@ class Backend { public: virtual ~Backend(){}; + virtual void shutdown() {} virtual const std::string& getVersion() const = 0; void setStereoState(const StereoState& stereo) { _stereo = stereo; } @@ -154,6 +155,7 @@ public: Context(); ~Context(); + void shutdown(); const std::string& getBackendVersion() const; void beginFrame(const glm::mat4& renderView = glm::mat4(), const glm::mat4& renderPose = glm::mat4()); diff --git a/libraries/gpu/src/gpu/Shader.h b/libraries/gpu/src/gpu/Shader.h index d898411006..fe92da1469 100755 --- a/libraries/gpu/src/gpu/Shader.h +++ b/libraries/gpu/src/gpu/Shader.h @@ -54,13 +54,11 @@ public: struct CompilationLog { std::string message; - std::vector binary; bool compiled{ false }; CompilationLog() {} CompilationLog(const CompilationLog& src) : message(src.message), - binary(src.binary), compiled(src.compiled) {} }; using CompilationLogs = std::vector; diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index ed9505766b..a92243f808 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -684,9 +684,9 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector< PROFILE_RANGE(render_gpu, "sphericalHarmonicsFromTexture"); +#ifndef USE_GLES auto mipFormat = cubeTexture.getStoredMipFormat(); std::function unpackFunc; - switch (mipFormat.getSemantic()) { case gpu::R11G11B10: unpackFunc = glm::unpackF2x11_1x10; @@ -698,6 +698,7 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector< assert(false); break; } +#endif const uint sqOrder = order*order; @@ -732,7 +733,11 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector< for(int face=0; face < gpu::Texture::NUM_CUBE_FACES; face++) { PROFILE_RANGE(render_gpu, "ProcessFace"); +#ifndef USE_GLES auto data = reinterpret_cast( cubeTexture.accessStoredMipFace(0, face)->readData() ); +#else + auto data = cubeTexture.accessStoredMipFace(0, face)->readData(); +#endif if (data == nullptr) { continue; } @@ -816,8 +821,15 @@ bool sphericalHarmonicsFromTexture(const gpu::Texture& cubeTexture, std::vector< glm::vec3 color{ 0.0f, 0.0f, 0.0f }; for (int i = 0; i < stride; ++i) { for (int j = 0; j < stride; ++j) { +#ifndef USE_GLES int k = (int)(x + i - halfStride + (y + j - halfStride) * width); color += unpackFunc(data[k]); +#else + const int NUM_COMPONENTS_PER_PIXEL = 4; + int k = NUM_COMPONENTS_PER_PIXEL * (int)(x + i - halfStride + (y + j - halfStride) * width); + // BGRA -> RGBA + color += glm::pow(glm::vec3(data[k + 2], data[k + 1], data[k]) / 255.0f, glm::vec3(2.2f)); +#endif } } diff --git a/libraries/gpu/src/gpu/Texture_ktx.cpp b/libraries/gpu/src/gpu/Texture_ktx.cpp index 129c125411..839cb915e2 100644 --- a/libraries/gpu/src/gpu/Texture_ktx.cpp +++ b/libraries/gpu/src/gpu/Texture_ktx.cpp @@ -352,7 +352,7 @@ ktx::KTXUniquePointer Texture::serialize(const Texture& texture) { if (!Texture::evalKTXFormat(mipFormat, texelFormat, header)) { return nullptr; } - + // Set Dimensions uint32_t numFaces = 1; switch (texture.getType()) { diff --git a/libraries/image/CMakeLists.txt b/libraries/image/CMakeLists.txt index 8073cc2b5e..6bc5c762f5 100644 --- a/libraries/image/CMakeLists.txt +++ b/libraries/image/CMakeLists.txt @@ -2,3 +2,4 @@ set(TARGET_NAME image) setup_hifi_library() link_hifi_libraries(shared gpu) target_nvtt() +target_etc2comp() diff --git a/libraries/image/src/image/Image.cpp b/libraries/image/src/image/Image.cpp index c1c07838c7..dc45257d2b 100644 --- a/libraries/image/src/image/Image.cpp +++ b/libraries/image/src/image/Image.cpp @@ -31,13 +31,24 @@ using namespace gpu; #define CPU_MIPMAPS 1 #include +#ifdef USE_GLES +#include +#include +#endif + static const glm::uvec2 SPARSE_PAGE_SIZE(128); +#ifdef Q_OS_ANDROID +static const glm::uvec2 MAX_TEXTURE_SIZE(2048); +#else static const glm::uvec2 MAX_TEXTURE_SIZE(4096); +#endif bool DEV_DECIMATE_TEXTURES = false; std::atomic DECIMATED_TEXTURE_COUNT{ 0 }; std::atomic RECTIFIED_TEXTURE_COUNT{ 0 }; -static const auto HDR_FORMAT = gpu::Element::COLOR_R11G11B10; +// we use a ref here to work around static order initialization +// possibly causing the element not to be constructed yet +static const auto& HDR_FORMAT = gpu::Element::COLOR_R11G11B10; static std::atomic compressColorTextures { false }; static std::atomic compressNormalTextures { false }; @@ -75,7 +86,13 @@ const QStringList getSupportedFormats() { return stringFormats; } + +// On GLES, we don't use HDR skyboxes +#ifndef USE_GLES QImage::Format QIMAGE_HDR_FORMAT = QImage::Format_RGB30; +#else +QImage::Format QIMAGE_HDR_FORMAT = QImage::Format_RGB32; +#endif TextureUsage::TextureLoader TextureUsage::getTextureLoaderForType(Type type, const QVariantMap& options) { switch (type) { @@ -548,13 +565,15 @@ void generateLDRMips(gpu::Texture* texture, QImage&& image, const std::atomic(localCopy.constBits()); + auto mipFormat = texture->getStoredMipFormat(); +#ifndef USE_GLES + const void* data = static_cast(localCopy.constBits()); nvtt::TextureType textureType = nvtt::TextureType_2D; nvtt::InputFormat inputFormat = nvtt::InputFormat_BGRA_8UB; nvtt::WrapMode wrapMode = nvtt::WrapMode_Mirror; @@ -584,8 +603,6 @@ void generateLDRMips(gpu::Texture* texture, QImage&& image, const std::atomicgetStoredMipFormat(); if (mipFormat == gpu::Element::COLOR_COMPRESSED_BCX_SRGB) { compressionOptions.setFormat(nvtt::Format_BC1); } else if (mipFormat == gpu::Element::COLOR_COMPRESSED_BCX_SRGBA_MASK) { @@ -669,21 +686,94 @@ void generateLDRMips(gpu::Texture* texture, QImage&& image, const std::atomic floatData; + floatData.resize(width * height); + for (int y = 0; y < height; y++) { + QRgb *line = (QRgb *) localCopy.scanLine(y); + for (int x = 0; x < width; x++) { + QRgb &pixel = line[x]; + floatData[x + y * width] = vec4(qRed(pixel), qGreen(pixel), qBlue(pixel), qAlpha(pixel)) / MAX_COLOR; + } + } + + // free up the memory afterward to avoid bloating the heap + localCopy = QImage(); // QImage doesn't have a clear function, so override it with an empty one. + + Etc::EncodeMipmaps( + (float *)floatData.data(), width, height, + etcFormat, errorMetric, effort, + numEncodeThreads, numEncodeThreads, + numMips, Etc::FILTER_WRAP_NONE, + mipMaps, &encodingTime + ); + + for (int i = 0; i < numMips; i++) { + if (mipMaps[i].paucEncodingBits.get()) { + if (face >= 0) { + texture->assignStoredMipFace(i, face, mipMaps[i].uiEncodingBitsBytes, static_cast(mipMaps[i].paucEncodingBits.get())); + } else { + texture->assignStoredMip(i, mipMaps[i].uiEncodingBitsBytes, static_cast(mipMaps[i].paucEncodingBits.get())); + } + } + } + + delete[] mipMaps; +#endif } #endif - - void generateMips(gpu::Texture* texture, QImage&& image, const std::atomic& abortProcessing = false, int face = -1) { #if CPU_MIPMAPS PROFILE_RANGE(resource_parse, "generateMips"); +#ifndef USE_GLES if (image.format() == QIMAGE_HDR_FORMAT) { generateHDRMips(texture, std::move(image), abortProcessing, face); } else { generateLDRMips(texture, std::move(image), abortProcessing, face); } +#else + generateLDRMips(texture, std::move(image), abortProcessing, face); +#endif #else texture->setAutoGenerateMips(true); #endif @@ -738,7 +828,6 @@ gpu::TexturePointer TextureUsage::process2DTextureColorFromImage(QImage&& srcIma gpu::Element formatMip; gpu::Element formatGPU; if (isColorTexturesCompressionEnabled()) { -#ifndef USE_GLES if (validAlpha) { // NOTE: This disables BC1a compression because it was producing odd artifacts on text textures // for the tutorial. Instead we use BC3 (which is larger) but doesn't produce the same artifacts). @@ -746,22 +835,16 @@ gpu::TexturePointer TextureUsage::process2DTextureColorFromImage(QImage&& srcIma } else { formatGPU = gpu::Element::COLOR_COMPRESSED_BCX_SRGB; } -#else - if (validAlpha) { - formatGPU = gpu::Element::COLOR_COMPRESSED_ETC2_SRGBA; - } else { - formatGPU = gpu::Element::COLOR_COMPRESSED_ETC2_SRGB; - } -#endif formatMip = formatGPU; } else { #ifdef USE_GLES // GLES does not support GL_BGRA - formatMip = gpu::Element::COLOR_SRGBA_32; + formatGPU = gpu::Element::COLOR_COMPRESSED_ETC2_SRGBA; + formatMip = formatGPU; #else + formatGPU = gpu::Element::COLOR_SRGBA_32; formatMip = gpu::Element::COLOR_SBGRA_32; #endif - formatGPU = gpu::Element::COLOR_SRGBA_32; } if (isStrict) { @@ -876,16 +959,18 @@ gpu::TexturePointer TextureUsage::process2DTextureNormalMapFromImage(QImage&& sr gpu::TexturePointer theTexture = nullptr; if ((image.width() > 0) && (image.height() > 0)) { - gpu::Element formatMip = gpu::Element::VEC2NU8_XY; - gpu::Element formatGPU = gpu::Element::VEC2NU8_XY; + gpu::Element formatMip; + gpu::Element formatGPU; if (isNormalTexturesCompressionEnabled()) { -#ifndef USE_GLES formatGPU = gpu::Element::COLOR_COMPRESSED_BCX_XY; -#else + } else { +#ifdef USE_GLES formatGPU = gpu::Element::COLOR_COMPRESSED_EAC_XY; +#else + formatGPU = gpu::Element::VEC2NU8_XY; #endif - formatMip = formatGPU; } + formatMip = formatGPU; theTexture = gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR)); theTexture->setSource(srcImageName); @@ -917,16 +1002,15 @@ gpu::TexturePointer TextureUsage::process2DTextureGrayscaleFromImage(QImage&& sr gpu::Element formatMip; gpu::Element formatGPU; if (isGrayscaleTexturesCompressionEnabled()) { -#ifndef USE_GLES formatGPU = gpu::Element::COLOR_COMPRESSED_BCX_RED; -#else - formatGPU = gpu::Element::COLOR_COMPRESSED_EAC_RED; -#endif - formatMip = formatGPU; } else { - formatMip = gpu::Element::COLOR_R_8; +#ifdef USE_GLES + formatGPU = gpu::Element::COLOR_COMPRESSED_EAC_RED; +#else formatGPU = gpu::Element::COLOR_R_8; +#endif } + formatMip = formatGPU; theTexture = gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR)); theTexture->setSource(srcImageName); @@ -1283,19 +1367,25 @@ gpu::TexturePointer TextureUsage::processCubeTextureColorFromImage(QImage&& srcI QImage image = processSourceImage(std::move(localCopy), true); if (image.format() != QIMAGE_HDR_FORMAT) { +#ifndef USE_GLES image = convertToHDRFormat(std::move(image), HDR_FORMAT); +#else + image = image.convertToFormat(QImage::Format_RGB32); +#endif } gpu::Element formatMip; gpu::Element formatGPU; if (isCubeTexturesCompressionEnabled()) { - // TODO: gles: pick HDR ETC format - formatMip = gpu::Element::COLOR_COMPRESSED_BCX_HDR_RGB; formatGPU = gpu::Element::COLOR_COMPRESSED_BCX_HDR_RGB; } else { - formatMip = HDR_FORMAT; +#ifdef USE_GLES + formatGPU = gpu::Element::COLOR_COMPRESSED_ETC2_SRGB; +#else formatGPU = HDR_FORMAT; +#endif } + formatMip = formatGPU; // Find the layout of the cubemap in the 2D image // Use the original image size since processSourceImage may have altered the size / aspect ratio @@ -1342,9 +1432,16 @@ gpu::TexturePointer TextureUsage::processCubeTextureColorFromImage(QImage&& srcI // Generate irradiance while we are at it if (generateIrradiance) { PROFILE_RANGE(resource_parse, "generateIrradiance"); - auto irradianceTexture = gpu::Texture::createCube(HDR_FORMAT, faces[0].width(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP)); + gpu::Element irradianceFormat; + // TODO: we could locally compress the irradiance texture on Android, but we don't need to +#ifndef USE_GLES + irradianceFormat = HDR_FORMAT; +#else + irradianceFormat = gpu::Element::COLOR_SRGBA_32; +#endif + auto irradianceTexture = gpu::Texture::createCube(irradianceFormat, faces[0].width(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP)); irradianceTexture->setSource(srcImageName); - irradianceTexture->setStoredMipFormat(HDR_FORMAT); + irradianceTexture->setStoredMipFormat(irradianceFormat); for (uint8 face = 0; face < faces.size(); ++face) { irradianceTexture->assignStoredMipFace(0, face, faces[face].byteCount(), faces[face].constBits()); } diff --git a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp index 8ecf527a14..650c9675a7 100755 --- a/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/KeyboardMouseDevice.cpp @@ -279,7 +279,7 @@ controller::Input KeyboardMouseDevice::InputDevice::makeInput(KeyboardMouseDevic * moved down. The data value is how far the average position of all touch points moved. * * - * @typedef Controller.Hardware-Keyboard + * @typedef {object} Controller.Hardware-Keyboard * @todo Currently, the mouse wheel in an ordinary mouse generates left/right wheel events instead of up/down. */ controller::Input::NamedVector KeyboardMouseDevice::InputDevice::getAvailableInputs() const { diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenVirtualPadDevice.cpp b/libraries/input-plugins/src/input-plugins/TouchscreenVirtualPadDevice.cpp index 8d63b82911..32194e1b84 100644 --- a/libraries/input-plugins/src/input-plugins/TouchscreenVirtualPadDevice.cpp +++ b/libraries/input-plugins/src/input-plugins/TouchscreenVirtualPadDevice.cpp @@ -350,6 +350,8 @@ void TouchscreenVirtualPadDevice::touchUpdateEvent(const QTouchEvent* event) { if (idxMoveStartingPointCandidate != -1) { _moveCurrentTouchId = tPoints[idxMoveStartingPointCandidate].id(); _unusedTouches.erase(_moveCurrentTouchId); + thisPoint.x = tPoints[idxMoveStartingPointCandidate].pos().x(); + thisPoint.y = tPoints[idxMoveStartingPointCandidate].pos().y(); moveTouchBegin(thisPoint); } else { moveTouchEnd(); @@ -359,6 +361,8 @@ void TouchscreenVirtualPadDevice::touchUpdateEvent(const QTouchEvent* event) { if (idxViewStartingPointCandidate != -1) { _viewCurrentTouchId = tPoints[idxViewStartingPointCandidate].id(); _unusedTouches.erase(_viewCurrentTouchId); + thisPoint.x = tPoints[idxViewStartingPointCandidate].pos().x(); + thisPoint.y = tPoints[idxViewStartingPointCandidate].pos().y(); viewTouchBegin(thisPoint); } else { viewTouchEnd(); @@ -368,6 +372,8 @@ void TouchscreenVirtualPadDevice::touchUpdateEvent(const QTouchEvent* event) { if (idxJumpStartingPointCandidate != -1) { _jumpCurrentTouchId = tPoints[idxJumpStartingPointCandidate].id(); _unusedTouches.erase(_jumpCurrentTouchId); + thisPoint.x = tPoints[idxJumpStartingPointCandidate].pos().x(); + thisPoint.y = tPoints[idxJumpStartingPointCandidate].pos().y(); jumpTouchBegin(thisPoint); } else { if (_jumpHasValidTouch) { @@ -424,6 +430,7 @@ void TouchscreenVirtualPadDevice::moveTouchBegin(glm::vec2 touchPoint) { } else { _moveRefTouchPoint = touchPoint; } + _moveCurrentTouchPoint = touchPoint; _moveHasValidTouch = true; } } diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h index 7e911bc9bf..ee13d6666c 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.h +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -179,7 +179,7 @@ public: * @function ModelCache.prefetch * @param {string} url - URL of the resource to prefetch. * @param {object} [extra=null] - * @returns {Resource} + * @returns {ResourceObject} */ /**jsdoc @@ -188,7 +188,7 @@ public: * @param {string} url - URL of the resource to load. * @param {string} [fallback=""] - Fallback URL if load of the desired URL fails. * @param {} [extra=null] - * @returns {Resource} + * @returns {object} */ diff --git a/libraries/model-networking/src/model-networking/TextureCache.h b/libraries/model-networking/src/model-networking/TextureCache.h index 898f0e3d3a..bca64806c4 100644 --- a/libraries/model-networking/src/model-networking/TextureCache.h +++ b/libraries/model-networking/src/model-networking/TextureCache.h @@ -195,7 +195,7 @@ public: * @function TextureCache.prefetch * @param {string} url - URL of the resource to prefetch. * @param {object} [extra=null] - * @returns {Resource} + * @returns {ResourceObject} */ /**jsdoc @@ -204,7 +204,7 @@ public: * @param {string} url - URL of the resource to load. * @param {string} [fallback=""] - Fallback URL if load of the desired URL fails. * @param {} [extra=null] - * @returns {Resource} + * @returns {object} */ @@ -261,7 +261,7 @@ protected: * @param {string} url * @param {number} type * @param {number} [maxNumPixels=67108864] - * @returns {Resource} + * @returns {ResourceObject} */ // Overload ResourceCache::prefetch to allow specifying texture type for loads Q_INVOKABLE ScriptableResource* prefetch(const QUrl& url, int type, int maxNumPixels = ABSOLUTE_MAX_TEXTURE_NUM_PIXELS); diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index e70e3e26d0..05f0ec12b5 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -743,6 +744,9 @@ void AccountManager::generateNewKeypair(bool isUserKeypair, const QUuid& domainI return; } + // Ensure openssl/Qt config is set up. + QSslConfiguration::defaultConfiguration(); + // make sure we don't already have an outbound keypair generation request if (!_isWaitingForKeypairResponse) { _isWaitingForKeypairResponse = true; @@ -751,94 +755,75 @@ void AccountManager::generateNewKeypair(bool isUserKeypair, const QUuid& domainI qCDebug(networking) << "Clearing current private key in DataServerAccountInfo"; _accountInfo.setPrivateKey(QByteArray()); - // setup a new QThread to generate the keypair on, in case it takes a while - QThread* generateThread = new QThread(this); - generateThread->setObjectName("Account Manager Generator Thread"); - - // setup a keypair generator + // Create a runnable keypair generated to create an RSA pair and exit. RSAKeypairGenerator* keypairGenerator = new RSAKeypairGenerator; if (!isUserKeypair) { - keypairGenerator->setDomainID(domainID); _accountInfo.setDomainID(domainID); } - // start keypair generation when the thread starts - connect(generateThread, &QThread::started, keypairGenerator, &RSAKeypairGenerator::generateKeypair); - // handle success or failure of keypair generation - connect(keypairGenerator, &RSAKeypairGenerator::generatedKeypair, this, &AccountManager::processGeneratedKeypair); - connect(keypairGenerator, &RSAKeypairGenerator::errorGeneratingKeypair, - this, &AccountManager::handleKeypairGenerationError); - - connect(keypairGenerator, &QObject::destroyed, generateThread, &QThread::quit); - connect(generateThread, &QThread::finished, generateThread, &QThread::deleteLater); - - keypairGenerator->moveToThread(generateThread); + connect(keypairGenerator, &RSAKeypairGenerator::generatedKeypair, this, + &AccountManager::processGeneratedKeypair); + connect(keypairGenerator, &RSAKeypairGenerator::errorGeneratingKeypair, this, + &AccountManager::handleKeypairGenerationError); qCDebug(networking) << "Starting worker thread to generate 2048-bit RSA keypair."; - generateThread->start(); + // Start on Qt's global thread pool. + QThreadPool::globalInstance()->start(keypairGenerator); } } -void AccountManager::processGeneratedKeypair() { +void AccountManager::processGeneratedKeypair(QByteArray publicKey, QByteArray privateKey) { qCDebug(networking) << "Generated 2048-bit RSA keypair. Uploading public key now."; - RSAKeypairGenerator* keypairGenerator = qobject_cast(sender()); + // hold the private key to later set our metaverse API account info if upload succeeds + _pendingPrivateKey = privateKey; - if (keypairGenerator) { - // hold the private key to later set our metaverse API account info if upload succeeds - _pendingPrivateKey = keypairGenerator->getPrivateKey(); + // upload the public key so data-web has an up-to-date key + const QString USER_PUBLIC_KEY_UPDATE_PATH = "api/v1/user/public_key"; + const QString DOMAIN_PUBLIC_KEY_UPDATE_PATH = "api/v1/domains/%1/public_key"; - // upload the public key so data-web has an up-to-date key - const QString USER_PUBLIC_KEY_UPDATE_PATH = "api/v1/user/public_key"; - const QString DOMAIN_PUBLIC_KEY_UPDATE_PATH = "api/v1/domains/%1/public_key"; - - QString uploadPath; - const auto& domainID = keypairGenerator->getDomainID(); - if (domainID.isNull()) { - uploadPath = USER_PUBLIC_KEY_UPDATE_PATH; - } else { - uploadPath = DOMAIN_PUBLIC_KEY_UPDATE_PATH.arg(uuidStringWithoutCurlyBraces(domainID)); - } - - // setup a multipart upload to send up the public key - QHttpMultiPart* requestMultiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); - - QHttpPart publicKeyPart; - publicKeyPart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream")); - - publicKeyPart.setHeader(QNetworkRequest::ContentDispositionHeader, - QVariant("form-data; name=\"public_key\"; filename=\"public_key\"")); - publicKeyPart.setBody(keypairGenerator->getPublicKey()); - requestMultiPart->append(publicKeyPart); - - if (!domainID.isNull()) { - const auto& key = getTemporaryDomainKey(domainID); - QHttpPart apiKeyPart; - publicKeyPart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream")); - apiKeyPart.setHeader(QNetworkRequest::ContentDispositionHeader, - QVariant("form-data; name=\"api_key\"")); - apiKeyPart.setBody(key.toUtf8()); - requestMultiPart->append(apiKeyPart); - } - - // setup callback parameters so we know once the keypair upload has succeeded or failed - JSONCallbackParameters callbackParameters; - callbackParameters.jsonCallbackReceiver = this; - callbackParameters.jsonCallbackMethod = "publicKeyUploadSucceeded"; - callbackParameters.errorCallbackReceiver = this; - callbackParameters.errorCallbackMethod = "publicKeyUploadFailed"; - - sendRequest(uploadPath, AccountManagerAuth::Optional, QNetworkAccessManager::PutOperation, - callbackParameters, QByteArray(), requestMultiPart); - - keypairGenerator->deleteLater(); + QString uploadPath; + const auto& domainID = _accountInfo.getDomainID(); + if (domainID.isNull()) { + uploadPath = USER_PUBLIC_KEY_UPDATE_PATH; } else { - qCWarning(networking) << "Expected processGeneratedKeypair to be called by a live RSAKeypairGenerator" - << "but the casted sender is NULL. Will not process generated keypair."; + uploadPath = DOMAIN_PUBLIC_KEY_UPDATE_PATH.arg(uuidStringWithoutCurlyBraces(domainID)); } + + // setup a multipart upload to send up the public key + QHttpMultiPart* requestMultiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + + QHttpPart publicKeyPart; + publicKeyPart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream")); + + publicKeyPart.setHeader(QNetworkRequest::ContentDispositionHeader, + QVariant("form-data; name=\"public_key\"; filename=\"public_key\"")); + publicKeyPart.setBody(publicKey); + requestMultiPart->append(publicKeyPart); + + // Currently broken? We don't have the temporary domain key. + if (!domainID.isNull()) { + const auto& key = getTemporaryDomainKey(domainID); + QHttpPart apiKeyPart; + publicKeyPart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream")); + apiKeyPart.setHeader(QNetworkRequest::ContentDispositionHeader, + QVariant("form-data; name=\"api_key\"")); + apiKeyPart.setBody(key.toUtf8()); + requestMultiPart->append(apiKeyPart); + } + + // setup callback parameters so we know once the keypair upload has succeeded or failed + JSONCallbackParameters callbackParameters; + callbackParameters.jsonCallbackReceiver = this; + callbackParameters.jsonCallbackMethod = "publicKeyUploadSucceeded"; + callbackParameters.errorCallbackReceiver = this; + callbackParameters.errorCallbackMethod = "publicKeyUploadFailed"; + + sendRequest(uploadPath, AccountManagerAuth::Optional, QNetworkAccessManager::PutOperation, + callbackParameters, QByteArray(), requestMultiPart); } void AccountManager::publicKeyUploadSucceeded(QNetworkReply& reply) { @@ -877,6 +862,4 @@ void AccountManager::handleKeypairGenerationError() { // reset our waiting state for keypair response _isWaitingForKeypairResponse = false; - - sender()->deleteLater(); } diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 88ebaf5656..9068966512 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -128,7 +128,7 @@ signals: private slots: void processReply(); void handleKeypairGenerationError(); - void processGeneratedKeypair(); + void processGeneratedKeypair(QByteArray publicKey, QByteArray privateKey); void publicKeyUploadSucceeded(QNetworkReply& reply); void publicKeyUploadFailed(QNetworkReply& reply); void generateNewKeypair(bool isUserKeypair = true, const QUuid& domainID = QUuid()); diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 977cabb57a..317be194b8 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -157,7 +157,11 @@ void AddressManager::storeCurrentAddress() { // be loaded over http(s) // url.scheme() == URL_SCHEME_HTTP || // url.scheme() == URL_SCHEME_HTTPS || - currentAddressHandle.set(url); + if (isConnected()) { + currentAddressHandle.set(url); + } else { + qCWarning(networking) << "Ignoring attempt to save current address because not connected to domain:" << url; + } } else { qCWarning(networking) << "Ignoring attempt to save current address with an invalid url:" << url; } diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index 7832b26c96..38eb7ee670 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -138,7 +138,7 @@ public: * * * - * @typedef location.LookupTrigger + * @typedef {number} location.LookupTrigger */ enum LookupTrigger { UserInput, @@ -184,7 +184,7 @@ public slots: /**jsdoc * Go to a specified metaverse address. * @function location.handleLookupString - * @param {string} address - The address to go to: a "hifi:/" address, an IP address (e.g., + * @param {string} address - The address to go to: a "hifi://" address, an IP address (e.g., * "127.0.0.1" or "localhost"), a domain name, a named path on a domain (starts with * "/"), a position or position and orientation, or a user (starts with "@"). * @param {boolean} fromSuggestions=false - Set to true if the address is obtained from the "Goto" dialog. diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index 08908dbaf6..4d98391104 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -137,7 +137,7 @@ public: * * * - * @typedef Window.ConnectionRefusedReason + * @typedef {number} Window.ConnectionRefusedReason */ enum class ConnectionRefusedReason : uint8_t { Unknown, diff --git a/libraries/networking/src/RSAKeypairGenerator.cpp b/libraries/networking/src/RSAKeypairGenerator.cpp index 8ca8b81ea3..e83615e3df 100644 --- a/libraries/networking/src/RSAKeypairGenerator.cpp +++ b/libraries/networking/src/RSAKeypairGenerator.cpp @@ -25,7 +25,10 @@ RSAKeypairGenerator::RSAKeypairGenerator(QObject* parent) : QObject(parent) { - +} + +void RSAKeypairGenerator::run() { + generateKeypair(); } void RSAKeypairGenerator::generateKeypair() { @@ -92,5 +95,5 @@ void RSAKeypairGenerator::generateKeypair() { OPENSSL_free(publicKeyDER); OPENSSL_free(privateKeyDER); - emit generatedKeypair(); + emit generatedKeypair(_publicKey, _privateKey); } diff --git a/libraries/networking/src/RSAKeypairGenerator.h b/libraries/networking/src/RSAKeypairGenerator.h index 36f4a9550b..552f12395b 100644 --- a/libraries/networking/src/RSAKeypairGenerator.h +++ b/libraries/networking/src/RSAKeypairGenerator.h @@ -13,25 +13,20 @@ #define hifi_RSAKeypairGenerator_h #include +#include #include -class RSAKeypairGenerator : public QObject { +class RSAKeypairGenerator : public QObject, public QRunnable { Q_OBJECT public: - RSAKeypairGenerator(QObject* parent = 0); + RSAKeypairGenerator(QObject* parent = nullptr); - void setDomainID(const QUuid& domainID) { _domainID = domainID; } - const QUuid& getDomainID() const { return _domainID; } - - const QByteArray& getPublicKey() const { return _publicKey; } - const QByteArray& getPrivateKey() const { return _privateKey; } - -public slots: + virtual void run() override; void generateKeypair(); signals: void errorGeneratingKeypair(); - void generatedKeypair(); + void generatedKeypair(QByteArray publicKey, QByteArray privateKey); private: QUuid _domainID; diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index 28266d0a7f..0b1334daba 100644 --- a/libraries/networking/src/ResourceCache.cpp +++ b/libraries/networking/src/ResourceCache.cpp @@ -218,8 +218,8 @@ ScriptableResource* ResourceCache::prefetch(const QUrl& url, void* extra) { } ResourceCache::ResourceCache(QObject* parent) : QObject(parent) { - auto nodeList = DependencyManager::get(); - if (nodeList) { + if (DependencyManager::isSet()) { + auto nodeList = DependencyManager::get(); auto& domainHandler = nodeList->getDomainHandler(); connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, &ResourceCache::clearATPAssets, Qt::DirectConnection); @@ -636,7 +636,10 @@ void Resource::attemptRequest() { << "- retrying asset load - attempt" << _attempts << " of " << MAX_ATTEMPTS; } - ResourceCache::attemptRequest(_self); + auto self = _self.lock(); + if (self) { + ResourceCache::attemptRequest(self); + } } void Resource::finishedLoading(bool success) { diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h index 18840cd11e..a4bd352563 100644 --- a/libraries/networking/src/ResourceCache.h +++ b/libraries/networking/src/ResourceCache.h @@ -87,7 +87,7 @@ private: class ScriptableResource : public QObject { /**jsdoc - * @constructor Resource + * @class ResourceObject * * @hifi-interface * @hifi-client-entity @@ -97,11 +97,6 @@ class ScriptableResource : public QObject { * @property {string} url - URL of this resource. * @property {Resource.State} state - Current loading state. */ - /**jsdoc - * @namespace Resource - * @variation 0 - * @property {Resource.State} State - */ Q_OBJECT Q_PROPERTY(QUrl url READ getURL) Q_PROPERTY(int state READ getState NOTIFY stateChanged) @@ -109,8 +104,7 @@ class ScriptableResource : public QObject { public: /**jsdoc - * @name Resource.State - * @static + * @typedef {object} Resource.State * @property {number} QUEUED - The resource is queued up, waiting to be loaded. * @property {number} LOADING - The resource is downloading. * @property {number} LOADED - The resource has finished downloaded by is not complete. @@ -131,7 +125,7 @@ public: /**jsdoc * Release this resource. - * @function Resource#release + * @function ResourceObject#release */ Q_INVOKABLE void release(); @@ -146,7 +140,7 @@ signals: /**jsdoc * Triggered when download progress for this resource has changed. - * @function Resource#progressChanged + * @function ResourceObject#progressChanged * @param {number} bytesReceived - Byytes downloaded so far. * @param {number} bytesTotal - Total number of bytes in the resource. * @returns {Signal} @@ -155,7 +149,7 @@ signals: /**jsdoc * Triggered when resource loading state has changed. - * @function Resource#stateChanged + * @function ResourceObject#stateChanged * @param {Resource.State} state - New state. * @returns {Signal} */ @@ -262,7 +256,7 @@ protected slots: * @function ResourceCache.prefetch * @param {string} url - URL of the resource to prefetch. * @param {object} [extra=null] - * @returns {Resource} + * @returns {ResourceObject} */ // Prefetches a resource to be held by the QScriptEngine. // Left as a protected member so subclasses can overload prefetch @@ -275,8 +269,9 @@ protected slots: * @param {string} url - URL of the resource to load. * @param {string} [fallback=""] - Fallback URL if load of the desired URL fails. * @param {} [extra=null] - * @returns {Resource} + * @returns {object} */ + // FIXME: The return type is not recognized by JavaScript. /// Loads a resource from the specified URL and returns it. /// If the caller is on a different thread than the ResourceCache, /// returns an empty smart pointer and loads its asynchronously. diff --git a/libraries/networking/src/ResourceManager.cpp b/libraries/networking/src/ResourceManager.cpp index d06b74b724..6df15129a2 100644 --- a/libraries/networking/src/ResourceManager.cpp +++ b/libraries/networking/src/ResourceManager.cpp @@ -38,6 +38,11 @@ ResourceManager::ResourceManager(bool atpSupportEnabled) : _atpSupportEnabled(at _thread.start(); } +ResourceManager::~ResourceManager() { + _thread.terminate(); + _thread.wait(); +} + void ResourceManager::setUrlPrefixOverride(const QString& prefix, const QString& replacement) { QMutexLocker locker(&_prefixMapLock); if (replacement.isEmpty()) { diff --git a/libraries/networking/src/ResourceManager.h b/libraries/networking/src/ResourceManager.h index 9fc636f5fe..a79222d2d8 100644 --- a/libraries/networking/src/ResourceManager.h +++ b/libraries/networking/src/ResourceManager.h @@ -28,6 +28,7 @@ class ResourceManager: public QObject, public Dependency { public: ResourceManager(bool atpSupportEnabled = true); + ~ResourceManager(); void setUrlPrefixOverride(const QString& prefix, const QString& replacement); QString normalizeURL(const QString& urlString); diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 02853543ba..c342ecffc5 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -29,17 +29,18 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::DomainList: return static_cast(DomainListVersion::GetMachineFingerprintFromUUIDSupport); case PacketType::EntityAdd: + case PacketType::EntityClone: case PacketType::EntityEdit: case PacketType::EntityData: case PacketType::EntityPhysics: - return static_cast(EntityVersion::MaterialData); + return static_cast(EntityVersion::CollisionMask16Bytes); case PacketType::EntityQuery: return static_cast(EntityQueryPacketVersion::ConicalFrustums); case PacketType::AvatarIdentity: case PacketType::AvatarData: case PacketType::BulkAvatarData: case PacketType::KillAvatar: - return static_cast(AvatarMixerPacketVersion::FBXReaderNodeReparenting); + return static_cast(AvatarMixerPacketVersion::ProceduralFaceMovementFlagsAndBlendshapes); case PacketType::MessagesData: return static_cast(MessageDataVersion::TextOrBinaryData); // ICE packets diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 7d374f3625..64054fd080 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -131,6 +131,8 @@ public: OctreeDataFileReply, OctreeDataPersist, + EntityClone, + NUM_PACKET_TYPE }; @@ -232,7 +234,9 @@ enum class EntityVersion : PacketVersion { SoftEntities, MaterialEntities, ShadowControl, - MaterialData + MaterialData, + CloneableData, + CollisionMask16Bytes }; enum class EntityScriptCallMethodVersion : PacketVersion { @@ -278,7 +282,9 @@ enum class AvatarMixerPacketVersion : PacketVersion { AvatarIdentityLookAtSnapping, UpdatedMannequinDefaultAvatar, AvatarJointDefaultPoseFlags, - FBXReaderNodeReparenting + FBXReaderNodeReparenting, + FixMannequinDefaultAvatarFeet, + ProceduralFaceMovementFlagsAndBlendshapes }; enum class DomainConnectRequestVersion : PacketVersion { diff --git a/libraries/octree/src/OctreeDataUtils.cpp b/libraries/octree/src/OctreeDataUtils.cpp index b57ab8db31..44a56fe97a 100644 --- a/libraries/octree/src/OctreeDataUtils.cpp +++ b/libraries/octree/src/OctreeDataUtils.cpp @@ -40,9 +40,10 @@ bool readOctreeFile(QString path, QJsonDocument* doc) { } bool OctreeUtils::RawOctreeData::readOctreeDataInfoFromJSON(QJsonObject root) { - if (root.contains("Id") && root.contains("DataVersion")) { + if (root.contains("Id") && root.contains("DataVersion") && root.contains("Version")) { id = root["Id"].toVariant().toUuid(); - version = root["DataVersion"].toInt(); + dataVersion = root["DataVersion"].toInt(); + version = root["Version"].toInt(); } readSubclassData(root); return true; @@ -76,11 +77,10 @@ bool OctreeUtils::RawOctreeData::readOctreeDataInfoFromFile(QString path) { } QByteArray OctreeUtils::RawOctreeData::toByteArray() { - const auto protocolVersion = (int)versionForPacketType((PacketTypeEnum::Value)dataPacketType()); QJsonObject obj { - { "DataVersion", QJsonValue((qint64)version) }, + { "DataVersion", QJsonValue((qint64)dataVersion) }, { "Id", QJsonValue(id.toString()) }, - { "Version", protocolVersion }, + { "Version", QJsonValue((qint64)version) }, }; writeSubclassData(obj); @@ -111,8 +111,8 @@ PacketType OctreeUtils::RawOctreeData::dataPacketType() const { void OctreeUtils::RawOctreeData::resetIdAndVersion() { id = QUuid::createUuid(); - version = OctreeUtils::INITIAL_VERSION; - qDebug() << "Reset octree data to: " << id << version; + dataVersion = OctreeUtils::INITIAL_VERSION; + qDebug() << "Reset octree data to: " << id << dataVersion; } void OctreeUtils::RawEntityData::readSubclassData(const QJsonObject& root) { diff --git a/libraries/octree/src/OctreeDataUtils.h b/libraries/octree/src/OctreeDataUtils.h index 485599096d..9060e7b460 100644 --- a/libraries/octree/src/OctreeDataUtils.h +++ b/libraries/octree/src/OctreeDataUtils.h @@ -28,6 +28,7 @@ constexpr Version INITIAL_VERSION = 0; class RawOctreeData { public: QUuid id { QUuid() }; + Version dataVersion { -1 }; Version version { -1 }; virtual PacketType dataPacketType() const; diff --git a/libraries/octree/src/OctreePersistThread.cpp b/libraries/octree/src/OctreePersistThread.cpp index e6afccab47..3dc051675d 100644 --- a/libraries/octree/src/OctreePersistThread.cpp +++ b/libraries/octree/src/OctreePersistThread.cpp @@ -179,8 +179,8 @@ bool OctreePersistThread::process() { OctreeUtils::RawOctreeData data; if (data.readOctreeDataInfoFromFile(_filename)) { - qDebug() << "Setting entity version info to: " << data.id << data.version; - _tree->setOctreeVersionInfo(data.id, data.version); + qDebug() << "Setting entity version info to: " << data.id << data.dataVersion; + _tree->setOctreeVersionInfo(data.id, data.dataVersion); } bool persistentFileRead; diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp index 9b6e9fe7a0..64eda975cf 100755 --- a/libraries/physics/src/CharacterController.cpp +++ b/libraries/physics/src/CharacterController.cpp @@ -109,7 +109,7 @@ void CharacterController::setDynamicsWorld(btDynamicsWorld* world) { } _dynamicsWorld = nullptr; } - int16_t collisionGroup = computeCollisionGroup(); + int32_t collisionGroup = computeCollisionGroup(); if (_rigidBody) { updateMassProperties(); } @@ -325,7 +325,7 @@ void CharacterController::playerStep(btCollisionWorld* collisionWorld, btScalar _ghost.setWorldTransform(_rigidBody->getWorldTransform()); } -void CharacterController::jump() { +void CharacterController::jump(const btVector3& dir) { _pendingFlags |= PENDING_FLAG_JUMP; } @@ -352,7 +352,7 @@ static const char* stateToStr(CharacterController::State state) { #endif // #ifdef DEBUG_STATE_CHANGE void CharacterController::updateCurrentGravity() { - int16_t collisionGroup = computeCollisionGroup(); + int32_t collisionGroup = computeCollisionGroup(); if (_state == State::Hover || collisionGroup == BULLET_COLLISION_GROUP_COLLISIONLESS) { _currentGravity = 0.0f; } else { @@ -433,7 +433,7 @@ void CharacterController::setCollisionless(bool collisionless) { } } -int16_t CharacterController::computeCollisionGroup() const { +int32_t CharacterController::computeCollisionGroup() const { if (_collisionless) { return _collisionlessAllowed ? BULLET_COLLISION_GROUP_COLLISIONLESS : BULLET_COLLISION_GROUP_MY_AVATAR; } else { @@ -446,7 +446,7 @@ void CharacterController::handleChangedCollisionGroup() { // ATM the easiest way to update collision groups is to remove/re-add the RigidBody if (_dynamicsWorld) { _dynamicsWorld->removeRigidBody(_rigidBody); - int16_t collisionGroup = computeCollisionGroup(); + int32_t collisionGroup = computeCollisionGroup(); _dynamicsWorld->addRigidBody(_rigidBody, collisionGroup, BULLET_COLLISION_MASK_MY_AVATAR); } _pendingFlags &= ~PENDING_FLAG_UPDATE_COLLISION_GROUP; @@ -538,7 +538,7 @@ void CharacterController::applyMotor(int index, btScalar dt, btVector3& worldVel btScalar angle = motor.rotation.getAngle(); btVector3 velocity = worldVelocity.rotate(axis, -angle); - int16_t collisionGroup = computeCollisionGroup(); + int32_t collisionGroup = computeCollisionGroup(); if (collisionGroup == BULLET_COLLISION_GROUP_COLLISIONLESS || _state == State::Hover || motor.hTimescale == motor.vTimescale) { // modify velocity @@ -679,7 +679,7 @@ void CharacterController::updateState() { btVector3 rayStart = _position; btScalar rayLength = _radius; - int16_t collisionGroup = computeCollisionGroup(); + int32_t collisionGroup = computeCollisionGroup(); if (collisionGroup == BULLET_COLLISION_GROUP_MY_AVATAR) { rayLength += _scaleFactor * DEFAULT_AVATAR_FALL_HEIGHT; } else { diff --git a/libraries/physics/src/CharacterController.h b/libraries/physics/src/CharacterController.h index 96e479dcad..50db2bea12 100644 --- a/libraries/physics/src/CharacterController.h +++ b/libraries/physics/src/CharacterController.h @@ -70,7 +70,7 @@ public: virtual void preStep(btCollisionWorld *collisionWorld) override; virtual void playerStep(btCollisionWorld *collisionWorld, btScalar dt) override; virtual bool canJump() const override { assert(false); return false; } // never call this - virtual void jump() override; + virtual void jump(const btVector3& dir = btVector3(0.0f, 0.0f, 0.0f)) override; virtual bool onGround() const override; void clearMotors(); @@ -120,7 +120,7 @@ public: bool isStuck() const { return _isStuck; } void setCollisionless(bool collisionless); - int16_t computeCollisionGroup() const; + int32_t computeCollisionGroup() const; void handleChangedCollisionGroup(); bool getRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation); diff --git a/libraries/physics/src/CharacterGhostObject.cpp b/libraries/physics/src/CharacterGhostObject.cpp index 331485dd01..a771a52384 100755 --- a/libraries/physics/src/CharacterGhostObject.cpp +++ b/libraries/physics/src/CharacterGhostObject.cpp @@ -29,13 +29,13 @@ CharacterGhostObject::~CharacterGhostObject() { } } -void CharacterGhostObject::setCollisionGroupAndMask(int16_t group, int16_t mask) { +void CharacterGhostObject::setCollisionGroupAndMask(int32_t group, int32_t mask) { _collisionFilterGroup = group; _collisionFilterMask = mask; // TODO: if this probe is in the world reset ghostObject overlap cache } -void CharacterGhostObject::getCollisionGroupAndMask(int16_t& group, int16_t& mask) const { +void CharacterGhostObject::getCollisionGroupAndMask(int32_t& group, int32_t& mask) const { group = _collisionFilterGroup; mask = _collisionFilterMask; } diff --git a/libraries/physics/src/CharacterGhostObject.h b/libraries/physics/src/CharacterGhostObject.h index 1e4625c6f6..44ab5c938a 100755 --- a/libraries/physics/src/CharacterGhostObject.h +++ b/libraries/physics/src/CharacterGhostObject.h @@ -28,8 +28,8 @@ public: CharacterGhostObject() { } ~CharacterGhostObject(); - void setCollisionGroupAndMask(int16_t group, int16_t mask); - void getCollisionGroupAndMask(int16_t& group, int16_t& mask) const; + void setCollisionGroupAndMask(int32_t group, int32_t mask); + void getCollisionGroupAndMask(int32_t& group, int32_t& mask) const; void setRadiusAndHalfHeight(btScalar radius, btScalar halfHeight); void setUpDirection(const btVector3& up); @@ -54,8 +54,8 @@ protected: btScalar _radius { 0.0f }; btConvexHullShape* _characterShape { nullptr }; // input, shape of character CharacterGhostShape* _ghostShape { nullptr }; // internal, shape whose Aabb is used for overlap cache - int16_t _collisionFilterGroup { 0 }; - int16_t _collisionFilterMask { 0 }; + int32_t _collisionFilterGroup { 0 }; + int32_t _collisionFilterMask { 0 }; bool _inWorld { false }; // internal, was added to world }; diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 7a0ead3e0d..594ea476f6 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -252,11 +252,9 @@ void EntityMotionState::getWorldTransform(btTransform& worldTrans) const { uint32_t thisStep = ObjectMotionState::getWorldSimulationStep(); float dt = (thisStep - _lastKinematicStep) * PHYSICS_ENGINE_FIXED_SUBSTEP; + _lastKinematicStep = thisStep; _entity->stepKinematicMotion(dt); - // bypass const-ness so we can remember the step - const_cast(this)->_lastKinematicStep = thisStep; - // and set the acceleration-matches-gravity count high so that if we send an update // it will use the correct acceleration for remote simulations _accelerationNearlyGravityCount = (uint8_t)(-1); @@ -448,17 +446,13 @@ bool EntityMotionState::shouldSendUpdate(uint32_t simulationStep) { // this case is prevented by setting _ownershipState to UNOWNABLE in EntityMotionState::ctor assert(!(_entity->getClientOnly() && _entity->getOwningAvatarID() != Physics::getSessionUUID())); - // shouldSendUpdate() sould NOT be triggering updates to maintain the queryAACube of dynamic entities. - // The server is supposed to predict the transform of such moving things. The client performs a "double prediction" - // where it predicts what the the server is doing, and only sends updates whent the entity's true transform - // differs significantly. That is what the remoteSimulationOutOfSync() logic is all about. - if (_entity->dynamicDataNeedsTransmit() || - (!_entity->getDynamic() && _entity->queryAACubeNeedsUpdate())) { + if (_entity->dynamicDataNeedsTransmit()) { return true; } - if (_entity->shouldSuppressLocationEdits()) { - return false; + // "shouldSuppressLocationEdits" really means: "the entity has a 'Hold' action therefore + // we don't need send an update unless the entity is not contained by its queryAACube" + return _entity->queryAACubeNeedsUpdate(); } return remoteSimulationOutOfSync(simulationStep); @@ -779,7 +773,7 @@ QString EntityMotionState::getName() const { } // virtual -void EntityMotionState::computeCollisionGroupAndMask(int16_t& group, int16_t& mask) const { +void EntityMotionState::computeCollisionGroupAndMask(int32_t& group, int32_t& mask) const { _entity->computeCollisionGroupAndFinalMask(group, mask); } diff --git a/libraries/physics/src/EntityMotionState.h b/libraries/physics/src/EntityMotionState.h index 376cc5afe2..603130b5ff 100644 --- a/libraries/physics/src/EntityMotionState.h +++ b/libraries/physics/src/EntityMotionState.h @@ -84,7 +84,7 @@ public: virtual QString getName() const override; - virtual void computeCollisionGroupAndMask(int16_t& group, int16_t& mask) const override; + virtual void computeCollisionGroupAndMask(int32_t& group, int32_t& mask) const override; bool shouldSendBid(); bool isLocallyOwned() const override; diff --git a/libraries/physics/src/ObjectDynamic.cpp b/libraries/physics/src/ObjectDynamic.cpp index 3341025a8f..28323a8b92 100644 --- a/libraries/physics/src/ObjectDynamic.cpp +++ b/libraries/physics/src/ObjectDynamic.cpp @@ -159,6 +159,13 @@ void ObjectDynamic::removeFromSimulation(EntitySimulationPointer simulation) con simulation->removeDynamic(myID); } +void ObjectDynamic::setOwnerEntity(const EntityItemPointer ownerEntity) { + if (!ownerEntity) { + activateBody(); + } + _ownerEntity = ownerEntity; +} + EntityItemPointer ObjectDynamic::getEntityByID(EntityItemID entityID) const { EntityItemPointer ownerEntity; withReadLock([&]{ diff --git a/libraries/physics/src/ObjectDynamic.h b/libraries/physics/src/ObjectDynamic.h index 7fdf2e323a..bfee79aca9 100644 --- a/libraries/physics/src/ObjectDynamic.h +++ b/libraries/physics/src/ObjectDynamic.h @@ -33,7 +33,7 @@ public: virtual void removeFromSimulation(EntitySimulationPointer simulation) const override; virtual EntityItemWeakPointer getOwnerEntity() const override { return _ownerEntity; } - virtual void setOwnerEntity(const EntityItemPointer ownerEntity) override { _ownerEntity = ownerEntity; } + virtual void setOwnerEntity(const EntityItemPointer ownerEntity) override; virtual void invalidate() {}; diff --git a/libraries/physics/src/ObjectMotionState.h b/libraries/physics/src/ObjectMotionState.h index e1cf5a4285..0ca28d6b09 100644 --- a/libraries/physics/src/ObjectMotionState.h +++ b/libraries/physics/src/ObjectMotionState.h @@ -111,7 +111,7 @@ public: virtual PhysicsMotionType getMotionType() const { return _motionType; } void setMass(float mass); - virtual float getMass() const; + float getMass() const; void setBodyLinearVelocity(const glm::vec3& velocity) const; void setBodyAngularVelocity(const glm::vec3& velocity) const; @@ -154,7 +154,7 @@ public: virtual QString getName() const { return ""; } - virtual void computeCollisionGroupAndMask(int16_t& group, int16_t& mask) const = 0; + virtual void computeCollisionGroupAndMask(int32_t& group, int32_t& mask) const = 0; bool isActive() const { return _body ? _body->isActive() : false; } @@ -184,7 +184,7 @@ protected: btRigidBody* _body { nullptr }; float _density { 1.0f }; - uint32_t _lastKinematicStep; + mutable uint32_t _lastKinematicStep; bool _hasInternalKinematicChanges { false }; }; diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index 83ffa21a55..ee87cab8e8 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -71,8 +71,8 @@ void PhysicsEngine::init() { } } -uint32_t PhysicsEngine::getNumSubsteps() { - return _numSubsteps; +uint32_t PhysicsEngine::getNumSubsteps() const { + return _dynamicsWorld->getNumSubsteps(); } // private @@ -105,6 +105,10 @@ void PhysicsEngine::addObjectToDynamicsWorld(ObjectMotionState* motionState) { } case MOTION_TYPE_DYNAMIC: { mass = motionState->getMass(); + if (mass != mass || mass < 1.0f) { + qCDebug(physics) << "mass is too low, setting to 1.0 Kg --" << mass; + mass = 1.0f; + } btCollisionShape* shape = const_cast(motionState->getShape()); assert(shape); shape->calculateLocalInertia(mass, inertia); @@ -148,7 +152,7 @@ void PhysicsEngine::addObjectToDynamicsWorld(ObjectMotionState* motionState) { body->setFlags(BT_DISABLE_WORLD_GRAVITY); motionState->updateBodyMaterialProperties(); - int16_t group, mask; + int32_t group, mask; motionState->computeCollisionGroupAndMask(group, mask); _dynamicsWorld->addRigidBody(body, group, mask); @@ -329,13 +333,9 @@ void PhysicsEngine::stepSimulation() { PHYSICS_ENGINE_FIXED_SUBSTEP, onSubStep); if (numSubsteps > 0) { BT_PROFILE("postSimulation"); - _numSubsteps += (uint32_t)numSubsteps; - ObjectMotionState::setWorldSimulationStep(_numSubsteps); - if (_myAvatarController) { _myAvatarController->postSimulation(); } - _hasOutgoingChanges = true; } diff --git a/libraries/physics/src/PhysicsEngine.h b/libraries/physics/src/PhysicsEngine.h index 0dfe3a7a7c..2ac195956a 100644 --- a/libraries/physics/src/PhysicsEngine.h +++ b/libraries/physics/src/PhysicsEngine.h @@ -52,7 +52,7 @@ public: ~PhysicsEngine(); void init(); - uint32_t getNumSubsteps(); + uint32_t getNumSubsteps() const; void removeObjects(const VectorOfMotionStates& objects); void removeSetOfObjects(const SetOfMotionStates& objects); // only called during teardown @@ -135,7 +135,6 @@ private: CharacterController* _myAvatarController; uint32_t _numContactFrames = 0; - uint32_t _numSubsteps; bool _dumpNextStats { false }; bool _saveNextStats { false }; diff --git a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp index 3f24851dce..07d5ceb9ac 100644 --- a/libraries/physics/src/ThreadSafeDynamicsWorld.cpp +++ b/libraries/physics/src/ThreadSafeDynamicsWorld.cpp @@ -59,14 +59,11 @@ int ThreadSafeDynamicsWorld::stepSimulationWithSubstepCallback(btScalar timeStep } } - /*//process some debugging flags - if (getDebugDrawer()) { - btIDebugDraw* debugDrawer = getDebugDrawer(); - gDisableDeactivation = (debugDrawer->getDebugMode() & btIDebugDraw::DBG_NoDeactivation) != 0; - }*/ if (subSteps) { //clamp the number of substeps, to prevent simulation grinding spiralling down to a halt int clampedSimulationSteps = (subSteps > maxSubSteps)? maxSubSteps : subSteps; + _numSubsteps += clampedSimulationSteps; + ObjectMotionState::setWorldSimulationStep(_numSubsteps); saveKinematicState(fixedTimeStep*clampedSimulationSteps); @@ -98,28 +95,24 @@ int ThreadSafeDynamicsWorld::stepSimulationWithSubstepCallback(btScalar timeStep // call this instead of non-virtual btDiscreteDynamicsWorld::synchronizeSingleMotionState() void ThreadSafeDynamicsWorld::synchronizeMotionState(btRigidBody* body) { btAssert(body); - if (body->getMotionState() && !body->isStaticObject()) { - //we need to call the update at least once, even for sleeping objects - //otherwise the 'graphics' transform never updates properly - ///@todo: add 'dirty' flag - //if (body->getActivationState() != ISLAND_SLEEPING) - { - if (body->isKinematicObject()) { - ObjectMotionState* objectMotionState = static_cast(body->getMotionState()); - if (objectMotionState->hasInternalKinematicChanges()) { - objectMotionState->clearInternalKinematicChanges(); - body->getMotionState()->setWorldTransform(body->getWorldTransform()); - } - return; - } - btTransform interpolatedTransform; - btTransformUtil::integrateTransform(body->getInterpolationWorldTransform(), - body->getInterpolationLinearVelocity(),body->getInterpolationAngularVelocity(), - (m_latencyMotionStateInterpolation && m_fixedTimeStep) ? m_localTime - m_fixedTimeStep : m_localTime*body->getHitFraction(), - interpolatedTransform); - body->getMotionState()->setWorldTransform(interpolatedTransform); + btAssert(body->getMotionState()); + + if (body->isKinematicObject()) { + ObjectMotionState* objectMotionState = static_cast(body->getMotionState()); + if (objectMotionState->hasInternalKinematicChanges()) { + // this is a special case where the kinematic motion has been updated by an Action + // so we supply the body's current transform to the MotionState + objectMotionState->clearInternalKinematicChanges(); + body->getMotionState()->setWorldTransform(body->getWorldTransform()); } + return; } + btTransform interpolatedTransform; + btTransformUtil::integrateTransform(body->getInterpolationWorldTransform(), + body->getInterpolationLinearVelocity(),body->getInterpolationAngularVelocity(), + (m_latencyMotionStateInterpolation && m_fixedTimeStep) ? m_localTime - m_fixedTimeStep : m_localTime*body->getHitFraction(), + interpolatedTransform); + body->getMotionState()->setWorldTransform(interpolatedTransform); } void ThreadSafeDynamicsWorld::synchronizeMotionStates() { @@ -164,24 +157,12 @@ void ThreadSafeDynamicsWorld::synchronizeMotionStates() { } void ThreadSafeDynamicsWorld::saveKinematicState(btScalar timeStep) { -///would like to iterate over m_nonStaticRigidBodies, but unfortunately old API allows -///to switch status _after_ adding kinematic objects to the world -///fix it for Bullet 3.x release DETAILED_PROFILE_RANGE(simulation_physics, "saveKinematicState"); BT_PROFILE("saveKinematicState"); - for (int i=0;igetActivationState() != ISLAND_SLEEPING) - { - if (body->isKinematicObject()) - { - //to calculate velocities next frame - body->saveKinematicState(timeStep); - } + for (int i=0;iisKinematicObject() && body->getActivationState() != ISLAND_SLEEPING) { + body->saveKinematicState(timeStep); } } } - - diff --git a/libraries/physics/src/ThreadSafeDynamicsWorld.h b/libraries/physics/src/ThreadSafeDynamicsWorld.h index 54c3ddb756..d8cee4d2de 100644 --- a/libraries/physics/src/ThreadSafeDynamicsWorld.h +++ b/libraries/physics/src/ThreadSafeDynamicsWorld.h @@ -37,6 +37,7 @@ public: btConstraintSolver* constraintSolver, btCollisionConfiguration* collisionConfiguration); + int getNumSubsteps() const { return _numSubsteps; } int stepSimulationWithSubstepCallback(btScalar timeStep, int maxSubSteps = 1, btScalar fixedTimeStep = btScalar(1.)/btScalar(60.), SubStepCallback onSubStep = []() { }); @@ -61,6 +62,7 @@ private: VectorOfMotionStates _deactivatedStates; SetOfMotionStates _activeStates; SetOfMotionStates _lastActiveStates; + int _numSubsteps { 0 }; }; #endif // hifi_ThreadSafeDynamicsWorld_h diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.h b/libraries/render-utils/src/AmbientOcclusionEffect.h index 3643e608ed..b3a93ab1de 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.h +++ b/libraries/render-utils/src/AmbientOcclusionEffect.h @@ -181,7 +181,7 @@ class DebugAmbientOcclusionConfig : public render::Job::Config { Q_PROPERTY(bool showCursorPixel MEMBER showCursorPixel NOTIFY dirty) Q_PROPERTY(glm::vec2 debugCursorTexcoord MEMBER debugCursorTexcoord NOTIFY dirty) public: - DebugAmbientOcclusionConfig() : render::Job::Config(true) {} + DebugAmbientOcclusionConfig() : render::Job::Config(false) {} bool showCursorPixel{ false }; glm::vec2 debugCursorTexcoord{ 0.5f, 0.5f }; diff --git a/libraries/render-utils/src/CauterizedModel.cpp b/libraries/render-utils/src/CauterizedModel.cpp index 1596f7ba83..b03cc071d3 100644 --- a/libraries/render-utils/src/CauterizedModel.cpp +++ b/libraries/render-utils/src/CauterizedModel.cpp @@ -87,7 +87,8 @@ void CauterizedModel::createVisibleRenderItemSet() { for (int partIndex = 0; partIndex < numParts; partIndex++) { auto ptr = std::make_shared(shared_from_this(), i, partIndex, shapeID, transform, offset); _modelMeshRenderItems << std::static_pointer_cast(ptr); - _modelMeshMaterialNames.push_back(getGeometry()->getShapeMaterial(shapeID)->getName()); + auto material = getGeometry()->getShapeMaterial(shapeID); + _modelMeshMaterialNames.push_back(material ? material->getName() : ""); _modelMeshRenderItemShapes.emplace_back(ShapeInfo{ (int)i }); shapeID++; } @@ -215,10 +216,7 @@ void CauterizedModel::updateRenderItems() { modelTransform.setRotation(self->getRotation()); bool isWireframe = self->isWireframe(); - bool isVisible = self->isVisible(); - bool canCastShadow = self->canCastShadow(); - bool isLayeredInFront = self->isLayeredInFront(); - bool isLayeredInHUD = self->isLayeredInHUD(); + auto renderItemKeyGlobalFlags = self->getRenderItemKeyGlobalFlags(); bool enableCauterization = self->getEnableCauterization(); render::Transaction transaction; @@ -234,7 +232,7 @@ void CauterizedModel::updateRenderItems() { bool useDualQuaternionSkinning = self->getUseDualQuaternionSkinning(); transaction.updateItem(itemID, [modelTransform, meshState, useDualQuaternionSkinning, cauterizedMeshState, invalidatePayloadShapeKey, - isWireframe, isVisible, isLayeredInFront, isLayeredInHUD, canCastShadow, enableCauterization](CauterizedMeshPartPayload& data) { + isWireframe, renderItemKeyGlobalFlags, enableCauterization](CauterizedMeshPartPayload& data) { if (useDualQuaternionSkinning) { data.updateClusterBuffer(meshState.clusterDualQuaternions, cauterizedMeshState.clusterDualQuaternions); @@ -276,8 +274,7 @@ void CauterizedModel::updateRenderItems() { data.updateTransformForCauterizedMesh(renderTransform); data.setEnableCauterization(enableCauterization); - data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, canCastShadow, render::ItemKey::TAG_BITS_ALL); - data.setLayer(isLayeredInFront, isLayeredInHUD); + data.updateKey(renderItemKeyGlobalFlags); data.setShapeKey(invalidatePayloadShapeKey, isWireframe, useDualQuaternionSkinning); }); } diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 0d8561ad21..f8f49921a3 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -122,6 +122,9 @@ static const gpu::Element TEXCOORD4_ELEMENT { gpu::VEC4, gpu::FLOAT, gpu::XYZW } static gpu::Stream::FormatPointer SOLID_STREAM_FORMAT; static gpu::Stream::FormatPointer INSTANCED_SOLID_STREAM_FORMAT; static gpu::Stream::FormatPointer INSTANCED_SOLID_FADE_STREAM_FORMAT; +static gpu::Stream::FormatPointer WIRE_STREAM_FORMAT; +static gpu::Stream::FormatPointer INSTANCED_WIRE_STREAM_FORMAT; +static gpu::Stream::FormatPointer INSTANCED_WIRE_FADE_STREAM_FORMAT; static const uint SHAPE_VERTEX_STRIDE = sizeof(GeometryCache::ShapeVertex); // position, normal, texcoords, tangent static const uint SHAPE_NORMALS_OFFSET = offsetof(GeometryCache::ShapeVertex, normal); @@ -690,6 +693,38 @@ gpu::Stream::FormatPointer& getInstancedSolidFadeStreamFormat() { return INSTANCED_SOLID_FADE_STREAM_FORMAT; } +gpu::Stream::FormatPointer& getWireStreamFormat() { + if (!WIRE_STREAM_FORMAT) { + WIRE_STREAM_FORMAT = std::make_shared(); // 1 for everyone + WIRE_STREAM_FORMAT->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, POSITION_ELEMENT); + WIRE_STREAM_FORMAT->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, NORMAL_ELEMENT); + } + return WIRE_STREAM_FORMAT; +} + +gpu::Stream::FormatPointer& getInstancedWireStreamFormat() { + if (!INSTANCED_WIRE_STREAM_FORMAT) { + INSTANCED_WIRE_STREAM_FORMAT = std::make_shared(); // 1 for everyone + INSTANCED_WIRE_STREAM_FORMAT->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, POSITION_ELEMENT); + INSTANCED_WIRE_STREAM_FORMAT->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, NORMAL_ELEMENT); + INSTANCED_WIRE_STREAM_FORMAT->setAttribute(gpu::Stream::COLOR, gpu::Stream::COLOR, COLOR_ELEMENT, 0, gpu::Stream::PER_INSTANCE); + } + return INSTANCED_WIRE_STREAM_FORMAT; +} + +gpu::Stream::FormatPointer& getInstancedWireFadeStreamFormat() { + if (!INSTANCED_WIRE_FADE_STREAM_FORMAT) { + INSTANCED_WIRE_FADE_STREAM_FORMAT = std::make_shared(); // 1 for everyone + INSTANCED_WIRE_FADE_STREAM_FORMAT->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, POSITION_ELEMENT); + INSTANCED_WIRE_FADE_STREAM_FORMAT->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, NORMAL_ELEMENT); + INSTANCED_WIRE_FADE_STREAM_FORMAT->setAttribute(gpu::Stream::COLOR, gpu::Stream::COLOR, COLOR_ELEMENT, 0, gpu::Stream::PER_INSTANCE); + INSTANCED_WIRE_FADE_STREAM_FORMAT->setAttribute(gpu::Stream::TEXCOORD2, gpu::Stream::TEXCOORD2, TEXCOORD4_ELEMENT, 0, gpu::Stream::PER_INSTANCE); + INSTANCED_WIRE_FADE_STREAM_FORMAT->setAttribute(gpu::Stream::TEXCOORD3, gpu::Stream::TEXCOORD3, TEXCOORD4_ELEMENT, 0, gpu::Stream::PER_INSTANCE); + INSTANCED_WIRE_FADE_STREAM_FORMAT->setAttribute(gpu::Stream::TEXCOORD4, gpu::Stream::TEXCOORD4, TEXCOORD4_ELEMENT, 0, gpu::Stream::PER_INSTANCE); + } + return INSTANCED_WIRE_FADE_STREAM_FORMAT; +} + QHash GeometryCache::_simplePrograms; gpu::ShaderPointer GeometryCache::_simpleShader; @@ -827,7 +862,7 @@ void GeometryCache::renderShape(gpu::Batch& batch, Shape shape) { } void GeometryCache::renderWireShape(gpu::Batch& batch, Shape shape) { - batch.setInputFormat(getSolidStreamFormat()); + batch.setInputFormat(getWireStreamFormat()); _shapes[shape].drawWire(batch); } @@ -839,7 +874,7 @@ void GeometryCache::renderShape(gpu::Batch& batch, Shape shape, const glm::vec4& } void GeometryCache::renderWireShape(gpu::Batch& batch, Shape shape, const glm::vec4& color) { - batch.setInputFormat(getSolidStreamFormat()); + batch.setInputFormat(getWireStreamFormat()); // Color must be set after input format batch._glColor4f(color.r, color.g, color.b, color.a); _shapes[shape].drawWire(batch); @@ -857,7 +892,7 @@ void GeometryCache::renderShapeInstances(gpu::Batch& batch, Shape shape, size_t } void GeometryCache::renderWireShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer) { - batch.setInputFormat(getInstancedSolidStreamFormat()); + batch.setInputFormat(getInstancedWireStreamFormat()); setupBatchInstance(batch, colorBuffer); _shapes[shape].drawWireInstances(batch, count); } @@ -883,7 +918,7 @@ void GeometryCache::renderFadeShapeInstances(gpu::Batch& batch, Shape shape, siz void GeometryCache::renderWireFadeShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer, gpu::BufferPointer& fadeBuffer1, gpu::BufferPointer& fadeBuffer2, gpu::BufferPointer& fadeBuffer3) { - batch.setInputFormat(getInstancedSolidFadeStreamFormat()); + batch.setInputFormat(getInstancedWireFadeStreamFormat()); setupBatchFadeInstance(batch, colorBuffer, fadeBuffer1, fadeBuffer2, fadeBuffer3); _shapes[shape].drawWireInstances(batch, count); } diff --git a/libraries/render-utils/src/LightClusters.h b/libraries/render-utils/src/LightClusters.h index f495dabebb..fa054c304a 100644 --- a/libraries/render-utils/src/LightClusters.h +++ b/libraries/render-utils/src/LightClusters.h @@ -195,7 +195,7 @@ class DebugLightClustersConfig : public render::Job::Config { Q_PROPERTY(bool doDrawClusterFromDepth MEMBER doDrawClusterFromDepth NOTIFY dirty) Q_PROPERTY(bool doDrawContent MEMBER doDrawContent NOTIFY dirty) public: - DebugLightClustersConfig() : render::Job::Config(true){} + DebugLightClustersConfig() : render::Job::Config(false){} bool doDrawGrid{ false }; diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 72a4b7e0a4..7cf8bc8297 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -79,28 +79,10 @@ void MeshPartPayload::removeMaterial(graphics::MaterialPointer material) { _drawMaterials.remove(material); } -void MeshPartPayload::updateKey(bool isVisible, bool isLayered, bool canCastShadow, uint8_t tagBits, bool isGroupCulled) { - ItemKey::Builder builder; +void MeshPartPayload::updateKey(const render::ItemKey& key) { + ItemKey::Builder builder(key); builder.withTypeShape(); - if (!isVisible) { - builder.withInvisible(); - } - - builder.withTagBits(tagBits); - - if (isLayered) { - builder.withLayered(); - } - - if (canCastShadow) { - builder.withShadowCaster(); - } - - if (isGroupCulled) { - builder.withSubMetaCulled(); - } - if (topMaterialExists()) { auto matKey = _drawMaterials.top().material->getKey(); if (matKey.isTranslucent()) { @@ -200,12 +182,6 @@ template <> const Item::Bound payloadGetBound(const ModelMeshPartPayload::Pointe } return Item::Bound(); } -template <> int payloadGetLayer(const ModelMeshPartPayload::Pointer& payload) { - if (payload) { - return payload->getLayer(); - } - return 0; -} template <> const ShapeKey shapeGetShapeKey(const ModelMeshPartPayload::Pointer& payload) { if (payload) { @@ -332,28 +308,10 @@ void ModelMeshPartPayload::updateTransformForSkinnedMesh(const Transform& render } // Note that this method is called for models but not for shapes -void ModelMeshPartPayload::updateKey(bool isVisible, bool isLayered, bool canCastShadow, uint8_t tagBits, bool isGroupCulled) { - ItemKey::Builder builder; +void ModelMeshPartPayload::updateKey(const render::ItemKey& key) { + ItemKey::Builder builder(key); builder.withTypeShape(); - if (!isVisible) { - builder.withInvisible(); - } - - builder.withTagBits(tagBits); - - if (isLayered) { - builder.withLayered(); - } - - if (canCastShadow) { - builder.withShadowCaster(); - } - - if (isGroupCulled) { - builder.withSubMetaCulled(); - } - if (_isBlendShaped || _isSkinned) { builder.withDeformed(); } @@ -368,20 +326,6 @@ void ModelMeshPartPayload::updateKey(bool isVisible, bool isLayered, bool canCas _itemKey = builder.build(); } -void ModelMeshPartPayload::setLayer(bool isLayeredInFront, bool isLayeredInHUD) { - if (isLayeredInFront) { - _layer = Item::LAYER_3D_FRONT; - } else if (isLayeredInHUD) { - _layer = Item::LAYER_3D_HUD; - } else { - _layer = Item::LAYER_3D; - } -} - -int ModelMeshPartPayload::getLayer() const { - return _layer; -} - void ModelMeshPartPayload::setShapeKey(bool invalidateShapeKey, bool isWireframe, bool useDualQuaternionSkinning) { if (invalidateShapeKey) { _shapeKey = ShapeKey::Builder::invalid(); diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h index 08ad7a8311..5c7177e890 100644 --- a/libraries/render-utils/src/MeshPartPayload.h +++ b/libraries/render-utils/src/MeshPartPayload.h @@ -32,7 +32,7 @@ public: typedef render::Payload Payload; typedef Payload::DataPointer Pointer; - virtual void updateKey(bool isVisible, bool isLayered, bool canCastShadow, uint8_t tagBits, bool isGroupCulled = false); + virtual void updateKey(const render::ItemKey& key); virtual void updateMeshPart(const std::shared_ptr& drawMesh, int partIndex); @@ -95,7 +95,7 @@ public: void notifyLocationChanged() override; - void updateKey(bool isVisible, bool isLayered, bool canCastShadow, uint8_t tagBits, bool isGroupCulled = false) override; + void updateKey(const render::ItemKey& key) override; // matrix palette skinning void updateClusterBuffer(const std::vector& clusterMatrices); @@ -105,11 +105,9 @@ public: void updateTransformForSkinnedMesh(const Transform& renderTransform, const Transform& boundTransform); // Render Item interface - int getLayer() const; render::ShapeKey getShapeKey() const override; // shape interface void render(RenderArgs* args) override; - void setLayer(bool isLayeredInFront, bool isLayeredInHUD); void setShapeKey(bool invalidateShapeKey, bool isWireframe, bool useDualQuaternionSkinning); // ModelMeshPartPayload functions to perform render @@ -139,13 +137,11 @@ private: gpu::BufferPointer _blendedVertexBuffer; render::ShapeKey _shapeKey { render::ShapeKey::Builder::invalid() }; - int _layer { render::Item::LAYER_3D }; }; namespace render { template <> const ItemKey payloadGetKey(const ModelMeshPartPayload::Pointer& payload); template <> const Item::Bound payloadGetBound(const ModelMeshPartPayload::Pointer& payload); - template <> int payloadGetLayer(const ModelMeshPartPayload::Pointer& payload); template <> const ShapeKey shapeGetShapeKey(const ModelMeshPartPayload::Pointer& payload); template <> void payloadRender(const ModelMeshPartPayload::Pointer& payload, RenderArgs* args); } diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 65b12ac0d4..9bf6c31784 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -103,11 +103,10 @@ Model::Model(QObject* parent, SpatiallyNestable* spatiallyNestableOverride) : _snapModelToRegistrationPoint(false), _snappedToRegistrationPoint(false), _url(HTTP_INVALID_COM), - _isVisible(true), - _canCastShadow(false), _blendNumber(0), _appliedBlendNumber(0), - _isWireframe(false) + _isWireframe(false), + _renderItemKeyGlobalFlags(render::ItemKey::Builder().withVisible().withTagBits(render::hifi::TAG_ALL_VIEWS).build()) { // we may have been created in the network thread, but we live in the main thread if (_viewState) { @@ -268,12 +267,7 @@ void Model::updateRenderItems() { modelTransform.setScale(glm::vec3(1.0f)); bool isWireframe = self->isWireframe(); - bool isVisible = self->isVisible(); - bool canCastShadow = self->canCastShadow(); - uint8_t viewTagBits = self->getViewTagBits(); - bool isLayeredInFront = self->isLayeredInFront(); - bool isLayeredInHUD = self->isLayeredInHUD(); - bool isGroupCulled = self->isGroupCulled(); + auto renderItemKeyGlobalFlags = self->getRenderItemKeyGlobalFlags(); render::Transaction transaction; for (int i = 0; i < (int) self->_modelMeshRenderItemIDs.size(); i++) { @@ -287,9 +281,7 @@ void Model::updateRenderItems() { bool useDualQuaternionSkinning = self->getUseDualQuaternionSkinning(); transaction.updateItem(itemID, [modelTransform, meshState, useDualQuaternionSkinning, - invalidatePayloadShapeKey, isWireframe, isVisible, - canCastShadow, viewTagBits, isLayeredInFront, - isLayeredInHUD, isGroupCulled](ModelMeshPartPayload& data) { + invalidatePayloadShapeKey, isWireframe, renderItemKeyGlobalFlags](ModelMeshPartPayload& data) { if (useDualQuaternionSkinning) { data.updateClusterBuffer(meshState.clusterDualQuaternions); } else { @@ -313,8 +305,7 @@ void Model::updateRenderItems() { } data.updateTransformForSkinnedMesh(renderTransform, modelTransform); - data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, canCastShadow, viewTagBits, isGroupCulled); - data.setLayer(isLayeredInFront, isLayeredInHUD); + data.updateKey(renderItemKeyGlobalFlags); data.setShapeKey(invalidatePayloadShapeKey, isWireframe, useDualQuaternionSkinning); }); } @@ -322,8 +313,9 @@ void Model::updateRenderItems() { Transform collisionMeshOffset; collisionMeshOffset.setIdentity(); foreach(auto itemID, self->_collisionRenderItemsMap.keys()) { - transaction.updateItem(itemID, [modelTransform, collisionMeshOffset](MeshPartPayload& data) { + transaction.updateItem(itemID, [renderItemKeyGlobalFlags, modelTransform, collisionMeshOffset](MeshPartPayload& data) { // update the model transform for this render item. + data.updateKey(renderItemKeyGlobalFlags); data.updateTransform(modelTransform, collisionMeshOffset); }); } @@ -773,110 +765,100 @@ void Model::calculateTriangleSets(const FBXGeometry& geometry) { } } -void Model::setVisibleInScene(bool isVisible, const render::ScenePointer& scene, uint8_t viewTagBits, bool isGroupCulled) { - if (_isVisible != isVisible || _viewTagBits != viewTagBits || _isGroupCulled != isGroupCulled) { - _isVisible = isVisible; - _viewTagBits = viewTagBits; - _isGroupCulled = isGroupCulled; +void Model::updateRenderItemsKey(const render::ScenePointer& scene) { + if (!scene) { + _needsFixupInScene = true; + return; + } + auto renderItemsKey = _renderItemKeyGlobalFlags; + render::Transaction transaction; + foreach(auto item, _modelMeshRenderItemsMap.keys()) { + transaction.updateItem(item, [renderItemsKey](ModelMeshPartPayload& data) { + data.updateKey(renderItemsKey); + }); + } + foreach(auto item, _collisionRenderItemsMap.keys()) { + transaction.updateItem(item, [renderItemsKey](ModelMeshPartPayload& data) { + data.updateKey(renderItemsKey); + }); + } + scene->enqueueTransaction(transaction); +} - bool isLayeredInFront = _isLayeredInFront; - bool isLayeredInHUD = _isLayeredInHUD; - bool canCastShadow = _canCastShadow; - render::Transaction transaction; - foreach (auto item, _modelMeshRenderItemsMap.keys()) { - transaction.updateItem(item, [isVisible, viewTagBits, isLayeredInFront, canCastShadow, - isLayeredInHUD, isGroupCulled](ModelMeshPartPayload& data) { - data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, canCastShadow, viewTagBits, isGroupCulled); - }); - } - foreach(auto item, _collisionRenderItemsMap.keys()) { - transaction.updateItem(item, [isVisible, viewTagBits, isLayeredInFront, canCastShadow, - isLayeredInHUD, isGroupCulled](ModelMeshPartPayload& data) { - data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, canCastShadow, viewTagBits, isGroupCulled); - }); - } - scene->enqueueTransaction(transaction); +void Model::setVisibleInScene(bool visible, const render::ScenePointer& scene) { + if (Model::isVisible() != visible) { + auto keyBuilder = render::ItemKey::Builder(_renderItemKeyGlobalFlags); + _renderItemKeyGlobalFlags = (visible ? keyBuilder.withVisible() : keyBuilder.withInvisible()); + updateRenderItemsKey(scene); } } -void Model::setCanCastShadow(bool canCastShadow, const render::ScenePointer& scene, uint8_t viewTagBits, bool isGroupCulled) { - if (_canCastShadow != canCastShadow) { - _canCastShadow = canCastShadow; +bool Model::isVisible() const { + return _renderItemKeyGlobalFlags.isVisible(); +} - bool isVisible = _isVisible; - bool isLayeredInFront = _isLayeredInFront; - bool isLayeredInHUD = _isLayeredInHUD; - - render::Transaction transaction; - foreach (auto item, _modelMeshRenderItemsMap.keys()) { - transaction.updateItem(item, - [isVisible, viewTagBits, canCastShadow, isLayeredInFront, isLayeredInHUD, isGroupCulled](ModelMeshPartPayload& data) { - data.updateKey(isVisible, viewTagBits, canCastShadow, isLayeredInFront || isLayeredInHUD, isGroupCulled); - }); - } - - scene->enqueueTransaction(transaction); +void Model::setCanCastShadow(bool castShadow, const render::ScenePointer& scene) { + if (Model::canCastShadow() != castShadow) { + auto keyBuilder = render::ItemKey::Builder(_renderItemKeyGlobalFlags); + _renderItemKeyGlobalFlags = (castShadow ? keyBuilder.withShadowCaster() : keyBuilder.withoutShadowCaster()); + updateRenderItemsKey(scene); } } -void Model::setLayeredInFront(bool isLayeredInFront, const render::ScenePointer& scene) { - if (_isLayeredInFront != isLayeredInFront) { - _isLayeredInFront = isLayeredInFront; +bool Model::canCastShadow() const { + return _renderItemKeyGlobalFlags.isShadowCaster(); +} - bool isVisible = _isVisible; - bool canCastShadow = _canCastShadow; - uint8_t viewTagBits = _viewTagBits; - bool isLayeredInHUD = _isLayeredInHUD; - bool isGroupCulled = _isGroupCulled; - - render::Transaction transaction; - foreach(auto item, _modelMeshRenderItemsMap.keys()) { - transaction.updateItem(item, [isVisible, viewTagBits, isLayeredInFront, canCastShadow, - isLayeredInHUD, isGroupCulled](ModelMeshPartPayload& data) { - data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, canCastShadow, viewTagBits, isGroupCulled); - data.setLayer(isLayeredInFront, isLayeredInHUD); - }); - } - foreach(auto item, _collisionRenderItemsMap.keys()) { - transaction.updateItem(item, [isVisible, viewTagBits, isLayeredInFront, canCastShadow, - isLayeredInHUD, isGroupCulled](ModelMeshPartPayload& data) { - data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, canCastShadow, viewTagBits, isGroupCulled); - data.setLayer(isLayeredInFront, isLayeredInHUD); - }); - } - scene->enqueueTransaction(transaction); +void Model::setLayeredInFront(bool layeredInFront, const render::ScenePointer& scene) { + if (Model::isLayeredInFront() != layeredInFront) { + auto keyBuilder = render::ItemKey::Builder(_renderItemKeyGlobalFlags); + _renderItemKeyGlobalFlags = (layeredInFront ? keyBuilder.withLayer(render::hifi::LAYER_3D_FRONT) : keyBuilder.withoutLayer()); + updateRenderItemsKey(scene); } } -void Model::setLayeredInHUD(bool isLayeredInHUD, const render::ScenePointer& scene) { - if (_isLayeredInHUD != isLayeredInHUD) { - _isLayeredInHUD = isLayeredInHUD; +bool Model::isLayeredInFront() const { + return _renderItemKeyGlobalFlags.isLayer(render::hifi::LAYER_3D_FRONT); +} - bool isVisible = _isVisible; - bool canCastShadow = _canCastShadow; - uint8_t viewTagBits = _viewTagBits; - bool isLayeredInFront = _isLayeredInFront; - bool isGroupCulled = _isGroupCulled; - - render::Transaction transaction; - foreach(auto item, _modelMeshRenderItemsMap.keys()) { - transaction.updateItem(item, [isVisible, viewTagBits, isLayeredInFront, canCastShadow, - isLayeredInHUD, isGroupCulled](ModelMeshPartPayload& data) { - data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, canCastShadow, viewTagBits, isGroupCulled); - data.setLayer(isLayeredInFront, isLayeredInHUD); - }); - } - foreach(auto item, _collisionRenderItemsMap.keys()) { - transaction.updateItem(item, [isVisible, viewTagBits, isLayeredInFront, canCastShadow, - isLayeredInHUD, isGroupCulled](ModelMeshPartPayload& data) { - data.updateKey(isVisible, isLayeredInFront || isLayeredInHUD, canCastShadow, viewTagBits, isGroupCulled); - data.setLayer(isLayeredInFront, isLayeredInHUD); - }); - } - scene->enqueueTransaction(transaction); +void Model::setLayeredInHUD(bool layeredInHUD, const render::ScenePointer& scene) { + if (Model::isLayeredInHUD() != layeredInHUD) { + auto keyBuilder = render::ItemKey::Builder(_renderItemKeyGlobalFlags); + _renderItemKeyGlobalFlags = (layeredInHUD ? keyBuilder.withLayer(render::hifi::LAYER_3D_HUD) : keyBuilder.withoutLayer()); + updateRenderItemsKey(scene); } } +bool Model::isLayeredInHUD() const { + return _renderItemKeyGlobalFlags.isLayer(render::hifi::LAYER_3D_HUD); +} + +void Model::setTagMask(uint8_t mask, const render::ScenePointer& scene) { + if (Model::getTagMask() != mask) { + auto keyBuilder = render::ItemKey::Builder(_renderItemKeyGlobalFlags); + _renderItemKeyGlobalFlags = keyBuilder.withTagBits(mask); + updateRenderItemsKey(scene); + } +} +render::hifi::Tag Model::getTagMask() const { + return (render::hifi::Tag) _renderItemKeyGlobalFlags.getTagBits(); +} + +void Model::setGroupCulled(bool groupCulled, const render::ScenePointer& scene) { + if (Model::isGroupCulled() != groupCulled) { + auto keyBuilder = render::ItemKey::Builder(_renderItemKeyGlobalFlags); + _renderItemKeyGlobalFlags = (groupCulled ? keyBuilder.withSubMetaCulled() : keyBuilder.withoutSubMetaCulled()); + updateRenderItemsKey(scene); + } +} +bool Model::isGroupCulled() const { + return _renderItemKeyGlobalFlags.isSubMetaCulled(); +} + +const render::ItemKey Model::getRenderItemKeyGlobalFlags() const { + return _renderItemKeyGlobalFlags; +} + bool Model::addToScene(const render::ScenePointer& scene, render::Transaction& transaction, render::Item::Status::Getters& statusGetters) { @@ -1092,15 +1074,11 @@ int Model::getLastFreeJointIndex(int jointIndex) const { void Model::setTextures(const QVariantMap& textures) { if (isLoaded()) { - _needsUpdateTextures = true; + _pendingTextures.clear(); _needsFixupInScene = true; _renderGeometry->setTextures(textures); } else { - // FIXME(Huffman): Disconnect previously connected lambdas so we don't set textures multiple - // after the geometry has finished loading. - connect(&_renderWatcher, &GeometryResourceWatcher::finished, this, [this, textures]() { - _renderGeometry->setTextures(textures); - }); + _pendingTextures = textures; } } @@ -1124,7 +1102,8 @@ void Model::setURL(const QUrl& url) { } _needsReload = true; - _needsUpdateTextures = true; + // One might be tempted to _pendingTextures.clear(), thinking that a new URL means an old texture doesn't apply. + // But sometimes, particularly when first setting the values, the texture might be set first. So let's not clear here. _visualGeometryRequestFailed = false; _needsFixupInScene = true; invalidCalculatedMeshBoxes(); @@ -1141,6 +1120,8 @@ void Model::setURL(const QUrl& url) { void Model::loadURLFinished(bool success) { if (!success) { _visualGeometryRequestFailed = true; + } else if (!_pendingTextures.empty()) { + setTextures(_pendingTextures); } emit setURLFinished(success); } @@ -1676,20 +1657,16 @@ void Model::addMaterial(graphics::MaterialLayer material, const std::string& par for (auto shapeID : shapeIDs) { if (shapeID < _modelMeshRenderItemIDs.size()) { auto itemID = _modelMeshRenderItemIDs[shapeID]; - bool visible = isVisible(); - uint8_t viewTagBits = getViewTagBits(); - bool layeredInFront = isLayeredInFront(); - bool layeredInHUD = isLayeredInHUD(); - bool canCastShadow = _canCastShadow; + auto renderItemsKey = _renderItemKeyGlobalFlags; bool wireframe = isWireframe(); auto meshIndex = _modelMeshRenderItemShapes[shapeID].meshIndex; bool invalidatePayloadShapeKey = shouldInvalidatePayloadShapeKey(meshIndex); bool useDualQuaternionSkinning = _useDualQuaternionSkinning; - transaction.updateItem(itemID, [material, visible, layeredInFront, layeredInHUD, viewTagBits, canCastShadow, + transaction.updateItem(itemID, [material, renderItemsKey, invalidatePayloadShapeKey, wireframe, useDualQuaternionSkinning](ModelMeshPartPayload& data) { data.addMaterial(material); // if the material changed, we might need to update our item key or shape key - data.updateKey(visible, layeredInFront || layeredInHUD, canCastShadow, viewTagBits); + data.updateKey(renderItemsKey); data.setShapeKey(invalidatePayloadShapeKey, wireframe, useDualQuaternionSkinning); }); } @@ -1704,19 +1681,16 @@ void Model::removeMaterial(graphics::MaterialPointer material, const std::string if (shapeID < _modelMeshRenderItemIDs.size()) { auto itemID = _modelMeshRenderItemIDs[shapeID]; bool visible = isVisible(); - uint8_t viewTagBits = getViewTagBits(); - bool layeredInFront = isLayeredInFront(); - bool layeredInHUD = isLayeredInHUD(); - bool canCastShadow = _canCastShadow; + auto renderItemsKey = _renderItemKeyGlobalFlags; bool wireframe = isWireframe(); auto meshIndex = _modelMeshRenderItemShapes[shapeID].meshIndex; bool invalidatePayloadShapeKey = shouldInvalidatePayloadShapeKey(meshIndex); bool useDualQuaternionSkinning = _useDualQuaternionSkinning; - transaction.updateItem(itemID, [material, visible, layeredInFront, layeredInHUD, viewTagBits, canCastShadow, + transaction.updateItem(itemID, [material, visible, renderItemsKey, invalidatePayloadShapeKey, wireframe, useDualQuaternionSkinning](ModelMeshPartPayload& data) { data.removeMaterial(material); // if the material changed, we might need to update our item key or shape key - data.updateKey(visible, layeredInFront || layeredInHUD, canCastShadow, viewTagBits); + data.updateKey(renderItemsKey); data.setShapeKey(invalidatePayloadShapeKey, wireframe, useDualQuaternionSkinning); }); } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 2b14a7c055..67e6d178ea 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -33,6 +33,7 @@ #include #include +#include "RenderHifi.h" #include "GeometryCache.h" #include "TextureCache.h" #include "Rig.h" @@ -87,13 +88,27 @@ public: const QUrl& getURL() const { return _url; } // new Scene/Engine rendering support - void setVisibleInScene(bool isVisible, const render::ScenePointer& scene, uint8_t viewTagBits, bool isGroupCulled); + void setVisibleInScene(bool isVisible, const render::ScenePointer& scene = nullptr); + bool isVisible() const; - bool canCastShadow() const { return _canCastShadow; } - void setCanCastShadow(bool canCastShadow, const render::ScenePointer& scene, uint8_t viewTagBits, bool isGroupCulled); + render::hifi::Tag getTagMask() const; + void setTagMask(uint8_t mask, const render::ScenePointer& scene = nullptr); + + bool isGroupCulled() const; + void setGroupCulled(bool isGroupCulled, const render::ScenePointer& scene = nullptr); + + bool canCastShadow() const; + void setCanCastShadow(bool canCastShadow, const render::ScenePointer& scene = nullptr); + + void setLayeredInFront(bool isLayeredInFront, const render::ScenePointer& scene = nullptr); + void setLayeredInHUD(bool isLayeredInHUD, const render::ScenePointer& scene = nullptr); + + bool isLayeredInFront() const; + bool isLayeredInHUD() const; + + // Access the current RenderItemKey Global Flags used by the model and applied to the render items representing the parts of the model. + const render::ItemKey getRenderItemKeyGlobalFlags() const; - void setLayeredInFront(bool isLayeredInFront, const render::ScenePointer& scene); - void setLayeredInHUD(bool isLayeredInHUD, const render::ScenePointer& scene); bool needsFixupInScene() const; bool needsReload() const { return _needsReload; } @@ -108,13 +123,7 @@ public: void removeFromScene(const render::ScenePointer& scene, render::Transaction& transaction); bool isRenderable() const; - bool isVisible() const { return _isVisible; } - uint8_t getViewTagBits() const { return _viewTagBits; } - - bool isLayeredInFront() const { return _isLayeredInFront; } - bool isLayeredInHUD() const { return _isLayeredInHUD; } - - bool isGroupCulled() const { return _isGroupCulled; } + void updateRenderItemsKey(const render::ScenePointer& scene); virtual void updateRenderItems(); void setRenderItemsNeedUpdate(); @@ -404,10 +413,6 @@ protected: QVector _blendshapeCoefficients; QUrl _url; - bool _isVisible; - uint8_t _viewTagBits{ render::ItemKey::TAG_BITS_ALL }; - - bool _canCastShadow; gpu::Buffers _blendedVertexBuffers; @@ -452,7 +457,7 @@ protected: bool _needsFixupInScene { true }; // needs to be removed/re-added to scene bool _needsReload { true }; bool _needsUpdateClusterMatrices { true }; - mutable bool _needsUpdateTextures { true }; + QVariantMap _pendingTextures { }; friend class ModelMeshPartPayload; Rig _rig; @@ -471,10 +476,16 @@ protected: int _renderInfoDrawCalls { 0 }; int _renderInfoHasTransparent { false }; - bool _isLayeredInFront { false }; - bool _isLayeredInHUD { false }; - - bool _isGroupCulled{ false }; + // This Render ItemKey Global Flags capture the Model wide global set of flags that should be communicated to all the render items representing the Model. + // The flags concerned are: + // - isVisible: if true the Model is visible globally in the scene, regardless of the other flags in the item keys (tags or layer or shadow caster). + // - TagBits: the view mask defined through the TagBits telling in which view the Model is rendered if visible. + // - Layer: In which Layer this Model lives. + // - CastShadow: if true and visible and rendered in the view, the Model cast shadows if in a Light volume casting shadows. + // - CullGroup: if true, the render items representing the parts of the Model are culled by a single Meta render item that knows about them, they are not culled individually. + // For this to work, a Meta RI must exists and knows about the RIs of this Model. + // + render::ItemKey _renderItemKeyGlobalFlags; bool shouldInvalidatePayloadShapeKey(int meshIndex); diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index 3ea56f8542..47cab54d09 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -27,6 +27,7 @@ #include #include +#include "RenderHifi.h" #include "RenderCommonTask.h" #include "LightingModel.h" #include "StencilMaskPass.h" @@ -200,8 +201,8 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren const auto overlaysInFrontRangeTimer = task.addJob("BeginOverlaysInFrontRangeTimer", "BeginOverlaysInFrontRangeTimer"); // Layered Overlays - const auto filteredOverlaysOpaque = task.addJob("FilterOverlaysLayeredOpaque", overlayOpaques, Item::LAYER_3D_FRONT); - const auto filteredOverlaysTransparent = task.addJob("FilterOverlaysLayeredTransparent", overlayTransparents, Item::LAYER_3D_FRONT); + const auto filteredOverlaysOpaque = task.addJob("FilterOverlaysLayeredOpaque", overlayOpaques, render::hifi::LAYER_3D_FRONT); + const auto filteredOverlaysTransparent = task.addJob("FilterOverlaysLayeredTransparent", overlayTransparents, render::hifi::LAYER_3D_FRONT); const auto overlaysInFrontOpaque = filteredOverlaysOpaque.getN(0); const auto overlaysInFrontTransparent = filteredOverlaysTransparent.getN(0); diff --git a/libraries/render-utils/src/RenderForwardTask.cpp b/libraries/render-utils/src/RenderForwardTask.cpp index 4388a39cf3..09a2afb711 100755 --- a/libraries/render-utils/src/RenderForwardTask.cpp +++ b/libraries/render-utils/src/RenderForwardTask.cpp @@ -23,6 +23,7 @@ #include +#include "RenderHifi.h" #include "StencilMaskPass.h" #include "ZoneRenderer.h" #include "FadeEffect.h" @@ -79,8 +80,8 @@ void RenderForwardTask::build(JobModel& task, const render::Varying& input, rend task.addJob("PrepareStencil", framebuffer); // Layered Overlays - const auto filteredOverlaysOpaque = task.addJob("FilterOverlaysLayeredOpaque", overlayOpaques, Item::LAYER_3D_FRONT); - const auto filteredOverlaysTransparent = task.addJob("FilterOverlaysLayeredTransparent", overlayTransparents, Item::LAYER_3D_FRONT); + const auto filteredOverlaysOpaque = task.addJob("FilterOverlaysLayeredOpaque", overlayOpaques, render::hifi::LAYER_3D_FRONT); + const auto filteredOverlaysTransparent = task.addJob("FilterOverlaysLayeredTransparent", overlayTransparents, render::hifi::LAYER_3D_FRONT); const auto overlaysInFrontOpaque = filteredOverlaysOpaque.getN(0); const auto overlaysInFrontTransparent = filteredOverlaysTransparent.getN(0); diff --git a/libraries/render-utils/src/RenderHifi.h b/libraries/render-utils/src/RenderHifi.h new file mode 100644 index 0000000000..c489a0ad1d --- /dev/null +++ b/libraries/render-utils/src/RenderHifi.h @@ -0,0 +1,43 @@ +// +// RenderHifi.h +// libraries/render-utils/src +// +// Created by Sam Gateau on 5/30/2018. +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +#ifndef hifi_RenderHifi_h +#define hifi_RenderHifi_h + +#include + + + +// In the library render-utils we are specializing the generic components of the render library to create the custom hifi render engine +// Objects and types serving this goal are define in the namespace render.hifi +// TODO: extend the namespace to all the classes where it make sense in render-utils +namespace render { + namespace hifi { + + // Tag is the alias names of render::ItemKey::Tag combinations used in the Hifi Render Engine + enum Tag : uint8_t { + TAG_NONE = render::ItemKey::TAG_BITS_NONE, // No Tags at all + TAG_MAIN_VIEW = render::ItemKey::TAG_BITS_0, // Main view + TAG_SECONDARY_VIEW = render::ItemKey::TAG_BITS_1, // Secondary View + TAG_ALL_VIEWS = TAG_MAIN_VIEW | TAG_SECONDARY_VIEW, // All views + }; + + // Layer is the alias names of the render::ItemKey::Layer used in the Hifi Render Engine + enum Layer : uint8_t { + LAYER_3D = render::ItemKey::LAYER_DEFAULT, + LAYER_3D_FRONT = render::ItemKey::LAYER_1, + LAYER_3D_HUD = render::ItemKey::LAYER_2, + LAYER_2D = render::ItemKey::LAYER_3, + LAYER_BACKGROUND = render::ItemKey::LAYER_BACKGROUND, + }; + } +} + +#endif // hifi_RenderHifi_h diff --git a/libraries/render-utils/src/RenderViewTask.cpp b/libraries/render-utils/src/RenderViewTask.cpp index 122fc16e61..82426a3a1f 100644 --- a/libraries/render-utils/src/RenderViewTask.cpp +++ b/libraries/render-utils/src/RenderViewTask.cpp @@ -19,7 +19,9 @@ void RenderViewTask::build(JobModel& task, const render::Varying& input, render: // Warning : the cull functor passed to the shadow pass should only be testing for LOD culling. If frustum culling // is performed, then casters not in the view frustum will be removed, which is not what we wish. - task.addJob("RenderShadowTask", cullFunctor, tagBits, tagMask); + if (isDeferred) { + task.addJob("RenderShadowTask", cullFunctor, tagBits, tagMask); + } const auto items = task.addJob("FetchCullSort", cullFunctor, tagBits, tagMask); assert(items.canCast()); diff --git a/libraries/render-utils/src/SubsurfaceScattering.h b/libraries/render-utils/src/SubsurfaceScattering.h index 30021fae40..780ce34d7f 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.h +++ b/libraries/render-utils/src/SubsurfaceScattering.h @@ -149,7 +149,7 @@ class DebugSubsurfaceScatteringConfig : public render::Job::Config { Q_PROPERTY(bool showCursorPixel MEMBER showCursorPixel NOTIFY dirty) Q_PROPERTY(glm::vec2 debugCursorTexcoord MEMBER debugCursorTexcoord NOTIFY dirty) public: - DebugSubsurfaceScatteringConfig() : render::Job::Config(true) {} + DebugSubsurfaceScatteringConfig() : render::Job::Config(false) {} bool showProfile{ false }; bool showLUT{ false }; diff --git a/libraries/render/src/render/CullTask.cpp b/libraries/render/src/render/CullTask.cpp index b5819f114f..3f55e6dedc 100644 --- a/libraries/render/src/render/CullTask.cpp +++ b/libraries/render/src/render/CullTask.cpp @@ -368,9 +368,9 @@ void CullShapeBounds::run(const RenderContextPointer& renderContext, const Input RenderArgs* args = renderContext->args; const auto& inShapes = inputs.get0(); - const auto& cullFilter = inputs.get1(); - const auto& boundsFilter = inputs.get2(); - const auto& antiFrustum = inputs.get3(); + const auto& cullFilter = inputs.get1(); + const auto& boundsFilter = inputs.get2(); + const auto& antiFrustum = inputs.get3(); auto& outShapes = outputs.edit0(); auto& outBounds = outputs.edit1(); @@ -380,7 +380,7 @@ void CullShapeBounds::run(const RenderContextPointer& renderContext, const Input if (!cullFilter.selectsNothing() || !boundsFilter.selectsNothing()) { auto& details = args->_details.edit(_detailType); Test test(_cullFunctor, args, details, antiFrustum); - auto scene = args->_scene; + auto scene = args->_scene; for (auto& inItems : inShapes) { auto key = inItems.first; @@ -395,26 +395,26 @@ void CullShapeBounds::run(const RenderContextPointer& renderContext, const Input if (antiFrustum == nullptr) { for (auto& item : inItems.second) { if (test.solidAngleTest(item.bound) && test.frustumTest(item.bound)) { - const auto shapeKey = scene->getItem(item.id).getKey(); - if (cullFilter.test(shapeKey)) { - outItems->second.emplace_back(item); - } - if (boundsFilter.test(shapeKey)) { - outBounds += item.bound; - } + const auto shapeKey = scene->getItem(item.id).getKey(); + if (cullFilter.test(shapeKey)) { + outItems->second.emplace_back(item); + } + if (boundsFilter.test(shapeKey)) { + outBounds += item.bound; + } } } } else { for (auto& item : inItems.second) { if (test.solidAngleTest(item.bound) && test.frustumTest(item.bound) && test.antiFrustumTest(item.bound)) { - const auto shapeKey = scene->getItem(item.id).getKey(); - if (cullFilter.test(shapeKey)) { - outItems->second.emplace_back(item); - } - if (boundsFilter.test(shapeKey)) { - outBounds += item.bound; - } - } + const auto shapeKey = scene->getItem(item.id).getKey(); + if (cullFilter.test(shapeKey)) { + outItems->second.emplace_back(item); + } + if (boundsFilter.test(shapeKey)) { + outBounds += item.bound; + } + } } } details._rendered += (int)outItems->second.size(); @@ -487,6 +487,7 @@ void FetchSpatialSelection::run(const RenderContextPointer& renderContext, if (filter.test(item.getKey())) { ItemBound itemBound(id, item.getBound()); outItems.emplace_back(itemBound); + } } } diff --git a/libraries/render/src/render/Engine.cpp b/libraries/render/src/render/Engine.cpp index b0842d63cd..da8e60d40a 100644 --- a/libraries/render/src/render/Engine.cpp +++ b/libraries/render/src/render/Engine.cpp @@ -36,12 +36,11 @@ public: } }; -Engine::Engine() : Task(EngineTask::JobModel::create("Engine")), - _renderContext(std::make_shared()) +RenderEngine::RenderEngine() : Engine(EngineTask::JobModel::create("Engine"), std::make_shared()) { } -void Engine::load() { +void RenderEngine::load() { auto config = getConfiguration(); const QString configFile= "config/render.json"; diff --git a/libraries/render/src/render/Engine.h b/libraries/render/src/render/Engine.h index 0271c71529..130ed6533f 100644 --- a/libraries/render/src/render/Engine.h +++ b/libraries/render/src/render/Engine.h @@ -25,7 +25,7 @@ namespace render { class RenderContext : public task::JobContext { public: - RenderContext() : task::JobContext(trace_render()) {} + RenderContext() : task::JobContext() {} virtual ~RenderContext() {} RenderArgs* args; @@ -33,7 +33,9 @@ namespace render { }; using RenderContextPointer = std::shared_ptr; - Task_DeclareTypeAliases(RenderContext) + Task_DeclareCategoryTimeProfilerClass(RenderTimeProfiler, trace_render); + + Task_DeclareTypeAliases(RenderContext, RenderTimeProfiler) // Versions of the COnfig integrating a gpu & batch timer class GPUJobConfig : public JobConfig { @@ -57,10 +59,10 @@ namespace render { class GPUTaskConfig : public TaskConfig { Q_OBJECT - Q_PROPERTY(double gpuRunTime READ getGPURunTime) - Q_PROPERTY(double batchRunTime READ getBatchRunTime) + Q_PROPERTY(double gpuRunTime READ getGPURunTime) + Q_PROPERTY(double batchRunTime READ getBatchRunTime) - double _msGPURunTime { 0.0 }; + double _msGPURunTime { 0.0 }; double _msBatchRunTime { 0.0 }; public: @@ -80,32 +82,25 @@ namespace render { // The render engine holds all render tasks, and is itself a render task. // State flows through tasks to jobs via the render and scene contexts - // the engine should not be known from its jobs. - class Engine : public Task { + class RenderEngine : public Engine { public: - Engine(); - ~Engine() = default; + RenderEngine(); + ~RenderEngine() = default; // Load any persisted settings, and set up the presets // This should be run after adding all jobs, and before building ui void load(); // Register the scene - void registerScene(const ScenePointer& scene) { _renderContext->_scene = scene; } + void registerScene(const ScenePointer& scene) { _context->_scene = scene; } // acces the RenderContext - RenderContextPointer getRenderContext() const { return _renderContext; } - - // Render a frame - // Must have a scene registered and a context set - void run() { assert(_renderContext); Task::run(_renderContext); } + RenderContextPointer getRenderContext() const { return _context; } protected: - RenderContextPointer _renderContext; - - void run(const RenderContextPointer& context) override { assert(_renderContext); Task::run(_renderContext); } }; - using EnginePointer = std::shared_ptr; + using EnginePointer = std::shared_ptr; } diff --git a/libraries/render/src/render/Item.cpp b/libraries/render/src/render/Item.cpp index 9c5efb9fa7..532964777f 100644 --- a/libraries/render/src/render/Item.cpp +++ b/libraries/render/src/render/Item.cpp @@ -29,25 +29,9 @@ const float Item::Status::Value::CYAN = 180.0f; const float Item::Status::Value::BLUE = 240.0f; const float Item::Status::Value::MAGENTA = 300.0f; -const int Item::LAYER_2D = 0; -const int Item::LAYER_3D = 1; -const int Item::LAYER_3D_FRONT = 2; -const int Item::LAYER_3D_HUD = 3; - -const uint8_t ItemKey::TAG_BITS_ALL { 0xFF }; -const uint8_t ItemKey::TAG_BITS_NONE { 0x00 }; -const uint8_t ItemKey::TAG_BITS_0 { 0x01 }; -const uint8_t ItemKey::TAG_BITS_1 { 0x02 }; -const uint8_t ItemKey::TAG_BITS_2 { 0x04 }; -const uint8_t ItemKey::TAG_BITS_3 { 0x08 }; -const uint8_t ItemKey::TAG_BITS_4 { 0x10 }; -const uint8_t ItemKey::TAG_BITS_5 { 0x20 }; -const uint8_t ItemKey::TAG_BITS_6 { 0x40 }; -const uint8_t ItemKey::TAG_BITS_7 { 0x80 }; - const uint32_t ItemKey::KEY_TAG_BITS_MASK = ((uint32_t) ItemKey::TAG_BITS_ALL) << FIRST_TAG_BIT; - +const uint32_t ItemKey::KEY_LAYER_BITS_MASK = ((uint32_t)ItemKey::LAYER_BITS_ALL) << FIRST_LAYER_BIT; void Item::Status::Value::setScale(float scale) { _scale = (std::numeric_limits::max() -1) * 0.5f * (1.0f + std::max(std::min(scale, 1.0f), 0.0f)); diff --git a/libraries/render/src/render/Item.h b/libraries/render/src/render/Item.h index e4dcc7ee03..28994d82b6 100644 --- a/libraries/render/src/render/Item.h +++ b/libraries/render/src/render/Item.h @@ -52,23 +52,45 @@ public: TAG_6, TAG_7, - NUM_TAGS + NUM_TAGS, + + // Tag bits are derived from the Tag enum + TAG_BITS_ALL = 0xFF, + TAG_BITS_NONE = 0x00, + TAG_BITS_0 = 0x01, + TAG_BITS_1 = 0x02, + TAG_BITS_2 = 0x04, + TAG_BITS_3 = 0x08, + TAG_BITS_4 = 0x10, + TAG_BITS_5 = 0x20, + TAG_BITS_6 = 0x40, + TAG_BITS_7 = 0x80, + }; + + // Items are organized in layers, an item belongs to one of the 8 Layers available. + // By default an item is in the 'LAYER_DEFAULT' meaning that it is NOT layered. + // THere is NO ordering relationship between layers. + enum Layer : uint8_t { + LAYER_DEFAULT = 0, // layer 0 aka Default is a 'NOT' layer, items are not considered layered, this is the default value + LAYER_1, + LAYER_2, + LAYER_3, + LAYER_4, + LAYER_5, + LAYER_6, + LAYER_BACKGROUND, // Last Layer is the background by convention + + NUM_LAYERS, + + // Layer bits are derived from the Layer enum, the number of bits needed to represent integer 0 to NUM_LAYERS + NUM_LAYER_BITS = 3, + LAYER_BITS_ALL = 0x07, }; - // Tag bits are derived from the Tag enum - const static uint8_t TAG_BITS_ALL; - const static uint8_t TAG_BITS_NONE; - const static uint8_t TAG_BITS_0; - const static uint8_t TAG_BITS_1; - const static uint8_t TAG_BITS_2; - const static uint8_t TAG_BITS_3; - const static uint8_t TAG_BITS_4; - const static uint8_t TAG_BITS_5; - const static uint8_t TAG_BITS_6; - const static uint8_t TAG_BITS_7; enum FlagBit : uint32_t { - TYPE_SHAPE = 0, // Item is a Shape - TYPE_LIGHT, // Item is a Light + TYPE_SHAPE = 0, // Item is a Shape: Implements the Shape Interface that draw a Geometry rendered with a Material + TYPE_LIGHT, // Item is a Light: Implements the Light Interface that + TYPE_CAMERA, // Item is a Camera: Implements the Camera Interface TYPE_META, // Item is a Meta: meanning it s used to represent a higher level object, potentially represented by other render items TRANSLUCENT, // Transparent and not opaque, for some odd reason TRANSPARENCY doesn't work... @@ -77,13 +99,15 @@ public: DEFORMED, // Deformed within bound, not solid INVISIBLE, // Visible or not in the scene? SHADOW_CASTER, // Item cast shadows - LAYERED, // Item belongs to one of the layers different from the default layer META_CULL_GROUP, // As a meta item, the culling of my sub items is based solely on my bounding box and my visibility in the view SUB_META_CULLED, // As a sub item of a meta render item set as cull group, need to be set to my culling to the meta render it FIRST_TAG_BIT, // 8 Tags available to organize the items and filter them against LAST_TAG_BIT = FIRST_TAG_BIT + NUM_TAGS, + FIRST_LAYER_BIT, // 8 Exclusive Layers (encoded in 3 bits) available to organize the items in layers, an item can only belong to ONE layer + LAST_LAYER_BIT = FIRST_LAYER_BIT + NUM_LAYER_BITS, + __SMALLER, // Reserved bit for spatialized item to indicate that it is smaller than expected in the cell in which it belongs (probably because it overlaps over several smaller cells) NUM_FLAGS, // Not a valid flag @@ -96,6 +120,12 @@ public: return (keyBits & ~KEY_TAG_BITS_MASK) | (((uint32_t)tagBits) << FIRST_TAG_BIT); } + // All the bits touching layer bits sets to true + const static uint32_t KEY_LAYER_BITS_MASK; + static uint32_t evalLayerBitsWithKeyBits(uint8_t layer, const uint32_t keyBits) { + return (keyBits & ~KEY_LAYER_BITS_MASK) | (((uint32_t)layer & LAYER_BITS_ALL) << FIRST_LAYER_BIT); + } + // The key is the Flags Flags _flags; @@ -119,23 +149,30 @@ public: Builder& withTypeMeta() { _flags.set(TYPE_META); return (*this); } Builder& withTransparent() { _flags.set(TRANSLUCENT); return (*this); } Builder& withViewSpace() { _flags.set(VIEW_SPACE); return (*this); } + Builder& withoutViewSpace() { _flags.reset(VIEW_SPACE); return (*this); } Builder& withDynamic() { _flags.set(DYNAMIC); return (*this); } Builder& withDeformed() { _flags.set(DEFORMED); return (*this); } Builder& withInvisible() { _flags.set(INVISIBLE); return (*this); } + Builder& withVisible() { _flags.reset(INVISIBLE); return (*this); } Builder& withShadowCaster() { _flags.set(SHADOW_CASTER); return (*this); } - Builder& withLayered() { _flags.set(LAYERED); return (*this); } + Builder& withoutShadowCaster() { _flags.reset(SHADOW_CASTER); return (*this); } Builder& withMetaCullGroup() { _flags.set(META_CULL_GROUP); return (*this); } + Builder& withoutMetaCullGroup() { _flags.reset(META_CULL_GROUP); return (*this); } Builder& withSubMetaCulled() { _flags.set(SUB_META_CULLED); return (*this); } + Builder& withoutSubMetaCulled() { _flags.reset(SUB_META_CULLED); return (*this); } Builder& withTag(Tag tag) { _flags.set(FIRST_TAG_BIT + tag); return (*this); } // Set ALL the tags in one call using the Tag bits Builder& withTagBits(uint8_t tagBits) { _flags = evalTagBitsWithKeyBits(tagBits, _flags.to_ulong()); return (*this); } + Builder& withLayer(uint8_t layer) { _flags = evalLayerBitsWithKeyBits(layer, _flags.to_ulong()); return (*this); } + Builder& withoutLayer() { return withLayer(LAYER_DEFAULT); } + // Convenient standard keys that we will keep on using all over the place static Builder opaqueShape() { return Builder().withTypeShape(); } static Builder transparentShape() { return Builder().withTypeShape().withTransparent(); } static Builder light() { return Builder().withTypeLight(); } - static Builder background() { return Builder().withViewSpace().withLayered(); } + static Builder background() { return Builder().withViewSpace().withLayer(LAYER_BACKGROUND); } }; ItemKey(const Builder& builder) : ItemKey(builder._flags) {} @@ -160,9 +197,6 @@ public: bool isShadowCaster() const { return _flags[SHADOW_CASTER]; } - bool isLayered() const { return _flags[LAYERED]; } - bool isSpatial() const { return !isLayered(); } - bool isMetaCullGroup() const { return _flags[META_CULL_GROUP]; } void setMetaCullGroup(bool cullGroup) { (cullGroup ? _flags.set(META_CULL_GROUP) : _flags.reset(META_CULL_GROUP)); } @@ -172,6 +206,11 @@ public: bool isTag(Tag tag) const { return _flags[FIRST_TAG_BIT + tag]; } uint8_t getTagBits() const { return ((_flags.to_ulong() & KEY_TAG_BITS_MASK) >> FIRST_TAG_BIT); } + uint8_t getLayer() const { return ((_flags.to_ulong() & KEY_LAYER_BITS_MASK) >> FIRST_LAYER_BIT); } + bool isLayer(uint8_t layer) const { return getLayer() == layer; } + bool isLayered() const { return getLayer() != LAYER_DEFAULT; } + bool isSpatial() const { return !isLayered(); } + // Probably not public, flags used by the scene bool isSmall() const { return _flags[__SMALLER]; } void setSmaller(bool smaller) { (smaller ? _flags.set(__SMALLER) : _flags.reset(__SMALLER)); } @@ -229,9 +268,6 @@ public: Builder& withNoShadowCaster() { _value.reset(ItemKey::SHADOW_CASTER); _mask.set(ItemKey::SHADOW_CASTER); return (*this); } Builder& withShadowCaster() { _value.set(ItemKey::SHADOW_CASTER); _mask.set(ItemKey::SHADOW_CASTER); return (*this); } - Builder& withoutLayered() { _value.reset(ItemKey::LAYERED); _mask.set(ItemKey::LAYERED); return (*this); } - Builder& withLayered() { _value.set(ItemKey::LAYERED); _mask.set(ItemKey::LAYERED); return (*this); } - Builder& withoutMetaCullGroup() { _value.reset(ItemKey::META_CULL_GROUP); _mask.set(ItemKey::META_CULL_GROUP); return (*this); } Builder& withMetaCullGroup() { _value.set(ItemKey::META_CULL_GROUP); _mask.set(ItemKey::META_CULL_GROUP); return (*this); } @@ -243,6 +279,9 @@ public: // Set ALL the tags in one call using the Tag bits and the Tag bits touched Builder& withTagBits(uint8_t tagBits, uint8_t tagMask) { _value = ItemKey::evalTagBitsWithKeyBits(tagBits, _value.to_ulong()); _mask = ItemKey::evalTagBitsWithKeyBits(tagMask, _mask.to_ulong()); return (*this); } + Builder& withoutLayered() { _value = ItemKey::evalLayerBitsWithKeyBits(ItemKey::LAYER_DEFAULT, _value.to_ulong()); _mask |= ItemKey::KEY_LAYER_BITS_MASK; return (*this); } + Builder& withLayer(uint8_t layer) { _value = ItemKey::evalLayerBitsWithKeyBits(layer, _value.to_ulong()); _mask |= ItemKey::KEY_LAYER_BITS_MASK; return (*this); } + Builder& withNothing() { _value.reset(); _mask.reset(); return (*this); } // Convenient standard keys that we will keep on using all over the place @@ -251,9 +290,7 @@ public: static Builder transparentShape() { return Builder().withTypeShape().withTransparent().withWorldSpace(); } static Builder light() { return Builder().withTypeLight(); } static Builder meta() { return Builder().withTypeMeta(); } - static Builder background() { return Builder().withViewSpace().withLayered(); } - static Builder opaqueShapeLayered() { return Builder().withTypeShape().withOpaque().withWorldSpace().withLayered(); } - static Builder transparentShapeLayered() { return Builder().withTypeShape().withTransparent().withWorldSpace().withLayered(); } + static Builder background() { return Builder().withViewSpace().withLayer(ItemKey::LAYER_BACKGROUND); } static Builder nothing() { return Builder().withNothing(); } }; @@ -376,7 +413,6 @@ public: public: virtual const ItemKey getKey() const = 0; virtual const Bound getBound() const = 0; - virtual int getLayer() const = 0; virtual void render(RenderArgs* args) = 0; virtual const ShapeKey getShapeKey() const = 0; @@ -421,13 +457,8 @@ public: // Get the bound of the item expressed in world space (or eye space depending on the key.isWorldSpace()) const Bound getBound() const { return _payload->getBound(); } - // Get the layer where the item belongs. - int getLayer() const { return _payload->getLayer(); } - - static const int LAYER_2D; - static const int LAYER_3D; - static const int LAYER_3D_FRONT; - static const int LAYER_3D_HUD; + // Get the layer where the item belongs, simply reflecting the key. + int getLayer() const { return _key.getLayer(); } // Render call for the item void render(RenderArgs* args) const { _payload->render(args); } @@ -477,7 +508,6 @@ inline QDebug operator<<(QDebug debug, const Item& item) { // Item shared interface supported by the payload template const ItemKey payloadGetKey(const std::shared_ptr& payloadData) { return ItemKey(); } template const Item::Bound payloadGetBound(const std::shared_ptr& payloadData) { return Item::Bound(); } -template int payloadGetLayer(const std::shared_ptr& payloadData) { return 0; } template void payloadRender(const std::shared_ptr& payloadData, RenderArgs* args) { } // Shape type interface @@ -504,7 +534,6 @@ public: // Payload general interface virtual const ItemKey getKey() const override { return payloadGetKey(_data); } virtual const Item::Bound getBound() const override { return payloadGetBound(_data); } - virtual int getLayer() const override { return payloadGetLayer(_data); } virtual void render(RenderArgs* args) override { payloadRender(_data, args); } diff --git a/libraries/render/src/render/Scene.h b/libraries/render/src/render/Scene.h index 2d8bc7f4dd..68acfe8d0f 100644 --- a/libraries/render/src/render/Scene.h +++ b/libraries/render/src/render/Scene.h @@ -21,7 +21,7 @@ namespace render { -class Engine; +class RenderEngine; class Scene; // Transaction is the mechanism to make any change to the scene. @@ -236,7 +236,7 @@ protected: StageMap _stages; - friend class Engine; + friend class RenderEngine; }; typedef std::shared_ptr ScenePointer; diff --git a/libraries/script-engine/src/AssetScriptingInterface.h b/libraries/script-engine/src/AssetScriptingInterface.h index 7f7a3a68b0..72d6901fb5 100644 --- a/libraries/script-engine/src/AssetScriptingInterface.h +++ b/libraries/script-engine/src/AssetScriptingInterface.h @@ -121,7 +121,7 @@ public: /**jsdoc * A set of properties that can be passed to {@link Assets.getAsset}. - * @typedef {Object} Assets.GetOptions + * @typedef {object} Assets.GetOptions * @property {string} [url] an "atp:" style URL, hash, or relative mapped path to fetch * @property {string} [responseType=text] the desired reponse type (text | arraybuffer | json) * @property {boolean} [decompress=false] whether to attempt gunzip decompression on the fetched data @@ -137,7 +137,7 @@ public: /**jsdoc * Result value returned by {@link Assets.getAsset}. - * @typedef {Object} Assets~getAssetResult + * @typedef {object} Assets~getAssetResult * @property {string} [url] the resolved "atp:" style URL for the fetched asset * @property {string} [hash] the resolved hash for the fetched asset * @property {string|ArrayBuffer|Object} [response] response data (possibly converted per .responseType value) @@ -159,7 +159,7 @@ public: /**jsdoc * A set of properties that can be passed to {@link Assets.putAsset}. - * @typedef {Object} Assets.PutOptions + * @typedef {object} Assets.PutOptions * @property {ArrayBuffer|string} [data] byte buffer or string value representing the new asset's content * @property {string} [path=null] ATP path mapping to automatically create (upon successful upload to hash) * @property {boolean} [compress=false] whether to gzip compress data before uploading @@ -174,7 +174,7 @@ public: /**jsdoc * Result value returned by {@link Assets.putAsset}. - * @typedef {Object} Assets~putAssetResult + * @typedef {object} Assets~putAssetResult * @property {string} [url] the resolved "atp:" style URL for the uploaded asset (based on .path if specified, otherwise on the resulting ATP hash) * @property {string} [path] the uploaded asset's resulting ATP path (or undefined if no path mapping was assigned) * @property {string} [hash] the uploaded asset's resulting ATP hash diff --git a/libraries/script-engine/src/Quat.h b/libraries/script-engine/src/Quat.h index 1ccdfdbf31..76b7ac45e3 100644 --- a/libraries/script-engine/src/Quat.h +++ b/libraries/script-engine/src/Quat.h @@ -43,7 +43,7 @@ * @hifi-server-entity * @hifi-assignment-client * - * @property IDENTITY {Quat} { x: 0, y: 0, z: 0, w: 1 } : The identity rotation, i.e., no rotation. + * @property {Quat} IDENTITY - { x: 0, y: 0, z: 0, w: 1 } : The identity rotation, i.e., no rotation. * Read-only. * @example Print the IDENTITY value. * print(JSON.stringify(Quat.IDENTITY)); // { x: 0, y: 0, z: 0, w: 1 } diff --git a/libraries/script-engine/src/SceneScriptingInterface.h b/libraries/script-engine/src/SceneScriptingInterface.h index fdfbc6f6c0..da42cf2df3 100644 --- a/libraries/script-engine/src/SceneScriptingInterface.h +++ b/libraries/script-engine/src/SceneScriptingInterface.h @@ -21,7 +21,7 @@ namespace SceneScripting { /**jsdoc - * @typedef Scene.Stage.Location + * @typedef {object} Scene.Stage.Location * @property {number} longitude * @property {number} latitude * @property {number} altitude @@ -49,7 +49,7 @@ namespace SceneScripting { using LocationPointer = std::unique_ptr; /**jsdoc - * @typedef Scene.Stage.Time + * @typedef {object} Scene.Stage.Time * @property {number} hour * @property {number} day */ @@ -73,7 +73,7 @@ namespace SceneScripting { using TimePointer = std::unique_ptr