From 7a2e20caac9db6f789a7b3fab344b9758af45335 Mon Sep 17 00:00:00 2001 From: Harvey Harrison Date: Sun, 19 Apr 2015 21:02:06 -0700 Subject: j3dcore: flatten the directory structure a bit Signed-off-by: Harvey Harrison --- src/javax/media/j3d/ViewSpecificGroupRetained.java | 759 +++++++++++++++++++++ 1 file changed, 759 insertions(+) create mode 100644 src/javax/media/j3d/ViewSpecificGroupRetained.java (limited to 'src/javax/media/j3d/ViewSpecificGroupRetained.java') diff --git a/src/javax/media/j3d/ViewSpecificGroupRetained.java b/src/javax/media/j3d/ViewSpecificGroupRetained.java new file mode 100644 index 0000000..69fde27 --- /dev/null +++ b/src/javax/media/j3d/ViewSpecificGroupRetained.java @@ -0,0 +1,759 @@ +/* + * Copyright 2001-2008 Sun Microsystems, Inc. All Rights Reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, + * CA 95054 USA or visit www.sun.com if you need additional information or + * have any questions. + * + */ + +package javax.media.j3d; + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Vector; + +/** + * The ViewSpecificGroup node retained object. + */ + +class ViewSpecificGroupRetained extends GroupRetained { + +ArrayList apiViewList = new ArrayList(); + + // Used by leaf objects particularly GAs + // Updated in a MT Safe manner and also used by RenderBin + ArrayList> cachedViewList = new ArrayList>(); + + // The object that contains the dynamic HashKey - a string type object + // Used in scoping + HashKey tempKey = new HashKey(250); + +// ArrayList of Integer indices +ArrayList> parentLists = new ArrayList>(); + + static final int SET_VIEW = 0x1; + static final int ADD_VIEW = 0x2; + static final int REMOVE_VIEW = 0x4; + + // Construct retained object + ViewSpecificGroupRetained() { + this.nodeType = NodeRetained.VIEWSPECIFICGROUP; + viewLists = new ArrayList>(); + } + + void addView(View view) { + int i; + Integer mtype = new Integer(ADD_VIEW); + + apiViewList.add(view); + if (source.isLive() && view != null) { + // Gather all affected leaf nodes and send a message to + // RenderingEnv and RenderBin + if (inSharedGroup) { + for (int k = 0; k < localToVworldKeys.length; k++) { + ArrayList parentList = parentLists.get(k); + // If the parentList contains this view or if this is the + // first VSG then .. + if (parentList == null || parentList.contains(view)) { + Object[] objAry = new Object[4]; + ArrayList addVsgList = new ArrayList(); + ArrayList addLeafList = new ArrayList(); + int[] addKeyList = new int[10]; + + HashKey key = localToVworldKeys[k]; + addVsgList.add(this); + addKeyList[0] = k; + objAry[0] = view; + objAry[1] = addVsgList; + objAry[2] = addLeafList; + /* + for (int n = 0; n < addLeafList.size(); n++) { + System.err.println("Shared:n = "+n+" addLeafList = "+addLeafList.get(n)); + } + */ + objAry[3] = super.processViewSpecificInfo(ADD_VIEW, + key, view, + addVsgList, addKeyList, addLeafList); + J3dMessage message = new J3dMessage(); + message.type = J3dMessage.VIEWSPECIFICGROUP_CHANGED; + message.threads = (J3dThread.UPDATE_RENDERING_ENVIRONMENT| + J3dThread.UPDATE_RENDER | + J3dThread.UPDATE_SOUND| + J3dThread.SOUND_SCHEDULER); + message.universe = universe; + message.args[0] = mtype; + message.args[1] = objAry; + VirtualUniverse.mc.processMessage(message); + } + } + + } + else { + ArrayList parentList = parentLists.get(0); + + // If the parentList contains this view or if this is the + // first VSG then .. + if (parentList == null || parentList.contains(view)) { + Object[] objAry = new Object[4]; + ArrayList addVsgList = new ArrayList(); + ArrayList addLeafList = new ArrayList(); + int[] addKeyList = new int[10]; + + objAry[0] = view; + objAry[1] = addVsgList; + objAry[2] = addLeafList; + + addVsgList.add(this); + addKeyList[0] = 0; + tempKey.reset(); + objAry[3] = super.processViewSpecificInfo(ADD_VIEW, + tempKey, view, + addVsgList, addKeyList, addLeafList); + + /* + for (int n = 0; n < addLeafList.size(); n++) { + System.err.println("n = "+n+" addLeafList = "+addLeafList.get(n)); + } + */ + + J3dMessage message = new J3dMessage(); + message.type = J3dMessage.VIEWSPECIFICGROUP_CHANGED; + message.threads = (J3dThread.UPDATE_RENDERING_ENVIRONMENT| + J3dThread.UPDATE_RENDER | + J3dThread.UPDATE_SOUND| + J3dThread.SOUND_SCHEDULER); + message.universe = universe; + message.args[0] = mtype; + message.args[1] = objAry; + VirtualUniverse.mc.processMessage(message); + } + } + + + } + } + + +void setView(View view, int index) { + View oldView = apiViewList.get(index); + Integer mtype = new Integer(SET_VIEW); + + if (oldView == view) + return; + + apiViewList.set(index, view); + if (source.isLive()) { + // Gather all affected leaf nodes and send a message to + // RenderingEnv and RenderBin + if (inSharedGroup) { + for (int k = 0; k < localToVworldKeys.length; k++) { + ArrayList parentList = parentLists.get(k); + Object[] objAry = new Object[8]; + ArrayList addVsgList = new ArrayList(); + ArrayList removeVsgList = new ArrayList(); + ArrayList addLeafList = new ArrayList(); + ArrayList removeLeafList = new ArrayList(); + int[] addKeyList = new int[10]; + int[] removeKeyList = new int[10]; + + objAry[0] = view; + objAry[1] = addVsgList ; + objAry[2] = addLeafList; + objAry[4] = oldView; + objAry[5] = removeVsgList; + objAry[6] = removeLeafList; + + HashKey key = localToVworldKeys[k]; + if (oldView != null && (parentList == null || parentList.contains(oldView))) { + removeVsgList.add(this); + removeKeyList[0] = k; + objAry[7] = super.processViewSpecificInfo(REMOVE_VIEW, key, + oldView, removeVsgList, removeKeyList, removeLeafList); + } + + if (view != null && (parentList == null || parentList.contains(view))) { + addVsgList.add(this); + addKeyList[0] = k; + objAry[3] = super.processViewSpecificInfo(ADD_VIEW, key, + view, addVsgList, addKeyList, addLeafList); + } + J3dMessage message = new J3dMessage(); + message.type = J3dMessage.VIEWSPECIFICGROUP_CHANGED; + message.threads = (J3dThread.UPDATE_RENDERING_ENVIRONMENT| + J3dThread.UPDATE_RENDER | + J3dThread.UPDATE_SOUND| + J3dThread.SOUND_SCHEDULER); + message.universe = universe; + message.args[0] = mtype; + message.args[1] = objAry; + VirtualUniverse.mc.processMessage(message); + } + + } + else { + ArrayList parentList = parentLists.get(0); + Object[] objAry = new Object[8]; + ArrayList addVsgList = new ArrayList(); + ArrayList removeVsgList = new ArrayList(); + ArrayList addLeafList = new ArrayList(); + ArrayList removeLeafList = new ArrayList(); + int[] addKeyList = new int[10]; + int[] removeKeyList = new int[10]; + + objAry[0] = view; + objAry[1] = addVsgList ; + objAry[2] = addLeafList; + objAry[4] = oldView; + objAry[5] = removeVsgList; + objAry[6] = removeLeafList; + + + + tempKey.reset(); + if (oldView != null && (parentList == null || parentList.contains(oldView))) { + removeVsgList.add(this); + removeKeyList[0] = 0; + objAry[7] = super.processViewSpecificInfo(REMOVE_VIEW, tempKey, + oldView, removeVsgList, removeKeyList, removeLeafList); + } + if (view != null && (parentList == null || parentList.contains(view))) { + tempKey.reset(); + addVsgList.add(this); + addKeyList[0] = 0; + objAry[3] = super.processViewSpecificInfo(ADD_VIEW, tempKey, + view, addVsgList, addKeyList, addLeafList); + } + J3dMessage message = new J3dMessage(); + message.type = J3dMessage.VIEWSPECIFICGROUP_CHANGED; + message.threads = (J3dThread.UPDATE_RENDERING_ENVIRONMENT| + J3dThread.UPDATE_RENDER | + J3dThread.UPDATE_SOUND| + J3dThread.SOUND_SCHEDULER); + + message.universe = universe; + message.args[0] = mtype; + message.args[1] = objAry; + VirtualUniverse.mc.processMessage(message); + } + + + } + + } + + @Override + int[] processViewSpecificInfo(int mode, HashKey key, View v, ArrayList vsgList, int[] keyList, ArrayList leaflist) { + int hkIndex = 0; + Integer hashInt = null; + int[] newKeyList = null; + // Get the intersection of the viewList with this view, + + if (source.isLive()) { + if (inSharedGroup) { + hkIndex = key.equals(localToVworldKeys, 0, localToVworldKeys.length); + } + + if (mode == ADD_VIEW) { + ArrayList parentList = parentLists.get(hkIndex); + parentList.add(v); + } + else if (mode == REMOVE_VIEW) { + ArrayList parentList = parentLists.get(hkIndex); + parentList.remove(v); + } + if(apiViewList.contains(v)) { + // System.err.println("processViewSpecificInfo, this = "+this+" key = "+key); + vsgList.add(this); + if (keyList.length< vsgList.size()) { + // System.err.println("====> allocating new array"); + newKeyList = new int[keyList.length+20]; + System.arraycopy(keyList, 0, newKeyList, 0, keyList.length); + keyList = newKeyList; + } + if (mode == ADD_VIEW) { + if (inSharedGroup) { + keyList[vsgList.size()-1] = hkIndex; + + } + else { + keyList[vsgList.size()-1] = 0; + } + } + else if (mode == REMOVE_VIEW) { + if (inSharedGroup) { + keyList[vsgList.size()-1] = hkIndex; + } + else { + keyList[vsgList.size()-1] = 0; + } + } + return super.processViewSpecificInfo(mode, key, v, vsgList, keyList, leaflist); + } + } + return keyList; + } + +View getView(int index) { + return apiViewList.get(index); +} + + void insertView(View view, int index) { + int i; + Integer mtype = new Integer(ADD_VIEW); + + apiViewList.add(index, view); + if (source.isLive() && view != null) { + // Gather all affected leaf nodes and send a message to + // RenderingEnv and RenderBin + if (inSharedGroup) { + for (int k = 0; k < localToVworldKeys.length; k++) { + ArrayList parentList = parentLists.get(k); + // If the parentList contains this view or if this is the + // first VSG then .. + if (parentList == null || parentList.contains(view)) { + Object[] objAry = new Object[4]; + ArrayList addVsgList = new ArrayList(); + ArrayList addLeafList = new ArrayList(); + int[] addKeyList = new int[10]; + + HashKey key = localToVworldKeys[k]; + addVsgList.add(this); + addKeyList[0] = k; + objAry[0] = view; + objAry[1] = addVsgList; + objAry[2] = addLeafList; + /* + for (int n = 0; n < addLeafList.size(); n++) { + System.err.println("Shared:n = "+n+" addLeafList = "+addLeafList.get(n)); + } + */ + objAry[3] = super.processViewSpecificInfo(ADD_VIEW, + key, view, + addVsgList, addKeyList, addLeafList); + J3dMessage message = new J3dMessage(); + message.type = J3dMessage.VIEWSPECIFICGROUP_CHANGED; + message.threads = (J3dThread.UPDATE_RENDERING_ENVIRONMENT| + J3dThread.UPDATE_RENDER | + J3dThread.UPDATE_SOUND| + J3dThread.SOUND_SCHEDULER); + message.universe = universe; + message.args[0] = mtype; + message.args[1] = objAry; + VirtualUniverse.mc.processMessage(message); + } + } + + } + else { + ArrayList parentList = parentLists.get(0); + + // If the parentList contains this view or if this is the + // first VSG then .. + if (parentList == null || parentList.contains(view)) { + Object[] objAry = new Object[4]; + ArrayList addVsgList = new ArrayList(); + ArrayList addLeafList = new ArrayList(); + int[] addKeyList = new int[10]; + + objAry[0] = view; + objAry[1] = addVsgList; + objAry[2] = addLeafList; + + addVsgList.add(this); + addKeyList[0] = 0; + tempKey.reset(); + objAry[3] = super.processViewSpecificInfo(ADD_VIEW, + tempKey, view, + addVsgList, addKeyList, addLeafList); + + /* + for (int n = 0; n < addLeafList.size(); n++) { + System.err.println("n = "+n+" addLeafList = "+addLeafList.get(n)); + } + */ + + J3dMessage message = new J3dMessage(); + message.type = J3dMessage.VIEWSPECIFICGROUP_CHANGED; + message.threads = (J3dThread.UPDATE_RENDERING_ENVIRONMENT| + J3dThread.UPDATE_RENDER | + J3dThread.UPDATE_SOUND| + J3dThread.SOUND_SCHEDULER); + message.universe = universe; + message.args[0] = mtype; + message.args[1] = objAry; + VirtualUniverse.mc.processMessage(message); + } + } + + + } + } + + void removeView(int index) { + int i; + View v = apiViewList.remove(index); + if (source.isLive() && v != null) { + // Gather all affected leaf nodes and send a message to + // RenderingEnv and RenderBin + if (inSharedGroup) { + for (int k = 0; k < localToVworldKeys.length; k++) { + ArrayList parentList = parentLists.get(k); + // If the parentList contains this view or if this is the + // first VSG then .. + if (parentList == null || parentList.contains(v)) { + Object[] objAry = new Object[4]; + ArrayList removeVsgList = new ArrayList(); + ArrayList removeLeafList = new ArrayList(); + int[] removeKeyList = new int[10]; + + objAry[0] = v; + objAry[1] = removeVsgList; + objAry[2] = removeLeafList; + HashKey key = localToVworldKeys[k]; + + removeVsgList.add(this); + removeKeyList[0] = k; + + objAry[3] = super.processViewSpecificInfo(REMOVE_VIEW, + key,v, + removeVsgList, removeKeyList, removeLeafList); + + + J3dMessage message = new J3dMessage(); + message.type = J3dMessage.VIEWSPECIFICGROUP_CHANGED; + message.threads = (J3dThread.UPDATE_RENDERING_ENVIRONMENT| + J3dThread.UPDATE_RENDER | + J3dThread.UPDATE_SOUND| + J3dThread.SOUND_SCHEDULER); + message.universe = universe; + message.args[0] = new Integer(REMOVE_VIEW); + message.args[1] = objAry; + VirtualUniverse.mc.processMessage(message); + } + } + + } + else { + ArrayList parentList = parentLists.get(0); + + // If the parentList contains this view or if this is the + // first VSG then .. + if (parentList == null || parentList.contains(v)) { + Object[] objAry = new Object[4]; + ArrayList removeVsgList = new ArrayList(); + ArrayList removeLeafList = new ArrayList(); + int[] removeKeyList = new int[10]; + + objAry[0] = v; + objAry[1] = removeVsgList; + objAry[2] = removeLeafList; + removeVsgList.add(this); + removeKeyList[0] = 0; + + tempKey.reset(); + objAry[3] = super.processViewSpecificInfo(REMOVE_VIEW, + tempKey, v, + removeVsgList, removeKeyList, removeLeafList); + + /* + for (int n = 0; n < removeKeyList.size(); n++) { + System.err.println("n = "+n+" keyValue = "+removeKeyList.get(n)); + } + */ + J3dMessage message = new J3dMessage(); + message.type = J3dMessage.VIEWSPECIFICGROUP_CHANGED; + message.threads = (J3dThread.UPDATE_RENDERING_ENVIRONMENT| + J3dThread.UPDATE_RENDER | + J3dThread.UPDATE_SOUND| + J3dThread.SOUND_SCHEDULER); + message.universe = universe; + message.args[0] = new Integer(REMOVE_VIEW); + message.args[1] = objAry; + VirtualUniverse.mc.processMessage(message); + } + } + + } + } + +Enumeration getAllViews() { + Vector viewList = new Vector(apiViewList.size()); + for (int i = 0; i < apiViewList.size(); i++) { + viewList.add(apiViewList.get(i)); + } + return viewList.elements(); +} + + int numViews() { + return apiViewList.size(); + } + + int indexOfView(View view) { + return apiViewList.indexOf(view); + } + + void removeView(View view) { + removeView(apiViewList.indexOf(view)); + } + + void removeAllViews() { + int size = apiViewList.size(); + for (int i = 0; i < size; i++) { + removeView(0); + } + } + + @Override + void compile(CompileState compState) { + super.compile(compState); + + // don't remove this group node + mergeFlag = SceneGraphObjectRetained.DONT_MERGE; + + // XXXX: complete this + } + + @Override + void setLive(SetLiveState s) { + if (inBackgroundGroup) { + throw new + IllegalSceneGraphException(J3dI18N.getString("ViewSpecificGroup3")); + } + + s.inViewSpecificGroup = true; + ArrayList> savedViewList = s.viewLists; + if (s.changedViewGroup == null) { + s.changedViewGroup = new ArrayList(); + s.changedViewList = new ArrayList>(); + s.keyList = new int[10]; + s.viewScopedNodeList = new ArrayList(); + s.scopedNodesViewList = new ArrayList>(); + } + super.setLive(s); + s.viewLists = savedViewList; + + } + + @Override + void clearLive(SetLiveState s) { + ArrayList> savedViewList = s.viewLists; + if (s.changedViewGroup == null) { + s.changedViewGroup = new ArrayList(); + s.changedViewList = new ArrayList>(); + s.keyList = new int[10]; + s.viewScopedNodeList = new ArrayList(); + s.scopedNodesViewList = new ArrayList>(); + } + // XXXX: This is a hack since removeNodeData is called before + // children are clearLives + int[] tempIndex = null; + // Don't keep the indices if everything will be cleared + if (inSharedGroup && (s.keys.length != localToVworld.length)) { + tempIndex = new int[s.keys.length]; + for (int i = 0; i < s.keys.length; i++) { + tempIndex[i] = s.keys[i].equals(localToVworldKeys, 0, localToVworldKeys.length); + } + } + super.clearLive(s); + // Do this after children clearlive since part of the viewLists may get cleared + // during removeNodeData + + // If the last SharedGroup is being cleared + if((!inSharedGroup) || (localToVworld == null)) { + viewLists.clear(); + } + else { + // Must be in reverse, to preserve right indexing. + for (int i = tempIndex.length-1; i >= 0 ; i--) { + if(tempIndex[i] >= 0) { + viewLists.remove(tempIndex[i]); + } + } + } + s.viewLists = savedViewList; + } + + + @Override + void removeNodeData(SetLiveState s) { + if((!inSharedGroup) || (s.keys.length == localToVworld.length)) { + s.changedViewGroup.add(this); + // Remove everything .. + int size = s.changedViewGroup.size(); + if (s.keyList.length < size) { + int[] newKeyList = new int[s.keyList.length+20]; + System.arraycopy(s.keyList, 0, newKeyList, 0, s.keyList.length); + s.keyList = newKeyList; + // System.err.println("====> RemovedNodeData: Allocating Non-shared"); + } + s.keyList[size -1] = -1; + parentLists.clear(); + } + // A path of the shared group is removed + else { + int i, index; + int size = s.changedViewGroup.size(); + if (s.keyList.length < size+1+s.keys.length) { + int[] newKeyList = new int[s.keyList.length+s.keys.length+20]; + System.arraycopy(s.keyList, 0, newKeyList, 0, s.keyList.length); + s.keyList = newKeyList; + // System.err.println("====> RemovedNodeData: Allocating Shared"); + } + // Must be in reverse, to preserve right indexing. + for (i = s.keys.length-1; i >= 0; i--) { + index = s.keys[i].equals(localToVworldKeys, 0, localToVworldKeys.length); + if(index >= 0) { + s.changedViewGroup.add(this); + s.keyList[s.changedViewGroup.size() -1] = index; + parentLists.remove(index); + } + } + } + s.viewLists =viewLists; + super.removeNodeData(s); + } + +void updateCachedInformation(int component, View view, int index) { + ArrayList list = cachedViewList.get(index); + + /* + System.err.println("updateCachedInformation v = "+this+" index = "+index+" list = "+list+" cachedViewList.size() = "+cachedViewList.size()); + for (int k = 0; k < cachedViewList.size(); k++) { + System.err.println("v = "+this+" k = "+k+" v.cachedViewList.get(k) = "+cachedViewList.get(k)); + } + */ + if ((component & ADD_VIEW) != 0) { + list.add(view); + } + else if ((component & REMOVE_VIEW) != 0) { + list.remove(view); + } + /* + System.err.println("After updateCachedInformation v = "+this+" index = "+index+" list = "+list+" cachedViewList.size() = "+cachedViewList.size()); + for (int k = 0; k < cachedViewList.size(); k++) { + System.err.println("v = "+this+" k = "+k+" v.cachedViewList.get(k) = "+cachedViewList.get(k)); + } + */ + + } + + @Override + void setNodeData(SetLiveState s) { + super.setNodeData(s); + if (!inSharedGroup) { + int size = s.changedViewGroup.size(); + if (s.keyList.length < size+1) { + int[] newKeyList = new int[s.keyList.length+20]; + System.arraycopy(s.keyList, 0, newKeyList, 0, s.keyList.length); + s.keyList = newKeyList; + // System.err.println("====> setNodeData: Allocating Non-shared"); + } + setAuxData(s, 0, 0); + } else { + // For inSharedGroup case. + int j, hkIndex; + + int size = s.changedViewGroup.size(); + if (s.keyList.length < size+1+s.keys.length) { + int[] newKeyList = new int[s.keyList.length+s.keys.length+20]; + System.arraycopy(s.keyList, 0, newKeyList, 0, s.keyList.length); + s.keyList = newKeyList; + // System.err.println("====> setNodeData: Allocating Shared"); + } + + for(j=0; j= 0) { + setAuxData(s, j, hkIndex); + + } else { + MasterControl.getCoreLogger().severe("Can't Find matching hashKey in setNodeData."); + } + } + } + // Make the VSG's viewLists as the relavant one for its children + s.viewLists = viewLists; + + } + + @Override + void setAuxData(SetLiveState s, int index, int hkIndex) { + ArrayList vl = new ArrayList(); + ArrayList parentList = null; + int size = apiViewList.size(); + if (s.viewLists != null) { + // System.err.println("=====> VSG: = "+this+" hkIndex = "+hkIndex+" s.viewLists = "+s.viewLists); + parentList = s.viewLists.get(hkIndex); + if (parentList != null) { + for (int i = 0; i < size; i++) { + View v = apiViewList.get(i); + // Get the intersection of the parentlist and this vsg's api list + for (int j = 0; j < parentList.size(); j++) { + if (v == parentList.get(j)) { + vl.add(v); + break; + } + } + } + } + else { + // Only include the non null ones in the apiViewList + for (int i = 0; i < size; i++) { + View v = apiViewList.get(i); + if (v != null) { + vl.add(v); + } + } + } + } + else { + // Only include the non null ones in the apiViewList + for (int i = 0; i < size; i++) { + View v = apiViewList.get(i); + if (v != null) { + vl.add(v); + } + } + } + if (parentList != null) { + parentLists.add(hkIndex, new ArrayList(parentList)); + } + else { + parentLists.add(hkIndex, null); + } + + viewLists.add(hkIndex,vl); + s.changedViewGroup.add(this); + s.changedViewList.add(vl); + if (localToVworldKeys != null) { + s.keyList[s.changedViewGroup.size() -1] = hkIndex; + } + else { + s.keyList[s.changedViewGroup.size() -1] = 0; + } + + + + } + +} -- cgit v1.2.3